C++异常处理

1.基础语法

try:可能出现异常的代码,使用throw抛出指定类型的异常。

throw:抛出异常语句。

catch:捕获指定类型的异常,并尝试做出处理。可以定义多个用于捕获各种类型的异常。

基本类型异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

double division(int a, int b)
{
if (b == 0) throw "Division by zero condition."; // 抛出const char* 类型异常
else return a / b;
}

int main()
{
try
{
std::cout << division(1, 0) << std::endl;
}
catch (const char* msg)
{
std::cout << msg << std::endl;
}
catch (...)
{
std::cout << "exception catched." << std::endl;
}
return 0;
}

C++标准异常

C++标准异常继承树

自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <exception>

class MyException : public std::exception // 继承自标准异常
{
public:
const char* what() const throw() // throw()表示该函数不抛出任何异常
{
return "MyException.";
}
};

int main()
{
try
{
throw MyException();
}
catch (MyException& e)
{
std::cout << e.what() << std::endl;
}
catch (std::exception& e) // 捕获C++标准异常
{
std::cout << e.what() << std::endl;
}
return 0;
}

2.x86异常处理

2.1 异常栈帧结构图

栈帧结构图

2.2 结构定义

2.2.1 FuncInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 函数信息结构
struct FuncInfo {
// 编译器版本号
// 0x19930520: up to VC6, 0x19930521: VC7.x(2002-2003), 0x19930522: VC8 (2005)
DWORD magicNumber;

// 最大栈展开数的下标
int maxState;

// 栈展开结构数组指针
UnwindMapEntry* pUnwindMap;

// 函数中try块个数
DWORD nTryBlocks;

// try块结构数组指针
TryBlockMapEntry* pTryBlockMap;

// x86未使用
DWORD nIPMapEntries;

// x86未使用
void* pIPtoStateMap;

// VC7+ only, expected exceptions list (function "throw" specifier)
ESTypeList* pESTypeList;

// VC8+ only, bit 0 set if function was compiled with /EHs
int EHFlags;
};

2.2.2 UnwindMapEntry

1
2
3
4
5
// 栈展开信息结构
struct UnwindMapEntry {
int toState; // 栈展开下标数
void (*action)(); // 展开执行函数地址
};

2.2.3 TryBlockMapEntry

1
2
3
4
5
6
7
8
9
// try块结构
// 用于判断异常产生在哪个try块中
struct TryBlockMapEntry {
int tryLow; // try块最小状态索引,范围检查
int tryHigh; // try块最大状态索引,范围检查
int catchHigh; // catch块的最高状态索引,范围检查
int nCatches; // catch块个数
HandlerType* pHandlerArray; //catch块描述数组指针
};

2.2.4 HandlerType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// catch块结构
struct HandlerType {
// 0x01: const, 0x02: volatile, 0x08: reference
DWORD adjectives;

// catch块要捕获的RTTI类型指针
TypeDescriptor* pType;

// 异常对象在当前ebp中的偏移位置
int dispCatchObj;

// address of the catch handler code.
// returns address where to continues execution (i.e. code after the try block)
// catch处理代码地址
void* addressOfHandler;
};

2.2.5 ESTypeList

1
2
3
4
5
6
7
8
// 实现了但是msvc默认不开启,可以使用 /d1Esrt 开启
struct ESTypeList {
// 类型数组的数量
int nCount;

// list of exceptions; it seems only pType field in HandlerType is used
HandlerType* pTypeArray;
};

2.2.6 TypeDescriptor

1
2
3
4
5
6
7
8
9
10
11
struct TypeDescriptor {
// type_info类的虚表
const void * pVFTable;

// used to keep the demangled name returned by type_info::name()
// type_info::name()得到的名字
void* spare;

// mangled type name, e.g. ".H" = "int", ".?AUA@@" = "struct A", ".?AVA@@" = "class A"
char name[0];
};

2.3 throw结构定义

2.3.1 ThrowInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ThrowInfo {
// 0x01: const, 0x02: volatile
DWORD attributes;

// 抛出异常对象的析构函数地址
void (*pmfnUnwind)();

// forward compatibility handler
int (*pForwardCompat)();

// catch块类型结构数组指针
CatchableTypeArray* pCatchableTypeArray;
};

struct CatchableTypeArray {
// number of entries in the following array
int nCatchableTypes;
CatchableType* arrayOfCatchableTypes[0];
};

2.3.2 CatchableTypeArray

1
2
3
4
5
6
struct CatchableTypeArray {
// 下面数组元素个数
int nCatchableTypes;
// catch类型信息
CatchableType* arrayOfCatchableTypes[0];
};

2.3.3 CatchableType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct CatchableType {
// 0x01: simple type (can be copied by memmove), 0x02: can be caught by reference only, 0x04: has virtual bases
DWORD properties;

// RTTI类型信息,catch块就是比较这个结构
TypeDescriptor* pType;

// 基类信息
PMD thisDisplacement;

// 抛出对象的字节大小
int sizeOrOffset;

// 抛出对象的拷贝构造函数地址
void (*copyFunction)();
};

// Pointer-to-member descriptor.
struct PMD {
// 基类偏移
int mdisp;

// 虚基类偏移 (-1表示没有)
int pdisp;

// 基类虚表偏移
int vdisp;
};

3. 示例分析

3.1 try/catch块分析

函数头部:

异常回调函数注册

异常处理函数体:

注册异常

函数信息结构内容:

FuncInfo结构

可以看出main函数中有2个栈展开结构,1一个try块。

其中,栈展开信息内容如下:

UnwindMapEntry结构

try块信息如下:

TryBlockMapEntry结构

可以看出该try块对应2个catch异常处理。

pHandlerArray信息如下:

HandlerType数组

其中,catch_code1和catch_code2就是编写的catch块内的异常处理代码,如下所示:

异常处理代码1

异常处理代码2

3.2 回调注册分析–对象类型异常

__CxxFrameHandler3函数将参数向下传递。

__CxxFrameHandler3

__InternalCxxFrameHandler函数主要是对一些信息进行校验,然后调用FindHandler函数查找try/catch块。

__InternalCxxFrameHandler

FindHandler函数中使用三层循环对try块的范围进行校验,并将符合要求的try与catch进行类型比对,比对成功调用CatchIt完成异常处理。

FindHandler

__TypeMatch函数中使用strcmp比较了RTTI的name字符串。

__TypeMatch

CatchIt函数中主要是处理异常对象、执行栈展开、执行catch块。

CatchIt

其中栈展开调用了如下API:

栈展开API

之后循环调用UnwindMapEntry.action完成局部对象的析构。

执行action

最终执行catch块代码,完成异常处理。

catch块执行

在catch块代码执行完后,会将eax修改为catch块结束位置,返回给上一层函数。

设置eax

最终由JumpToContinuation函数跳转到catch块结束位置完成异常处理。

JumpToContinuation函数

3.3 throw过程分析

异常对象抛出位置,可以看到ThrowInfo作为参数传递。如果抛出的是对象类型的异常,这里会调用它的构造函数。

抛出异常位置

相关结构信息如下:

抛出异常结构

根据上述信息可以看出抛出的异常类型为 基本类型 ,可以被char*、void*类型的catch块捕获。

在__CxxThrowException函数中调用API向内核抛出异常。

API抛异常

之后就是内核对异常的处理,实现方式由系统决定。

4.x64异常处理

​ x64中几乎每个函数都存在一个RUNTIME_FUNCTION结构(除了不操作rsp、没有异常处理的函数、不会调用其他函数),保存在pe文件的.pdata段。从这个结构出发可以找到UNWIND_INFO,最终找到FuncInfo结构,需要注意的是x64下指针位置都是RVA,需要加上ImageBase来定位真实地址。

4.1 结构定义

4.1.1 RUNTIME_FUNCTION

1
2
3
4
5
6
// 该结构在内存中必须4字节对齐
typedef struct _RUNTIME_FUNCTION { // 均为RVA
ULONG BeginAddress; // 函数起始地址
ULONG EndAddress; // 函数结束地址
ULONG UnwindData; // UNWIND_INFO指针
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

4.1.2 UNWIND_INFO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _UNWIND_INFO {			// 均为RVA
UCHAR Version : 3; // 结构体版本号
UCHAR Flags : 5; // 异常处理信息
UCHAR SizeOfProlog; // 函数序言部分大小
UCHAR CountOfCodes; // 后面跟随的 UNWIND_CODE 结构所占的字节数
UCHAR FrameRegister : 4; // 帧寄存器
UCHAR FrameOffset : 4; // 帧寄存器偏移量
UNWIND_CODE UnwindCode[1]; // UNWIND_CODE数组

ULONG FunctionEntry; // CxxFrameHandler3地址
ULONG ExceptionData; // FuncInfo结构的rva


} UNWIND_INFO, *PUNWIND_INFO;

Flag:
UNW_FLAG_NHANDLER (0x0): 表示既没有 EXCEPT_FILTER 也没有 EXCEPT_HANDLER
UNW_FLAG_EHANDLER (0x1): 表示该函数有 EXCEPT_FILTER & EXCEPT_HANDLER
UNW_FLAG_UHANDLER (0x2): 表示该函数有 FINALLY_HANDLER
UNW_FLAG_CHAININFO (0x4): 表示该函数有多个 UNWIND_INFO,它们串接在一起(所谓的 chain)

4.1.3 UNWIND_CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef union _UNWIND_CODE {
struct {
UCHAR CodeOffset; // 回滚位置距离函数入口的偏移
UCHAR UnwindOp : 4; // 对应下面的枚举,表示栈回滚类型
UCHAR OpInfo : 4; // 归滚操作的具体信息,如栈提升多少个字节
};
USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

typedef enum _UNWIND_OP_CODES {
UWOP_PUSH_NONVOL = 0, // 0 压入非易失寄存器,操作信息是寄存器编号
UWOP_ALLOC_LARGE, // 1 堆栈中分配一个大区域,操作信息为0时,分配大小除以8放在下一个字节上;1时,未缩放的大小放在之后的两个字节上
UWOP_ALLOC_SMALL, // 2 堆栈中分配一个小区域,操作信息为分配大小除以8再减去1
UWOP_SET_FPREG, // 3 通过将寄存器设置为当前 RSP 的某个偏移量来建立帧指针寄存器,操作信息为偏移量除以16
UWOP_SAVE_NONVOL, // 4 使用mov指令保存非易失寄存器,操作信息为寄存器编号
UWOP_SAVE_NONVOL_FAR, // 5 使用mov指令保存长偏移非易失寄存器,操作信息为寄存器编号,未缩放的堆栈偏移保存在后两个字节
UWOP_SPARE_CODE1, // 6
UWOP_SPARE_CODE2, // 7
UWOP_SAVE_XMM128, // 8 堆栈上保存xmm寄存器,操作信息为寄存器编号
UWOP_SAVE_XMM128_FAR, // 9 堆栈上保存长偏移xmm寄存器,操作信息为寄存器编号
UWOP_PUSH_MACHFRAME // 10 硬件中断相关
} UNWIND_OP_CODES, *PUNWIND_OP_CODES;

4.1.3 FuncInfo变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// x64函数信息结构
struct FuncInfo { // RVA
// 编译器版本号
// 0x19930520: up to VC6, 0x19930521: VC7.x(2002-2003), 0x19930522: VC8 (2005)
DWORD magicNumber;

// 最大栈展开数的下标
int maxState;

// 栈展开结构数组指针
UnwindMapEntry* pUnwindMap;

// 函数中try块个数
DWORD nTryBlocks;

// try块结构数组指针
TryBlockMapEntry* pTryBlockMap;

// IP映射表的数量
DWORD nIPMapEntries;

// IP映射表的RVA,指向IPtoStateMapEntry结构
void* pIPtoStateMap;

// 异常展开帮助RVA
UnWindMapEntry* pUnWindMapEntry;

// 异常类型列表RVA
ESTypeList* pESTypeList;

// 一些功能标志
int EHFlags;
};

4.1.4 IptoStateMapEntry

1
2
3
4
struct IptoStateMapEntry {
ULONG __Ip; // try块起始rip的RVA
ULONG State; // try块状态索引
};

4.2 结构关系图

​ x86应用程序中,使用栈空间的一个变量标 识try块的状态索引,在x64中不再使用该变量,而是通过产生异常的 地址(RIP)查询IP状态映射表来获取try块的状态索引,结构如下图所示:

FuncInfo结构关系

4.3 示例分析

查看main函数的交叉引用可以定位到它的RUNTIME_FUNCTION结构,如下图所示:

定位RUNTIME_FUNCTIONJ结构

根据RUNTIME_FUNCTION的第三个成员定位到UNWIND_INFO结构,如下图所示:

定位UNWIND_INFO

UNWIND_INFO结构信息如下:

UNWIND_INFO信息

根据rva定位到FuncInfo结构,如下图所示:

定位FuncInfo

进而定位到TryBlockMapEntry数组以及HandlerType数组,得到try1的索引为0~0,如下图所示:

定位TryBlockMapEntry

定位HandlerType

最终定位到catch块地址,如下图所示:

catch块地址

根据FuncInfo定位IP状态表,如下图所示:

IP状态表

根据try1的索引00可以在IP表中确定边界为0x1400A8BC90x1400A8BEB,验证如下:

try范围

catch1的范围为0x1401DC1B40x1401DC1ED,catch2的范围为0x1401DC2100x1401DC249,如下图所示:

catch1范围

catch2范围

5. 总结

  • 基于Windows SEH实现,在函数头部构造异常链,以及异常处理回调函数的注册。
  • 异常处理结构信息在编译阶段生成好。
  • throw调用了Windows API来完成异常的抛出。
  • 类型识别基于RTTI实现,本质就是编译器生成的类型字符串,使用strcmp比对来寻找合适的catch进行执行。
  • 栈展开机制是在当前函数中找不到catch时,需要向上层函数寻找时被触发,展开过程需要调用局部对象的析构函数。

C++异常处理
http://helloymf.github.io/2022/10/02/c-yi-chang-chu-li/
作者
JNZ
许可协议