本文将围绕上图来介绍C++异常的数据结构。
在C++中如果函数中包含异常处理,将会在此函数中的开始部分注册一个异常回调函数,当函数中有异常抛出的时候,便会调用这个回调函数,也就是在SEH中注册一个函数(异常回调函数)。
这个异常回调函数指向的地址的汇编码通常是这样的:
1 | 00D963A4 mov eax,0D9A064h |
很明显代码中的eax的值保存了关键信息。
这个0D9A064h所指向的地址就是上图中的FuncInfo结构体:
1 | struct FuncInfo |
这个struct包含了两个struct,分别为UnwindMapEntry和TryBlockMapEntry。
先来看看UnwindMapEntry,UnwindMapEntry表配合maxState项来使用。
maxState记录了异常发生时try块展开的次数,展开时执行的函数由UnwindMapEntry表结构记录,结构体信息如下:
1 |
在try块展开的过程中,可能存在多个对象,每个对象的析构信息会以数组的形式记录。
toState用来判断结构是否位于数组中,lpFuncAction保存析构函数所在的地址。
TryBlockMapEntry结构如下:
1 | struct TryBlockMapEntry{ |
这个struct用于判断异常产生在哪个try块中。t
ryLow和tryHigh用于检查长生的异常是否来源于try块中。
catchHigh用于匹配catch块时的检查项。
每个catch块都会对应一个_msRttiDscr表结构,保存在pCatchHandlerArray指向的地址中(数组方式存放)。
再来看看_msRttiDscr吧
1 | struct _msRttiDscr |
nFlag同来检查catch块匹配的类型,含义值如下:
- 1:常量
- 2:变量
- 4:未知
- 8:引用
此结构中的pType和CatchProc为关键数据,当抛出异常对象时,需要赋值抛出的异常对象信息,dispCatchObjOffset用于定位异常对象在当前EBP中的偏移位置。
CatchProc项中保存了异常处理catch块的首地址,这样在匹配异常后,就可以正确的执行catch语句块,异常的匹配信息记录在pType所指向的结构中,结构信息如下:
1 |
当异常发生时,就可以通过以上信息于抛出异常时的信息进行对比,得到对应表的结构,最后通过_msRttiDscr表中的CatchProc项得到catch块的首地址。从而走到正确的catch块中。
现在我们再来说说throw吧,抛出异常时的代码通常如下:
1 | 00D918BC push offset __TI1H (0D9A094h) |
在抛出一场函数时传递了一个全局参数__TI1H。
这个地址中指向就是抛出异常时需要的结构信息ThrowInfo:
1 |
上述结构体包含了类型信息,用于匹配抛出的异常类型。
nFlag = 1= 常量类型异常
nFlag = 2 = 变量类型异常
由于在try块中发生异常后不会再反汇try块中,pDestructor的作用就是记录try块中的异常对象的析构函数地址,在异常处理完成后调用。
抛出异常所对应的catch块的类型信息就被记录在pCatchTableTypeArray所指向的结构中。
结构体CatchTableTypeArray如下:
1 |
ppCatchTableType指向指针数组,里面保存了CatchTableType的地址列表。
dwCount来描述数组中有多少个元素。
来看看CatchTableType里面有什么:
1 |
还记得上文中的TypeDescriptor结构吗,在异常处理的时候,可以与上述结构中的pTypeInfo进行对比,并找到正确的catch块。
flag标记用于判断异常对象属于那种类型,类如,指针、引用等。
标记值含义如下:
- 0x1:简单类型复制
- 0x2:已被捕获
- 0x4:有虚表基类复制
- 0x8:指针和引用类型复制
如果异常类型是对象,那么就会把他们的结构信息存储下来,存储在thisDisPlacement中:
1 |
以上就是C++异常处理所用到的所有的数据结构,建议读者阅读结束后,再看下文首中的图片加深记忆。