diff --git a/2023/11/27/WindowsSEH2/index.html b/2023/11/27/WindowsSEH2/index.html index 7de5ec3..f5a1d81 100644 --- a/2023/11/27/WindowsSEH2/index.html +++ b/2023/11/27/WindowsSEH2/index.html @@ -141,7 +141,7 @@
如果运行这个程序,会发现输出有些奇怪。看起来_except_handler调用了两次,这是为什么?
1 | Home Grown handler: Exception Code: C0000005 Exception Flags 0 |
两次异常标志不同,第二次的异常标志是2,而这是由于**展开(unwind)**。
当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。
当异常发生时,系统遍历EXCEPTION_REGISTRATION结构链表,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次遍历这个链表,直到处理这个异常的结点为止。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义为EH_UNWINDING。
为何要这样设置(即调用两次未处理异常的函数)呢?这是为了给这个函数最后一个清理的机会。一个绝好的例子是C++类的析构函数。当一个函数的异常处理程序拒绝处理某个异常时,通常执行流程并不会正常地从那个函数退出。现在,想像一个定义了一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些类似于调用析构函数和__finally块之类的清理工作。
在异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。一定要记住,仅仅把指令指针设置到所需的代码处就开始执行是不行的。流程恢复执行处的代码的堆栈指针和栈帧指针(在Intel CPU上是ESP和EBP)也必须被恢复成它们在处理这个异常的函数的栈帧上的值。因此,这个处理异常的回调函数必须负责把堆栈指针和栈帧指针恢复成它们在包含处理这个异常的SEH代码的函数的堆栈上的值。
通常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是 EXCEPTION_REGISTRATION结构链表上处理异常的那个结构之前的所有EXCEPTION_REGISTRATION结构都被移除了。这很好理解,因为这些EXCEPTION_REGISTRATION结构通常都被创建在堆栈上。在异常被处理后,堆栈指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION结构链表上移除的EXCEPTION_REGISTRATION结构高。
链表的最后一个节点是操作系统提供的默认异常处理函数,它总是会选择处理异常。这个函数是在用户代码执行前插入的。下为BaseProcessStart函数写的伪代码,它是Windows NT KERNEL32.DLL的一个内部例程。这个函数带一个参数——线程入口点函数的地址。BaseProcessStart运行在新进程的环境中,并且它调用这个进程的第一个线程的入口点函数。
两次异常标志不同,第二次的异常标志是2,而这是由于 展开(unwind) 。
当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。
当异常发生时,系统遍历EXCEPTION_REGISTRATION结构链表,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次遍历这个链表,直到处理这个异常的结点为止。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义为EH_UNWINDING。
为何要这样设置(即调用两次未处理异常的函数)呢?这是为了给这个函数最后一个清理的机会。一个绝好的例子是C++类的析构函数。当一个函数的异常处理程序拒绝处理某个异常时,通常执行流程并不会正常地从那个函数退出。现在,想像一个定义了一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些类似于调用析构函数和__finally块之类的清理工作。
在异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。一定要记住,仅仅把指令指针设置到所需的代码处就开始执行是不行的。流程恢复执行处的代码的堆栈指针和栈帧指针(在Intel CPU上是ESP和EBP)也必须被恢复成它们在处理这个异常的函数的栈帧上的值。因此,这个处理异常的回调函数必须负责把堆栈指针和栈帧指针恢复成它们在包含处理这个异常的SEH代码的函数的堆栈上的值。
通常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是 EXCEPTION_REGISTRATION结构链表上处理异常的那个结构之前的所有EXCEPTION_REGISTRATION结构都被移除了。这很好理解,因为这些EXCEPTION_REGISTRATION结构通常都被创建在堆栈上。在异常被处理后,堆栈指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION结构链表上移除的EXCEPTION_REGISTRATION结构高。
链表的最后一个节点是操作系统提供的默认异常处理函数,它总是会选择处理异常。这个函数是在用户代码执行前插入的。下为BaseProcessStart函数写的伪代码,它是Windows NT KERNEL32.DLL的一个内部例程。这个函数带一个参数——线程入口点函数的地址。BaseProcessStart运行在新进程的环境中,并且它调用这个进程的第一个线程的入口点函数。
1 | BaseProcessStart(PVOID lpfnEntryPoint) |
到这里👴实际上已经有点迷糊了(
那展开的意思是_except_handler会被调用两次,那么自己写个正常插入的异常试试:
关于编译器级的SEH我已经在[第一篇关于SEH的学习笔记]中写过了(https://www.yunzh1jun.com/2022/05/27/WindowsSEH/),此处总结几个要点。
+关于编译器级的SEH我已经在第一篇关于SEH的学习笔记中写过了,此处总结几个要点。
简单地说,在一个函数中,一个__try块中的所有代码就通过创建在这个函数的堆栈帧上的一个EXCEPTION_REGISTRATION结构来保护。在函数的入口处,这个新的EXCEPTION_REGISTRATION结构被放在异常处理程序链表的头部。在__try块结束后,相应的 EXCEPTION_REGISTRATION结构从这个链表的头部被移除。正如前面所说,异常处理程序链表的头部被保存在FS:[0]处。因此,如果你在调试器中单步跟踪时看到类似MOV DWORD PTR FS:[00000000],ESP或者MOV DWORD PTR FS:[00000000],ECX的指令时,就能非常确定这段代码正在进入或退出一个__try/__except块。
1 | struct _EXCEPTION_REGISTRATION |