学习前言

异常处理在做题中经常会出现,这篇文章就是记录学习异常处理的学习过程,先说一说异常处理的过程

在一个异常发生的时候,会发生中断,CPU放权给IDT,通过IDT查找处理函数,此时上下文环境会被保存在CONTEXT中,而异常原因会保存在ExceptionCode中,这两个结构也会在后续操作中入栈,因为事先做过了异常处理Hook,所以对VEH有些许了解,不过十分片面,在异常中好好学习一下,当时做的是添加一种异常处理的回调函数,然后故意放出断点异常,让系统用字写的回调函数处理,所以在异常处理中,大致就是通过安装不同的对应的回调函数,用SEH或VEH机制来修复异常

IDT

中断描述符表,存储着解决异常的函数的表

TIB

线程信息块,看上去和TEB很像,因为他就在TEB的头部,那么就好理解了,保存着栈底栈顶之类的信息

SEH

都是一种异常处理机制,其作用是告诉系统,遇到报错了该怎么做

先说SEH,这个机制个人认为基础就是**_EXCEPTION_REGISTRATION_RECORD**结构,很多个这种结构互相指向,每个结构里存着一个异常处理回调函数,出现异常了,就用SEH,遍历所有的结构,也就是遍历所有的回调函数,然后解决问题,VEH基本同理,书中没介绍不了解了

_EXCEPTION_REGISTRATION_RECORD

结构位于TEB的头头,gs:[0],很长的名字,但是结构很短,两个元素,第一个是指针指向下一个结构,第二个元素是异常处理回调函数的地址

CONTEXT与EXCEPTION_RECORD

简单说就是一些基础信息,前者是保存的上下文信息,后者是保存的异常信息

EXCEPTION_POINTERS

在前言中说,处理问题的时候要保存信息,也就是前面那两个CONTEXT与EXCEPTION_RECORD,这两个结构会保存在栈里,EXCEPTION_POINTERS的作用就是,两个元素,一个指向CONTEXT,一个指向EXCEPTION_RECORD

SEH安装与卸载

因为结构是一个指一个的那种,所以想要安装一个回调函数,其实就是让结构的指针多指向一个,因为我们知道结构的开始是在gs:[0],所以将gs:[0]内的内容改成安装的结构,结构的指针元素改成第一个结构,地址改成回调函数就实现了安装,卸载

栈展开

说实话不是特别懂,我个人的理解是,如果这个函数里没有可解决的异常解决函数了,那就需要退出到引用他的程序,依次返回,直到可执行,还是很不懂,以后回来补充

——是这样的,这里先去看看try catch和throw,实践一下,就知道为什么了,然后再回来学习

重新解释一遍,正如C++那样,这个异常在当前函数处理不了,就会回到调用该函数的地址然后再次进行处理,并且这个函数就算是完了,不会再回去了,所以此时的栈还保存了异常函数里的栈环境,再根据前文的异常处理原理,栈中的某些在处理函数时候的数据如:fs:[0]还没有释放,在调用函数这里如果用到了push或者他的操作,可能会对fs:[0]进行改变

所以需要栈展开了

中间说明

在此之前的内容,栈展开这个模块十分不解,不过根据之前的困难疑惑问题解决思路来说,遇到问题不一定要立马和他刚死,后续懂得多了会自然理解,所以接着向后记录学习

——之前写的有点道理确实,看过C++异常处理以后,对于栈展开就明白了,准备接着进行学习

编译器对异常处理的增强

1
2
3
4
5
6
7
8
__try
{
//可能出现的异常
}
__except(/*异常代码*/)
{
//异常处理代码
}
1
2
3
4
5
6
7
8
__try
{
//可能出现的异常
}
__finally
{
//终结处理代码
}

以上是对于代码部分新增添的对异常处理的部分,作用上和就是try块中检测有无异常,except块中处理异常,如果是finally就不管什么异常都会处理,并且这个也属于SEH的安装使用等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<Windows.h>

int main() {
int* pValue = NULL;
__try{
printf("In Try\n");
*pValue = 0x55555555;
}
__except (printf("In Filter\n"), EXCEPTION_EXECUTE_HANDLER) {
printf("In hander\n");
}
}
//In Try
//In Filter
//In hander

简单示例

在这个后面也有对于try catch throw的叙述,前面说过就不多记了

顶层异常处理

对于一个进程来讲,并不是一下就可以到达进程开始的地方,而是由各种的系统函数去调用出来的,一般的程序是由kernel32!BaseProcessStartThunk启动的,就会相当于有一个程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
VOID __stdcall BaseProcessStart(PVOID ThresdStartAddress)
{
DWORD dwExitCode
__try
{
//这里来一个执行主程序
dwExitCode=ThresdStartAddress();
//退出线程
ExitThread(dwExitCode);
}
__except(...)
{
if()
...;
else
...;
}

所以在线程创建前,就已经安装了一个SEH异常处理,如果在一个程序中,卸载了SEH链,比如在程序还没进行之前就卸载了他,那么这个程序就会直接崩溃退出,但如果把卸载的那步撤销了,就会有异常报错出现,所以这里的异常报错便是顶层异常

向量化异常处理VEH

VEH,和SEH差不多,但是VEH所在模块被卸载后,VEH上的回调函数地址不会自动移除,需要手动完成卸载任务,与SEH的不同,VEH的安装是需要函数(AddVectoredExceptionHandler)来进行的,函数安装过后就有一种看不见摸不到的感觉,不过一旦出现异常就会被相应的捕捉

VEH与SEH区别

在此之前我们再捋一下

SEH主要是try except或者try catch这类的或者就是通过push fs:[0]这样的方式来安装SEH有关的函数

VEH呢是通过一个AddVectoredExceptionHandler函数来实现函数安装异常处理函数

我们知道VEH是函数安装的,有一种高高在上的执法官,而SEH是栈中或者代码中的,感觉像是片警,所以遇到异常,执法官的权利高,先让VEH看看可不可以解决,不行了再丢给SEH

注册机制,SEH都是往fs:[0],SEH链表里插入结构,所以就呈现一种后来者居上之情,而VEH存储在ntdll中,在注册的时候可以指定头尾

优先级,前面说了,VEH优先,如果VEH处理了,就会通过CONTEXT里的eip信息,回到主程序,SEH就摸不到了

作用范围,SEH管好自己范围里的东西就足够了,VEH就要大很多了,对于整个进程都是有效的

栈展开问题,再次说明,SEH安装的链表在栈中,所以如果当前函数解决不了,就得栈展开,而VEH安装在ntdll中,云层之上,是不会被影响到的,所以VEH不用栈展开

总结

总体上主要是学习了SEH和VEH两种机制,刚开始不太好理解,慢慢来吧