查看原文
其他

发现了C++中的try...catch的秘密!

轩辕之风 轩辕的编程宇宙
2024-10-21

大家好,我是轩辕。

注意看下面这段代码:

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(NULL0);
}

我定义了两个异常类,分别代表内存异常和除数异常。在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关键字抛出异常的地方,反汇编是什么样的:

继续滑动看下一个
轩辕的编程宇宙
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存