展開
在挖掘展開(Unwinding)的實現代碼之前讓我們先來搞清楚它的意思。我在前面已經講過所有可能的異常處理程序是如何被組織在一個由線程信息塊的第一個DWORD(FS:[0])所指向的鏈表中的。由於針對某個特定異常的處理程序可能不在這個鏈表的開頭,因此就需要從鏈表中依次移除實際處理異常的那個異常處理程序之前的所有異常處理程序。
正如你在Visual C++的__except_handler3函數中看到的那樣,展開是由__global_unwind2這個運行時庫(RTL)函數來完成的。這個函數只是對RtlUnwind這個未公開的API進行了非常簡單的封裝。(現在這個API已經被公開了,但給出的信息極其簡單,詳細信息可以參考最新的Platform SDK文檔。)
__global_unwind2(void * pRegistFrame)
{
_RtlUnwind( pRegistFrame, &__ret_label, 0, 0 );
__ret_label:
}
雖然從技術上講RtlUnwind是一個KERNEL32函數,但它只是轉發到了NTDLL.DLL中的同名函數上。下面是我為此函數寫的偽代碼。
RtlUnwind 函數的偽代碼:
void _RtlUnwind( PEXCEPTION_REGISTRATION pRegistrationFrame,
PVOID returnAddr, // 並未使用!(至少是在i386機器上)
PEXCEPTION_RECORD pExcptRec,
DWORD _eax_value)
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_RECORD pExcptRec;
EXCEPTION_RECORD exceptRec;
CONTEXT context;
// 從FS:[4]和FS:[8]處獲取堆棧的界限
RtlpGetStackLimits( &stackUserBase, &stackUserTop );
if ( 0 == pExcptRec ) // 正常情況
{
pExcptRec = &excptRec;
pExcptRec->ExceptionFlags = 0;
pExcptRec->ExceptionCode = STATUS_UNWIND;
pExcptRec->ExceptionRecord = 0;
pExcptRec->ExceptionAddress = [ebp+4]; // RtlpGetReturnAddress()—獲取返回地址
pExcptRec->ExceptionInformation[0] = 0;
}
if ( pRegistrationFrame )
pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;
else // 這兩個標志合起來被定義為EXCEPTION_UNWIND_CONTEXT
pExcptRec->ExceptionFlags|=(EXCEPTION_UNWINDING|EXCEPTION_EXIT_UNWIND);
context.ContextFlags =( CONTEXT_i486 | CONTEXT_CONTROL |
CONTEXT_INTEGER | CONTEXT_SEGMENTS);
RtlpCaptureContext( &context );
context.Esp += 0x10;
context.Eax = _eax_value;
PEXCEPTION_REGISTRATION pExcptRegHead;
pExcptRegHead = RtlpGetRegistrationHead(); // 返回FS:[0]的值
// 開始遍歷EXCEPTION_REGISTRATION結構鏈表
while ( -1 != pExcptRegHead )
{
EXCEPTION_RECORD excptRec2;
if ( pExcptRegHead == pRegistrationFrame )
{
NtContinue( &context, 0 );
}
else
{
// 如果存在某個異常幀在堆棧上的位置比異常鏈表的頭部還低
// 說明一定出現了錯誤
if ( pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead) )
{
// 生成一個異常
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &exceptRec2 );
}
}
PVOID pStack = pExcptRegHead + 8; // 8 = sizeof(EXCEPTION_REGISTRATION)
// 確保pExcptRegHead在堆棧范圍內,並且是4的倍數
if ( (stackUserBase <= pExcptRegHead )
&& (stackUserTop >= pStack )
&& (0 == (pExcptRegHead & 3)) )
{
DWORD pNewRegistHead;
DWORD retValue;
retValue = RtlpExecutehandlerForUnwind(pExcptRec, pExcptRegHead, &context,
&pNewRegistHead, pExceptRegHead->handler );
if ( retValue != DISPOSITION_CONTINUE_SEARCH )
{
if ( retValue != DISPOSITION_COLLIDED_UNWIND )
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
else
pExcptRegHead = pNewRegistHead;
}
PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead;
pExcptRegHead = pExcptRegHead->prev;
RtlpUnlinkHandler( pCurrExcptReg );
}
else // 堆棧已經被破壞!生成一個異常
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_BAD_STACK;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
RtlRaiseException( &excptRec2 );
}
}
// 如果執行到這裡,說明已經到了EXCEPTION_REGISTRATION
// 結構鏈表的末尾,正常情況下不應該發生這種情況。
//(因為正常情況下異常應該被處理,這樣就不會到鏈表末尾)
if ( -1 == pRegistrationFrame )
NtContinue( &context, 0 );
else
NtRaiseException( pExcptRec, &context, 0 );
}
RtlUnwind函數的偽代碼到這裡就結束了,以下是它調用的幾個函數的偽代碼:
PEXCEPTION_REGISTRATION RtlpGetRegistrationHead( void )
{
return FS:[0];
}
RtlpUnlinkHandler( PEXCEPTION_REGISTRATION pRegistrationFrame )
{
FS:[0] = pRegistrationFrame->prev;
}
void RtlpCaptureContext( CONTEXT * pContext )
{
pContext->Eax = 0;
pContext->Ecx = 0;
pContext->Edx = 0;
pContext->Ebx = 0;
pContext->Esi = 0;
pContext->Edi = 0;
pContext->SegCs = CS;
pContext->SegDs = DS;
pContext->SegEs = ES;
pContext->SegFs = FS;
pContext->SegGs = GS;
pContext->SegSs = SS;
pContext->EFlags = flags; // 它對應的匯編代碼為__asm{ PUSHFD / pop [xxxxxxxx] }
pContext->Eip = 此函數的調用者的調用者的返回地址 // 讀者看一下這個函數的
pContext->Ebp = 此函數的調用者的調用者的EBP // 匯編代碼就會清楚這一點
pContext->Esp = pContext->Ebp + 8;
}
雖然 RtlUnwind 函數的規模看起來很大,但是如果你按一定方法把它分開,其實並不難理解。它首先從FS:[4]和FS:[8]處獲取當前線程堆棧的界限。它們對於後面要進行的合法性檢查非常重要,以確保所有將要被展開的異常幀都在堆棧范圍內。
RtlUnwind 接著在堆棧上創建了一個空的EXCEPTION_RECORD結構並把STATUS_UNWIND賦給它的ExceptionCode域,同時把 EXCEPTION_UNWINDING標志賦給它的 ExceptionFlags 域。指向這個結構的指針作為其中一個參數被傳遞給每個異常回調函數。然後,這個函數調用RtlCaptureContext函數來創建一個空的CONTEXT結構,這個結構也變成了在展開階段調用每個異常回調函數時傳遞給它們的一個參數。
RtlUnwind函數的其余部分遍歷EXCEPTION_REGISTRATION結構鏈表。對於其中的每個幀,它都調用 RtlpExecuteHandlerForUnwind 函數,後面我會講到這個函數。正是這個函數帶 EXCEPTION_UNWINDING 標志調用了異常處理回調函數。每次回調之後,它調用RtlpUnlinkHandler 移除相應的異常幀。
RtlUnwind 函數的第一個參數是一個幀的地址,當它遍歷到這個幀時就停止展開異常幀。上面所說的這些代碼之間還有一些安全性檢查代碼,它們用來確保不出問題。如果出現任何問題,RtlUnwind 就引發一個異常,指示出了什麼問題,並且這個異常帶有EXCEPTION_NONCONTINUABLE 標志。當一個進程被設置了這個標志時,它就不允許再運行,必須終止。
未處理異常
在文章的前面,我並沒有全面描述 UnhandledExceptionFilter 這個 API。通常情況下你並不直接調用它(盡管你可以這麼做)。大多數情況下它都是由 KERNEL32 中進行默認異常處理的過濾器表達式代碼調用。前面 BaseProcessStart 函數的偽代碼已經表明了這一點。
圖十三是我為 UnhandledExceptionFilter 函數寫的偽代碼。這個API有點奇怪(至少在我看來是這樣)。如果異常的類型是 EXCEPTION_ACCESS_VIOLATION,它就調用_BasepCheckForReadOnlyResource。雖然我沒有提供這個函數的偽代碼,但可以簡要描述一下。如果是因為要對 EXE 或 DLL 的資源節(.rsrc)進行寫操作而導致的異常,_BasepCurrentTopLevelFilter 就改變出錯頁面正常的只讀屬性,以便允許進行寫操作。如果是這種特殊的情況,UnhandledExceptionFilter 返回 EXCEPTION_CONTINUE_EXECUTION,使系統重新執行出錯指令。
圖十三 UnHandledExceptionFilter 函數的偽代碼
UnhandledExceptionFilter( STRUCT _EXCEPTION_POINTERS *pExceptionPtrs )
{
PEXCEPTION_RECORD pExcptRec;
DWORD currentESP;
DWORD retValue;
DWORD DEBUGPORT;
DWORD dwTemp2;
DWORD dwUseJustInTimeDebugger;
CHAR szDbgCmdFmt[256]; // 從AeDebug這個注冊表鍵值返回的字符串
CHAR szDbgCmdLine[256]; // 實際的調試器命令行參數(已填入進程ID和事件ID)
STARTUPINFO startupinfo;
PROCESS_INFORMATION pi;
HARDERR_STRUCT harderr; // ???
BOOL fAeDebugAuto;
TIB * pTib; // 線程信息塊
pExcptRec = pExceptionPtrs->ExceptionRecord;
if ( (pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
&& (pExcptRec->ExceptionInformation[0]) )
{
retValue=BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);
if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
return EXCEPTION_CONTINUE_EXECUTION;
}
// 查看這個進程是否運行於調試器下
retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort,
&debugPort, sizeof(debugPort), 0 );
if ( (retValue >= 0) && debugPort ) // 通知調試器
return EXCEPTION_CONTINUE_SEARCH;
// 用戶調用SetUnhandledExceptionFilter了嗎?
// 如果調用了,那現在就調用他安裝的異常處理程序
if ( _BasepCurrentTopLevelFilter )
{
retValue = _BasepCurrentTopLevelFilter( pExceptionPtrs );
if ( EXCEPTION_EXECUTE_HANDLER == retValue )
return EXCEPTION_EXECUTE_HANDLER;
if ( EXCEPTION_CONTINUE_EXECUTION == retValue )
return EXCEPTION_CONTINUE_EXECUTION;
// 只有返回值為EXCEPTION_CONTINUE_SEARCH時才會繼續執行下去
}
// 調用過SetErrorMode(SEM_NOGPFAULTERRORBOX)嗎?
{
harderr.elem0 = pExcptRec->ExceptionCode;
harderr.elem1 = pExcptRec->ExceptionAddress;
if ( EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode )
harderr.elem2 = pExcptRec->ExceptionInformation[2];
else
harderr.elem2 = pExcptRec->ExceptionInformation[0];
dwTemp2 = 1;
fAeDebugAuto = FALSE;
harderr.elem3 = pExcptRec->ExceptionInformation[1];
pTib = FS:[18h];
DWORD someVal = pTib->pProcess->0xC;
if ( pTib->threadID != someVal )
{
__try
{
char szDbgCmdFmt[256];
retValue = GetProfileStringA( "AeDebug", "Debugger", 0,
szDbgCmdFmt, sizeof(szDbgCmdFmt)-1 );
if ( retValue )
dwTemp2 = 2;
char szAuto[8];
retValue = GetProfileStringA( "AeDebug", "Auto", "0",
szAuto, sizeof(szAuto)-1 );
if ( retValue )
if ( 0 == strcmp( szAuto, "1" ) )
if ( 2 == dwTemp2 )
fAeDebugAuto = TRUE;
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
ESP = currentESP;
dwTemp2 = 1;
fAeDebugAuto = FALSE;
}
}
if ( FALSE == fAeDebugAuto )
{
retValue=NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | 0x10000000,
4, 0, &harderr,_BasepAlreadyHadHardError ? 1 : dwTemp2,
&dwUseJustInTimeDebugger );
}
else
{
dwUseJustInTimeDebugger = 3;
retValue = 0;
}
if (retValue >= 0 && (dwUseJustInTimeDebugger == 3)
&& (!_BasepAlreadyHadHardError)&&(!_BaseRunningInServerProcess))
{
_BasepAlreadyHadHardError = 1;
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };
HANDLE hEvent = CreateEventA( &secAttr, TRUE, 0, 0 );
memset( &startupinfo, 0, sizeof(startupinfo) );
sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);
startupinfo.cb = sizeof(startupinfo);
startupinfo.lpDesktop = "Winsta0\Default"
CsrIdentifyAlertableThread(); // ???
retValue = CreateProcessA( 0, // 應用程序名稱
szDbgCmdLine, // 命令行
0, 0, // 進程和線程安全屬性
1, // bInheritHandles
0, 0, // 創建標志、環境
0, // 當前目錄
&statupinfo, // STARTUPINFO
&pi); // PROCESS_INFORMATION
if ( retValue && hEvent )
{
NtWaitForSingleObject( hEvent, 1, 0 );
return EXCEPTION_CONTINUE_SEARCH;
}
}
if ( _BasepAlreadyHadHardError )
NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);
}
return EXCEPTION_EXECUTE_HANDLER;
}
LPTOP_LEVEL_EXCEPTION_FILTER
SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter )
{
// _BasepCurrentTopLevelFilter是KERNEL32.DLL中的一個全局變量
LPTOP_LEVEL_EXCEPTION_FILTER previous= _BasepCurrentTopLevelFilter;
// 設置為新值
_BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;
return previous; // 返回以前的值
}
UnhandledExceptionFilter接下來的任務是確定進程是否運行於Win32調試器下。也就是進程的創建標志中是否帶有標志DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。它使用NtQueryInformationProcess函數來確定進程是否正在被調試,我在本月的Under the Hood專欄中講解了這個函數。如果正在被調試,UnhandledExceptionFilter就返回 EXCEPTION_CONTINUE_SEARCH,這告訴系統去喚醒調試器並告訴它在被調試程序(debuggee)中產生了一個異常。
UnhandledExceptionFilter接下來調用用戶安裝的未處理異常過濾器(如果存在的話)。通常情況下,用戶並沒有安裝回調函數,但是用戶可以調用 SetUnhandledExceptionFilter這個API來安裝。上面我也提供了這個API的偽代碼。這個函數只是簡單地用用戶安裝的回調函數的地址來替換一個全局變量,並返回替換前的值。
有了初步的准備之後,UnhandledExceptionFilter就開始做它的主要工作:用一個時髦的應用程序錯誤對話框來通知你犯了低級的編程錯誤。有兩種方法可以避免出現這個對話框。第一種方法是調用SetErrorMode函數並指定SEM_NOGPFAULTERRORBOX標志。另一種方法是將AeDebug子鍵下的Auto的值設為1。此時UnhandledExceptionFilter跳過應用程序錯誤對話框直接啟動AeDebug 子鍵下的Debugger的值所指定的調試器。如果你熟悉“即時調試(Just In Time Debugging,JIT)”的話,這就是操作系統支持它的地方。接下來我會詳細講。
大多數情況下,上面的兩個條件都為假。這樣UnhandledExceptionFilter就調用NTDLL.DLL中的 NtRaiseHardError函數。正是這個函數產生了應用程序錯誤對話框。這個對話框等待你單擊“確定”按鈕來終止進程,或者單擊“取消”按鈕來調試它。(單擊“取消”按鈕而不是“確定”按鈕來加載調試器好像有點顛倒了,可能這只是我個人的感覺吧。)
如果你單擊“確定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。調用UnhandledExceptionFilter 的進程通常通過終止自身來作為響應(正像你在BaseProcessStart的偽代碼中看到的那樣)。這就產生了一個有趣的問題——大多數人都認為是系統終止了產生未處理異常的進程,而實際上更准確的說法應該是,系統進行了一些設置使得產生未處理異常的進程將自身終止掉了。
UnhandledExceptionFilter執行時真正有意思的部分是當你單擊應用程序錯誤對話框中的“取消”按鈕,此時系統將調試器附加(attach)到出錯進程上。這段代碼首先調用 CreateEvent來創建一個事件內核對象,調試器成功附加到出錯進程之後會將此事件對象變成有信號狀態。這個事件句柄以及出錯進程的ID都被傳到 sprintf函數,由它將其格式化成一個命令行,用來啟動調試器。一切就緒之後,UnhandledExceptionFilter就調用 CreateProcess來啟動調試器。如果CreateProcess成功,它就調用NtWaitForSingleObject來等待前面創建的那個事件對象。此時這個調用被阻塞,直到調試器進程將此事件變成有信號狀態,以表明它已經成功附加到出錯進程上。UnhandledExceptionFilter函數中還有一些其它的代碼,我在這裡只講重要的。
進入地獄
如果你已經走了這麼遠,不把整個過程講完對你有點不公平。我已經講了當異常發生時操作系統是如何調用用戶定義的回調函數的。我也講了這些回調的內部情況,以及編譯器是如何使用它們來實現__try和__except的。我甚至還講了當某個異常沒有被處理時所發生的情況以及系統所做的掃尾工作。剩下的就只有異常回調過程最初是從哪裡開始的這個問題了。好吧,讓我們深入系統內部來看一下結構化異常處理的開始階段吧。
圖十四是我為 KiUserExceptionDispatcher 函數和一些相關函數寫的偽代碼。這個函數在NTDLL.DLL中,它是異常處理執行的起點。為了絕對准確起見,我必須指出:剛才說的並不是絕對准確。例如在Intel平台上,一個異常導致CPU將控制權轉到ring 0(0特權級,即內核模式)的一個處理程序上。這個處理程序由中斷描述符表(Interrupt Descriptor Table,IDT)中的一個元素定義,它是專門用來處理相應異常的。我跳過所有的內核模式代碼,假設當異常發生時CPU直接將控制權轉到了 KiUserExceptionDispatcher 函數。
圖十四 KiUserExceptionDispatcher 的偽代碼:
KiUserExceptionDispatcher( PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext )
{
DWORD retValue;
// 注意:如果異常被處理,那麼 RtlDispatchException 函數就不會返回
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;
// 從FS:[4]和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 ) // 確保堆棧按DWORD對齊
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}
if ( someProcessFlag )
{
hLog = RtlpLogExceptionHandler( pExcptRec, pContext, 0,
pRegistrationFrame, 0x10 );
}
DWORD retValue, dispatcherContext;
retValue= RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame,
pContext, &dispatcherContext,
pRegistrationFrame->handler );
if ( someProcessFlag )
RtlpLogLastExceptionDisposition( hLog, retValue );
if ( 0 == pRegistrationFrame )
{
pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // 關閉標志
}
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; // 轉到前一個幀
}
return DISPOSITION_DISMISS;
}
_RtlpExecuteHandlerForException: // 處理異常(第一次)
MOV EDX,XXXXXXXX
JMP ExecuteHandler
RtlpExecutehandlerForUnwind: // 處理展開(第二次)
MOV EDX,XXXXXXXX
int ExecuteHandler( PEXCEPTION_RECORD pExcptRec,
PEXCEPTION_REGISTRATION pExcptReg,
CONTEXT * pContext,
PVOID pDispatcherContext,
FARPROC handler ) // 實際上是指向_except_handler()的指針
{
// 安裝一個EXCEPTION_REGISTRATION幀,EDX指向相應的handler代碼
PUSH EDX
PUSH FS:[0]
MOV FS:[0],ESP
// 調用異常處理回調函數
EAX = handler( pExcptRec, pExcptReg, pContext, pDispatcherContext );
// 移除EXCEPTION_REGISTRATION幀
MOV ESP,DWORD PTR FS:[00000000]
POP DWORD PTR FS:[00000000]
return EAX;
}
_RtlpExecuteHandlerForException使用的異常處理程序:
{
// 如果設置了展開標志,返回DISPOSITION_CONTINUE_SEARCH
// 否則,給pDispatcherContext賦值並返回DISPOSITION_NESTED_EXCEPTION
return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT ?
DISPOSITION_CONTINUE_SEARC : ( *pDispatcherContext =
pRegistrationFrame->scopetable,
DISPOSITION_NESTED_EXCEPTION );
}
_RtlpExecuteHandlerForUnwind使用的異常處理程序:
{
// 如果設置了展開標志,返回DISPOSITION_CONTINUE_SEARCH
// 否則,給pDispatcherContext賦值並返回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中是誰調用了誰
KiUserExceptionDispatcher()
RtlDispatchException()
RtlpExecuteHandlerForException()
ExecuteHandler() // 通常到 __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(中文)
每個分配堆棧空間、調用其他函數、保存非易失寄存器或使用異常處理的函數必須具有 Prolog,Prolog 的地址限制在與各自的函數表項關聯的展開數據中予以說明(請參見異常處理 (x64))。Prolog 將執行以下操作:必要時將參數寄存器保存在其內部地址中;將非易失寄存器推入堆棧;為局部變量和臨時變量分配堆棧的固定部分;(可選)建立幀指針。關聯的展開數據必須描述 Prolog 的操作,必須提供撤消 Prolog 代碼的影響所需的信息。
如果堆棧中的固定分配超過一頁(即大於 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 必須首先釋放堆棧的固定部分,彈出非易失寄存器,然後將控制返回調用函數。例如,
add RSP, fixed-allocation-size
pop R13
pop R14
pop R15
ret
如果函數中使用了幀指針,則在執行 Epilog 之前必須將堆棧修整為其固定分配。這在技術上不屬於 Epilog。例如,下面的 Epilog 可用於撤消前面使用的 Prolog:
lea RSP, -128[R13]
; epilogue proper starts here
add RSP, fixed-allocation-size
pop R13
pop R14
pop R15
ret
在實際應用中,使用幀指針時,沒有必要分兩個步驟調整 RSP,因此應改用以下 Epilog:
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 其余部分的執行,從而允許重新創建調用函數的上下文。