发现了C++中的try...catch的秘密!
大家好,我是轩辕。
注意看下面这段代码:
void exception_test(void* ptr, int number) {
try {
if (!ptr) {
throw MemoryException(ptr);
}
if (number == 0) {
throw DivisionException(number);
}
printf("test done\n");
}
catch (MemoryException& e) {
printf("%s\n", e.GetMessage());
}
catch (DivisionException& e) {
printf("%s\n", e.GetMessage());
}
}
这个函数里面,对两个输入参数进行了检查,如果发现参数错误,就抛出异常。
那么现在有一个问题:程序运行的时候抛出的异常,如何知道该交给哪个catch块来处理呢?毕竟C++不像Java有反射啊?
我的知识星球上有位小伙伴就提了这么一个问题:
今天这篇文章,我们就从逆向的方式来探究这个问题的答案。
首先要明确一个事情:C++作为一门编程语言,有很多种编译器都支持,像我们Windows平台接触的VC++,还有Linux下的GCC-G++,还有clang等其他编译器。C++的语言规范里面只对异常处理的语法特性做了规定,但如何实现这个异常处理,它没有规定,各家编译器自行实现,只要最后表现出来的符合标准语法规定即可。
这里就以咱们逆向分析常见的Windows平台上的VC++来为例进行分析,但要记住,本文分析的内容,只限定与VC++,其他编译器并不适用。
在谈C++的异常处理实现之前,先给大家介绍一下Windows上的结构化异常处理机制,这个在之前的课程中给大家简要提过,这里我们再来复习一下。
在计算机中,有两种异常。一种是CPU异常,一种是软件异常。CPU异常一般指的是CPU执行指令过程中发生的异常情况,比如执行除法指令的时候,发现除数是0。比如访问内存的时候,发现地址异常等等,这些都是属于CPU异常。
软件异常,一般是指程序在运行过程中,发现错误,自己主动抛出异常。比如C++中的throw
关键字抛出的异常,就属于这一类。
不管是CPU异常,还是软件异常,最终都会走到统一的异常派遣分发流程,这里面的过程非常繁琐复杂。总体来说,操作系统收到这些异常后,会通过一系列的流程检查,然后去寻找处理这些异常的函数,如果没有任何函数可以处理这些异常,程序弹个报错窗口,然后崩溃退出。
这里面结构化异常处理SEH就是一个重要的机制,多个异常处理函数的地址存放在栈中,通过单向链表的形式串接起来,然后通过FS寄存器指向的TEB中的一个字段进行定位。当异常发生的时候,操作系统库函数就沿着这个链表,依次寻找可以处理当前异常的函数。
而C++的异常处理,在VC++编译器中的实现,就与这个有很大关系。
来再一次看之前的代码:
#include <exception>
using namespace std;
// 内存异常
class MemoryException : public exception {
public:
MemoryException(void* addr) {
this->address = addr;
}
const char* GetMessage() {
sprintf_s(message, sizeof(message), "bad address: %p", address);
return message;
}
private:
void* address;
char message[100];
};
// 除数异常
class DivisionException : public exception {
public:
DivisionException(int divisor) {
this->divisor = divisor;
}
const char* GetMessage() {
sprintf_s(message, sizeof(message), "bad divisor: %d", divisor);
return message;
}
private:
int divisor;
char message[100];
};
void exception_test(void* ptr, int number) {
printf("enter exception_test\n");
try
{
if (!ptr) {
throw MemoryException(ptr);
}
if (number == 0) {
throw DivisionException(number);
}
printf("test done\n");
}
catch (MemoryException& e)
{
printf("%s\n", e.GetMessage());
}
catch (DivisionException& e)
{
printf("%s\n", e.GetMessage());
}
printf("leave exception_test\n");
}
int main() {
exception_test(NULL, 0);
}
我定义了两个异常类,分别代表内存异常和除数异常。在exception_test函数中检查了这个函数的两个参数,当发现参数错误的时候,通过throw
关键字,抛出了异常。
星球小伙伴的第一个问题:这里有两个catch块,当异常发生的时候,程序怎么知道该调用哪个catch块呢?毕竟C++并不像Java,具有反射这样的类型动态识别机制。
实际上,很多人不知道,C++其实有一个低配版的运行时类型识别能力,叫做RTTI
,可以通过这个机制来获取类的一些信息。之所以称之为低配版,是因为不具备像Java那样能根据类型名称动态创建对象的能力。
C++标准提供了type_id
关键字和type_info
结构体,通过这两个东西可以获得类的运行时名字,比如下面的代码:
const type_info &ti = typeid(MemoryException);
cout << ti.name() << endl;
运行后输出如下:
VC++在进行异常分发的时候,就离不开这个东西的支持。
我们来看一下前面的代码在通过throw关键字抛出异常的地方,反汇编是什么样的: