文章目录
异常产生后,首先要记录异常信息(异常的类型 异常发生的位置等),然后要寻找异常的处理函数,我们称为异常分发,最后找到异常处理函数并调用,我们称为异常处理
CPU异常记录
异常的分类
CPU产生的异常
例如下面的代码:
void Test()
{
int x = 10;
int y = 0;
int z = x/y;
}
当代码执行时,CPU检测到除数为零,这个时候就CPU就会抛出异常
软件模拟产生的异常
在C++或者是C#等一些高级语言中,在程序需要的时候也可以主动抛出异常,这种高级语言抛出的异常就是模拟产生的异常,并不是真正的异常。
CPU异常的处理流程
- CPU指令检测到异常,例如除零
- 查IDT表(如下表),执行中断处理函数
- CommonDispatchException
- KiDispatchException
以除零异常为例,在Windows内核文件中分析一下整体的流程,打开ntkrnlpa.idb
首先按Alt+T
键搜索_IDT
,找到中断处理函数表。
其中_KiTrap00
就是除零异常对应的处理函数
首先,异常处理函数会保存当前的寄存器环境到TrapFrame
,也就是零环的堆栈
接着,异常处理函数并没有直接处理异常,而是调用了CommonDispatchException
函数。
CommonDispatchException
内部又调用了_KiDispatchException
。CPU这么设计的目的是为了让程序员有机会对异常进行处理。
CommonDispatchException函数分析
CommonDispatchException主要就做了一件事情,就是将异常相关的信息存储到一个_EXCEPTION_RECORD
结构体里,这个结构体的作用就是用来记录异常信息
type struct _EXCEPTION_RECORD
{
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags; //异常状态
struct _EXCEPTION_RECORD* ExceptionRecord; //下一个异常
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters; //附加参数个数
ULONG_PTR ExceptionInformation
[EXCEPTION_MAXIMUM_PARAMETERS]; //附加参数指针
}
首先来解释一下异常代码和异常发生时的地址,回到CommonDispatchException函数调用之前
.text:004663F7 sti
.text:004663F8 mov ebx, [ebp+68h] ; 发生异常时的EIP
.text:004663FB mov eax, 0C0000094h ; 异常代码
.text:00466400 jmp loc_4661E3 ; 跳转到CommonDispatchException
这里传递了两个参数,一个是发生异常时的EIP,另一个是异常代码,这个是CPU自己在内部定义的,除零异常对应的异常代码就是0C0000094。如下图
接着来解释一下ExceptionFlags异常状态,通过异常状态可以区分是CPU产生的异常还是软件模拟产生的异常。所有的CPU产生的异常这个位置的值是0,所有软件模拟产生的异常这个位置存储的值是1,如果堆栈错误里面存储的值是8,如果出现嵌套异常,里面存储的值是0x10。
总结
CPU异常的执行流程:
- CPU指令检测到异常
- 查IDT表,执行中断处理函数
- 调用CommonDispatchException,构建EXCEPTION_RECORD结构体
- 调用KiDispatchException函数分发异常,目的是为了找到异常处理函数
模拟异常记录
模拟异常的执行流程
示例代码如下:
void Test()
{
throw 1;
}
当通过软件抛出异常的时候,实际上就是调用了CxxThrowException
。
CxxThrowException
会调用Kernel32.dll
里的RaiseException
RaiseException
会调用ntdlld.dll
里的RtlRaiseException
函数。
而这个函数继续往下会调用NtRaiseException
和KiRaiseException
,最后调用KiDispatchException
RaiseException函数分析
用IDA打开kernel32.dll,找到RaiseException
函数
这个函数要做的事情也很简单,就是在堆栈里构建一个EXCEPTION_RECORD
结构体,然后对结构体进行赋值。
CPU产生的异常也是要填充这样一个结构体,两者之间有一些不同之处。
ExceptionCode 异常代码
首先是ExceptionCode的差异,CPU异常每种不同类型的异常都对应一个具体的32位的值,但是软件模拟的异常和当前的编译环境有关。
ExceptionAddress 异常发生地址
第二个区别就是ExceptionAddress,CPU异常记录的位置是真正的异常发生时的地址
软件模拟产生的异常里面存储的是RaiseException
这个函数的地址。
KiRaiseException函数分析
这个函数主要做了两件事
- 把EXCEPTION_RECORD结构体的ExceptionCode最高位清零,用于区分CPU异常
- 调用KiDispatchException开始分发异常