SEH学习笔记一
2010-12-15 20:19 curer 阅读(3692) 评论(3) 编辑 收藏 举报SEH(structured exception handling),结构化异常处理。在windows本身开发中运用的非常广泛,而且MS并没有独享,并且通过vs为开发者提供了方便几个关键字来支持。__try, __exception,__finally。但是讲解的却非常少。本文希望能够给大家抛砖引玉一下。
http://www.microsoft.com/msj/0197/exception/exception.aspx,这篇是理解SEH必须的文章,虽然他的时间悠久,但是却真正的解释了SEH的编译器级实现,下面的一些示例代码也来自这里。
相关的不错的SEH文章,http://www.woodmann.com/crackz/Tutorials/Seh.htm。
http://blogs.msdn.com/b/cbrumme/archive/2003/10/01/51524.aspx 这里讲了一些.net 异常机制,之前讲一些SEH也很不错。
SEH中,在《windows 核心编程》中有一些讲解,但是我相信绝大多数,想我这样的初学者,并不能理解Jeffrey Richter的意思。其中最富有争议的就是“栈展开”(stack unwind),这个可以说是非常有想象力的一个词,伴随这个还有全局展开(global unwind),和局部展开(local unwind)。以下内容,主要围绕《windows 核心编程》中比较容易让我这样的初学者困惑的地方展开(unwind? :P)。
首先我们需要对SEH有一个大体的认识,
- 当异常出现的时候,我们可以有选择性的处理异常,将相同的异常处理函数集中一起,大大减少了代码的维护工作,这意味着处理异常的时候,将有类似非局部跳转的能力。
- 异常和返回值判断的最根本的不同是,异常真正的做到了健壮性,甚至连栈溢出的问题都可以恢复运行(当然,这个恢复没有任何意义,主要是能够保存错误信息)。所以异常是和操作系统结合的,所以必然导致了复杂性的大大提高,效率上的降低。
程序的执行,需要一些最基本的运行环境,而在windows 中则是contex,(上下文),其中保存了大量的寄存器的值,而通过这些可以保证程序的执行环境正确,而这是在进行非局部跳转必须做到的事情。所以,在遇到__try block的时候,编译器会在栈空间上保存一些信息,做为一个结点并将这些信息用链表联系起来,这样,当异常发生的时候,操作系统找到链表的头结点,然后遍历list,执行我们的代码,并找到相对应的处理异常的代码。而这个头结点,就保存在FS:[0]。当windows 遍历list,并找到相对应的代码时,由于程序控制流程的改变,在发生异常,到找到执行代码的这部分之间的一些临时变量都没有被释放掉(这里面不仅有我们的,还有一些是编译器默默为我们做的,比如之前提到的__try所加入的节点必须从之前的list删掉)。而这个做的释放过程就是unwind。处理多个__try的为global unwind,处理当前的__try 上的__try则是local unwind(这里不是很准确,后面会详细解释)。
结束处理程序(Termination Handlers),看起来简单也十分让人疑惑,为什么 return, goto,longjump,异常,控制流离开__try block的时候,可以去执行__finally block呢? 同样,为什么ExitProcess, ExitThread, TerminateProcess, or TerminateThread则不能被执行呢?为什么可以使用goto到__try外面,而不能跳入一个__try block?等等。
异常处理程序(Exception Handlers),则更让新手疑惑,特别是在结合了结束处理程序情况下,在程序的执行流程则变的诡异起来,而我们看到在vc中的SEH并不能够支持__finally 和__except结合一起使用,这又是为什么?使用SEH是否为我们程序增加了相当的负担?SEH是否安全?
为了清楚的认识这些问题,我们必须更进一步的去探究SEH的具体实现过程,由于不同厂商不同编译器的实现方式不同,所以以下的部分来自MS自己的vc。而其由于SEH涉及到了一些安全问题和硬件的部分,所以在不同的vc 版本,不同的操作系统不同的计算机下的情况也不同。当然,为了简单,我们先看最简单的vc6。在我们正式进入细节的时候,让我们先暂时忘记那些__try关键字。
异常是操作系统传给我们写的程序,我们写好处理异常的代码,那么操作系统是如何调用我们写的函数呢?当然是通过回调函数做的,那么这个回调函数是什么样子的呢?
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);
在EXCPT.H中,我们可以找到这个定义。
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
EXCEPTION_RECORD 定义异常,更多的可以参考msdn,http://msdn.microsoft.com/en-us/library/aa363082(VS.85).aspx
contex的定义则根据不同的硬件有不同的定义,这里面定义了线程运行的环境,上下文。找到了回调函数,和异常的样子,那么操作系统是如何调用呢?还记得之前提到的list么?fs:[0],那里,有我们需要的,我们需要知道另一个结构体。这是一个汇编上的定义。
_EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends
prev记录了上一个_EXCEPTION_REGISTRATION结构体的地址,而handler则是我们回调函数的地址,操作系统通过fs:[0],找到了一系列的我们写的回调函数。
让我们先试一下。
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 | //================================================== // MYSEH - Matt Pietrek 1997 // Microsoft Systems Journal, January 1997 // FILE: MYSEH.CPP // To compile: CL MYSEH.CPP //================================================== #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { unsigned i; // Indicate that we made it to our exception handler printf ( "Hello from an exception handler\n" ); // Change EAX in the context record so that it points to someplace // where we can successfully write ContextRecord->Eax = ( DWORD )&scratch; // Tell the OS to restart the faulting instruction return ExceptionContinueExecution; } int main( int argc, char * argv[]) { DWORD handler = ( DWORD )_except_handler; __asm { // Build EXCEPTION_REGISTRATION record: push handler // Address of handler function push FS:[0] // Address of previous handler mov FS:[0],ESP // Install new EXECEPTION_REGISTRATION } __asm { mov eax,0 // Zero out EAX mov [eax], 1 // Write to EAX to deliberately cause a fault } printf ( "After writing!\n" ); __asm { // Remove our EXECEPTION_REGISTRATION record mov eax,[ESP] // Get pointer to previous record mov FS:[0], EAX // Install previous record add esp, 8 // Clean our EXECEPTION_REGISTRATION off stack } return 0; } |
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
vc通过类似的代码生成,在我们的这段代码,mov eax,0 mov [eax], 1,在栈空间上分配了一个EXCEPTION_REGISTRATION结构体,并插入了fs:[0]链表的表头。 当然,在最后跳出这个代码块的时候,这个栈空间的EXCEPTION_REGISTRATION结构体也必须从fs:[0]中卸载掉。而在_except_handler返回的ExceptionContinueExecution,则意味着告诉OS,需要从发生异常的那个语句重新执行,一切都是那么的简单和自然。为了简单,我们在首节点就处理了这个异常,让我们再进一步,看一下异常是如何传递的。
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 | EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { printf ( "Home Grown handler: Exception Code: %08X Exception Flags %X" , ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags ); if ( ExceptionRecord->ExceptionFlags & 1 ) printf ( " EH_NONCONTINUABLE" ); if ( ExceptionRecord->ExceptionFlags & 2 ) printf ( " EH_UNWINDING" ); if ( ExceptionRecord->ExceptionFlags & 4 ) printf ( " EH_EXIT_UNWIND" ); if ( ExceptionRecord->ExceptionFlags & 8 ) printf ( " EH_STACK_INVALID" ); if ( ExceptionRecord->ExceptionFlags & 0x10 ) printf ( " EH_NESTED_CALL" ); printf ( "\n" ); // Punt... We don't want to handle this... Let somebody else handle it return ExceptionContinueSearch; } void HomeGrownFrame( void ) { DWORD handler = ( DWORD )_except_handler; __asm { // Build EXCEPTION_REGISTRATION record: push handler // Address of handler function push FS:[0] // Address of previous handler mov FS:[0],ESP // Install new EXECEPTION_REGISTRATION } *(PDWORD)0 = 0; // Write to address 0 to cause a fault printf ( "I should never get here!\n" ); __asm { // Remove our EXECEPTION_REGISTRATION record mov eax,[ESP] // Get pointer to previous record mov FS:[0], EAX // Install previous record add esp, 8 // Clean our EXECEPTION_REGISTRATION off stack } } int _tmain( int argc, _TCHAR* argv[]) { _try { HomeGrownFrame(); } _except( EXCEPTION_EXECUTE_HANDLER ) { printf ( "Caught the exception in main()\n" ); } return 0; } |
我们在_except_handler中返回了ExceptionContinueSearch,这会告诉windows,我们这个回调函数不处理这个异常,你找其他去吧。我们看到了这个输出结果。
Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the exception in main()
第一个我们很好理解,但是第二次是什么情况呢?这个就是之前提到的unwind过程。windows依次调用fs:[0]上的exceptionlist的回调函数,并根据返回值判断该如何执行,如果是ExceptionContinueSearch,则通过EXCEPTION_REGISTRATION 的prev寻找下一个,直到找到处理异常的函数(windows在创建线程的时候,已经为我们准备好了处理异常的程序)。在找到处理异常的代码后,windows会再一次遍历list,直到处理异常的地方。这一次和第一次不同的是Exception Flags | = EH_UNWINDING,这一次,正是给那些拒绝处理这个异常的代码块一次清理自己的机会,包括一些编译器默默为我们生成的一些临时东东的移除,c++一些临时对象的析构函数调用,从fs:[0],list上删除EXCEPTION_REGISTRATION 等等,当然,我们的finally block也正好趁着这个机会把自己执行了一次。但是,在我们开心的找到回调函数地址的时候,我们却不能直接执行这个地址的代码,因为在之前,很可能运行的环境已经变化了,许多寄存器的数值已经变化了,而且更重要的是ebp esp,很可能根本和我们的这个程序不符合,程序根本不能正确执行(之前做了很多的非局部跳转),所以,必须也把函数运行的状态保存起来,这样我们才能真正的执行我们的回调函数。那么这些状态保存在哪里呢?EXCEPTION_REGISTRATION结构体的地址,在windows fs:[0]可以找到, 那么我们只需要在原有的EXCEPTION_REGISTRATION成员下增加数据就可以找到这些状态。从而正确的恢复执行。
在进一步了解之前,让我们先回顾一下文法。
__try
{
//Guarded body
}
__except(exception filter)
{
// Exception handler
}
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 | void FuncOStimpy1() { //1. Do any processing here. ... __try { //2. Call another function. FuncORen1(); // Code here never executes. } __except( /* 6. Evaluate filter. */ EXCEPTION_EXECUTE_HANDLER) { //8. After the unwind, the exception handler executes. MessageBox(…); } //9. Exception handled--continue execution. } void FuncORen1() { DWORD dwTemp = 0; //3. Do any processing here. __try { //4. Request permission to access protected data. WaitForSingleObject(g_hSem, INFINITE); //5. Modify the data. // An exception is generated here. g_dwProtectedData = 5 / dwTemp; } __finally { //7. Global unwind occurs because filter evaluated // to EXCEPTION_EXECUTE_HANDLER. // Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } // Continue processing--never executes. ... } |
有了现在的基础,在看上面的代码,在执行代码顺序上,已经没有疑惑了。我们所指的回调函数,其实就是exception filter,当异常在5处发生的时候,系统首先要遍历fs:[0],找到处理这个异常的代码,执行流程跑到了6,返回了EXCEPTION_EXECUTE_HANDLER,这告诉系统我认出了这个异常,然后,系统再次遍历fs:[0],这个就是unwind,然后,我们在7处的finally代码才执行,最后执行Exception handler的代码,然后程序从9处恢复执行。Jeffrey Richter中描述的global unwind,local unwind,又是什么意思呢?书写什么样的代码可以最大的提高效率?以及异常处理的效率为什么要慢呢?这背后还有许许多多的小问题,比如为什么goto 只能跳出__try block,而不能跳入__try block?GetExceptionCode为什么能够在filter expression 和exception-handler block,为什么不能在filter function中调用?而如果想弄清楚这一系列问题,我们需要更深入的了解SEH。当然,这才是学习的重点。由于这部分和系统相关,在异常的转发过程中,需要编译器和操作系统的支持,所以,我们需要找一个稍微简单一点的编译器和os,如果是第一次接触这个,那么最好是 vc6 + xp sp1或2000。如果对vc6有极大的抵触情绪(比如本人),使用08的时候需要在编译器中加入/GS-,否则编译器会在栈中生成其他代码(检测是否有溢出)越高的系统还可能会加入safeSEH,SEHOP,而且,具体的实现可能也会稍有不同,一上来全部接触,可能难度稍微有些大(对本人来说),所以,我们从最简单的开始。
让我们看下vc(vc6 vs2008),下的结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct _EXCEPTION_REGISTRATION { struct _EXCEPTION_REGISTRATION *prev; //上一个结构体 void (*handler)(PEXCEPTION_RECORD, //我们的回调函数 PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD); struct scopetable_entry *scopetable; int trylevel; }; typedef struct _SCOPETABLE { DWORD previousTryLevel; DWORD lpfnFilter //我们的filter code address DWORD lpfnHandler //我们的exception handler block 或是 finally handler bloack address } SCOPETABLE, *PSCOPETABLE; |
这个trylevel有是什么呢?为什么要有SCOPETABLE?
我们考虑这样的一个问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | { ... __try { __try { } __except() { } } __except() { } ... __try { } __except() { } ... } |
当一个函数中,有非常多的__try block时,如果我们每遇到一个__try,就生成一个_EXCEPTION_REGISTRATION ,加入fs:[0]然后离开之后,在从fs:[0]中卸载掉,这个的确是一个浪费时间,浪费空间的做法。vc 在做的时候,每个函数只是生成一个_EXCEPTION_REGISTRATION 结构体,而在一个函数内,可能有嵌套的__try block,也可能又并列的try block(以下把__try 简写成try,这个的确不是一个好的书写,但是这个__是在是太麻烦了,try block 是c++的异常,和SEH很像,但也是有些不同的),那么如何才能分辨出到底是哪一个try block?trylevel 和SCOPETABLE,则是为了满足这个要求而实现的。在进入函数的时候,vc会把trylevel初始化为-1,这个表示目前的代码在当前的_EXCEPTION_REGISTRATION 下,不属于try block保护下,遇到第一个try block的时候,vc把trylevel改为0,进入下一个并列的try block则为1….。struct scopetable_entry *则,保存了一个数组,previousTryLevel,告诉我们这个嵌套try block 的上一层block的index….。
可见,vc通过这些手段,在我们的代码之中,维护了一个树的结构,来标示每一个try block,并提供从内层到外层的遍历方法。handler,按理来所,应该跑我们的lpfnFilter ,这里会不会重复? 当然不会,vc实现_EXCEPTION_REGISTRATION 中,handler指向了同一个代码,vc 的运行时库函数 __except_handler ,根据vc版本后面3啊4啊什么的。原因也很简单,整个东东都有了嵌套,必然需要遍历,为了减少重复代码,和代码的安全,当然会都从一个函数入口开始,然后再去调用我们的代码。所以代码的地址,也需要保存。lpfnFilter 我们的except filter代码入口,lpfnHandler,则是我们的except block 入口。 那么,我们的finally在那里呢?由于,finally 并没有filter的概念,所以,当lpfnFilter == null的时候,vc会认为我们跑的是finally block,那么lpfnHandler则是我们的finally 的terminal handle。这也就告诉我们,为什么SEH中,不能同时存在finally 和except block了。
整个事情越来越有趣了,但是一大堆的论述,的确没有任何意思。还是让我们看看代码。我在原有的代码上加上了查看trylevel的代码。
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 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | #ifndef _MSC_VER #error Visual C++ Required (Visual C++ specific information is displayed) #endif //---------------------------------------------------------------------------- // Structure Definitions //---------------------------------------------------------------------------- // The basic, OS defined exception frame struct EXCEPTION_REGISTRATION { EXCEPTION_REGISTRATION* prev; FARPROC handler; }; // Data structure(s) pointed to by Visual C++ extended exception frame struct scopetable_entry { DWORD previousTryLevel; FARPROC lpfnFilter; FARPROC lpfnHandler; }; // The extended exception frame used by Visual C++ struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION { scopetable_entry * scopetable; int trylevel; int _ebp; }; //---------------------------------------------------------------------------- // Prototypes //---------------------------------------------------------------------------- // __except_handler3 is a Visual C++ RTL function. We want to refer to // it in order to print it's address. However, we need to prototype it since // it doesn't appear in any header file. extern "C" int _except_handler3(PEXCEPTION_RECORD, EXCEPTION_REGISTRATION *, PCONTEXT, PEXCEPTION_RECORD); //---------------------------------------------------------------------------- // Code //---------------------------------------------------------------------------- // // Display the information in one exception frame, along with its scopetable // void ShowSEHFrame( VC_EXCEPTION_REGISTRATION * pVCExcRec ) { printf ( "Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X\n" , pVCExcRec, pVCExcRec->handler, pVCExcRec->prev, pVCExcRec->scopetable ); scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable; for ( unsigned i = 0; i <= pVCExcRec->trylevel; i++ ) { printf ( " scopetable[%u] PrevTryLevel: %08X " "filter: %08X __except: %08X\n" , i, pScopeTableEntry->previousTryLevel, pScopeTableEntry->lpfnFilter, pScopeTableEntry->lpfnHandler ); pScopeTableEntry++; } printf ( "\n" ); } // // Walk the linked list of frames, displaying each in turn // void WalkSEHFrames( void ) { VC_EXCEPTION_REGISTRATION * pVCExcRec; // Print out the location of the __except_handler3 function printf ( "_except_handler3 is at address: %08X\n" , _except_handler3 ); printf ( "\n" ); // Get a pointer to the head of the chain at FS:[0] __asm mov eax, FS:[0] __asm mov [pVCExcRec], EAX // Walk the linked list of frames. 0xFFFFFFFF indicates the end of list while ( 0xFFFFFFFF != (unsigned)pVCExcRec ) { ShowSEHFrame( pVCExcRec ); pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev); } } void Function1( void ) { int tl=0; __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); } __except(EXCEPTION_CONTINUE_SEARCH) { } // Set up 3 nested _try levels (thereby forcing 3 scopetable entries) __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); WalkSEHFrames(); // Now show all the exception frames } __except( EXCEPTION_CONTINUE_SEARCH ) { } } __except( EXCEPTION_CONTINUE_SEARCH ) { } } __except( EXCEPTION_CONTINUE_SEARCH ) { } } int main( int argc, char * argv[]) { int i; int tl=0; __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); Function1(); // Call a function that sets up more exception frames } __except( EXCEPTION_EXECUTE_HANDLER ) { i = 0x4321; // Do nothing (in reverse) } __try { __asm mov eax, [ebp-4] __asm mov tl, eax printf ( "try leval = %d\n" , tl); Function1(); // Call a function that sets up more exception frames } __except( EXCEPTION_EXECUTE_HANDLER ) { // Should never get here, since we aren't expecting an exception printf ( "Caught Exception in main\n" ); } return 0; } |
这里我们可以看到如下情况,当然,这个是在2003下的,win7,会有不同的结果。最好还是先不用win7。win7的问题,我也不清楚。这个只能先放下了。
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 | try leval = -1 try leval = 0 try leval = 0 try leval = 1 try leval = 2 try leval = 3 _except_handler3 is at address: 004014C0 Frame: 0012FEFC Handler: 004014C0 Prev: 0012FF70 Scopetable: 004210B8 scopetable[0] PrevTryLevel: FFFFFFFF filter: 00401203 __except: 00401206 scopetable[1] PrevTryLevel: FFFFFFFF filter: 004012A4 __except: 004012A7 scopetable[2] PrevTryLevel: 00000001 filter: 0040128E __except: 00401291 scopetable[3] PrevTryLevel: 00000002 filter: 00401278 __except: 0040127B Frame: 0012FF70 Handler: 004014C0 Prev: 0012FFB0 Scopetable: 00420150 scopetable[0] PrevTryLevel: FFFFFFFF filter: 0040135F __except: 00401365 Frame: 0012FFB0 Handler: 004014C0 Prev: 0012FFE0 Scopetable: 00420278 scopetable[0] PrevTryLevel: FFFFFFFF filter: 00401788 __except: 004017A3 Frame: 0012FFE0 Handler: 7C82B798 Prev: FFFFFFFF Scopetable: 7C8123D8 scopetable[0] PrevTryLevel: FFFFFFFF filter: 7C8571C8 __except: 7C8571DE try leval = 1 try leval = 0 try leval = 1 try leval = 2 try leval = 3 _except_handler3 is at address: 004014C0 Frame: 0012FEFC Handler: 004014C0 Prev: 0012FF70 Scopetable: 004210B8 scopetable[0] PrevTryLevel: FFFFFFFF filter: 00401203 __except: 00401206 scopetable[1] PrevTryLevel: FFFFFFFF filter: 004012A4 __except: 004012A7 scopetable[2] PrevTryLevel: 00000001 filter: 0040128E __except: 00401291 scopetable[3] PrevTryLevel: 00000002 filter: 00401278 __except: 0040127B Frame: 0012FF70 Handler: 004014C0 Prev: 0012FFB0 Scopetable: 00420150 scopetable[0] PrevTryLevel: FFFFFFFF filter: 0040135F __except: 00401365 scopetable[1] PrevTryLevel: FFFFFFFF filter: 004013A2 __except: 004013A8 Frame: 0012FFB0 Handler: 004014C0 Prev: 0012FFE0 Scopetable: 00420278 scopetable[0] PrevTryLevel: FFFFFFFF filter: 00401788 __except: 004017A3 Frame: 0012FFE0 Handler: 7C82B798 Prev: FFFFFFFF Scopetable: 7C8123D8 scopetable[0] PrevTryLevel: FFFFFFFF filter: 7C8571C8 __except: 7C8571DE |
有了实践,这部分比较好懂了。明白了vc如何维护try block 之后,想要更清楚一点,只能从汇编的角度来看了。
EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable pointer
EBP-0C handler function address
EBP-10 previous EXCEPTION_REGISTRATION
EBP-14 LPEXCEPTION_POINTERS
EBP-18 Standard ESP in frame
这是try except block生成时的堆栈。[ebp –10],这里保存了vc 的EXCEPTION_REGISTRATION结构体,就和之前一样,对windows来说,他只是知道最基本的EXCEPTION_REGISTRATION,也就是只是关注prev 和handler,而其他的则是vc 编译器为了生成高效代码为我们加上去的。对windows当然是透明的。从一开始的例子也可以看出,我们只是使用最基本的EXCEPTION_REGISTRATION,依然能够执行SEH。
同样,EBP-14 GetExceptionPointers, EBP-18 Standard ESP in frame也是vc帮我们加入的。[EBP-14 ]这个就是函数当调用GetExceptionInformation会返回[EBP-14], 所以,这个函数其实是一个vc相关的函数。同样的还有GetExceptioncode这个地方还有一点不同的是,vc通过on the flay的方式处理这个数据,也就是说,当异常真的发生的时候,这个数据才会添入数据(这个真是一个废话,没有发生异常,那里来的异常信息?)EBP-18 Standard ESP in frame就不用说了,想要非局部跳转,光搞定ebp是不行的,没有esp的修正,并不能将控制流转到那里。
为了正确理解整个过程,我们需要理解__except_handler 的代码,可惜,Matt Pietrek的有一些细节问题,可能会给我们这样的初学者疑惑,所以可以先看下http://bbs.pediy.com/showthread.php?t=53778,也是一位大牛的文章中,有vc6的__except_handler code。当然,他多了一个ValidateEH3RN,这个和SEH的安全机制有关,我们目前先跳过去。__except_handler 的代码去了ValidateEH3RN,比较容易理解,当然,细扣细节的话,可能不同。在下一篇文章中,我们会着重关注这些细节。
知道了这么多后,我们在看看我们现在可以解决什么样的问题了。Jeffrey Richter 告诉了我们很多有关于展开的,并且告诉了我们很多可能导致额外负担的代码,那么下面我们就看看,为什么会有额外代码。
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 | DWORD Funcenstein1() { DWORD dwTemp; //1. Do any processing here. __try { //2. Request permission to access // protected data, and then use it. WaitForSingleObject(g_hSem, INFINITE); g_dwProtectedData = 5; dwTemp = g_dwProtectedData; // Return the new value. return (dwTemp); } __finally { //3. Allow others to use protected data. ReleaseSemaphore(g_hSem, 1, NULL); } //4. Continue processing. return (dwTemp); } .text:00401000 push ebp .text:00401001 mov ebp, esp .text:00401003 push 0FFFFFFFFh .text:00401005 push offset stru_4021F8 .text:0040100A push offset __except_handler3 .text:0040100F mov eax, large fs:0 .text:00401015 push eax .text:00401016 mov large fs:0, esp .text:0040101D sub esp, 0Ch .text:00401020 push ebx .text:00401021 push esi .text:00401022 push edi .text:00401023 mov [ebp+var_4], 0 .text:0040102A push 0FFFFFFFFh ; dwMilliseconds .text:0040102C mov eax, ?g_hSem@@3PAXA ; void * g_hSem .text:00401031 push eax ; hHandle .text:00401032 call ds:__imp__WaitForSingleObject@8 ; WaitForSingleObject(x,x) .text:00401038 mov esi, 5 .text:0040103D mov ?g_dwProtectedData@@3KA, esi ; ulong g_dwProtectedData .text:00401043 mov [ebp+dwTemp], esi .text:00401046 push 0FFFFFFFFh .text:00401048 lea ecx, [ebp+var_10] .text:0040104B push ecx .text:0040104C call __local_unwind2 ;这里应该就是Jeffrey Richter 告诉我们的局部展开。 .text:00401051 add esp, 8 .text:00401054 mov eax, esi .text:00401056 mov ecx, [ebp+var_10] .text:00401059 mov large fs:0, ecx .text:00401060 pop edi .text:00401061 pop esi .text:00401062 pop ebx .text:00401063 mov esp, ebp .text:00401065 pop ebp .text:00401066 retn |
那local unwind到底做了什么呢?当然是将本EXCEPTION_REGISTRATION内嵌套的那些try block遍历,并展开了。这里贴出local unwind伪代码。这个和我们想象的一样。当然,我这里掩去了一个很重要很重要的部分,是有关于异常嵌套的问题。这个问题会在下一篇中在描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | void _local_unwind2(EXCEPTION_REGISTRATION*pEh3Exce, int targetLevel) { scopetable_entry *scopetable = peh3Exce->scopetable; int trylevel = peh3Exce->trylevel; while (trylevel != -1) { if (targetLevel == -1 || trylevel > targetLevel) break ; if (scopetable[trylevel]->lpfnFilter == NULL) //__finally block { eax = scopetable[trylevel]->lpfnHandler; _NLG_Notify(101); eax = scopetable[trylevel]->lpfnHandler; __NLG_Call(); // call eax } trylevel = scopetable[targetLevel].previousTryLevel; peh3Exce->trylevel = trylevel; } return ; } |
那当我们把return 换成__leave时,又是什么样子呢?__leave我们并没有看到local unwind,我们需要明白return 和__leave的区别。从return发生local unwind,我们可以看出多少端倪,local unwind 的作用在于遍历本地的except frame,那么return和__leave的区别就在于,__leave不会跳出多个try block 而 return 是有可能的。 所以return 必须要产生额外的负担去执行local unwind,__leave则相当于,goto到try block 的结束并正常跳出try block。所以,如果我们只是想跳出本次try要注意不要直接return。
写给自己。
这一篇其实没有写完,虽然历时1个多月,最近实在是太忙了。这篇文章有2点遗憾。
1、最后应该写上global unwind,但是的确是不想去重复大牛们的内容了,global unwind 其实是系统RtlUnwind的封装,上边的链接中有讲这个的,也很详细。只是由于时间悠久和我们现在的编译器和操作系统距离很远了。如果对这些感兴趣,可以看看wince的代码,http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/EXDSPTCH.C.html,
http://www.2beanet.com/wince/src/COREOS/NK/KERNEL/X86/MDX86.C.html。这个和我们的xp2比较像。
2、本来想尽可能的在这一篇中没有或是少有汇编,但是这个的确对我来说,是一个比较复杂的问题,而且越到最后,其实汇编也是不可避免的,因为真实的代码也很有可能就是汇编写的,我们实在是没有必要去把他翻译成c。
这篇文章里面的问题还是很多的,也很有可能会给第一次接触这些的同学一些误解,下一篇将更深入的理解SEH机制,将尽可能的减少这些误解(也包括自己理解错误),内容包括global unwind,异常嵌套和一些很基础很基础的SEH安全机制的总结。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述