前言
前一篇学习了,Windows平台的编译器了扩展了SEH,通过try_except块简化了挂入SEH的过程。本篇就来研究一下编译器是如何通过try_except将异常挂入SEH链表的。本篇依旧在Windows XP系统下的Visual C++6.0编译器上进行实验。
_try_except实现细节
手动挂入
首先我们来回顾一下手动挂入链表的过程:

由图,手动挂入链表需要自身SEH结构的地址赋值给FS:[0]的位置,这样相当于把自己挂到链表首位。当然,还需要将自己的next指针指向原先FS:[0]处所指向的地址,这里没有显示出。
自动挂入
以如下代码为例:
1 |
|
在TestException处下断点,观察反汇编。

可以看到,_try_except实际上的操作过程与手动挂入链表类似,同样是让FS:[0]指向新的链表头。接下来,我们来看另一种场景。
_try_except嵌套重复
修改TestException如下:
1 | void TestException(){ |
编译,下断点,观察反汇编

会神奇的发现,在try_except嵌套与重复的情况下(递归不包括,因为递归属于重复调用函数,会挂入多个SEH),经过编译器汇编后,仍然只有一个异常处理函数_except_handler3(该函数不同编译器不一样,此实验环境为VC6.0),并且只挂入了一次SEH。这是什么原因呢?
原来编译器扩展了SEH结构体,在原先Windows要求下,SEH结构体至少要包含2个字段(Next:指向下一个SEH块,Handler:异常处理函数),其堆栈结构如下所示:

但是经过扩展后的SEH结构体原型如下(稍后会分析该结构体):
1 | struct _EXCEPTION_REGISTRATION{ |
这样一来,堆栈的结构也就发生了变化,也就可以对应上之前分析的汇编代码。

接下来,研究该结构体的额外字段的作用,便可了解编译器是如何通过只挂一个SEH实现所有嵌套重复try_except块的功能。
扩展的_EXCEPTION_REGISTRATION结构体
再来看_EXCEPTION_REGISTRATION这个结构:
1 | struct _EXCEPTION_REGISTRATION{ |
它多出了3个成员,其中最为重要的是scopetable和trylevel这两个字段,先来看scopetable。
ScopeTable
scopetable是一个指针,它指向一个结构体数组,结构体如下:
1 | struct scopetable_entry |
这三个成员的含义如何理解呢?根据注释,知道它是两个指针以及1个编号。下面用一个程序理清他们的作用。
编译运行如下代码(环境VC++6.0),并在a函数调用处设下断点。
1 |
|
进入反汇编,根据_EXCEPTION_REGISTRATION结构的位置,找到scopetable的值,并在内存中定位到scopetable指向的结构体数组地址,如下图所示:
将数组中各结构体的成员标出后如下图:
这样来看,lpfnFilter与lpfnHandler的作用就清晰了很多:
- lpfnFilter:指向except括号内的内容,在前一篇文章中提到过滤表达式,也就是except括号内常量值,这里编译器将其优化成了一段可以返回的代码(注意ret指令),所以当异常发生时,代码已经不是顺序执行的了,而是会经过多次跳转和返回。
- lpfnHandler:指向异常处理函数,这就相当于默认SEH结构的Handler的值。
综上,可以看出,之所以经过编译器扩展后仅有一个SEH块,原因是编译器通过对SEH块进行扩展,将每一个try_except块对应的过滤表达式与异常处理函数放到了scopetable指向的结构体数组中。这样就能在一个SEH块中容纳下多个try_except。
前面提到了scopetable中的两个指针成员lpfnFilter与lpfnHandler,还有一个成员previousTryLevel还未提。这个成员有什么用呢?再回顾一下,这3个try_except块对应的previousTryLevel的值
结合之前的一张图,可以看出两个值为-1的previousTryLevel对应两个外层的try_except块,值为1的previousTryLevel则对应内嵌在第二个try_except中的try_except块。这样就能理解了,previousTryLevel指的是当前try_except块所在的外层try_except块的下标是多少。例如前两个try_except块,它们的外层已经没有try_except块了,因此值为-1。内嵌的try_except块,位于第二个try_except块中,这里说的第二个的意思就是在scopetable指向的结构体数组中位于第二个,也就是下标为1。因此内嵌的try_except的previousTryLevel的值为1。
TryLevel
理解了scopetable及其指向的结构体内的字段后,trylevel也就好理解了。它指的是当前代码执行到哪个try_except块了。如何理解呢?来看下面代码:
1 |
|
同样在调用函数a处下断点,并进入反汇编。

可以看到,在初始化SEH结构体时,trylevel的值被设置为-1,位于[ebp-4]的位置。

观察这几处trylevel值的变化,它在进入第一个try_except块时,被设置为0(该try_except块对应的结构体位于scopetable[0]),在进入第二个try_except块时,又被设置成了1,一旦离开第二个try_except块,回到第一个try_except块所在空间时,又被设置成了0,等到了离开第一个try_except块时,被设置成了-1。

同样,在执行到另外几个try_except块时,trylevel也会被设置成该try_except块位于scopetable指向的结构体数组中的下标。所以可以看出,trylevel的作用,就是用来表明,当前程序位于哪个try_except块中,若不位于try_except块中,则设置为-1。
ebp
ebp就是ebp,可以认为是寻址用的,例如[ebp-4]就是trylevel的值。
_except_handler3执行过程
至此,已经基本上了解一个异常处理的完整流程,这里就简单的回顾一遍这个过程。这里以用户触发除零异常为例:
- CPU检测到异常
- 查询IDT表,执行中断处理函数
- CommonDispatchException
- KiDispatchException
- KiDebugRoutine(判断内核调试器)
- DbgkForwardException(判断用户调试器)
- KiUserExceptionDispatcher
- RtlDispatchException(3环)
- VEH
- SEH
- 执行_except_handler3函数
- 根据trylevel选择scopetable数组中的结构体
- 调用scopetable数组中对应结构体的lpfnFilter
- EXCEPTION_EXECUTE_HANDLER(1) 执行except代码
- EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个
- EXCEPTION_CONTINUE_EXECUTION(-1) 重新执行
- 如果lpfnFilter函数返回0,则向上遍历,直到previousTryLevel为-1
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎
参考教程:
- 海哥逆向中级班预习班
参考链接:
- https://blog.csdn.net/qq_38474570/article/details/104346489 (鬼手56-编译器扩展SEH学习笔记)
- https://blog.csdn.net/weixin_42052102/article/details/83551306 (My classmates-编译器扩展SEH学习笔记)