如果你已經走了這么遠,不把整個過程講完對你有點不公平。我已經講了當異常發生時操作系統是如何調用用戶定義的回調函數的。我也講了這些回調的內部情況,以及編譯器是如何使用它們來實現__try和__except的。我甚至還講了當某個異常沒有被處理時所發生的情況以及系統所做的掃尾工作。剩下的就只有異常回調過程最初是從哪里開始的這個問題了。好吧,讓我們深入系統內部來看一下結構化異常處理的開始階段吧。
圖十四是我為 KiUserExceptionDispatcher 函數和一些相關函數寫的偽代碼。這個函數在NTDLL.DLL中,它是異常處理執行的起點。為了絕對准確起見,我必須指出:剛才說的並不是絕對准確。例如在Intel平台上,一個異常導致CPU將控制權轉到ring 0(0特權級,即內核模式)的一個處理程序上。這個處理程序由中斷描述符表(Interrupt Descriptor Table,IDT)中的一個元素定義,它是專門用來處理相應異常的。我跳過所有的內核模式代碼,假設當異常發生時CPU直接將控制權轉到了 KiUserExceptionDispatcher 函數。

KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD retValue; // Note: If the exception is handled, RtlDispatchException() never returns if ( RtlDispatchException( pExceptRec, pContext ) ) retValue = NtContinue( pContext, 0 ); else retValue = NtRaiseException( pExceptRec, pContext, 0 ); EXCEPTION_RECORD excptRec2; excptRec2.ExceptionCode = retValue; excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE; excptRec2.ExceptionRecord = pExcptRec; excptRec2.NumberParameters = 0; RtlRaiseException( &excptRec2 ); } int RtlDispatchException( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext ) { DWORD stackUserBase; DWORD stackUserTop; PEXCEPTION_REGISTRATION pRegistrationFrame; DWORD hLog; // Get stack boundaries from FS:[4] and FS:[8] RtlpGetStackLimits( &stackUserBase, &stackUserTop ); pRegistrationFrame = RtlpGetRegistrationHead(); while ( -1 != pRegistrationFrame ) { PVOID justPastRegistrationFrame = &pRegistrationFrame + 8; if ( stackUserBase > justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( stackUsertop < justPastRegistrationFrame ) { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( pRegistrationFrame & 3 ) // Make sure stack is DWORD aligned { pExcptRec->ExceptionFlags |= EH_STACK_INVALID; return DISPOSITION_DISMISS; // 0 } if ( someProcessFlag ) { // Doesn't seem to do a whole heck of a lot. hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0, pRegistrationFrame, 0x10 ); } DWORD retValue, dispatcherContext; retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame, pContext, &dispatcherContext, pRegistrationFrame->handler ); // Doesn't seem to do a whole heck of a lot. if ( someProcessFlag ) RtlpLogLastExceptionDisposition( hLog, retValue ); if ( 0 == pRegistrationFrame ) { pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // Turn off flag } EXCEPTION_RECORD excptRec2; DWORD yetAnotherValue = 0; if ( DISPOSITION_DISMISS == retValue ) { if ( pExcptRec->ExceptionFlags & EH_NONCONTINUABLE ) { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0 RtlRaiseException( &excptRec2 ); } else return DISPOSITION_CONTINUE_SEARCH; } else if ( DISPOSITION_CONTINUE_SEARCH == retValue ) { } else if ( DISPOSITION_NESTED_EXCEPTION == retValue ) { pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND; if ( dispatcherContext > yetAnotherValue ) yetAnotherValue = dispatcherContext; } else // DISPOSITION_COLLIDED_UNWIND { excptRec2.ExceptionRecord = pExcptRec; excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION; excptRec2.ExceptionFlags = EH_NONCONTINUABLE; excptRec2.NumberParameters = 0 RtlRaiseException( &excptRec2 ); } pRegistrationFrame = pRegistrationFrame->prev; // Go to previous frame } return DISPOSITION_DISMISS; } _RtlpExecuteHandlerForException: // Handles exception (first time through) MOV EDX,XXXXXXXX JMP ExecuteHandler RtlpExecutehandlerForUnwind: // Handles unwind (second time through) MOV EDX,XXXXXXXX int ExecuteHandler( PEXCEPTION_RECORD pExcptRec PEXCEPTION_REGISTRATION pExcptReg CONTEXT * pContext PVOID pDispatcherContext, FARPROC handler ) // Really a ptr to an _except_handler() // Set up an EXCEPTION_REGISTRATION, where EDX points to the // appropriate handler code shown below PUSH EDX PUSH FS:[0] MOV FS:[0],ESP // Invoke the exception callback function EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext ); // Remove the minimal EXCEPTION_REGISTRATION frame MOV ESP,DWORD PTR FS:[00000000] POP DWORD PTR FS:[00000000] return EAX; } Exception handler used for _RtlpExecuteHandlerForException: { // If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else // assign pDispatcher context and return DISPOSITION_NESTED_EXCEPTION return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_NESTED_EXCEPTION; } Exception handler used for _RtlpExecuteHandlerForUnwind: { // If unwind flag set, return DISPOSITION_CONTINUE_SEARCH, else // assign pDispatcher context and return DISPOSITION_COLLIDED_UNWIND return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ? DISPOSITION_CONTINUE_SEARCH : *pDispatcherContext = pRegistrationFrame->scopetable, DISPOSITION_COLLIDED_UNWIND; }
KiUserExceptionDispatcher 的核心是對 RtlDispatchException 的調用。這拉開了搜索已注冊的異常處理程序的序幕。如果某個處理程序處理這個異常並繼續執行,那么對 RtlDispatchException 的調用就不會返回。如果它返回了,只有兩種可能:或者調用了NtContinue以便讓進程繼續執行,或者產生了新的異常。如果是這樣,那異常就不能再繼續處理了,必須終止進程。
現在把目光對准 RtlDispatchException 函數的代碼,這就是我通篇提到的遍歷異常幀的代碼。這個函數獲取一個指向EXCEPTION_REGISTRATION 結構鏈表的指針,然后遍歷此鏈表以尋找一個異常處理程序。由於堆棧可能已經被破壞了,所以這個例程非常謹慎。在調用每個EXCEPTION_REGISTRATION結構中指定的異常處理程序之前,它確保這個結構是按DWORD對齊的,並且是在線程的堆棧之中,同時在堆棧中比前一個EXCEPTION_REGISTRATION結構高。
RtlDispatchException並不直接調用EXCEPTION_REGISTRATION結構中指定的異常處理程序。相反,它調用 RtlpExecuteHandlerForException來完成這個工作。根據RtlpExecuteHandlerForException的執行情況,RtlDispatchException或者繼續遍歷異常幀,或者引發另一個異常。這第二次的異常表明異常處理程序內部出現了錯誤,這樣就不能繼續執行下去了。
RtlpExecuteHandlerForException的代碼與RtlpExecuteHandlerForUnwind的代碼極其相似。你可能會回憶起來在前面討論展開時我提到過它。這兩個“函數”都只是簡單地給EDX寄存器加載一個不同的值然后就調用ExecuteHandler函數。也就是說,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是 ExecuteHanlder這個公共函數的前端。
ExecuteHandler查找EXCEPTION_REGISTRATION結構的handler域的值並調用它。令人奇怪的是,對異常處理回調函數的調用本身也被一個結構化異常處理程序封裝着。在SEH自身中使用SEH看起來有點奇怪,但你思索一會兒就會理解其中的含義。如果在異常回調過程中引發了另外一個異常,操作系統需要知道這個情況。根據異常發生在最初的回調階段還是展開回調階段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。這兩者都是“紅色警報!現在把一切都關掉!”類型的代碼。
如果你像我一樣,那不僅理解所有與SEH有關的函數非常困難,而且記住它們之間的調用關系也非常困難。為了幫助我自己記憶,我畫了一個調用關系圖(圖十五)。
圖十五 在SEH中是誰調用了誰

Figure 15 Who Calls Who in SEH KiUserExceptionDispatcher() RtlDispatchException() RtlpExecuteHandlerForException() ExecuteHandler() // Normally goes to __except_handler3 --------- __except_handler3() scopetable filter-expression() __global_unwind2() RtlUnwind() RtlpExecuteHandlerForUnwind() scopetable __except block()
現在要問:在調用ExecuteHandler之前設置EDX寄存器的值有什么用呢?這非常簡單。如果ExecuteHandler在調用用戶安裝的異常處理程序的過程中出現了什么錯誤,它就把EDX指向的代碼作為原始的異常處理程序。它把EDX寄存器的值壓入堆棧作為原始的 EXCEPTION_REGISTRATION結構的handler域。這基本上與我在MYSEH和MYSEH2中對原始的結構化異常處理的使用情況一樣。
結論
結構化異常處理是Win32一個非常好的特性。多虧有了像Visual C++之類的編譯器的支持層對它的封裝,一般的程序員才能付出比較小的學習代價就能利用SEH所提供的便利。但是在操作系統層面上,事情遠比Win32文檔說的復雜。
不幸的是,由於人人都認為系統層面的SEH是一個非常困難的問題,因此至今這方面的資料都不多。在本文中,我已經向你指出了系統層面的SEH就是圍繞着簡單的回調在打轉。如果你理解了回調的本質,在此基礎上分層理解,系統層面的結構化異常處理也不是那么難掌握。
附錄:關於 “prolog 和 epilog ”
在 Visual C++ 文檔中,微軟對 prolog 和 epilog 的解釋是:“保護現場和恢復現場” 此附錄摘自微軟 MSDN 庫,詳細信息參見:
http:
//msdn.microsoft.com/en-us/library/tawsa7cb(VS.80).aspx(英文)
http:
//msdn.microsoft.com/zh-cn/library/tawsa7cb(VS.80).aspx(中文)
如果堆棧中的固定分配超過一頁(即大於 4096 字節),則該堆棧分配的范圍可能超過一個虛擬內存頁,因此在實際分配之前必須檢查分配情況。為此,提供了一個特殊的例程,該例程可從 Prolog 調用,並且不會損壞任何參數寄存器。
保存非易失寄存器的首選方法是:在進行固定堆棧分配之前將這些寄存器移入堆棧。如果在保存非易失寄存器之前執行了固定堆棧分配,則很可能需要 32 位位移以便對保存的寄存器區域進行尋址(據說寄存器的壓棧操作與移動操作一樣快,並且在可預見的未來一段時間內都應該是這樣,盡管壓棧操作之間存在隱含的相關性)。可按任何順序保存非易失寄存器。但是,在 Prolog 中第一次使用非易失寄存器時必須對其進行保存。
典型的 Prolog 代碼可以為:

mov [RSP + 8], RCX push R15 push R14 push R13 sub RSP, fixed-allocation-size lea R13, 128[RSP
此 Prolog 執行以下操作:將參數寄存器 RCX 存儲在其標識位置;保存非易失寄存器 R13、R14、R15;分配堆棧幀的固定部分;建立幀指針,該指針將 128 字節地址指向固定分配區域。使用偏移量以后,便可以通過單字節偏移量對多個固定分配區域進行尋址。
如果固定分配大小大於或等於一頁內存,則在修改 RSP 之前必須調用 helper 函數。此 __chkstk helper 函數負責探測待分配的堆棧范圍,以確保對堆棧進行正確的擴展。在這種情況下,前面的 Prolog 示例應變為:
mov [RSP + 8], RCX
push R15
push R14
push R13
mov RAX, fixed-allocation-size
call __chkstk
sub RSP, RAX
lea R13, 128[RSP]
..
|
.除了 R10、R11 和條件代碼以外,此 __chkstk helper 函數不會修改任何寄存器。特別是,此函數將返回未更改的 RAX,並且不會修改所有非易失寄存器和參數傳遞寄存器。
Epilog 代碼位於函數的每個出口。通常只有一個 Prolog,但可以有多個 Epilog。Epilog 代碼執行以下操作:必要時將堆棧修整為其固定分配大小;釋放固定堆棧分配;從堆棧中彈出非易失寄存器的保存值以還原這些寄存器;返回。
對於展開代碼,Epilog 代碼必須遵守一組嚴格的規則,以便通過異常和中斷進行可靠的展開。這樣可以減少所需的展開數據量,因為描述每個 Epilog 不需要額外數據。通過向前掃描整個代碼流以標識 Epilog,展開代碼可以確定 Epilog 正在執行。
如果函數中沒有使用任何幀指針,則 Epilog 必須首先釋放堆棧的固定部分,彈出非易失寄存器,然后將控制返回調用函數。例如,
1
2
3
4
5
6
7
8
9
|
add RSP, fixed-allocation-size
pop R13
pop R14
pop R15
ret
|
如果函數中使用了幀指針,則在執行 Epilog 之前必須將堆棧修整為其固定分配。這在技術上不屬於 Epilog。例如,下面的 Epilog 可用於撤消前面使用的 Prolog:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
lea RSP, -128[R13]
; epilogue proper starts here
add RSP, fixed-allocation-size
pop R13
pop R14
pop R15
ret
|
在實際應用中,使用幀指針時,沒有必要分兩個步驟調整 RSP,因此應改用以下 Epilog:
1
2
3
4
5
6
7
8
9
|
lea RSP, fixed-allocation-size – 128[R13]
pop R13
pop R14
pop R15
ret
|
以上是 Epilog 的唯一合法形式。它必須由 add RSP,constant 或 lea RSP,constant[FPReg] 組成,后跟一系列零或多個 8 字節寄存器 pop、一個 return 或一個 jmp。(Epilog 中只允許 jmp 語句的子集。僅限於具有 ModRM 內存引用的 jmp 類,其中 ModRM mod 字段值為 00。在 ModRM mod 字段值為 01 或 10 的 Epilog 中禁止使用 jmp。有關允許使用的 ModRM 引用的更多信息,請參見“AMD x86-64 Architecture Programmer’s Manual Volume 3: General Purpose and System Instructions”(AMD x86-64 結構程序員手冊第 3 卷:通用指令和系統指令)中的表 A-15。)不能出現其他代碼。特別是,不能在 Epilog 內進行調度,包括加載返回值。
請注意,未使用幀指針時,Epilog 必須使用 add RSP,constant 釋放堆棧的固定部分,而不能使用 lea RSP,constant[RSP]。由於此限制,在搜索 Epilog 時展開代碼具有較少的識別模式。
通過遵守這些規則,展開代碼便可以確定某個 Epilog 當前正在執行,並可以模擬該 Epilog 其余部分的執行,從而允許重新創建調用函數的上下文