异常处理的基本流程

CPU异常记录

异常记录-异常分发-异常处理
当cpu遇到除0的时候,就会去中断表中查询
1.CPU检测到异常(例:除0)
2.查IDT表,执行中断处理函数(中断处理函数不会处理异常,他会调用下面的函数)
3.CommonDispatchException(构建EXCEPTION_RECORD,这个函数会在自己的堆栈上构建一个结构体,把异常的信息存在这个结构体里)

//这个结构体的作用就是来记录异常信息
type struct _EXCEPTION_RECORD
{
    DWORD ExceptionCode;    //异常代码*
    DWORD ExceptionFlags;   //异常状态,这里可以区分CPU产生的异常(0)还是软件模拟的异常(1),是给后面的处理程序看的,堆栈错误这里的值是8,
        //如果异常处理里面又嵌套了一个异常,这里的值是0x10
    
    struct _EXCEPTION_RECORD* ExceptionRecord;  //下一个异常,通常是空的,如果出现嵌套异常的时候,会通过这里指向下一个异常,只有这种情况会用
    PVOID ExceptionAddress; //异常发生地址*,在那一行发生的
    DWORD NumberParameters; //附加参数个数
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];   //附加指针参数
}

有两个参数必须要给的就是上面星号

4.KiDispatchException(异常分发:目的是找到异常处理函数)

模拟异常记录

软件模拟异常的时候,其异常产生曲线

CxxThrowException

(KERNEL32.DLL)RaiseException(DWORD dwExceptionCode,DWORD dwExceptionFlags,DWORD nNumberOfArguments,const ULONG_PTR *lpArguments)

调用的是kernel32中的一个函数
软件异常和CPU异常有两点不同:一个是ERRORCODE,一个是异常地址
这个函数会构建一个结构体,就是上面写的那个结构体,然后给结构体进行赋值
CPU产生异常的时候会记录一个ERROR_CODE,这里我们需要知道的是软件模拟异常的ERRORCODE和异常发生地址是什么
软件模拟异常的ERRORCODE和CPU异常的ERROR_CODE是有差异的,我们会发现CPU异常会有一个具体的16进制的值,但是
如果是软件模拟异常的时候和我们编译环境有关系,例如我们使用VC6,它的值就是固定的,抛出的异常永远都是这个值,如果编
译环境改变了,这个值还会不同
软件异常的位置,这个位置存储的并不是抛出异常的位置,而是存了一个固定的值就是RaiseException函数的地址

NTDLL.DLL!RtlRaiseException()

NT!NtRaiseException(函数走到这里的时候就进内核了,这个函数的主要功能就是调用了下面这个函数)

NT!KiRaiseException
1).EXCEPTION_RECORD ExceptionCode最高位清零,用于区分CPU异常。
2).调用KiDispatchException开始分发异常
总结:

这两种异常的不同的阶段仅仅是在异常记录的阶段,当到了异常分发的阶段,就无法在区分是什么异常

内核异常处理流程

用户层异常与内核层异常
学习网址:https://www.anquanke.com/post/id/175293
https://www.anquanke.com/post/id/175753
https://www.anquanke.com/member/141205
异常可以发生在用户空间,也可以发生在内核空间
无论是CPU异常还是模拟异常,是用户层还是内核异常,都要通过KiDispatchException函数进行分发。理解这个函数是学好异常的关键

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,   //ExceptionRecord也就是前面提的描述异常的结构,
    IN PKEXCEPTION_FRAME ExceptionFrame,    
    IN PKTRAP_FRAME TrapFrame,//TrapFrame指向的结构用来描述发生异常时候的上下文
    IN KPROCESSOR_MODE PreviousMode,//PreviousMode来说明异常来自Kernel还是User,
    IN BOOLEAN FirstChance//最后的FirstChance用来表示异常是不是第一次被处理
    )

实际上这些结构的集合就形成了一个虚拟的、完整的“异常”结构,再去进行下面的处理。
KiDispatchException这个函数管理内核层和用户层的异常,当内核出现异常的时候,只需要在内核中进行处理即可,但是用户层出现异常的时候,由于用户层的异常处理也是在用户层,所以我们要分发用户层异常的时候还需要返回到三环,1.所以分发的函数在进入函数之后首先是备份Trap_frame备份到context中,为返回三环做准备(因为它不知道你是三环的异常还是0环的异常)
2.判断先前模式,0是内核调用,1是用户层调用
用户层异常直接就跳走了,我们这里主要关注内核层
3.是否是第一次
接下来会判断这个函数是第几次执行,如果是第一次执行,就继续向下走,不是就跳走
4.是否有内核调试器
KiDebugRoutine,然后看内核调试器是否存在(windbg)(有内核调试器,这个之为非0,否则为0 )
没有内核调试器直接跳转
如果有内核调试器,会先调用内核调试器,如果调用返回成功(1),说明内核调试器已经处理了,就将COHTEXT再转成Trap_Frame直接返回,如果调试器没有处理,那就让3环处理,如果内核调试器没有处理也就是返回失败,直接跳转

5.如果没有内核调试器或者调试器不处理->6

6.调用RtIDispatchException(在0环的处理异常的时候会调用这个函数,在3环的时候也会调用这个函数)
RtIdispatchException函数的执行流程

typedef struct _EXCEPTION_REGISTRATION_RECORD{
    struct_EXCEPTION_REGISTRATION_RECORD *Next;;
    PEXCEPTION_ROUTINE Handler;
}EXCEPTION_REGISTRATION_RECORD;

RtIDispatchException的作用就是:
遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1,。如果当前异常处理函数不能处理异常,那么调用下一个,以此类推。
如果最后也没有人处理这个异常,返回 0.
7.如果返回FALSE也就是0

8.再次判断是否有内核调试器,有就调用,没有直接蓝屏

最后修改:2020 年 08 月 26 日
如果觉得我的文章对你有用,请随意赞赏