购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.4 分发异常

当异常发生时,中断异常处理程序得到控制权,并在一个寄存器上下文KTRAP_FRAME保存了寄存器的状态,在EXCEPTION_RECORD结构中描述了异常相关的参数,然后调用KiDispatchException函数分发异常。此函数把异常分发到内核模式(KernelMode)或用户模式(UserMode)相应的例程中去处理。如果当前模式是内核模式,该异常被直接处理;如果当前模式是用户模式,陷阱框架和异常记录的内容会被复制到用户栈,控制权转回给用户后执行用户态异常处理程序。

CONTEXT上下文记录保存完整寄存器信息,部分是从KTRAP_FRAME复制来的。上下文记录描述了在发生异常时的机器状态。这个记录与硬件体系结构相关,是不可移植的。因此,在一般情况下,软件不应该使用该记录包含的信息。但与硬件体系结构相关的代码,如数学库,可以利用这些信息来优化某些操作。对于硬件触发的异常,上下文会记录在异常发生时机器的完整状态。对于一个软件触发的异常,上下文只会记录异常发生那一刻机器的状态。

CONTEXT结构专门用于保存CPU寄存器,包括浮点寄存器和扩展寄存器,KTRAP_FRAME则局限得多。中断处理例程push寄存器保存在KTRAP_FRAME,KTRAP_FRAME的存在有其独特的用途,专用于异常处理例程设计,意思是不要和其他寄存器上下文混淆了。笔者修正过一些代码,设计CONTEXT来兼容KTRAP_FRAME是可行的。KeTrapFrameToContext函数非常简单,把TrapFrame结构中寄存器的值复制到CONTEXT结构里。CONTEXT在KiDispatchException函数运行期间向下传递,返回时调用KeTrapFrameToContext函数还原KTRAP_FRAME的值。如果CONTEXT改变,KTRAP_FRAME也会发生改变。

对于内核态产生的异常(PreviousMode=KernelMode),第一次机会(FirstChance=TRUE)交给内核调试分发函数KdpTrap。KiDebugRoutine是全局函数指针,在KdpInitSystem函数初始化时指向KdpTrap,可见调试器拥有最先处理异常的能力。如果调试器不处理此异常,异常返回后交给RtlDispatchException函数处理,给异常修正的机会,异常被恢复后还能正常执行,RtlDispatchException也依赖try/catch代码块在编译时产生的异常栈框架(参阅第5章)。如果异常在RtlDispatchException函数中处理失败,异常再次交给内核调试分发函数KdpTrap,如果这次调试器也不能处理,它就会调用KeBugCheckEx使系统崩溃。

对于用户态产生的异常,理论上它不应交给内核处理,但是内核必须有能力去处理用户态产生的异常,一旦发生异常,首先进入的是内核态。

第一次机会,试图将信息发送到进程相关联的调试端口。这个信息包括异常记录和用户线程的识别。用户调试器可以处理异常(如断点或单步),并适当修改线程的状态,或交给用户异常处理程序(SEH)去处理。如果用户调试器回复异常已处理,那么机器状态恢复,用户线程继续执行。如果它没有处理这个异常,或者也没有调试端口,则在用户栈中分配空间,异常记录和上下文信息会复制到用户栈。线程的机器状态被修改,交给用户态默认的异常调度器,调度器搜索异常处理程序。如果没有基于栈的异常处理程序被发现,或者没有基于栈的处理程序能够处理该异常,那么NtLastChance系统服务被执行。

第二次机会,该NtLastChance系统服务的目的是提供第二次机会来处理异常,并提供与该线程、进程相关的系统服务子系统,执行任何特定子系统的异常处理的机会。内核进入第二次机会尝试处理异常阶段,也将信息发送到相关联的调试端口,这个信息也包括异常记录和用户线程的识别。用户调试器可以处理异常(例如,查询用户,并获得处置),并适当修改线程的状态,或交给与线程、进程相关的系统服务子系统处理。如果用户调试器回复它已处理该异常,那么这台机器状态恢复,线程继续执行。如果调试器回复它尚未处理该异常,或没有调试端口,然后试图将信息发送到相关联的子系统。这个信息包括异常记录和用户线程的识别。该子系统可以处理异常和适当修改线程的状态或交给任何默认程序处理。如果子系统答复,它已处理该异常,那么这台机器状态恢复,线程继续执行。

第三次机会,子系统答复没有处理异常,执行默认处理。大多数情况下会导致线程所在进程被终止,调用ZwTerminateProcess函数后,流程不会再返回来,进程结束后,内核调试器切换到获得执行权限的线程。

创建控制台工程exceptions_try_except_Statement,编写图3.4所示代码,编译工程。在WinDbg中打开菜单File,单击Open Executable运行程序exceptions_try_except_Statement.exe。

图3.4

WinDbg中断后在左边源文件“return GetExceptionCode()”这一行按F9键下源码断点,在命令行窗口用bp KiUserExceptionDispatcher设置符号断点,按F5键继续运行。当程序运行到“*p=13”时,自动触发#PF异常进入内核_KiTrap0E->KiDispatchException函数。当前模式(PreviousMode=UserMode),如果是第一次机会(FirstChance=TRUE),交给内核调试引擎处理,如果不处理,这时通过DbgkForwardException函数把异常交给用户态调试器。如果用户态调试存在(WinDbg)显示“(2524.2634):“Access violation-code c0000005(first chance)””并断下,按F5键继续运行,DbgkForwardException函数返回FALSE,设置用户栈,复制异常记录,调整寄存器,设eip为用户模式异常分发KeUserExceptionDispatcher。KeUserEx-ceptionDispatcher是全局函数指针,在内核函数PspLookupKernelUserEntryPoints中被初始化,它指向进程的用户态ntdll!KiUserExceptionDispatcher函数。所以当内核中断返回时,回到用户态进程的KiUserExceptionDispatcher函数,由于我们在KiUserExceptionDispatcher设置了断点,WinDbg会中断下来,再按F5键会继续运行,ntdll!KiUserExceptionDispatcher函数会寻找线程异常处理函数,最后运行到__except代码块。如果没有调试器存在或没有try/execpt代码块,进程会调用ntdll!NtRaiseException函数发起第二次异常。进入KiDis-patchException函数,这个时侯(FirstChance!=TRUE),调用DbgkForwardException函数,DebugPort=TRUE,SecondChance=TRUE;如果返回FALSE,再次调用DbgkForwardException函数,DebugPort=FALSE,SecondChance=TRUE;如果还是没处理,便到了第三次机会,直接调用ZwTerminateProcess终止当前进程。理论上ZwTerminateProcess不会返回到KiDis-patchException函数,如果真返回了,说明某些未预料的情况出现,这不在我们设计范围内,调用KeBugCheckEx显示蓝屏信息后系统崩溃。我们看一下在调试系统下专门设计的KeBugCheckEx函数:

调试系统的KeBugCheckEx函数并不显示真正的蓝屏代码,它只是循环在那里,这也算是一个崩溃函数。当然如果真运行到这里,我们希望调试器得到通知,所以顺便加了断点代码,KeBugCheckEx函数的完整实现请查看ntoskrnl内核模块代码。它们是不同的函数,调用的入口不同。如果内核下真有不可预料的情况发生,调用KeBugCheckEx就会显示崩溃信息。 gtjO5PQDAGaVpJxav38b8VGywHvLiYvO57RszESO/3FSzxCWQDgL0ELApy/fMheg

点击中间区域
呼出菜单
上一章
目录
下一章
×