Windows异常学习笔记(四)—— 编译器扩展SEH
Windows异常学习笔记(四)—— 编译器扩展SEH
- 要点回顾
- 编译器支持的SEH
- 过滤表达式
- 实验一:理解_try_except
- 实验二:_try_except 嵌套
- 拓展SEH结构体
- scopetable
- 实验三:理解scopetable
- trylevel
- 实验四:理解trylevel
- 总结
- __try__finally
- 实验五:理解__try__finally
- 局部展开
- 实验六:理解局部展开
- 全局展开
- 实验七:理解全局展开
要点回顾
当我们通过编程实现windows的异常处理方式使用SEH结构化异常时,我们必须做如下几件事情
注意:这种方式相对而言不太方便,因此现在的编译器都对SEH结构化异常提供了语法支持,并在其之上做了一定的拓展
编译器支持的SEH
语法:
_try //挂入链表 {//代码 } _except(过滤表达式) //异常过滤 {//异常处理程序 //异常处理 }拆解:
_try //相当于 __asm {mov eax, FS:[0]mov temp, eaxlea ecx, myExceptionmov FS:[0], ecx } _except(过滤表达式) //相当于 if( ExceptionRecord->ExceptionCode == 0xC0000094 ) //除0异常过滤表达式
三个值:
1. EXCEPTION_EXECUTE_HANDLER(1) 执行except代码 2. EXCEPTION_CONTINUE_SEARCH(0) 不处理异常,寻找下一个异常处理函数 3. EXCEPTION_CONTINUE_EXECUTION(-1) 返回出错位置重新执行三种写法:
1)常量
2)表达式
try {//代码 } _except(GetExceptionCode()==0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) {//异常处理程序 }3)调用函数
int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo) {printf("%x", pExceptionInfo->ExceptionRecord->ExceptionCode);printf("%p", pExceptionInfo->ContextRecord->Eip);return EXCEPTION_CONTINUE_EXECUTION; }try {//代码 } _except(ExceptFilter(GetExceptionInformation())) {//异常处理程序 }实验一:理解_try_except
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>void TestException() {_try{}_except(1){} }int main() {TestException();getchar();return 0; }2)查看TestException函数的反汇编
4: void TestException() 5: { 00401020 push ebp 00401021 mov ebp,esp 00401023 push 0FFh 00401025 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) 0040102A push offset __except_handler3 (0040127c) 0040102F mov eax,fs:[00000000] 00401035 push eax 00401036 mov dword ptr fs:[0],esp 0040103D add esp,0FFFFFFB8h 00401040 push ebx 00401041 push esi 00401042 push edi 00401043 mov dword ptr [ebp-18h],esp 00401046 lea edi,[ebp-58h] 00401049 mov ecx,10h 0040104E mov eax,0CCCCCCCCh 00401053 rep stos dword ptr [edi] 6: _try 00401055 mov dword ptr [ebp-4],0 7: { 8: 9: } 0040105C mov dword ptr [ebp-4],0FFFFFFFFh 00401063 jmp $L53962+0Ah (00401075) 10: _except(1) 00401065 mov eax,1 $L53963: 0040106A ret $L53962: 0040106B mov esp,dword ptr [ebp-18h] 11: { 12: 13: } 0040106E mov dword ptr [ebp-4],0FFFFFFFFh 14: } 00401075 mov ecx,dword ptr [ebp-10h] 00401078 mov dword ptr fs:[0],ecx 0040107F pop edi 00401080 pop esi 00401081 pop ebx 00401082 mov esp,ebp 00401084 pop ebp 00401085 ret3)总结
在编译器支持的情况下,当我们写入_try_except程序块时,编译器就会自动在当前堆栈链表中挂入一个结构体,并且在挂入时指定的处理函数时固定的,当我们手动挂入时,处理函数是我们手动编写的
实验二:_try_except 嵌套
描述:当我们手动挂入链表时,若我们需要两个处理函数时,就需要挂入两个处理函数,需要多少个就要挂多少个,而当我们使用编译器提供的这种格式,无论我们嵌套了多少个异常处理,编译器只会生成一个异常处理函数
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>void TestException() {_try{_try{}_except(1){}}_except(1){}_try{}_except(1){} }int main() {TestException();getchar();return 0; }2)查看TestException函数的反汇编
4: void TestException() 5: { 00401020 push ebp 00401021 mov ebp,esp 00401023 push 0FFh 00401025 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) 0040102A push offset __except_handler3 (0040127c) 0040102F mov eax,fs:[00000000] 00401035 push eax 00401036 mov dword ptr fs:[0],esp //只有此处修改了fs,只挂入了一个异常处理函数 0040103D add esp,0FFFFFFB8h 00401040 push ebx 00401041 push esi 00401042 push edi 00401043 mov dword ptr [ebp-18h],esp 00401046 lea edi,[ebp-58h] 00401049 mov ecx,10h 0040104E mov eax,0CCCCCCCCh 00401053 rep stos dword ptr [edi] 6: _try 00401055 mov dword ptr [ebp-4],0 7: { 8: _try 0040105C mov dword ptr [ebp-4],1 9: { 10: 11: } 00401063 mov dword ptr [ebp-4],0 0040106A jmp $L53968+0Ah (0040107c) 12: _except(1) 0040106C mov eax,1 $L53969: 00401071 ret $L53968: 00401072 mov esp,dword ptr [ebp-18h] 13: { 14: 15: } 00401075 mov dword ptr [ebp-4],0 16: } 0040107C mov dword ptr [ebp-4],0FFFFFFFFh 00401083 jmp $L53964+0Ah (00401095) 17: _except(1) 00401085 mov eax,1 $L53965: 0040108A ret $L53964: 0040108B mov esp,dword ptr [ebp-18h] 18: { 19: 20: } 0040108E mov dword ptr [ebp-4],0FFFFFFFFh 21: 22: _try 00401095 mov dword ptr [ebp-4],2 23: { 24: 25: } 0040109C mov dword ptr [ebp-4],0FFFFFFFFh 004010A3 jmp $L53972+0Ah (004010b5) 26: _except(1) 004010A5 mov eax,1 $L53973: 004010AA ret $L53972: 004010AB mov esp,dword ptr [ebp-18h] 27: { 28: 29: } 004010AE mov dword ptr [ebp-4],0FFFFFFFFh 30: } 004010B5 mov ecx,dword ptr [ebp-10h] 004010B8 mov dword ptr fs:[0],ecx 004010BF pop edi 004010C0 pop esi 004010C1 pop ebx 004010C2 mov esp,ebp 004010C4 pop ebp 004010C5 ret3)总结:当我们使用编译器提供的这种格式,在一个函数中,无论我们嵌套了多少个异常处理,编译器只会往FS:[0]中挂入一次,并且指向的异常处理函数也是确定的(递归除外,递归相当于一次次调用了新的函数)
思考:编译器只挂入一次异常处理函数,它如何实现对所有异常进行处理?
答案: 拓展_EXCEPTION_REGISTRATION_RECORD结构体
拓展SEH结构体
原先的SEH结构体
typedef struct _EXCEPTION_REGISTRATION_RECORD{struct _EXCEPTION_REGISTRATION_RECORD *Next;PEXCEPTION_ROUTINE Handler; }EXCEPTION_REGISTRATION_RECORD;拓展后如下(原先的两个成员必须存在)
struct _EXCEPTION_REGISTRATION{struct _EXCEPTION_REGISTRATION *prev;void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);struct scopetable_entry *scopetable;int trylevel;int _ebp; }新的堆栈结构如下:
以实验一的汇编代码为例,该结构体在函数初始化时被压入堆栈
注意:一般情况下,当我们将代码分别编译为debug与release版本时,其汇编代码变化较大(release进行了优化),而当函数中存在_try_except这样的语法时,两者变化较小(保证异常处理语句的堆栈结构)
scopetable
描述:结构体指针,指向了一堆结构体数组
struct scopetable_entry{DWORD previousTryLevel //上一个try{}结构编号PDWRD lpfnFilter //过滤函数的起始地址PDWRD lpfnHandler //异常处理程序的地址 }/* scopetable[0].previousTryLevel = -1; scopetable[0].lpfnFilter = 过滤函数1; scopetable[0].lpfnHandler = 异常处理函数1;scopetable[1].previousTryLevel = -1; scopetable[1].lpfnFilter = 过滤函数2; scopetable[1].lpfnHandler = 异常处理函数2;scopetable[2].previousTryLevel = 1; scopetable[2].lpfnFilter = 过滤函数3; scopetable[2].lpfnHandler = 异常处理函数3; */实验三:理解scopetable
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>int ExceptFilter() {return EXCEPTION_CONTINUE_EXECUTION; }void TestException() {_try{}_except(EXCEPTION_EXECUTE_HANDLER){printf("异常处理函数X\n");}_try{_try{}_except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){printf("异常处理函数Y\n");}}_except(ExceptFilter()){printf("异常处理函数Z\n");} }int main() {TestException();getchar();return 0; }2)观察TestException函数的反汇编
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh 00401055 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) //scopetable 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 mov dword ptr [ebp-18h],esp 00401076 lea edi,[ebp-5Ch] 00401079 mov ecx,11h 0040107E mov eax,0CCCCCCCCh 00401083 rep stos dword ptr [edi] 11: _try 00401085 mov dword ptr [ebp-4],0 12: { 13: 14: } 0040108C mov dword ptr [ebp-4],0FFFFFFFFh 00401093 jmp $L53975+17h (004010b2) 15: _except(EXCEPTION_EXECUTE_HANDLER) 00401095 mov eax,1 $L53976: 0040109A ret $L53975: 0040109B mov esp,dword ptr [ebp-18h] 16: { 17: printf("异常处理函数X\n"); 0040109E push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdX\n" (00422fc8) 004010A3 call printf (0040de70) 004010A8 add esp,4 18: } 004010AB mov dword ptr [ebp-4],0FFFFFFFFh 19: 20: _try 004010B2 mov dword ptr [ebp-4],1 21: { 22: _try 004010B9 mov dword ptr [ebp-4],2 23: { 24: 25: } 004010C0 mov dword ptr [ebp-4],1 004010C7 jmp $L53983+17h (004010fa) 26: _except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) 004010C9 mov eax,dword ptr [ebp-14h] 004010CC mov ecx,dword ptr [eax] 004010CE mov edx,dword ptr [ecx] 004010D0 mov dword ptr [ebp-1Ch],edx 004010D3 mov eax,dword ptr [ebp-1Ch] 004010D6 xor ecx,ecx 004010D8 cmp eax,0C0000094h 004010DD sete cl 004010E0 mov eax,ecx $L53984: 004010E2 ret $L53983: 004010E3 mov esp,dword ptr [ebp-18h] 27: { 28: printf("异常处理函数Y\n"); 004010E6 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdY\n" (00422fb8) 004010EB call printf (0040de70) 004010F0 add esp,4 29: } 004010F3 mov dword ptr [ebp-4],1 30: } 004010FA mov dword ptr [ebp-4],0FFFFFFFFh 00401101 jmp $L53979+17h (00401120) 31: _except(ExceptFilter()) 00401103 call @ILT+10(ExceptFilter) (0040100f) $L53980: 00401108 ret $L53979: 00401109 mov esp,dword ptr [ebp-18h] 32: { 33: printf("异常处理函数Z\n"); 0040110C push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdZ\n" (00422fa8) 00401111 call printf (0040de70) 00401116 add esp,4 34: } 00401119 mov dword ptr [ebp-4],0FFFFFFFFh 35: } 00401120 mov ecx,dword ptr [ebp-10h] 00401123 mov dword ptr fs:[0],ecx 0040112A pop edi 0040112B pop esi 0040112C pop ebx 0040112D add esp,5Ch 00401130 cmp ebp,esp 00401132 call __chkesp (00401690) 00401137 mov esp,ebp 00401139 pop ebp 0040113A ret3)查看scopetable内存
//scopetable_entry X 00422020 FF FF FF FF .... //不存在上级异常处理 00422024 95 10 40 00 ..@. 00422028 9B 10 40 00 ..@. //scopetable_entry Z 0042202C FF FF FF FF .... //不存在上级异常处理 00422030 03 11 40 00 ..@. 00422034 09 11 40 00 .@. //scopetable_entry Y 00422038 01 00 00 00 .... //存在一个上级异常处理 0042203C C9 10 40 00 ..@. 00422040 E3 10 40 00思考:如何判断当前需要调用第几个scopetable_entry?
答案:根据trylevel的值进行判断
trylevel
描述:标识当前代码处于第几个try中,从而调用对应的scopetable_entry结构体中的函数,初始值为-1(0xff)
实验四:理解trylevel
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>int ExceptFilter() {return EXCEPTION_CONTINUE_EXECUTION; }void TestException() {_try{_try{}_except(EXCEPTION_EXECUTE_HANDLER){printf("异常处理函数\n");}}_except(EXCEPTION_EXECUTE_HANDLER){printf("异常处理函数X\n");}_try{_try{}_except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){printf("异常处理函数Y\n");}}_except(ExceptFilter()){printf("异常处理函数Z\n");} }int main() {TestException();getchar();return 0; }2)查看TestException函数的反汇编
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh //trylevel,初始值为-1,位于[ebp-4] 00401055 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n"+10h (00423018) 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 mov dword ptr [ebp-18h],esp 00401076 lea edi,[ebp-5Ch] 00401079 mov ecx,11h 0040107E mov eax,0CCCCCCCCh 00401083 rep stos dword ptr [edi] 11: _try 00401085 mov dword ptr [ebp-4],0 //修改trylevel为0 12: { 13: _try 0040108C mov dword ptr [ebp-4],1 //修改trylevel为1 14: { 15: 16: } 00401093 mov dword ptr [ebp-4],0 //恢复到0 0040109A jmp $L53982+17h (004010b9) 17: _except(EXCEPTION_EXECUTE_HANDLER) 0040109C mov eax,1 $L53983: 004010A1 ret $L53982: 004010A2 mov esp,dword ptr [ebp-18h] 18: { 19: printf("异常处理函数\n"); 004010A5 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n" (00423008) 004010AA call printf (0040de70) 004010AF add esp,4 20: } 004010B2 mov dword ptr [ebp-4],0 //恢复到0 21: } 004010B9 mov dword ptr [ebp-4],0FFFFFFFFh 004010C0 jmp $L53978+17h (004010df) 22: _except(EXCEPTION_EXECUTE_HANDLER) 004010C2 mov eax,1 $L53979: 004010C7 ret $L53978: 004010C8 mov esp,dword ptr [ebp-18h] 23: { 24: printf("异常处理函数X\n"); 004010CB push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdX\n" (00422fc8) 004010D0 call printf (0040de70) 004010D5 add esp,4 25: } 004010D8 mov dword ptr [ebp-4],0FFFFFFFFh //恢复到-1 26: 27: _try 004010DF mov dword ptr [ebp-4],2 //修改trylevel为2 28: { 29: _try 004010E6 mov dword ptr [ebp-4],3 //修改trylevel为3 30: { 31: 32: } 004010ED mov dword ptr [ebp-4],2 //恢复到2 004010F4 jmp $L53990+17h (00401127) 33: _except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) 004010F6 mov eax,dword ptr [ebp-14h] 004010F9 mov ecx,dword ptr [eax] 004010FB mov edx,dword ptr [ecx] 004010FD mov dword ptr [ebp-1Ch],edx 00401100 mov eax,dword ptr [ebp-1Ch] 00401103 xor ecx,ecx 00401105 cmp eax,0C0000094h 0040110A sete cl 0040110D mov eax,ecx $L53991: 0040110F ret $L53990: 00401110 mov esp,dword ptr [ebp-18h] 34: { 35: printf("异常处理函数Y\n"); 00401113 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdY\n" (00422fb8) 00401118 call printf (0040de70) 0040111D add esp,4 36: } 00401120 mov dword ptr [ebp-4],2 //恢复到2 37: } 00401127 mov dword ptr [ebp-4],0FFFFFFFFh //恢复到-1 0040112E jmp $L53986+17h (0040114d) 38: _except(ExceptFilter()) 00401130 call @ILT+10(ExceptFilter) (0040100f) $L53987: 00401135 ret $L53986: 00401136 mov esp,dword ptr [ebp-18h] 39: { 40: printf("异常处理函数Z\n"); 00401139 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdZ\n" (00422fa8) 0040113E call printf (0040de70) 00401143 add esp,4 41: } 00401146 mov dword ptr [ebp-4],0FFFFFFFFh //恢复到-1 42: } 0040114D mov ecx,dword ptr [ebp-10h] 00401150 mov dword ptr fs:[0],ecx 00401157 pop edi 00401158 pop esi 00401159 pop ebx 0040115A add esp,5Ch 0040115D cmp ebp,esp 0040115F call __chkesp (00401690) 00401164 mov esp,ebp 00401166 pop ebp 00401167 ret总结
异常处理整体流程:
CPU检测到异常 ↓ 查中断表执行处理函数 ↓ CommonDispatchException //存储异常相关信息 ↓ KiDispatchException //异常分发处理函数,判断0环异常还是3环异常//若为3环,修正EIP,指向KiUserExceptionDispatcher ↓ KiUserExceptionDispatcher //通过RtlDispatchException查找异常处理函数位置,VEH/SEH ↓ RtlDispatchException //先查找VEH,若找不到,查找SEH(从FS:[0]开始遍历) ↓ VEH/SEH_except_handler3执行过程:
1)根据trylevel选择scopetable数组
2)调用scopetable数组中对应的lpfnFilter函数
4)如果lpfnFilter函数返回0,向上遍历,直到previousTryLevel = -1
__try__finally
描述:即使try中的代码不会产生异常,finally中的代码一定会执行
语法格式:
实验五:理解__try__finally
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>void TestException() {for(int i=0; i<10; i++){__try{//continue;//break;//return;printf("其他代码\n");}__finally{printf("一定会执行的代码\n");}} }int main() {TestException();getchar();return 0; }2)取消continue注释,查看执行结果
3)取消break注释,查看执行结果
4)取消return注释,查看执行结果
局部展开
展开时机:当__try __finally中__try代码提前流程代码块时会产生比如:Continue、Break、Return等
实验六:理解局部展开
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>void TestException() {for(int i=0; i<10; i++){__try{return;printf("其他代码\n");}__finally{printf("一定会执行的代码\n");}} }int main() {TestException();getchar();return 0; }2)查看TestException函数的汇编代码
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh 00401055 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n"+10h (00423018) 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 lea edi,[ebp-5Ch] 00401076 mov ecx,11h 0040107B mov eax,0CCCCCCCCh 00401080 rep stos dword ptr [edi] 11: for(int i=0; i<10; i++) 00401082 mov dword ptr [ebp-1Ch],0 00401089 jmp TestException+44h (00401094) 0040108B mov eax,dword ptr [ebp-1Ch] 0040108E add eax,1 00401091 mov dword ptr [ebp-1Ch],eax 00401094 cmp dword ptr [ebp-1Ch],0Ah 00401098 jge $L53977+2 (004010c1) 12: { 13: __try 0040109A mov dword ptr [ebp-4],0 004010A1 push 0FFh 14: { 004010A3 lea ecx,[ebp-10h] 004010A6 push ecx 004010A7 call __local_unwind2 (004011c6) //局部展开,就是这个函数找到了finally中的代码//需要中途从__try中出来时由编译器生成 004010AC add esp,8 15: return; 004010AF jmp $L53977+2 (004010c1) //跳转至退出函数前的准备工作 16: printf("其他代码\n"); 17: } 18: __finally 19: { 20: printf("一定会执行的代码\n"); 004010B1 push offset string "\xd2\xbb\xb6\xa8\xbb\xe1\xd6\xb4\xd0\xd0\xb5\xc4\xb4\xfa\xc2\xeb\n" (0042 004010B6 call printf (0040de70) 004010BB add esp,4 $L53975: 004010BE ret 21: } 22: } 004010BF jmp TestException+3Bh (0040108b) 23: } 004010C1 mov ecx,dword ptr [ebp-10h] 004010C4 mov dword ptr fs:[0],ecx 004010CB pop edi 004010CC pop esi 004010CD pop ebx 004010CE add esp,5Ch 004010D1 cmp ebp,esp 004010D3 call __chkesp (00401690) 004010D8 mov esp,ebp 004010DA pop ebp 004010DB ret3)观察scopetable,可以发现异常过滤代码为0,局部展开函数由此判断except与finally
4)跟进__local_unwind2
5)执行结果
全局展开
展开时机:每次执行__except代码之前,会重新从异常发生位置遍历一次__finally,如果存在则依次调用局部展开函数
实验七:理解全局展开
1)编译并运行以下代码
#include <stdio.h> #include <windows.h>void TestException() {__try{__try{__try{*(int*)0 = 1;}__finally{printf("一定会执行的代码A\n");}}__finally{printf("一定会执行的代码B\n");}}__except(1){printf("异常处理函数\n");} }int main() {TestException();getchar();return 0; }2)执行结果
总结
以上是生活随笔为你收集整理的Windows异常学习笔记(四)—— 编译器扩展SEH的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Windows异常学习笔记(二)—— 内
- 下一篇: Windows异常学习笔记(五)—— 未