副标题#e#
展开
在挖掘展开(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;
}
#p#分页标题#e#
固然 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 符号。当一个历程被配置了这个符号时,它就不答允再运行,必需终止。
#p#副标题#e#
未处理惩罚异常
#p#分页标题#e#
在文章的前面,我并没有全面描写 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; // 返回以前的值
}
#p#分页标题#e#
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函数。正是这个函数发生了应用措施错误对话框。这个对话框期待你单击“确定”按钮来终止历程,可能单击“打消”按钮来调试它。(单击“打消”按钮而不是“确定”按钮来加载调试器仿佛有点颠倒了,大概这只是我小我私家的感受吧。)
#p#分页标题#e#
假如你单击“确定”,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 );
}
#p#分页标题#e#
KiUserExceptionDispatcher 的焦点是对 RtlDispatchException 的挪用。这拉开了搜索已注册的异常处理惩罚措施的序幕。假如某个处理惩罚措施处理惩罚这个异常并继承执行,那么对 RtlDispatchException 的挪用就不会返回。假如它返回了,只有两种大概:可能挪用了NtContinue以便让历程继承执行,可能发生了新的异常。假如是这样,那异常就不能再继承处理惩罚了,必需终止历程。
#p#分页标题#e#
此刻把眼光瞄准 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 挪用,而且不会损坏任何参数寄存器。
#p#分页标题#e#
生存非易失寄存器的首选要领是:在举办牢靠仓库分派之前将这些寄存器移入仓库。假如在生存非易失寄存器之前执行了牢靠仓库分派,则很大概需要 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 其余部门的执行,从而答允从头建设挪用函数的上下文。