CPU和软件模拟异常的执行流程


异常产生后,首先要记录异常信息(异常的类型 异常发生的位置等),然后要寻找异常的处理函数,我们称为异常分发,最后找到异常处理函数并调用,我们称为异常处理

CPU异常记录

异常的分类

CPU产生的异常

例如下面的代码:

void Test()
{
	int x = 10;
	int y = 0;
	int z = x/y;
}

当代码执行时,CPU检测到除数为零,这个时候就CPU就会抛出异常

软件模拟产生的异常

在C++或者是C#等一些高级语言中,在程序需要的时候也可以主动抛出异常,这种高级语言抛出的异常就是模拟产生的异常,并不是真正的异常。

CPU异常的处理流程

  1. CPU指令检测到异常,例如除零
  2. 查IDT表(如下表),执行中断处理函数
  3. CommonDispatchException
  4. 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异常的执行流程:

  1. CPU指令检测到异常
  2. 查IDT表,执行中断处理函数
  3. 调用CommonDispatchException,构建EXCEPTION_RECORD结构体
  4. 调用KiDispatchException函数分发异常,目的是为了找到异常处理函数

模拟异常记录

模拟异常的执行流程

示例代码如下:

void Test()
{
    throw 1;
}

在这里插入图片描述

当通过软件抛出异常的时候,实际上就是调用了CxxThrowException

在这里插入图片描述

CxxThrowException会调用Kernel32.dll里的RaiseException

在这里插入图片描述

RaiseException会调用ntdlld.dll里的RtlRaiseException函数。

而这个函数继续往下会调用NtRaiseExceptionKiRaiseException,最后调用KiDispatchException

RaiseException函数分析

在这里插入图片描述

用IDA打开kernel32.dll,找到RaiseException函数

在这里插入图片描述

这个函数要做的事情也很简单,就是在堆栈里构建一个EXCEPTION_RECORD结构体,然后对结构体进行赋值。

CPU产生的异常也是要填充这样一个结构体,两者之间有一些不同之处。

ExceptionCode 异常代码

在这里插入图片描述

首先是ExceptionCode的差异,CPU异常每种不同类型的异常都对应一个具体的32位的值,但是软件模拟的异常和当前的编译环境有关。

ExceptionAddress 异常发生地址

第二个区别就是ExceptionAddress,CPU异常记录的位置是真正的异常发生时的地址

在这里插入图片描述

软件模拟产生的异常里面存储的是RaiseException这个函数的地址。

KiRaiseException函数分析

这个函数主要做了两件事

  1. 把EXCEPTION_RECORD结构体的ExceptionCode最高位清零,用于区分CPU异常
  2. 调用KiDispatchException开始分发异常

总结

在这里插入图片描述

发布了109 篇原创文章 · 获赞 115 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/104346316