我们知道,异常就像中断,不管是什么原因(“软异常”除外)所引起,一旦发生首先进入的是内核中的异常响应/ 处理程序的入口,这就是类似于KiTrap0()那样的底层内核函数,只是因为引起异常的原因不同而进入不同的入口,就像对于不同的中断向量有不同的入口 一样。在内核中,仍以页面异常为例,正如读者已经看到,CPU会从KiTrap14()进入函数KiPageFaultHandler()。在那儿,如果 所发生的并非如“缺页”或“写时复制(Copy-On-Write)”那样的“正常”异常,就要根据CPU在发生异常时所处的空间而分别调用 KiKernelTrapHandler()或KiUserTrapHandler()。如果调用的是KiKernelTrapHandler(),就会 顺着KPCR数据结构中的“异常(处理)队列”、即ExceptionList,依次让各个节点认领。如果被认领,就会通过SEHLongJmp()长程 跳转到当初通过_SEH_HANDLE{}给定的代码中。这读者已经见到了。
但是,如果异常发生于用户空间,受到调用的就是KiUserTrapHandler()。
[_KiTrap14() > KiPageFaultHandler() > KiUserTrapHandler()]
ULONG NTAPI
KiUserTrapHandler(PKTRAP_FRAME Tf, ULONG ExceptionNr, PVOID Cr2)
{
EXCEPTION_RECORD Er;
if (ExceptionNr == 0)
{
Er.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO;
}
else if (ExceptionNr == 1)
{
Er.ExceptionCode = STATUS_SINGLE_STEP;
}
else if (ExceptionNr == 3)
{
Er.ExceptionCode = STATUS_BREAKPOINT;
}
else if (ExceptionNr == 4)
{
Er.ExceptionCode = STATUS_INTEGER_OVERFLOW;
}
else if (ExceptionNr == 5)
{
Er.ExceptionCode = STATUS_ARRAY_BOUNDS_EXCEEDED;
}
else if (ExceptionNr == 6)
{
Er.ExceptionCode = STATUS_ILLEGAL_INSTRUCTION;
}
else
{
Er.ExceptionCode = STATUS_ACCESS_VIOLATION;
}
Er.ExceptionFlags = 0;
Er.ExceptionRecord = NULL;
Er.ExceptionAddress = (PVOID)Tf->Eip;
if (ExceptionNr == 14)
{
Er.NumberParameters = 2;
Er.ExceptionInformation[0] = Tf->ErrCode & 0x1;
Er.ExceptionInformation[1] = (ULONG)Cr2;
}
else
{
Er.NumberParameters = 0;
}
/* FIXME: Which exceptions are noncontinuable? */
Er.ExceptionFlags = 0;
KiDispatchException(&Er, 0, Tf, UserMode, TRUE);
return(0);
}
显然,这个函数不是仅为14号异常所用的。只要是发生于用户空间,别的异常也会进入这个函数。同样,这里也是先在堆栈上准备好一个“异常记录块”,然后调 用KiDispatchException(),只不过这一次的第四个实际参数是UserMode。读者在上一篇漫谈中看到, KiKernelTrapHandler()也调用这同一个函数,但是那里的第四个实际参数是KernelMode。
所以,KiDispatchException()是个十分重要的函数,现在我们又要回过头来看这个函数的代码。当然,这一次观察的角度不同了。
[_KiTrap14() > KiPageFaultHandler() > KiUserTrapHandler() > KiDispatchException()]
VOID NTAPI
KiDispatchException(PEXCEPTION_RECORD ExceptionRecord,
PKEXCEPTION_FRAME ExceptionFrame,
PKTRAP_FRAME TrapFrame,
KPROCESSOR_MODE PreviousMode,
BOOLEAN FirstChance)
{
. . . . . .
/* Set the context flags */
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
/* Check if User Mode */
if (PreviousMode == UserMode)
{
/* Add the FPU Flag */
Context.ContextFlags |= CONTEXT_FLOATING_POINT;
if (KeI386FxsrPresent) Context.ContextFlags |= CONTEXT_EXTENDED_REGISTERS;
}
/* Get a Context */
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);
/* Handle kernel-mode first, it's simpler */
if (PreviousMode == KernelMode)
{
. . . . . .
}
else
{
/* User mode exception, was it first-chance? */
if (FirstChance)
{
/* Enter Debugger if available */
Action = KdpEnterDebuggerException(ExceptionRecord, PreviousMode,
&Context, TrapFrame, TRUE, TRUE);
/* Exit if we're continuing */
if (Action == kdContinue) goto Handled;
/* FIXME: Forward exception to user mode debugger */
/* Set up the user-stack */
_SEH_TRY
{
/* Align context size and get stack pointer */
Size = (sizeof(CONTEXT) + 3) & ~3;
Stack = (Context.Esp & ~3) - Size;
DPRINT("Stack: %lx/n", Stack);
/* Probe stack and copy Context */
ProbeForWrite((PVOID)Stack, Size, sizeof(ULONG));
RtlCopyMemory((PVOID)Stack, &Context, sizeof(CONTEXT));
/* Align exception record size and get stack pointer */
Size = (sizeof(EXCEPTION_RECORD) -
(EXCEPTION_MAXIMUM_PARAMETERS -
ExceptionRecord->NumberParameters) * sizeof(ULONG) + 3) & ~3;
NewStack = Stack - Size;
DPRINT("NewStack: %lx/n", NewStack);
/* Probe stack and copy exception record. Don't forget to add the two params */
ProbeForWrite((PVOID)(NewStack - 2 * sizeof(ULONG_PTR)),
Size + 2 * sizeof(ULONG_PTR),
sizeof(ULONG));
RtlCopyMemory((PVOID)NewStack, ExceptionRecord, Size);
/* Now write the two params for the user-mode dispatcher */
*(PULONG_PTR)(NewStack - 1 * sizeof(ULONG_PTR)) = Stack;
*(PULONG_PTR)(NewStack - 2 * sizeof(ULONG_PTR)) = NewStack;
/* Set new Stack Pointer */
KiEspToTrapFrame(TrapFrame, NewStack - 2 * sizeof(ULONG_PTR));
/* Set EIP to the User-mode Dispathcer */
TrapFrame->Eip = (ULONG)KeUserExceptionDispatcher;
UserDispatch = TRUE;
_SEH_LEAVE;
}
_SEH_HANDLE
{
/* Do second-chance */
}
_SEH_END;
}
/* If we dispatch to user, return now */
if (UserDispatch) return;
/* FIXME: Forward the exception to the debugger for 2nd chance */
/* 3rd strike, kill the thread */
DPRINT1("Unhandled UserMode exception, terminating thread/n");
ZwTerminateThread(NtCurrentThread(), ExceptionRecord->ExceptionCode);
KEBUGCHECKWITHTF(KMODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG_PTR)ExceptionRecord->ExceptionAddress,
ExceptionRecord->ExceptionInformation[0],
ExceptionRecord->ExceptionInformation[1],
TrapFrame);
}
Handled:
/* Convert the context back into Trap/Exception Frames */
KeContextToTrapFrame(&Context, NULL, TrapFrame, Context.ContextFlags, PreviousMode);
return;
}
首先通过KeTrapFrameToContext()从堆栈上的异常框架整理出一个上下文数据结构来。不过,对于用户空间的异常处理上下文中需要有更全 面的信息,所以在调用这个函数之前把上下文结构中的CONTEXT_FLOATING_POINT等标志位设成1。这些标志位实质上就是对 KeTrapFrameToContext()的指令。
这一次我们把注意集中在PreviousMode为UserMode的分支上。
读者不妨想想,对于发生于用户空间的异常,这里应该做些什么。显然,用户空间的异常不应靠内核里面的程序处理,应用软件理应为此作好了准备。前面讲过, Windows的SEH机制并不是仅为内核而设计的,用户空间的程序同样可以使用类似于_SEH_TRY{} _SEH_HANDLE{} _SEH_END那样的手段为应用程序提供保护。事实上,在通过NtCreateThread()创建的线程首次被调度运行时,整个线程的执行都是作为一 个SEH域而受到保护的:
VOID STDCALL
BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress)
{
UINT uExitCode = 0;
_SEH_TRY
{
/* Set our Start Address */
NtSetInformationThread(NtCurrentThread(),ThreadQuerySetWin32StartAddress,
&lpStartAddress, sizeof(PPROCESS_START_ROUTINE));
/* Call the Start Routine */
uExitCode = (lpStartAddress)();
}
_SEH_EXCEPT(BaseExceptionFilter)
{
/* Get the SEH Error */
uExitCode = _SEH_GetExceptionCode();
}
_SEH_END;
/* Exit the Process with our error */
ExitProcess(uExitCode);
}
这里BaseProcessStartup()是所有线程在用户空间的总入口,而lpStartAddress是具体线程的代码入口。这里引用的宏操作之 一是_SEH_EXCEPT,而不是_SEH_HANDLE,因而可以提供一个过滤函数。这个过滤函数是BaseExceptionFilter(),它 又通过一个函数指针调用实际的过滤函数,默认为UnhandledExceptionFilter()。而 UnhandledExceptionFilter()在一般情况下都返回EXCEPTION_EXECUTE_HANDLER。不过,应用程序可以通过 一个函数SetUnhandledExceptionFilter()将其替换成自己想要的过滤函数。
与此相应,用户空间的每个线程都有一个ExceptionList,只不过这个队列在每个线程的TEB中,而不是在KPCR中。既然内核中的 ExceptionList是由KiDispatchException()加以处理的,用户空间就应该有个类似于 KiDispatchException()的函数。事实上,动态连接库ntdll.dll中的KiUserExceptionDispatcher() 就是用户空间SEH处理的总入口。
可是,尽管是发生于用户空间的异常,对异常的初期响应和处理毕竟是在内核中,现在的目的就是要从内核中的KiDispatchException()启动用户空间这个函数的执行。
对于内核中的KiDispatchException(),这就是针对用户空间异常的主要操作。不过具体的实现还要再复杂一些,就像针对系统空间异常一样,内核中涉及用户空间异常的处理也分三步:
第一步、参数FirstChance为1时,先通过KdpEnterDebuggerException()交由内核调试程序(Kernel Debugger)处理。如果内核调试程序解决了问题、或者认为无需提交用户空间,则返回值就是kdContinue,这就行了。否则就要把异常提交给用 户空间,由用户空间的程序加以处理。代码中的_SEH_TRY{}里面就是启动用户空间异常处理的过程。对于绝大多数的用户空间异常,这就可以了,因为用 户空间的ExceptionList中应该有节点可以认领和处理本次异常,例如通过预先的安排实施用户空间的长程跳转。
第二步、然而,万一用户空间处理不了,例如ExceptionList中没有安排下可以认领、处理本次异常的节点,就会通过 RtlRaiseException()、从而通过系统调用ZwRaiseException()发起一次“软异常”(见后),把问题交还内核。此时 CPU再次进入KiDispatchException(),但是此时的实际参数FirstChance为0,所以直接进入第二步措施。在Windows 内核中,这第二次努力是通过进程间通信向用户空间的调试程序(Debugger)发送一个报文、将其唤醒,由调试程序作进一步的处理。例如,对于由用户空 间调试程序设置的断点(INT3),就只能由用户空间调试程序加以处理。不过,在ReactOS 0.3.0版的代码中这一步尚未实现,所以这里有个注释说:“FIXME: Forward the exception to the debugger for 2nd chance”。
第三步、如果用户空间调试程序不存在,或者也不能解决,那就属于不可恢复的问题了。于是就有第三步措施,那就是通过ZwTerminateThread ()结束当前线程的运行。正常情况下针对当前线程本身的ZwTerminateThread()是不返回的;而倘若竟然返回了,那对于整个系统都是不可恢 复的问题了,所以通过宏操作KEBUGCHECKWITHTF()显示出错信息、转储(Dump)当时的内存映像,并进入一个 Ke386HaltProcessor()的无限循环。换言之,整个系统就“死”了。
显然,这里最关键的一步、也是最有希望的一步,是把异常提交给用户空间。怎么提交呢?首先要把上下文数据结构Context和异常纪录块 ExceptionRecord拷贝到用户空间堆栈上去,再在用户空间堆栈上安上两个指针,分别指向这两个数据结构的用户空间副本,并相应调整异常框架中 的用户空间堆栈指针。下面就会看到,这两个指针将被用作用户空间的函数调用参数。最后、也是最关键的,则是把异常框架中的用户空间返回地址设置成函数指针 KeUserExceptionDispatcher所指向的函数。顺利完成了这些准备以后,就把局部量UserDispatch设成1,因此紧接着就从 本次异常处理返回了。当然,这是返回到了指针KeUserExceptionDispatcher所指向的函数中。已经熟悉APC机制的读者应该很容易由 此联想到对用户空间APC函数的调用。事实上也确实非常相似,如果说APC相当于对用户空间软件的中断机制,则异常的提交就相当于对用户空间软件的异常机 制。
但是,将两个数据结构复制到用户空间堆栈的过程本身又是有可能会引起异常的,所以这里又用_SEH_TRY{} _SEH_HANDLE{} _SEH_END将这段代码保护起来,为可能发生的异常作好准备。倘若果真在此过程中发生(系统空间)异常,就直接进入上述的第二步努力。
再看函数指针KeUserExceptionDispatcher。这是内核中的一个全局量,实际上提供了Ntdll.dll映像中的 KiUserExceptionDispatcher()在当前进程内的地址。就像对于LdrInitializeThunk()和 KiUserApcDispatcher()以及其它几个函数一样,这也是在内核的初始化过程中,初次装入Ntdll.dll的映像的时候从其映像中获取 的:
[KiSystemStartup() > ExpInitializeExecutive() > PsLocateSystemDll()
> PspLookupKernelUserEntryPoints()]
NTSTATUS STDCALL INIT_FUNCTION
PspLookupKernelUserEntryPoints(VOID)
{
. . . . . .
/* Retrieve ntdll's startup address */
RtlInitAnsiString(&ProcedureName, "LdrInitializeThunk");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &PspSystemDllEntryPoint);
. . . . . .
/* Get User APC Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiUserApcDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserApcDispatcher);
. . . . . .
DPRINT("Getting Entrypoint/n");
RtlInitAnsiString(&ProcedureName, "KiUserExceptionDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserExceptionDispatcher);
. . . . . .
/* Get Callback Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiUserCallbackDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserCallbackDispatcher);
. . . . . .
/* Get Raise Exception Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiRaiseUserExceptionDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeRaiseUserExceptionDispatcher);
. . . . . .
return(STATUS_SUCCESS);
}
顺便提一下,这里的另一个指针KeRaiseUserExceptionDispatcher提供了Ntdll.dll映像中函数KiRaiseUserExceptionDispatcher()的地址,但是似乎只在NtClose()中用到。
于是,CPU从本次异常返回,回到用户空间时就进入了KiUserExceptionDispatcher(),那就是用户空间的异常响应/处理程序的入 口。与内核中有_KiTrap0()、_KiTrap14()等等入口不同,用户空间就只有这么一个总的入口。至于发生异常的原因与性质,则由异常纪录块 中的ExceptionCode字段加以界定。
VOID NTAPI
KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord,
PCONTEXT Context)
{
EXCEPTION_RECORD NestedExceptionRecord;
NTSTATUS Status;
/* call the vectored exception handlers */
if(RtlpExecuteVectoredExceptionHand lers(ExceptionRecord, Context)
!= ExceptionContinueExecution)
{
goto ContinueExecution;
}
else
{
/* Dispatch the exception and check the result */
if(RtlDispatchException(ExceptionRecord, Context))
{
ContinueExecution:
/* Continue executing */
Status = NtContinue(Context, FALSE);
}
else
{
/* Raise an exception */
Status = NtRaiseException(ExceptionRecord, Context, FALSE);
}
}
/* Setup the Exception record */
NestedExceptionRecord.ExceptionCode = Status;
NestedExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
NestedExceptionRecord.ExceptionRecord = ExceptionRecord;
NestedExceptionRecord.NumberParameters = Status;
/* Raise the exception */
RtlRaiseException(&NestedExceptionRecord);
}
注意这里的异常纪录块和上下文结构都是从内核复制到用户空间堆栈上的。
用户空间的异常机制是对于系统空间的异常机制的模拟。在内核中,并非所有的异常都是一来就进入“基于SEH框架(Frame-Based)”的异常处理, 而是先进入_KiTrap14()等等类似于向量中断的入口,在那里可以被拦截进行一些优先的处理,例如页面换入和对Copy-On-Write页面的处 理等等。这些处理是全局性质的,而不属于某个SEH域。这相当于是一层全局性的过滤。只有不属于这个层次的异常才会进入基于SEH框架的异常处理。
为此,用户空间的每个进程还有一个“向量式异常处理”程序入口的队列RtlpVectoredExceptionHead,队列中的每个节点都指向一个具 体的异常处理函数。就像内核中“登记”中断处理程序一样,在用户空间也可以通过RtlAddVectoredExceptionHandler()登记向 量式的异常处理程序,这样就可以在异常的源头上加以拦截(不过在Windows的资料中并未见到有用户空间向量式异常处理的存在,但是在Wine的代码中 倒是有)。这里RtlpExecuteVectoredExceptionHand lers()的作用就是先扫描这个队列,让队列中的各个节点依次认领,如果被认领并且得到了处理,就不再进入基于SEH框架、即 ExceptionList的处理了(代码中if语句的条件“!=”疑为“==”之误,待考),所以处理完之后就通过系统调用NtContinue()返 回内核,而进入内核以后又会返回用户空间当初因异常而被中断的地方。所以,系统调用NtContinue()实质上就是对“中断返回”的模拟。在讲述 APC机制的时候,读者已经看到用户空间的APC函数也是通过NtContinue()实行“中断返回”的(相当于Linux的系统调用 sigreturn())。
要是在向量式异常处理队列中没有得到处理,那就要通过RtlDispatchException()进行基于SEH框架的异常处理了。
读者在上一篇漫谈中已经看到过RtlDispatchException()的代码,大致如下:
[KiUserExceptionDispatcher() > RtlDispatchException()]
RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT Context)
{
. . . . . .
/* Get the current stack limits and registration frame */
RtlpGetStackLimits(&StackLow, &StackHigh);
RegistrationFrame = RtlpGetExceptionList();
/* Now loop every frame */
while (RegistrationFrame != EXCEPTION_CHAIN_END)
{
. . . . . . /* 扫描ExceptionList,直至有节点认领/处理本次异常 */
}
/* Unhandled, return false */
return FALSE;
}
但是,那是在内核中,作为异常处理的第一步措施(FirstChance),由KiDispatchException()针对发生于系统空间的异常而加 以调用的。而现在,则显然是在用户空间调用这个函数。那莫非在用户空间就可以直接调用内核中的函数吗?不是的,这个 RtlDispatchException()并非那个RtlDispatchException(),这只是“代码重用”而已。这个 RtlDispatchException()编译以后连接在Ntdll.dll的映像中(不过并不导出),并且所处理的ExceptionList是在 用户空间,所使用的堆栈也是用户空间堆栈。
ExceptionList是通过RtlpGetExceptionList()获取的。
_RtlpGetExceptionList@0:
/* Return the exception list */
mov eax, [fs:TEB_EXCEPTION_LIST]
ret
常数TEB_EXCEPTION_LIST定义为0,所以总之就是[fs:0]。当CPU运行于系统空间时,其段寄存器FS指向KPCR;而当运行于用户 空间时则指向TEB。所以,不管是在系统空间还是用户空间,RtlpGetExceptionList()总能取到各自的ExceptionList指 针。
RtlpGetStackLimits()也是一样:
_RtlpGetStackLimits@8:
/* Get the stack limits */
mov eax, [fs:TEB_STACK_LIMIT]
mov ecx, [fs:TEB_STACK_BASE]
/* Return them */
mov edx, [esp+4]
mov [edx], eax
mov edx, [esp+8]
mov [edx], ecx
/* return */
ret 8
常数TEB_STACK_LIMIT定义为8,TEB_STACK_BASE定义为4,分别是StackLimit和StackBase两个字段相对于 TEB起点的位移。而在KPCR数据结构中同样也有这两个字段,而且也在相同的位置上。当然,前者用于用户空间堆栈,而后者用于系统空间堆栈。由此可见, 这些数据结构确实是经过精心设计的。另一方面,在处理ExceptionList的过程中所涉及的各种数据结构也都既用于系统空间又用于用户空间。
这样,同一个函数RtlDispatchException()的代码就既可用于内核,也可用于用户空间的程序库。既然如此,我们也就不必再看一遍RtlDispatchException()的代码了。
回到KiUserExceptionDispatcher(),执行RtlDispatchException()的结果主要有三种可能。
一种是当前异常为ExceptionList中的某个节点所认领、即顺利通过了其过滤函数的过滤,并执行了长程跳转。显然,在这种情况下 RtlDispatchException()不会返回,包括KiUserExceptionDispatcher()在内的函数调用框架均因长程跳转而 被跨越和丢弃。不仅如此,复制到用户空间堆栈上的两个数据结构也因为长程跳转而被丢弃。
第二种是被某个节点认领了,也作了某些处理,也许还执行了这个节点的善后函数,但是并未执行长程跳转,此时RtlDispatchException() 返回TRUE,于是便通过系统调用NtContinue()完成“中断返回”。由于堆栈上的上下文数据结构含有当初因异常而进入系统空间时所保留的现场, 最后就返回到了(用户空间中)当初因异常而被中断了的地方。
第三种是没有任何节点认领,RtlDispatchException()中的while循环穷尽了所有的节点,所以返回FALSE。这一定是出了什么问 题,例如ExceptionList被损坏了,因为在正常情况下这是不应该发生的。前面已经提及,在用户空间,整个线程的代码都是放在一个SEH域里执行 的,所以ExceptionList一定是非空,并且其最后一个节点(最早进入的)就是前面在BaseProcessStartup()中进入的那个 SEH域。这个SEH域的过滤函数一般都返回_SEH_EXECUTE_HANDLER,所以是来者不拒。因此,既然穷尽了ExceptionList, 就一定是发生了严重的问题,所以要通过系统调用NtRaiseException()引起一次软异常,以进入针对当前异常的第二步措施。注意这里调用 NtRaiseException()时的第三个参数为FALSE,表示这已经不是第一次尝试。
此外还有一种可能,就是在RtlDispatchException()内部就已调用了RtlRaiseException(),例如针对某个节点的 RtlpExecuteHandlerForException()返回ExceptionContinueExecution,而异常纪录块中 ExceptionFlags的标志位EXCEPTION_NONCONTINUABLE却又是1,此时就要通过RtlRaiseException() 引起原因为STATUS_NONCONTINUABLE_EXCEPTION的软异常。
在正常的情况下,对于调用点而言,系统调用NtContinue()和NtRaiseException()是不返回的,返回就说明系统调用本身(而不是 对异常的处理)出了错,例如参数有问题。因此,要是果真从这两个系统调用返回,那么正常的处理已经山穷水尽,只能又求助于软异常了,这就是下面对于 RtlRaiseException()的调用。
调用RtlRaiseException()的目的在于模拟一次异常,这“异常”已经不是原来所要处理的异常,而是对原来的异常处理不下去了。此时需要解 决的已经不是引起原来那个异常的问题,而是为什么处理不下去的问题。所以原来的“异常代码”ExceptionCode可能是 STATUS_ACCESS_VIOLATION,而现在的ExceptionCode则是从NtRaiseException()或 NtContinue()返回的出错代码Status。同样的道理,通过RtlDispatchException()发起的则是 STATUS_NONCONTINUABLE_EXCEPTION软异常。正因为现在要发起的异常并非原来要处理的异常,所以要为 RtlRaiseException()准备一个新的异常纪录块。
另一方面,之所以通过RtlRaiseException()发起各种不同类型的软异常,是建立在一个前提上的,那就是相信这个异常最终总会得到处理。这 包括两个方面,首先是ExceptionList中应该有能够认领/处理此类异常的节点,即已经准备好了应对此类问题所需的程序。其次,即使 ExceptionList中没有这样的节点,终归也有应对的办法,那就是前面讲的三个步骤,包括Debug、结束当前线程、甚至“死机”在内。注意“死 机”也有受控和失控之分,使CPU进入停机状态是有控制的死机,那也比任由CPU在程序中乱跳一气要好。
然而,尽管现在要发起的是与原来不同的异常,但是这毕竟是在处理原来异常的过程中出的问题,与原来的异常是有关系的,应该让认领新发起异常的处理者知道这 一点。所以异常纪录块中有个指针ExceptionRecord,遇到像这样的情况就让新的异常纪录块通过这个指针指向原来的异常纪录块。所以,软异常的 纪录块可以成串,每个纪录块都可以指向本次异常的祸端,只有硬异常的纪录块不会指向别的纪录块。
显然,RtlRaiseException()是个重要的函数。同样,这个函数的代码也是内核和用户空间两栖的。注意下列的调用路线只是许多情景中的一种,实际上调用这个函数的地方很多。
[KiUserExceptionDispatcher() > RtlRaiseException()]
VOID NTAPI RtlRaiseException(PEXCEPTION_RECORD ExceptionRecord)
{
CONTEXT Context;
. . . . . .
/* Capture the context */
RtlCaptureContext(&Context);
/* Save the exception address */
ExceptionRecord->ExceptionAddress = RtlpGetExceptionAddress();
/* Write the context flag */
Context.ContextFlags = CONTEXT_FULL;
/* Check if we're being debugged (user-mode only) */
if (!RtlpCheckForActiveDebugger(TRUE))
{
/* Raise an exception immediately */
Status = ZwRaiseException(ExceptionRecord, &Context, TRUE);
}
else
{
/* Dispatch the exception and check if we should continue */
if (RtlDispatchException(ExceptionRecord, &Context))
{
/* Raise the exception */
Status = ZwRaiseException(ExceptionRecord, &Context, FALSE);
}
else
{
/* Continue, go back to previous context */
Status = ZwContinue(&Context, FALSE);
}
}
/* If we returned, raise a status */
RtlRaiseStatus(Status);
}
调用参数ExceptionRecord指向一个异常纪录块。如上所述,这个纪录块中记载着异常的性质,并且一般都通过指针指向另一个异常纪录块。
首先通过RtlCaptureContext()获取当时的上下文,并通过RtlpGetExceptionAddress()获取RtlRaiseException()的返回地址,以此作为发生本次异常的地址。
具体的操作视RtlpCheckForActiveDebugger()的结果而异。这个函数有系统空间和用户空间两个不同的版本。目前系统空间的版本只 是返回调用参数,所以在这里总是返回TRYE。而用户空间的版本则返回NtCurrentPeb()->BeingDebugged,所以只在受调 试时返回TRUE。
所以,如果是在用户空间调用RtlRaiseException(),而又不是在受调试,就启动系统调用ZwRaiseException()。注意此时 的第三个参数为TRUE,表示这是第一次努力。可见,在这种情况下RtlRaiseException()是通过系统调用 ZwRaiseException()实现的。而且所用的函数名是ZwRaiseException(),在用户空间和系统空间都可以调用。
反之如果是在系统空间,或者虽在用户空间但是在受调试,那就先直接调用RtlDispatchException(),看看本空间的 ExceptionList中是否有节点可以认领和处理。如果有、并且实施了长程跳转,那就不返回了。而如果返回的话,则视返回值为TRUE或FALSE 而分别启动系统调用ZwRaiseException()或ZwContinue()。如果RtlDispatchException()返回TRUE, 就说明已经得到ExceptionList中某个节点的认领、但是并未执行长程跳转而返回ExceptionContinueExecution,此时通 过ZwRaiseException()进行第二次努力,注意这里调用ZwRaiseException()时的第3个参数为FALSE,表示对 ExceptionList的搜索已经失败,下面该采取第二步措施了。而如果RtlDispatchException()返回FALSE,则说明 ExceptionList中根本就没有节点可以认领这次异常,所以就直接通过ZwContinue()返回到刚才获取的那个上下文中,那就是 RtlRaiseException()本次被调用的地方。
在我们现在所考察的情景中,RtlRaiseException()是在KiUserExceptionDispatcher()中受到调用的,并且在此 之前已经有过对ZwRaiseException()或ZwContinue()的调用。那么现在又来调用ZwRaiseException()或 ZwContinue()是否重复呢?不重复,因为使用的是不同的异常纪录块,不同的异常代码,这是为不同原因而发起的软异常。
还有,为什么在受调试的情况下反倒要直接调用RtlDispatchException(),避开调试处理呢?因为调试工具所安排的都是针对正常条件下的 异常,例如页面访问异常、除数为0等等,而现在发生的是在处理异常的过程中本来不应该发生的问题,是属于系统的问题,那不是属于应该由调试工具来处理的问 题。而如果直接通过ZwRaiseException()发起一次软中断,则必将首先进入调试程序。
但是,如果异常发生于用户空间,受到调用的就是KiUserTrapHandler()。
[_KiTrap14() > KiPageFaultHandler() > KiUserTrapHandler()]
ULONG NTAPI
KiUserTrapHandler(PKTRAP_FRAME Tf, ULONG ExceptionNr, PVOID Cr2)
{
EXCEPTION_RECORD Er;
if (ExceptionNr == 0)
{
Er.ExceptionCode = STATUS_INTEGER_DIVIDE_BY_ZERO;
}
else if (ExceptionNr == 1)
{
Er.ExceptionCode = STATUS_SINGLE_STEP;
}
else if (ExceptionNr == 3)
{
Er.ExceptionCode = STATUS_BREAKPOINT;
}
else if (ExceptionNr == 4)
{
Er.ExceptionCode = STATUS_INTEGER_OVERFLOW;
}
else if (ExceptionNr == 5)
{
Er.ExceptionCode = STATUS_ARRAY_BOUNDS_EXCEEDED;
}
else if (ExceptionNr == 6)
{
Er.ExceptionCode = STATUS_ILLEGAL_INSTRUCTION;
}
else
{
Er.ExceptionCode = STATUS_ACCESS_VIOLATION;
}
Er.ExceptionFlags = 0;
Er.ExceptionRecord = NULL;
Er.ExceptionAddress = (PVOID)Tf->Eip;
if (ExceptionNr == 14)
{
Er.NumberParameters = 2;
Er.ExceptionInformation[0] = Tf->ErrCode & 0x1;
Er.ExceptionInformation[1] = (ULONG)Cr2;
}
else
{
Er.NumberParameters = 0;
}
/* FIXME: Which exceptions are noncontinuable? */
Er.ExceptionFlags = 0;
KiDispatchException(&Er, 0, Tf, UserMode, TRUE);
return(0);
}
显然,这个函数不是仅为14号异常所用的。只要是发生于用户空间,别的异常也会进入这个函数。同样,这里也是先在堆栈上准备好一个“异常记录块”,然后调 用KiDispatchException(),只不过这一次的第四个实际参数是UserMode。读者在上一篇漫谈中看到, KiKernelTrapHandler()也调用这同一个函数,但是那里的第四个实际参数是KernelMode。
所以,KiDispatchException()是个十分重要的函数,现在我们又要回过头来看这个函数的代码。当然,这一次观察的角度不同了。
[_KiTrap14() > KiPageFaultHandler() > KiUserTrapHandler() > KiDispatchException()]
VOID NTAPI
KiDispatchException(PEXCEPTION_RECORD ExceptionRecord,
PKEXCEPTION_FRAME ExceptionFrame,
PKTRAP_FRAME TrapFrame,
KPROCESSOR_MODE PreviousMode,
BOOLEAN FirstChance)
{
. . . . . .
/* Set the context flags */
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
/* Check if User Mode */
if (PreviousMode == UserMode)
{
/* Add the FPU Flag */
Context.ContextFlags |= CONTEXT_FLOATING_POINT;
if (KeI386FxsrPresent) Context.ContextFlags |= CONTEXT_EXTENDED_REGISTERS;
}
/* Get a Context */
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);
/* Handle kernel-mode first, it's simpler */
if (PreviousMode == KernelMode)
{
. . . . . .
}
else
{
/* User mode exception, was it first-chance? */
if (FirstChance)
{
/* Enter Debugger if available */
Action = KdpEnterDebuggerException(ExceptionRecord, PreviousMode,
&Context, TrapFrame, TRUE, TRUE);
/* Exit if we're continuing */
if (Action == kdContinue) goto Handled;
/* FIXME: Forward exception to user mode debugger */
/* Set up the user-stack */
_SEH_TRY
{
/* Align context size and get stack pointer */
Size = (sizeof(CONTEXT) + 3) & ~3;
Stack = (Context.Esp & ~3) - Size;
DPRINT("Stack: %lx/n", Stack);
/* Probe stack and copy Context */
ProbeForWrite((PVOID)Stack, Size, sizeof(ULONG));
RtlCopyMemory((PVOID)Stack, &Context, sizeof(CONTEXT));
/* Align exception record size and get stack pointer */
Size = (sizeof(EXCEPTION_RECORD) -
(EXCEPTION_MAXIMUM_PARAMETERS -
ExceptionRecord->NumberParameters) * sizeof(ULONG) + 3) & ~3;
NewStack = Stack - Size;
DPRINT("NewStack: %lx/n", NewStack);
/* Probe stack and copy exception record. Don't forget to add the two params */
ProbeForWrite((PVOID)(NewStack - 2 * sizeof(ULONG_PTR)),
Size + 2 * sizeof(ULONG_PTR),
sizeof(ULONG));
RtlCopyMemory((PVOID)NewStack, ExceptionRecord, Size);
/* Now write the two params for the user-mode dispatcher */
*(PULONG_PTR)(NewStack - 1 * sizeof(ULONG_PTR)) = Stack;
*(PULONG_PTR)(NewStack - 2 * sizeof(ULONG_PTR)) = NewStack;
/* Set new Stack Pointer */
KiEspToTrapFrame(TrapFrame, NewStack - 2 * sizeof(ULONG_PTR));
/* Set EIP to the User-mode Dispathcer */
TrapFrame->Eip = (ULONG)KeUserExceptionDispatcher;
UserDispatch = TRUE;
_SEH_LEAVE;
}
_SEH_HANDLE
{
/* Do second-chance */
}
_SEH_END;
}
/* If we dispatch to user, return now */
if (UserDispatch) return;
/* FIXME: Forward the exception to the debugger for 2nd chance */
/* 3rd strike, kill the thread */
DPRINT1("Unhandled UserMode exception, terminating thread/n");
ZwTerminateThread(NtCurrentThread(), ExceptionRecord->ExceptionCode);
KEBUGCHECKWITHTF(KMODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG_PTR)ExceptionRecord->ExceptionAddress,
ExceptionRecord->ExceptionInformation[0],
ExceptionRecord->ExceptionInformation[1],
TrapFrame);
}
Handled:
/* Convert the context back into Trap/Exception Frames */
KeContextToTrapFrame(&Context, NULL, TrapFrame, Context.ContextFlags, PreviousMode);
return;
}
首先通过KeTrapFrameToContext()从堆栈上的异常框架整理出一个上下文数据结构来。不过,对于用户空间的异常处理上下文中需要有更全 面的信息,所以在调用这个函数之前把上下文结构中的CONTEXT_FLOATING_POINT等标志位设成1。这些标志位实质上就是对 KeTrapFrameToContext()的指令。
这一次我们把注意集中在PreviousMode为UserMode的分支上。
读者不妨想想,对于发生于用户空间的异常,这里应该做些什么。显然,用户空间的异常不应靠内核里面的程序处理,应用软件理应为此作好了准备。前面讲过, Windows的SEH机制并不是仅为内核而设计的,用户空间的程序同样可以使用类似于_SEH_TRY{} _SEH_HANDLE{} _SEH_END那样的手段为应用程序提供保护。事实上,在通过NtCreateThread()创建的线程首次被调度运行时,整个线程的执行都是作为一 个SEH域而受到保护的:
VOID STDCALL
BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress)
{
UINT uExitCode = 0;
_SEH_TRY
{
/* Set our Start Address */
NtSetInformationThread(NtCurrentThread(),ThreadQuerySetWin32StartAddress,
&lpStartAddress, sizeof(PPROCESS_START_ROUTINE));
/* Call the Start Routine */
uExitCode = (lpStartAddress)();
}
_SEH_EXCEPT(BaseExceptionFilter)
{
/* Get the SEH Error */
uExitCode = _SEH_GetExceptionCode();
}
_SEH_END;
/* Exit the Process with our error */
ExitProcess(uExitCode);
}
这里BaseProcessStartup()是所有线程在用户空间的总入口,而lpStartAddress是具体线程的代码入口。这里引用的宏操作之 一是_SEH_EXCEPT,而不是_SEH_HANDLE,因而可以提供一个过滤函数。这个过滤函数是BaseExceptionFilter(),它 又通过一个函数指针调用实际的过滤函数,默认为UnhandledExceptionFilter()。而 UnhandledExceptionFilter()在一般情况下都返回EXCEPTION_EXECUTE_HANDLER。不过,应用程序可以通过 一个函数SetUnhandledExceptionFilter()将其替换成自己想要的过滤函数。
与此相应,用户空间的每个线程都有一个ExceptionList,只不过这个队列在每个线程的TEB中,而不是在KPCR中。既然内核中的 ExceptionList是由KiDispatchException()加以处理的,用户空间就应该有个类似于 KiDispatchException()的函数。事实上,动态连接库ntdll.dll中的KiUserExceptionDispatcher() 就是用户空间SEH处理的总入口。
可是,尽管是发生于用户空间的异常,对异常的初期响应和处理毕竟是在内核中,现在的目的就是要从内核中的KiDispatchException()启动用户空间这个函数的执行。
对于内核中的KiDispatchException(),这就是针对用户空间异常的主要操作。不过具体的实现还要再复杂一些,就像针对系统空间异常一样,内核中涉及用户空间异常的处理也分三步:
第一步、参数FirstChance为1时,先通过KdpEnterDebuggerException()交由内核调试程序(Kernel Debugger)处理。如果内核调试程序解决了问题、或者认为无需提交用户空间,则返回值就是kdContinue,这就行了。否则就要把异常提交给用 户空间,由用户空间的程序加以处理。代码中的_SEH_TRY{}里面就是启动用户空间异常处理的过程。对于绝大多数的用户空间异常,这就可以了,因为用 户空间的ExceptionList中应该有节点可以认领和处理本次异常,例如通过预先的安排实施用户空间的长程跳转。
第二步、然而,万一用户空间处理不了,例如ExceptionList中没有安排下可以认领、处理本次异常的节点,就会通过 RtlRaiseException()、从而通过系统调用ZwRaiseException()发起一次“软异常”(见后),把问题交还内核。此时 CPU再次进入KiDispatchException(),但是此时的实际参数FirstChance为0,所以直接进入第二步措施。在Windows 内核中,这第二次努力是通过进程间通信向用户空间的调试程序(Debugger)发送一个报文、将其唤醒,由调试程序作进一步的处理。例如,对于由用户空 间调试程序设置的断点(INT3),就只能由用户空间调试程序加以处理。不过,在ReactOS 0.3.0版的代码中这一步尚未实现,所以这里有个注释说:“FIXME: Forward the exception to the debugger for 2nd chance”。
第三步、如果用户空间调试程序不存在,或者也不能解决,那就属于不可恢复的问题了。于是就有第三步措施,那就是通过ZwTerminateThread ()结束当前线程的运行。正常情况下针对当前线程本身的ZwTerminateThread()是不返回的;而倘若竟然返回了,那对于整个系统都是不可恢 复的问题了,所以通过宏操作KEBUGCHECKWITHTF()显示出错信息、转储(Dump)当时的内存映像,并进入一个 Ke386HaltProcessor()的无限循环。换言之,整个系统就“死”了。
显然,这里最关键的一步、也是最有希望的一步,是把异常提交给用户空间。怎么提交呢?首先要把上下文数据结构Context和异常纪录块 ExceptionRecord拷贝到用户空间堆栈上去,再在用户空间堆栈上安上两个指针,分别指向这两个数据结构的用户空间副本,并相应调整异常框架中 的用户空间堆栈指针。下面就会看到,这两个指针将被用作用户空间的函数调用参数。最后、也是最关键的,则是把异常框架中的用户空间返回地址设置成函数指针 KeUserExceptionDispatcher所指向的函数。顺利完成了这些准备以后,就把局部量UserDispatch设成1,因此紧接着就从 本次异常处理返回了。当然,这是返回到了指针KeUserExceptionDispatcher所指向的函数中。已经熟悉APC机制的读者应该很容易由 此联想到对用户空间APC函数的调用。事实上也确实非常相似,如果说APC相当于对用户空间软件的中断机制,则异常的提交就相当于对用户空间软件的异常机 制。
但是,将两个数据结构复制到用户空间堆栈的过程本身又是有可能会引起异常的,所以这里又用_SEH_TRY{} _SEH_HANDLE{} _SEH_END将这段代码保护起来,为可能发生的异常作好准备。倘若果真在此过程中发生(系统空间)异常,就直接进入上述的第二步努力。
再看函数指针KeUserExceptionDispatcher。这是内核中的一个全局量,实际上提供了Ntdll.dll映像中的 KiUserExceptionDispatcher()在当前进程内的地址。就像对于LdrInitializeThunk()和 KiUserApcDispatcher()以及其它几个函数一样,这也是在内核的初始化过程中,初次装入Ntdll.dll的映像的时候从其映像中获取 的:
[KiSystemStartup() > ExpInitializeExecutive() > PsLocateSystemDll()
> PspLookupKernelUserEntryPoints()]
NTSTATUS STDCALL INIT_FUNCTION
PspLookupKernelUserEntryPoints(VOID)
{
. . . . . .
/* Retrieve ntdll's startup address */
RtlInitAnsiString(&ProcedureName, "LdrInitializeThunk");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &PspSystemDllEntryPoint);
. . . . . .
/* Get User APC Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiUserApcDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserApcDispatcher);
. . . . . .
DPRINT("Getting Entrypoint/n");
RtlInitAnsiString(&ProcedureName, "KiUserExceptionDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserExceptionDispatcher);
. . . . . .
/* Get Callback Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiUserCallbackDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeUserCallbackDispatcher);
. . . . . .
/* Get Raise Exception Dispatcher */
RtlInitAnsiString(&ProcedureName, "KiRaiseUserExceptionDispatcher");
Status = LdrGetProcedureAddress((PVOID)PspSystemDllBase, &ProcedureName,
0, &KeRaiseUserExceptionDispatcher);
. . . . . .
return(STATUS_SUCCESS);
}
顺便提一下,这里的另一个指针KeRaiseUserExceptionDispatcher提供了Ntdll.dll映像中函数KiRaiseUserExceptionDispatcher()的地址,但是似乎只在NtClose()中用到。
于是,CPU从本次异常返回,回到用户空间时就进入了KiUserExceptionDispatcher(),那就是用户空间的异常响应/处理程序的入 口。与内核中有_KiTrap0()、_KiTrap14()等等入口不同,用户空间就只有这么一个总的入口。至于发生异常的原因与性质,则由异常纪录块 中的ExceptionCode字段加以界定。
VOID NTAPI
KiUserExceptionDispatcher(PEXCEPTION_RECORD ExceptionRecord,
PCONTEXT Context)
{
EXCEPTION_RECORD NestedExceptionRecord;
NTSTATUS Status;
/* call the vectored exception handlers */
if(RtlpExecuteVectoredExceptionHand lers(ExceptionRecord, Context)
!= ExceptionContinueExecution)
{
goto ContinueExecution;
}
else
{
/* Dispatch the exception and check the result */
if(RtlDispatchException(ExceptionRecord, Context))
{
ContinueExecution:
/* Continue executing */
Status = NtContinue(Context, FALSE);
}
else
{
/* Raise an exception */
Status = NtRaiseException(ExceptionRecord, Context, FALSE);
}
}
/* Setup the Exception record */
NestedExceptionRecord.ExceptionCode = Status;
NestedExceptionRecord.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
NestedExceptionRecord.ExceptionRecord = ExceptionRecord;
NestedExceptionRecord.NumberParameters = Status;
/* Raise the exception */
RtlRaiseException(&NestedExceptionRecord);
}
注意这里的异常纪录块和上下文结构都是从内核复制到用户空间堆栈上的。
用户空间的异常机制是对于系统空间的异常机制的模拟。在内核中,并非所有的异常都是一来就进入“基于SEH框架(Frame-Based)”的异常处理, 而是先进入_KiTrap14()等等类似于向量中断的入口,在那里可以被拦截进行一些优先的处理,例如页面换入和对Copy-On-Write页面的处 理等等。这些处理是全局性质的,而不属于某个SEH域。这相当于是一层全局性的过滤。只有不属于这个层次的异常才会进入基于SEH框架的异常处理。
为此,用户空间的每个进程还有一个“向量式异常处理”程序入口的队列RtlpVectoredExceptionHead,队列中的每个节点都指向一个具 体的异常处理函数。就像内核中“登记”中断处理程序一样,在用户空间也可以通过RtlAddVectoredExceptionHandler()登记向 量式的异常处理程序,这样就可以在异常的源头上加以拦截(不过在Windows的资料中并未见到有用户空间向量式异常处理的存在,但是在Wine的代码中 倒是有)。这里RtlpExecuteVectoredExceptionHand lers()的作用就是先扫描这个队列,让队列中的各个节点依次认领,如果被认领并且得到了处理,就不再进入基于SEH框架、即 ExceptionList的处理了(代码中if语句的条件“!=”疑为“==”之误,待考),所以处理完之后就通过系统调用NtContinue()返 回内核,而进入内核以后又会返回用户空间当初因异常而被中断的地方。所以,系统调用NtContinue()实质上就是对“中断返回”的模拟。在讲述 APC机制的时候,读者已经看到用户空间的APC函数也是通过NtContinue()实行“中断返回”的(相当于Linux的系统调用 sigreturn())。
要是在向量式异常处理队列中没有得到处理,那就要通过RtlDispatchException()进行基于SEH框架的异常处理了。
读者在上一篇漫谈中已经看到过RtlDispatchException()的代码,大致如下:
[KiUserExceptionDispatcher() > RtlDispatchException()]
RtlDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
IN PCONTEXT Context)
{
. . . . . .
/* Get the current stack limits and registration frame */
RtlpGetStackLimits(&StackLow, &StackHigh);
RegistrationFrame = RtlpGetExceptionList();
/* Now loop every frame */
while (RegistrationFrame != EXCEPTION_CHAIN_END)
{
. . . . . . /* 扫描ExceptionList,直至有节点认领/处理本次异常 */
}
/* Unhandled, return false */
return FALSE;
}
但是,那是在内核中,作为异常处理的第一步措施(FirstChance),由KiDispatchException()针对发生于系统空间的异常而加 以调用的。而现在,则显然是在用户空间调用这个函数。那莫非在用户空间就可以直接调用内核中的函数吗?不是的,这个 RtlDispatchException()并非那个RtlDispatchException(),这只是“代码重用”而已。这个 RtlDispatchException()编译以后连接在Ntdll.dll的映像中(不过并不导出),并且所处理的ExceptionList是在 用户空间,所使用的堆栈也是用户空间堆栈。
ExceptionList是通过RtlpGetExceptionList()获取的。
_RtlpGetExceptionList@0:
/* Return the exception list */
mov eax, [fs:TEB_EXCEPTION_LIST]
ret
常数TEB_EXCEPTION_LIST定义为0,所以总之就是[fs:0]。当CPU运行于系统空间时,其段寄存器FS指向KPCR;而当运行于用户 空间时则指向TEB。所以,不管是在系统空间还是用户空间,RtlpGetExceptionList()总能取到各自的ExceptionList指 针。
RtlpGetStackLimits()也是一样:
_RtlpGetStackLimits@8:
/* Get the stack limits */
mov eax, [fs:TEB_STACK_LIMIT]
mov ecx, [fs:TEB_STACK_BASE]
/* Return them */
mov edx, [esp+4]
mov [edx], eax
mov edx, [esp+8]
mov [edx], ecx
/* return */
ret 8
常数TEB_STACK_LIMIT定义为8,TEB_STACK_BASE定义为4,分别是StackLimit和StackBase两个字段相对于 TEB起点的位移。而在KPCR数据结构中同样也有这两个字段,而且也在相同的位置上。当然,前者用于用户空间堆栈,而后者用于系统空间堆栈。由此可见, 这些数据结构确实是经过精心设计的。另一方面,在处理ExceptionList的过程中所涉及的各种数据结构也都既用于系统空间又用于用户空间。
这样,同一个函数RtlDispatchException()的代码就既可用于内核,也可用于用户空间的程序库。既然如此,我们也就不必再看一遍RtlDispatchException()的代码了。
回到KiUserExceptionDispatcher(),执行RtlDispatchException()的结果主要有三种可能。
一种是当前异常为ExceptionList中的某个节点所认领、即顺利通过了其过滤函数的过滤,并执行了长程跳转。显然,在这种情况下 RtlDispatchException()不会返回,包括KiUserExceptionDispatcher()在内的函数调用框架均因长程跳转而 被跨越和丢弃。不仅如此,复制到用户空间堆栈上的两个数据结构也因为长程跳转而被丢弃。
第二种是被某个节点认领了,也作了某些处理,也许还执行了这个节点的善后函数,但是并未执行长程跳转,此时RtlDispatchException() 返回TRUE,于是便通过系统调用NtContinue()完成“中断返回”。由于堆栈上的上下文数据结构含有当初因异常而进入系统空间时所保留的现场, 最后就返回到了(用户空间中)当初因异常而被中断了的地方。
第三种是没有任何节点认领,RtlDispatchException()中的while循环穷尽了所有的节点,所以返回FALSE。这一定是出了什么问 题,例如ExceptionList被损坏了,因为在正常情况下这是不应该发生的。前面已经提及,在用户空间,整个线程的代码都是放在一个SEH域里执行 的,所以ExceptionList一定是非空,并且其最后一个节点(最早进入的)就是前面在BaseProcessStartup()中进入的那个 SEH域。这个SEH域的过滤函数一般都返回_SEH_EXECUTE_HANDLER,所以是来者不拒。因此,既然穷尽了ExceptionList, 就一定是发生了严重的问题,所以要通过系统调用NtRaiseException()引起一次软异常,以进入针对当前异常的第二步措施。注意这里调用 NtRaiseException()时的第三个参数为FALSE,表示这已经不是第一次尝试。
此外还有一种可能,就是在RtlDispatchException()内部就已调用了RtlRaiseException(),例如针对某个节点的 RtlpExecuteHandlerForException()返回ExceptionContinueExecution,而异常纪录块中 ExceptionFlags的标志位EXCEPTION_NONCONTINUABLE却又是1,此时就要通过RtlRaiseException() 引起原因为STATUS_NONCONTINUABLE_EXCEPTION的软异常。
在正常的情况下,对于调用点而言,系统调用NtContinue()和NtRaiseException()是不返回的,返回就说明系统调用本身(而不是 对异常的处理)出了错,例如参数有问题。因此,要是果真从这两个系统调用返回,那么正常的处理已经山穷水尽,只能又求助于软异常了,这就是下面对于 RtlRaiseException()的调用。
调用RtlRaiseException()的目的在于模拟一次异常,这“异常”已经不是原来所要处理的异常,而是对原来的异常处理不下去了。此时需要解 决的已经不是引起原来那个异常的问题,而是为什么处理不下去的问题。所以原来的“异常代码”ExceptionCode可能是 STATUS_ACCESS_VIOLATION,而现在的ExceptionCode则是从NtRaiseException()或 NtContinue()返回的出错代码Status。同样的道理,通过RtlDispatchException()发起的则是 STATUS_NONCONTINUABLE_EXCEPTION软异常。正因为现在要发起的异常并非原来要处理的异常,所以要为 RtlRaiseException()准备一个新的异常纪录块。
另一方面,之所以通过RtlRaiseException()发起各种不同类型的软异常,是建立在一个前提上的,那就是相信这个异常最终总会得到处理。这 包括两个方面,首先是ExceptionList中应该有能够认领/处理此类异常的节点,即已经准备好了应对此类问题所需的程序。其次,即使 ExceptionList中没有这样的节点,终归也有应对的办法,那就是前面讲的三个步骤,包括Debug、结束当前线程、甚至“死机”在内。注意“死 机”也有受控和失控之分,使CPU进入停机状态是有控制的死机,那也比任由CPU在程序中乱跳一气要好。
然而,尽管现在要发起的是与原来不同的异常,但是这毕竟是在处理原来异常的过程中出的问题,与原来的异常是有关系的,应该让认领新发起异常的处理者知道这 一点。所以异常纪录块中有个指针ExceptionRecord,遇到像这样的情况就让新的异常纪录块通过这个指针指向原来的异常纪录块。所以,软异常的 纪录块可以成串,每个纪录块都可以指向本次异常的祸端,只有硬异常的纪录块不会指向别的纪录块。
显然,RtlRaiseException()是个重要的函数。同样,这个函数的代码也是内核和用户空间两栖的。注意下列的调用路线只是许多情景中的一种,实际上调用这个函数的地方很多。
[KiUserExceptionDispatcher() > RtlRaiseException()]
VOID NTAPI RtlRaiseException(PEXCEPTION_RECORD ExceptionRecord)
{
CONTEXT Context;
. . . . . .
/* Capture the context */
RtlCaptureContext(&Context);
/* Save the exception address */
ExceptionRecord->ExceptionAddress = RtlpGetExceptionAddress();
/* Write the context flag */
Context.ContextFlags = CONTEXT_FULL;
/* Check if we're being debugged (user-mode only) */
if (!RtlpCheckForActiveDebugger(TRUE))
{
/* Raise an exception immediately */
Status = ZwRaiseException(ExceptionRecord, &Context, TRUE);
}
else
{
/* Dispatch the exception and check if we should continue */
if (RtlDispatchException(ExceptionRecord, &Context))
{
/* Raise the exception */
Status = ZwRaiseException(ExceptionRecord, &Context, FALSE);
}
else
{
/* Continue, go back to previous context */
Status = ZwContinue(&Context, FALSE);
}
}
/* If we returned, raise a status */
RtlRaiseStatus(Status);
}
调用参数ExceptionRecord指向一个异常纪录块。如上所述,这个纪录块中记载着异常的性质,并且一般都通过指针指向另一个异常纪录块。
首先通过RtlCaptureContext()获取当时的上下文,并通过RtlpGetExceptionAddress()获取RtlRaiseException()的返回地址,以此作为发生本次异常的地址。
具体的操作视RtlpCheckForActiveDebugger()的结果而异。这个函数有系统空间和用户空间两个不同的版本。目前系统空间的版本只 是返回调用参数,所以在这里总是返回TRYE。而用户空间的版本则返回NtCurrentPeb()->BeingDebugged,所以只在受调 试时返回TRUE。
所以,如果是在用户空间调用RtlRaiseException(),而又不是在受调试,就启动系统调用ZwRaiseException()。注意此时 的第三个参数为TRUE,表示这是第一次努力。可见,在这种情况下RtlRaiseException()是通过系统调用 ZwRaiseException()实现的。而且所用的函数名是ZwRaiseException(),在用户空间和系统空间都可以调用。
反之如果是在系统空间,或者虽在用户空间但是在受调试,那就先直接调用RtlDispatchException(),看看本空间的 ExceptionList中是否有节点可以认领和处理。如果有、并且实施了长程跳转,那就不返回了。而如果返回的话,则视返回值为TRUE或FALSE 而分别启动系统调用ZwRaiseException()或ZwContinue()。如果RtlDispatchException()返回TRUE, 就说明已经得到ExceptionList中某个节点的认领、但是并未执行长程跳转而返回ExceptionContinueExecution,此时通 过ZwRaiseException()进行第二次努力,注意这里调用ZwRaiseException()时的第3个参数为FALSE,表示对 ExceptionList的搜索已经失败,下面该采取第二步措施了。而如果RtlDispatchException()返回FALSE,则说明 ExceptionList中根本就没有节点可以认领这次异常,所以就直接通过ZwContinue()返回到刚才获取的那个上下文中,那就是 RtlRaiseException()本次被调用的地方。
在我们现在所考察的情景中,RtlRaiseException()是在KiUserExceptionDispatcher()中受到调用的,并且在此 之前已经有过对ZwRaiseException()或ZwContinue()的调用。那么现在又来调用ZwRaiseException()或 ZwContinue()是否重复呢?不重复,因为使用的是不同的异常纪录块,不同的异常代码,这是为不同原因而发起的软异常。
还有,为什么在受调试的情况下反倒要直接调用RtlDispatchException(),避开调试处理呢?因为调试工具所安排的都是针对正常条件下的 异常,例如页面访问异常、除数为0等等,而现在发生的是在处理异常的过程中本来不应该发生的问题,是属于系统的问题,那不是属于应该由调试工具来处理的问 题。而如果直接通过ZwRaiseException()发起一次软中断,则必将首先进入调试程序。