前言
在之前的文章中提过,异常处理机制的执行流程分为
- 异常记录
- 异常的分发
- 异常的处理
本篇开始,开始学习异常分发的过程。
用户层异常与内核层异常
异常可以发生在用户空间,也可以发生在内核空间。正如APC一样。
无论是CPU异常还是模拟异常,是用户层异常还是内核异常,都要通过KiDispatchException函数进行分发。理解这个函数是学习异常的核心。(就像APC处理都需要调用KiDeliverApc一样)
本篇先学习较为简单的内核层异常,因为它进入KiDispatcherException异常分发函数后,主要处理过程都在内核进行,不需要再返回三环,因而逻辑较为简单。
KiDispatchException
这是处理异常最重要的函数,就像处理APC的KiDeliverApc函数一样
函数原型
先从函数原型开始,了解每个参数的含义
1 | VOID KiDispatchException ( |
- ExceptionRecord:异常记录做的事情就是初始化这个结构体。
- ExceptionFrame:对于x86系统,这个值是NULL。
- TrapFrame:这个就非常熟悉了,3环进0环时,保存的现场就都在这里面。
- PreviousMode:先前模式,0表示内核模式,1表示用户模式。
- FirstChance:判断是否是第一轮分发这个异常,对于一个异常,Windows系统最多分发两轮。1表示第一次分发,0表示第二次分发。
执行流程(内核部分)
接下来,进入IDA,分析KiDispatchException处理内核异常时的执行过程:
进入函数主体(KiDispatchException):
这里作了裁剪,以便于查看。接下来还是按照方框来分析:
红色方框:首先从这里看起,它做了一件事,将_Trap_Frame备份到Context里,这部分和处理用户APC时很像,调用的也是同一函数。
橙色方框:这里会根据KiDispatchException参数PreviousMode(先前模式),判断是内核异常还是用户层异常。本篇只关注内核异常,所以这里不跳转。
绿色方框:这里根据KiDispatchException参数FirstChance,判断是不是第一次分发,如果不是第一次就跳走。
紫色方框:这里比较关键,先看第一个紫色方框,它会先查看内核变量KiDebugRoutine标识的内核调试器是否存在(例如Windbg):
如果不存在,就跳转。
如果内核调试器存在,它就会调用内核调试器,并判断返回值。如果返回值为1,说明异常成功被处理掉,接下来会将Context的内容返还给_Trap_Frame,异常处理过程结束,退出KiDispatchException函数。如果返回值为0,说明异常未被处理掉,则跳转。
值得注意的是,两个跳转的位置是一样的,即不存在内核调试器或者内核调试器未处理掉异常时,会跳转到同一个地方。
粉色方框:若不存在内核调试器或者内核调试器未处理掉异常时,会跳转到这里。此时会传递两个参数Context和ExceptionRecord,并调用RtlDispatchException函数。这是负责调用异常处理函数的函数。
RtlDispatchException部分:
RtlDispatchException在内部调用了RtlpGetRegistrationHead,这个函数返回了fs:[0]处保存的值。根据之前学习,在0环fs:[0]指向的是KPCR,KPCR的第一个成员NtTib,而NtTib的第一个字段是ExceptionList,ExceptionList这个字段是一个指针,它指向了一个结构体_EXCEPTION_REGISTRATION_RECORD:
c1
2
3
4
5typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD,如果没有下一个结构体,则此处值为-1。第二个成员是异常处理函数。其内部结构如下:
所以RtlDispatchException的作用就是遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1。如果当前异常处理函数不能处理该异常,那么调用下一个,以此类推。如果到最后也没有异常处理函数处理这个异常,返回0。
第二次分发:
- 绿色方框:这里是RtlDispatchException执行结束的地方。
- 主要逻辑:然后跟着跳转,向下看,这部分有4处跳转,其中2处橙色跳转的地方是一样的,2处红色跳转的地方是一样的。逻辑如下,首先,会判断RtlDispatchException函数的返回值,若该值为1,说明异常已经被处理,则触发橙色跳转;若值不为1,继续向下执行。这时会再次判断KiDebugRoutine标识的内核调试器是否存在,这便是二次分发。若仍然没有内核调试器,则触发红色跳转。若有,则和第一次分发时的处理一样,再次判断内核调试器是否对异常进行处理,若处理完成,触发橙色跳转,否则,触发红色跳转。
- 触发橙色跳转:若触发了橙色跳转,说明异常已被处理。接下来会调用KeContextToKframes将Context备份的内容返还给_Trap_Frame,异常分发结束。
- 触发红色跳转:若触发了红色跳转,说明异常未被处理,将会调用KeBugCheckEx函数,系统蓝屏。
总结
内核异常的总体流程可以参考下图,当然用户异常的流程也在图中,下一篇会讲到。
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p244~p246 —— 张银奎
参考教程:
- 海哥中级预习班课程
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83507711 (CSDN-My classmates学习笔记)
- https://blog.csdn.net/qq_38474570/article/details/104346374 (CSDN-鬼手56学习笔记)