C++基于SEH二次封装的异常处理 - 之数据结构篇

1571397583862

本文将围绕上图来介绍C++异常的数据结构。

在C++中如果函数中包含异常处理,将会在此函数中的开始部分注册一个异常回调函数,当函数中有异常抛出的时候,便会调用这个回调函数,也就是在SEH中注册一个函数(异常回调函数)。

这个异常回调函数指向的地址的汇编码通常是这样的:

1
2
00D963A4  mov         eax,0D9A064h  
00D963A9 jmp ___CxxFrameHandler3 (0D910FFh)

很明显代码中的eax的值保存了关键信息。

这个0D9A064h所指向的地址就是上图中的FuncInfo结构体:

1
2
3
4
5
6
7
8
struct FuncInfo
{
DWORD magicNumber; //编译器生成的固定数字
DWORD maxState; //最大栈展开数的下标值
DWORD pUnwindMap; //指向栈展开函数表的指针,指向UnwindMapEntry表结构
DWORD dwTryCount; //try块的数量
DWORD pTryBlockMap; //try块列表,指向TryBlockMapEntry结构体
};

这个struct包含了两个struct,分别为UnwindMapEntry和TryBlockMapEntry。

先来看看UnwindMapEntry,UnwindMapEntry表配合maxState项来使用。

maxState记录了异常发生时try块展开的次数,展开时执行的函数由UnwindMapEntry表结构记录,结构体信息如下:

1
2


在try块展开的过程中,可能存在多个对象,每个对象的析构信息会以数组的形式记录。

toState用来判断结构是否位于数组中,lpFuncAction保存析构函数所在的地址。

TryBlockMapEntry结构如下:

1
2
3
4
5
6
7
struct TryBlockMapEntry{
DWORD tryLow ;//try块的最小状态索引,用于范围检查
DWORD tryHigh ;//try块的最大状态索引,用于范围检查
DWORD catchHigh ;//catch块的最高状态索引,用域范围检查
DWORD dwCatchCount; //catch块个数
DWORD pCatchHandlerArray ; //catch块的描述,指向_msRttiDscr表结构
}

这个struct用于判断异常产生在哪个try块中。t

ryLow和tryHigh用于检查长生的异常是否来源于try块中。

catchHigh用于匹配catch块时的检查项。

每个catch块都会对应一个_msRttiDscr表结构,保存在pCatchHandlerArray指向的地址中(数组方式存放)。

再来看看_msRttiDscr吧

1
2
3
4
5
6
7
struct _msRttiDscr 
{
DWORD nFlag ;//用域catch块的匹配检查
DWORD pType ;//catch块要捕捉的类型,指向TypeDescriptor表结构
DWORD dispCatchObjOffset; //同于定位异常对象在当前EBP中的偏移位置
DWORD CatchProc; //catch块的首地址
}

nFlag同来检查catch块匹配的类型,含义值如下:

  • 1:常量
  • 2:变量
  • 4:未知
  • 8:引用

此结构中的pType和CatchProc为关键数据,当抛出异常对象时,需要赋值抛出的异常对象信息,dispCatchObjOffset用于定位异常对象在当前EBP中的偏移位置。

CatchProc项中保存了异常处理catch块的首地址,这样在匹配异常后,就可以正确的执行catch语句块,异常的匹配信息记录在pType所指向的结构中,结构信息如下:

1
2


当异常发生时,就可以通过以上信息于抛出异常时的信息进行对比,得到对应表的结构,最后通过_msRttiDscr表中的CatchProc项得到catch块的首地址。从而走到正确的catch块中。

现在我们再来说说throw吧,抛出异常时的代码通常如下:

1
2
3
4
00D918BC  push        offset __TI1H (0D9A094h)  
00D918C1 lea eax,[ebp-0F0h]
00D918C7 push eax
00D918C8 call __CxxThrowException@8 (0D913A7h)

在抛出一场函数时传递了一个全局参数__TI1H。

这个地址中指向就是抛出异常时需要的结构信息ThrowInfo:

1
2


上述结构体包含了类型信息,用于匹配抛出的异常类型。

nFlag = 1= 常量类型异常

nFlag = 2 = 变量类型异常

由于在try块中发生异常后不会再反汇try块中,pDestructor的作用就是记录try块中的异常对象的析构函数地址,在异常处理完成后调用。

抛出异常所对应的catch块的类型信息就被记录在pCatchTableTypeArray所指向的结构中。

结构体CatchTableTypeArray如下:

1
2


ppCatchTableType指向指针数组,里面保存了CatchTableType的地址列表。

dwCount来描述数组中有多少个元素。

来看看CatchTableType里面有什么:

1
2


还记得上文中的TypeDescriptor结构吗,在异常处理的时候,可以与上述结构中的pTypeInfo进行对比,并找到正确的catch块。

flag标记用于判断异常对象属于那种类型,类如,指针、引用等。

标记值含义如下:

  • 0x1:简单类型复制
  • 0x2:已被捕获
  • 0x4:有虚表基类复制
  • 0x8:指针和引用类型复制

如果异常类型是对象,那么就会把他们的结构信息存储下来,存储在thisDisPlacement中:

1
2


以上就是C++异常处理所用到的所有的数据结构,建议读者阅读结束后,再看下文首中的图片加深记忆。

Author: YuanBi
Link: https://www.basicbit.cn/2018/11/12/2018-11-14-C++异常处理的二次封装/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.