-
-
[原创]Windows内核学习笔记之异常(下)
-
2021-12-31 21:24 13629
-
五.被编译器扩展的SEH
1.概览
在实际开发中,直接通过手动在栈中加入结构化异常处理器的方式存在以下两点明显不足:
需要编写符合SehHandler函数原型的处理函数,要理解_CONTEXT等较复杂的数据结构和概念
因为需要直接操作栈指针,所以无法将登记与销毁异常处理器的代码封装到一个普通的C/C++函数中。这就需要在每个需要保护的程序块前后插入两点嵌入式汇编代码,这会影响程序的简洁性
为了解决上述的问题,编译器对该机制进行了优化,主要完成以下两项任务:
定义必要的关键字来表示异常处理逻辑,供编程人员使用。比如,VC编译器定义了try,except和__finally这3个扩展关键字,运行C和C++程序使用这套关键字来编写异常处理代码
实现对以上关键字的编译,将使用这些关键字编写的异常处理代码与操作系统的SEH机制衔接起来
2.语法
VC编译器优化以后的SEH为程序员提供了如下两种功能:
异常处理功能:用于接收和处理被保护块中的代码所发生的异常
终结处理功能:保证终结处理块始终可以得到执行
A.异常处理
异常处理的语法结构如下:
1 2 3 4 5 6 7 8 | __try { / / 被保护体,也就是要保护的代码块 } __except(过滤表达式) { / / 异常处理块(exception - handling block) } |
除了try和except关键字以外,Visual C++编译器还提供了如下两个宏来辅助编写异常处理代码:
DWORD GetExceptionCode():返回异常代码。只能在过滤表达式或异常处理块中使用这个宏
LPEXCEPTION_POINTERS GetExceptionInformation():返回一个指向EXCEPTION_POINTERS结构的指针。只能在过滤表达式中使用这个宏
过滤表达式既可以是常量,函数调用,也可以是用条件表达式或其他表达式,只要表达式的结果为0,1,-1这三个值之一,它们的含义如下:
值 | 名称 | 含义 |
---|---|---|
0 | EXCEPTION_CONTINUE_SEARCH | 本保护块不处理该异常,让系统继续寻找其他异常保护块 |
-1 | EXCEPTION_CONTINUE_EXECUTION | 已经处理异常,让程序回到异常发生点继续执行,如果导致异常的情况没有被消除,那么可能还会再次发生异常 |
1 | EXCEPTION_EXECUTE_HANDLER | 这是本保护块预计到的异常,让系统执行本块中的异常处理代码,执行完后会继续执行本异常处理块下面的代码,即except块之后的第一条指令 |
以下是常见的三种编写过滤表达式的方法:
直接使用常量:比如except(EXCEPTION_EXECUTE_HANDLER),或者直接写为except(-1)等
使用条件运算符:比如__except(GetExceptionCode == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)。其含义是,如果发生的异常是非法访问异常,那么久执行异常处理块;否则,就继续搜索其他异常保护块
调用其他函数,通常将GetExceptionCode()得到的异常代码或GetExceptionInformation()得到的异常信息作为参数传给该函数。例如__except(ExcptFilter(GetExceptionInformation()))
可见,可以根据具体需要设计不同复杂度的表达式,可短到一个常熟,可长到编写过滤函数进行一系列操作。
以下代码是对于上述内容的样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #include <cstdio> #include <Windows.h> DWORD x, y, z; int ExceptFilter(LPEXCEPTION_POINTERS pException) { / / 如果是除 0 异常 if (pException - >ExceptionRecord - >ExceptionCode = = EXCEPTION_INT_DIVIDE_BY_ZERO) { y = 10 ; printf( "ExceptFilter函数执行,异常已被处理\n" ); return EXCEPTION_CONTINUE_EXECUTION; / / 继续执行出错的代码 } return EXCEPTION_CONTINUE_SEARCH; / / 该异常块不处理 } void test1() { __try { z = x / y; printf( "在try中输出z的值为:%d\n" , z); } __except (ExceptFilter(GetExceptionInformation())) { printf( "test1的 except块被执行\n" ); } } void test2() { __try { z = x / y; } / / 判断是否为除 0 错误,如果是执行错误处理代码,否则不执行 __except (GetExceptionCode() = = EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { y = 10 ; z = x / y; printf( "test2的except被执行,错误已被处理, z的值为:%d\n" , z); } } int main() { x = 1900 , y = 0 , z = 0 ; test1(); x = 1900 , y = 0 , z = 0 ; test2(); return 0 ; } |
程序的运行结果如下:
B.终结处理
终结处理的语法结构如下:
1 2 3 4 5 6 7 8 | __try { / / 被保护体(guarded body),也就是要保护的代码块 } __finally { / / 终结代码块 } |
终结处理由两部分构造,使用try关键字定义的被保护体和使用_finally关键字定义的终结处理块。终结处理的目标是只要被保护体被执行,那么终结处理块就也会被执行,除非被保护体中的代码终止了当前线程(比如调用ExitThread或ExitProcess退出线程或整个进程)。因为终结处理块的这种特征,终结处理非常适合做状态恢复或资源释放等工作。比如释放被保护块中获得的信号量以防止被保护块内发生意外时因没有释放这个信号量而导致线程死锁的问题。
根据被保护块的执行路线,SEH把被保护块的退出(执行完毕)分为正常结束和非正常结束两种。如果被保护块得到自然执行并顺序进入终结处理块,就认为被保护块是正常结束的。如果被保护块是因为发生异常或由于return, goto, break或continue等流程控制语句离开被保护块的,就认为被保护块是非正常结束的。在终结处理块中可以调用以下函数来知道被保护块的退出方式:
1 | BOOL AbnormalTermination(void); |
如果被保护块正常结束,那么该函数返回FALSE;否则,返回TRUE。该函数只能在终结块中调用。
除了上面出现的try和finally关键字,终结处理还有一个关键字leave。该关键字的作用是立即离开(停止执行)被保护块,或者理解为立即跳转到被保护块的末尾(try块的右大括号)。__leave关键字只能出现在被保护体中,使用该关键字的退出属于正常退出。
以下代码是上述内容的简单样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <cstdio> #include <windows.h> int main() { __try { printf( "开始执行__try中的代码\n" ); printf( "执行__leave之前的代码\n" ); __leave; printf( "执行__leave之后的代码\n" ); } __finally { printf( "执行__finally的代码\n" ); } return 0 ; } |
输出如下:
3.SEH的编译
A.编译器的扩展
不同的编译器对SEH的编译优化结果稍有不同,对于Visual Studio2017来说,其主要做了以下两个方面的修改:
编译器编译try{}catch()结构时,总是将统一的__except_handler3登记为异常处理函数。这样做有利于代码复用和减少目标文件的大小
为了使用统一的异常处理函数(excepthandler3)来满足不同SEH块的需求。在栈上准备EXCEPTION_REGISTRATION_RECORD结构前,编译器产生的代码会压入一个trylevel的整数和一个执行scopetable_entry结构的scopetable指针
总之,编译器会将EXCEPTION_REGISTRATION_RECORD结构扩展为如下的形式:
1 2 3 4 5 6 7 8 | struct _EXCEPTION_REGISTRATION { struct _EXCEPTION_REGISTRATION * prev; void ( * handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD); struct scopetable_entry * scopetable; int trylevel; int _ebp; }; |
字段 | 作用 |
---|---|
prev | 指向上一个结构体的地址 |
handler | 指向异常处理函数 |
scopetable | 范围表的起始地址 |
trylevel | 这个结构对应的__try块的编号 |
ebp | 栈帧的基地址 |
其中前两个字段是操作系统规定的标准登记结构,后三个字段是编译器扩展的。登记处理函数整数依靠这几个扩展字段来寻找过滤表达式和异常处理块。
B.范围表
为了描述应用程序代码中的tryexcept结构,编译器在编译每个使用此结构的函数时会为其建立一个数组,并存储在模块文件的数据区(通常称为异常处理范围表)中。数组的每个元素是一个scopetable_entry结构,用来描述一个tryexcept结构。
1 2 3 4 5 6 | struct scopetable_entry { DWORD previousTryLevel / / 上一个 try {}结构编号 PARPROC lpfnFilter / / 过滤函数的起始地址 PARPROC lpfnHandler / / 异常处理程序的地址 }; |
其中lpfnFilter和lpfnHandler分别用来描述try{}except结构的过滤表达式和异常处理块的起始地址。
C.TryLevel
编译器是以函数为单位来登记异常处理器的,在函数的入口处进行登记,在出口处进行注销。为了确定导致异常的代码是否在保护块中,以及,如果在有多个保护块的情况下,判断属于哪个保护块。编译器为每个try结构进行编号,然后使用一个局部变量来记录当前处于哪个try结构中,这个局部变量称为trylevel,也就是在栈上形成的异常处理结构的trylevel字段。
编号从0开始,常量TRYLEVEL_NONE(-1)作为特殊值代表不再任何try结构中。也就是说,trylevel变量被初始化为-1,然后执行到_try结构中时,便将它的编号赋给TryLevel变量。
D.tryexcept()结构的执行
当位于__try{}结构保护块中的代码发生异常时,异常分发函数便会调用_except_handler3这样的处理函数。_except_handler3函数所指向的主要操作如下:
将第二个参数pRegistrationRecord从系统默认的EXCEPTION_REGISTRATION_RECORD结构强制转换为包含扩展字段的_EXCEPTION_REGISTRATION结构
先从pRegistrationRecord结构中取出trylevel字段的值并且赋给一个局部变量nTryLevel ,然后根据nTryLevel的值从scopetable字段所 指定的数组中找到一个scopetable_entry结构
从scopetable_entry结构中取出lpfnFilter字段,如果不为空,则调用这个函数,即评估过滤表达式,如果为空,则跳到第5步
如果lpfnFilter函数的返回值不等于EXCEPTION_CONTINUE_SEARCH,则准备指向lpfnHanlder字段所指定的函数,并且不再返回。如果过滤表达式返回的是EXCEPTION_CONTINUE_SEARCH,则自然进入(fall through)第五步
判断scopetable_entry结构的previousTryLevel字段的取值。如果它不等于-1,则将previousTryLevel赋给nTryLevel并返回第2步继续循环;如果previousTryLevel等于-1,那么继续第6步
返回DISPOSITION_CONTINUE_SEARCH,让系统(RtlDispatchException)继续寻找其他异常处理器
以以下代码为例,体会上述内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | DWORD x = 1900 , y = 0 , z = 0 ; void test() { __try { __try { z = x / y; } __except (EXCEPTION_CONTINUE_SEARCH) { } } __except (GetExceptionCode() = = EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { y = 10 ; z = x / y; printf( "z = %d\n" , z); } } |
以下是对test函数的反汇编结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | void test() { 00401000 push ebp / / 填充ebp 00401001 mov ebp,esp 00401003 push 0FFFFFFFFh / / 填充trylevel为 - 1 00401005 push 48ADD8h / / 填充scopetable范围表的起始地址 0040100A push offset _except_handler3 ( 0402968h ) / / 填充异常处理函数 0040100F mov eax,dword ptr fs:[ 00000000h ] 00401015 push eax / / 填充prev 00401016 mov dword ptr fs:[ 0 ],esp / / 将异常处理器挂到fs:[ 0 ]中 0040101D add esp, 0FFFFFFB0h 00401020 push ebx 00401021 push esi 00401022 push edi 00401023 mov dword ptr [ebp - 18h ],esp 00401026 mov ecx,offset _D3472036_test@cpp ( 048F012h ) 0040102B call __CheckForDebuggerJustMyCode ( 04011D0h ) __try 00401030 mov dword ptr [ebp - 4 ], 0 / / 进入 try 块,将trylevel赋值为 0 { __try 00401037 mov dword ptr [ebp - 4 ], 1 / / 进入 try 块,将trylevel赋值为 1 { z = x / y; 0040103E mov eax,dword ptr [x ( 048D000h )] / / 执行 try 块代码 00401043 xor edx,edx 00401045 div eax,dword ptr [y ( 048D910h )] 0040104B mov dword ptr [z ( 048D914h )],eax } 00401050 mov dword ptr [ebp - 4 ], 0 / / 离开 try 块,将trylevel赋值为 0 00401057 jmp test + 66h ( 0401066h ) / / 执行跳转,略过 except 块中内容 __except (EXCEPTION_CONTINUE_SEARCH) 00401059 xor eax,eax / / 对过滤表达式进行运算,这里返回值是 0 (EXCEPTION_CONTINUE_SEARCH) $LN16: 0040105B ret $LN13: 0040105C mov esp,dword ptr [ebp - 18h ] } 0040105F mov dword ptr [ebp - 4 ], 0 / / 离开 try 块,将trylevel赋值为 0 { } } 00401066 mov dword ptr [ebp - 4 ], 0FFFFFFFFh / / 离开 try 块,将trylevel赋值为 - 1 0040106D jmp $LN9 + 39h ( 04010CFh ) / / 执行跳转,略过 except 块中内容 __except (GetExceptionCode() = = EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) 0040106F mov eax,dword ptr [ebp - 14h ] 00401072 mov ecx,dword ptr [eax] 00401074 mov edx,dword ptr [ecx] 00401076 mov dword ptr [ebp - 5Ch ],edx 00401079 cmp dword ptr [ebp - 5Ch ], 0C0000094h 00401080 jne test + 8Bh ( 040108Bh ) 00401082 mov dword ptr [ebp - 60h ], 1 00401089 jmp test + 92h ( 0401092h ) 0040108B mov dword ptr [ebp - 60h ], 0 00401092 mov eax,dword ptr [ebp - 60h ] / / 对过滤表达式进行运算, 这里返回值是 1 (EXCEPTION_EXECUTE_HANDLER) $LN17: 00401095 ret $LN9: 00401096 mov esp,dword ptr [ebp - 18h ] { y = 10 ; 00401099 mov dword ptr [y ( 048D910h )], 0Ah / / 执行异常处理代码 z = x / y; 004010A3 mov eax,dword ptr [x ( 048D000h )] 004010A8 xor edx,edx 004010AA div eax,dword ptr [y ( 048D910h )] 004010B0 mov dword ptr [z ( 048D914h )],eax printf( "z = %d\n" , z); 004010B5 mov eax,dword ptr [z ( 048D914h )] 004010BA push eax 004010BB push offset string "z = %d\n" ( 04701B0h ) 004010C0 call printf ( 0401180h ) 004010C5 add esp, 8 { } } 004010C8 mov dword ptr [ebp - 4 ], 0FFFFFFFFh / / 离开 try 块,将trylevel赋值为 - 1 } } 004010CF mov ecx,dword ptr [ebp - 10h ] 004010D2 mov dword ptr fs:[ 0 ],ecx / / 将异常处理器从fs:[ 0 ]中摘除 004010D9 pop edi 004010DA pop esi 004010DB pop ebx 004010DC mov esp,ebp 004010DE pop ebp 004010DF ret |
根据上面的代码,可以得出如下结论:
无论是多少个tryexcept结构,在函数中只挂载了一个异常处理器
无论是当进入一个try块,还是离开一个try块,都会修改trylevel的值,来代表目前处于哪个try结构中
try块,条件过滤表达式, except块的代码是按顺序保存下来,但是每个块后面都会有跳转语句,避免其顺序执行(jmp, ret)
此时查看scopetable数组的内容:
1 2 3 4 5 6 | 0x0048ADD8 ff ff ff ff / / 第一个 try 块的previoustrylevel 0x0048ADDC 6f 10 40 00 / / 第一个 try 块的条件过滤表达式地址 0x0048ADE0 96 10 40 00 / / 第一个 try 块的异常处理块地址 0x0048ADE4 00 00 00 00 / / 第二个 try 块的previoustrylevel 0x0048ADE8 59 10 40 00 / / 第二个 try 块的条件过滤表达式的地址 0x0048ADEC 5c 10 40 00 / / 第二个 try 块的异常处理块地址 |
可以看到数组中的每个元素都对应了try块的信息,这样就可以让异常处理函数根据每个数组元素保存的数据来找到相应的处理函数。
4.栈展开
相比于tryexcept结构的异常处理,tryfinally结构的终结处理是为了保证finally块中的代码可以执行。比如以下的代码,无论是否将出现异常的代码注释掉,finally块的代码都会被执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | DWORD x = 1900 , y = 0 , z = 0 ; void test() { __try { printf( "try块执行\n" ); / / z = x / y; } __finally { printf( "finally代码执行\n" ); } } |
根据以下的反汇编代码可以知道,当使用tryfinally结构的时候,程序会在离开try块之前的代码加入一个call指令,该call指令的地址就是finally块的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | void test() { 004010B0 push ebp 004010B1 mov ebp,esp 004010B3 push 0FFFFFFFFh 004010B5 push 48AE08h 004010BA push offset _except_handler3 ( 04029A8h ) 004010BF mov eax,dword ptr fs:[ 00000000h ] 004010C5 push eax 004010C6 mov dword ptr fs:[ 0 ],esp 004010CD add esp, 0FFFFFFB8h 004010D0 push ebx 004010D1 push esi 004010D2 push edi 004010D3 mov ecx,offset _D3472036_test@cpp ( 048F012h ) 004010D8 call __CheckForDebuggerJustMyCode ( 0401210h ) __try 004010DD mov dword ptr [ebp - 4 ], 0 / / 进入 try 块,trylevel赋值为 0 { printf( "try块执行\n" ); 004010E4 push offset string "try\xbf\xe9\xd6\xb4\xd0\xd0\n" ( 04701B0h ) 004010E9 call printf ( 04011C0h ) 004010EE add esp, 4 / / z = x / y; } 004010F1 mov dword ptr [ebp - 4 ], 0FFFFFFFFh / / 离开 try 块,trylevel赋值为 - 1 004010F8 call test + 4Fh ( 04010FFh ) / / 调用 finally 块中的代码 004010FD jmp $LN8 ( 040110Dh ) __finally { printf( "finally代码执行\n" ); 004010FF push offset string "finally\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" ( 04701BCh ) 00401104 call printf ( 04011C0h ) 00401109 add esp, 4 $LN9: 0040110C ret } } 0040110D mov ecx,dword ptr [ebp - 10h ] } } 00401110 mov dword ptr fs:[ 0 ],ecx 00401117 pop edi 00401118 pop esi 00401119 pop ebx 0040111A mov esp,ebp 0040111C pop ebp 0040111D ret |
但是出现异常的时候,是无法执行离开try块的代码的。而此时之所以finally块的代码会被执行,就依赖于栈展开机制。
此时查看scopetable数组的值,可以看到此时的注册表达式的起始地址为0,异常处理块的起始地址变为finally块的起始地址。此时异常处理函数在执行代码时,根据注册表达式的起始地址为0得知,此时需要将第三个参数作为finally块的地址进行调用。
1 2 3 | 0x0048AE08 ff ff ff ff 0x0048AE0C 00 00 00 00 0x0048AE10 ff 10 40 00 / / finally 块的地址 |
而对于以下的代码,程序在输出异常处理块的内容之前会先输出finally块中的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | DWORD x = 1900 , y = 0 , z = 0 ; void test() { __try { __try { z = x / y; } __finally { printf( "finally代码执行\n" ); } } __except (EXCEPTION_EXECUTE_HANDLER) { printf( "except代码执行\n" ); } } |
代码的反汇编结果就是异常处理块和终结处理块的组合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | void test() { 00401000 push ebp 00401001 mov ebp,esp 00401003 push 0FFFFFFFFh 00401005 push 48ADF8h 0040100A push offset _except_handler3 ( 0402938h ) 0040100F mov eax,dword ptr fs:[ 00000000h ] 00401015 push eax 00401016 mov dword ptr fs:[ 0 ],esp 0040101D add esp, 0FFFFFFB8h 00401020 push ebx 00401021 push esi 00401022 push edi 00401023 mov dword ptr [ebp - 18h ],esp 00401026 mov ecx,offset _D3472036_test@cpp ( 048F012h ) 0040102B call __CheckForDebuggerJustMyCode ( 04011A0h ) __try 00401030 mov dword ptr [ebp - 4 ], 0 / / 进入 try 块,将trylevel赋值为 0 { __try 00401037 mov dword ptr [ebp - 4 ], 1 / / 进入 try 块,将trylevel赋值为 1 { z = x / y; 0040103E mov eax,dword ptr [x ( 048D000h )] / / try 块中的代码 00401043 xor edx,edx 00401045 div eax,dword ptr [y ( 048D910h )] 0040104B mov dword ptr [z ( 048D914h )],eax } 00401050 mov dword ptr [ebp - 4 ], 0 / / 离开 try 块,将trylevel赋值为 0 00401057 call test + 5Eh ( 040105Eh ) / / 执行 finally 块中的代码 0040105C jmp test + 6Ch ( 040106Ch ) __finally { printf( "finally代码执行\n" ); 0040105E push offset string "finally\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" ( 04701B0h ) / / finally 块中的代码 00401063 call printf ( 0401150h ) 00401068 add esp, 4 0040106B ret } } 0040106C mov dword ptr [ebp - 4 ], 0FFFFFFFFh 00401073 jmp $LN7 + 17h ( 0401090h ) __except (EXCEPTION_EXECUTE_HANDLER) 00401075 mov eax, 1 / / 条件过滤表达式 00401078 ret 00401079 mov esp,dword ptr [ebp - 18h ] { printf( "except代码执行\n" ); / / except 块中的内容 0040107C push offset string "except\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" ( 04701C4h ) 00401081 call printf ( 0401150h ) 00401086 add esp, 4 } } 00401089 mov dword ptr [ebp - 4 ], 0FFFFFFFFh } } 00401090 mov ecx,dword ptr [ebp - 10h ] 00401093 mov dword ptr fs:[ 0 ],ecx 0040109A pop edi 0040109B pop esi 0040109C pop ebx 0040109D mov esp,ebp 0040109F pop ebp 004010A0 ret |
此时查看socpetable数组的值,可以看到外层try块的元素内容没有变化,而内层try块的条件过滤表达式变为0,异常处理块地址也变成了finally块的地址。因此,异常处理函数在执行的时候,就可以根据条件过滤表达式是否为0来判断要不要执行第三个成员保存的函数地址。
1 2 3 4 5 6 | 0x0048ADF8 ff ff ff ff / / 第一个块的previoustrylevel 0x0048ADFC 75 10 40 00 / / 第一个块条件过滤表达式地址 0x0048AE00 79 10 40 00 / / 第一个块异常处理地址 0x0048AE04 00 00 00 00 / / 第二个块的previoustrylevel 0x0048AE08 00 00 00 00 / / 第二个块条件过滤表达式 0x0048AE0C 5e 10 40 00 / / 第二个块 finally 块的地址 |
异常处理函数就通过这种方式实现了在执行except块的代码之前先执行finally块的代码,结果如下:
六.未处理异常
一个新建进程的起始地址其实并不是PE文件中的AddressOfEntryPoint字段的偏移地址,而是kernel32.dll中BaseProcessStart函数,该函数的伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void BaseProcessStart(PPROCESS_START_ROUTINE lpfnEntryPoint) { __try { NtSetInformationThread(GetCurrentThread(), ThreadQuerySetWin32StartAddress, &lpfnEntryPoint, sizeof(lpfnEntryPoint)); ExitThread((lpfnEntryPoint)()); } __except(UnhandledExceptionFilter(GetExceptionInformation())) { ExitThread(GetExceptionCode()); } } |
根据以上伪代码可以知道,在应用程序的入口函数被调用前,BaseProcessStart会为其先设置一个结构化异常处理器,它是初始线程中最早注册的异常处理器。因为当分发异常时,系统是从最晚注册的异常处理器来查找的,所以这个最早注册的结构化异常处理器是最后得到处理机会的。这样便保证了只有当应用程序自己设计的代码没有处理异常时,这个默认的结构化异常处理器才会得到处理机会。
以上代码中的lpfnEntryPoint指向的就是入口函数。在try块中,根据函数的返回值作为参数来调用ExitThread函数退出线程。而如果执行到了异常处理块中,则会将GetExceptionCode函数的返回值作为参数调用ExitThread函数。
当程序出现异常的时候,是否要执行异常块中的代码则是由函数UnhandledExceptionFilter决定的,该函数的执行流程如下:
通过NtQueryInformationProcess查询当前进程是否被调试。如果正在被调试,函数将会返回EXCEPTION_CONTINUE_SEARCH,随后调用ZwRaiseException来进行第二次异常分发
如果当前进程没有被调试:
查询是否通过SetUnhandlerExceptionFilter注册了处理函数,如果已经注册了,那么就调用已注册的处理函数
如果没有注册处理函数,就会弹出窗口让用户选择是终止程序还是启动即时调试器。如果用户启动了即时调试器,接下来就会由调试器来接管进程,函数就会返回EXCEPTION_EXECUTION_HANDLER
七.参考资料
《软件调试(第二版)》卷一
《软件调试(第二版)》卷二
[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法