C++异常处理实现(libstdc++)

news2024/12/28 5:48:48

摘要:为了更好的理解C++中异常处理的实现,本文简单描述了Itanium ABI中异常处理的流程和llvm/libsdc++简要实现。
关键字:C++,exception,llvm,clang

  C++他提供了异常处理机制来对程序中的错误进行处理,避免在一些异常情况下无法恢复现场而导致额外的损失。虽然在很多Google C++ Guide中禁止使用异常,但是作为C++的一部分我们仍然有足够的理由去了解其技术细节。

  由于C++的ABI并不统一,主流的ABI有两种Itanium ABI和MSVC的ABI,也就是常用的clang/gcc、msvc编译器采用的两种ABI。因此具体的异常实现也不同。比如clang/gcc使用SJLJ(SetJmp/LongJmp)实现,而Windows上虽然大体流程相似但是细节上存在一些差异。本为主要以llvm/Itanium ABI为参考描述,对Windows实现有兴趣的可以参考Exception Handling using the Windows Runtime。

  再次在在此之前需要了解几个概念:

  landing pad:A section of user code intended to catch, or otherwise clean up after, an exception. It gains control from the exception runtime via the personality routine, and after doing the appropriate processing either merges into the normal user code or returns to the runtime by resuming or raising a new exception.

  Itanium ABI中将实现相关和实现无关的内容拆分开,来保证ABI的灵活度。因此,Itanium C++ ABI中Exception Handling分成Level 1 Base ABI and Level 2 C++ ABI两部分。Base ABI描述了语言无关的stack unwinding部分,定义了_Unwind_* API。Level 2 则和C++实现相关,定义了__cxa_* API(__cxa_allocate_exception, __cxa_throw, __cxa_begin_catch等)。

  首先简单说一下,C++ 异常处理的基本流程:

  1. 调用__cxa_allocate_exception创建异常需要的一些对象,比如exception object等;
  2. 调用cxa_throw抛出异常;
  3. 异常抛出后开始栈展开并搜索匹配的异常类型;
    1. 阶段一,不断展开栈直到搜索到匹配的异常类型,并执行personality routine;
    2. 阶段二,从抛出异常的位置开始执行每个栈帧的cleanup内容;
  4. 如果没有匹配到则调用std::terminate终止程序,否则执行根据表格指引执行landing pad;
  5. 最后执行清理操作销毁异常对象,还原现场。

1 数据结构

1.1 Exception Objects

  一个完整的 C++ 异常对象由一个头部组成,这个头部是围绕着一个带有额外的 C++ 特定信息的 unwind 对象头部的包装器,然后是抛出的 C++ 异常对象本身。头部的结构如下:

struct __cxa_exception { 
	std::type_info *	exceptionType;
	void (*exceptionDestructor) (void *); 
	unexpected_handler	unexpectedHandler;
	terminate_handler	terminateHandler;
	__cxa_exception *	nextException;
	int			handlerCount;
	int			handlerSwitchValue;
	const char *		actionRecord;
	const char *		languageSpecificData;
	void *			catchTemp;
	void *			adjustedPtr;
	_Unwind_Exception	unwindHeader;
};
  • exceptionType字段编码了抛出的异常的类型。exceptionDestructor字段包含了一个指向被抛出类型的析构函数的函数指针,可能为空。这些指针必须存储在异常对象中,因为非多态和内置类型可以被抛出。
  • unexpectedHandlerterminateHandler字段包含了指向异常抛出点处的未预期和终止处理程序的指针。
  • nextException字段用于创建异常的链表(每个线程)。
  • handlerCount字段包含捕获此异常对象的处理程序数量。它还用于确定异常的生存时间。
  • handlerSwitchValueactionRecordlanguageSpecificDatacatchTempadjustedPtr字段缓存在第一遍计算时得到的信息,但在第二遍时也很有用。通过将这些信息存储在异常对象中,清理阶段可以避免重新检查操作记录。这些字段保留供包含要调用的处理程序的栈帧的自定义函数使用。
  • unwindHeader结构用于在多种语言或相同语言的多个运行时存在的情况下正确操作异常。
      按照惯例,一个 __cxa_exception 指针指向抛出的 C++ 异常对象的表示,紧随其后是头部。头部结构可以通过从 __cxa_exception 指针的负偏移访问。这种布局允许对来自不同语言(或同一语言的不同实现)的异常对象进行一致的处理,并允许在保持二进制兼容性的同时对头部结构进行将来的扩展。

  下面是llvm/libstdc++中的定义,具体实现在llvm-project/libcxxabi/src/cxa_exception.h中。可以看到大体上的layout和ABI中定义的相同,但是在头部多了一些字段就是上面提及的为了兼容性实际使用时需要通过负偏移来读取。

struct _LIBCXXABI_HIDDEN __cxa_exception {
#if defined(__LP64__) || defined(_WIN64) || defined(_LIBCXXABI_ARM_EHABI)
    // Now _Unwind_Exception is marked with __attribute__((aligned)),
    // which implies __cxa_exception is also aligned. Insert padding
    // in the beginning of the struct, rather than before unwindHeader.
    void *reserve;

    // This is a new field to support C++11 exception_ptr.
    // For binary compatibility it is at the start of this
    // struct which is prepended to the object thrown in
    // __cxa_allocate_exception.
    size_t referenceCount;
#endif

    //  Manage the exception object itself.
    std::type_info *exceptionType;
#ifdef __USING_WASM_EXCEPTIONS__
    // In Wasm, a destructor returns its argument
    void *(_LIBCXXABI_DTOR_FUNC *exceptionDestructor)(void *);
#else
    void (_LIBCXXABI_DTOR_FUNC *exceptionDestructor)(void *);
#endif
    std::unexpected_handler unexpectedHandler;
    std::terminate_handler  terminateHandler;

    __cxa_exception *nextException;

    int handlerCount;

#if defined(_LIBCXXABI_ARM_EHABI)
    __cxa_exception* nextPropagatingException;
    int propagationCount;
#else
    int handlerSwitchValue;
    const unsigned char *actionRecord;
    const unsigned char *languageSpecificData;
    void *catchTemp;
    void *adjustedPtr;
#endif

#if !defined(__LP64__) && !defined(_WIN64) && !defined(_LIBCXXABI_ARM_EHABI)
    // This is a new field to support C++11 exception_ptr.
    // For binary compatibility it is placed where the compiler
    // previously added padding to 64-bit align unwindHeader.
    size_t referenceCount;
#endif
    _Unwind_Exception unwindHeader;
};

1.2 Caught Exception Stack

  c+±rt中每个线程都包含一个全局的对象来描述当前线程异常的状况。

struct __cxa_eh_globals {
	__cxa_exception *	caughtExceptions;
	unsigned int		uncaughtExceptions;
};
  • caughtExceptions字段是一个活动异常列表,按照最近的异常排在前面,通过异常头部的nextException字段链接成stack。
  • uncaughtExceptions字段是未捕获异常的计数,供 C++ 库的uncaught_exceptions使用。
      这些信息是基于每个线程维护的。因此,caughtExceptions是当前线程抛出并捕获的异常列表,uncaughtExceptions是当前线程抛出但尚未捕获的异常计数。(这包括重新抛出的异常,它们可能仍然具有活动的处理程序,但不被视为已捕获。)
      下面是llvm/libstdc++中的定义,具体实现在llvm-project/libcxxabi/src/cxa_exception.h中。
struct _LIBCXXABI_HIDDEN __cxa_eh_globals {
    __cxa_exception *   caughtExceptions;
    unsigned int        uncaughtExceptions;
#if defined(_LIBCXXABI_ARM_EHABI)
    __cxa_exception* propagatingExceptions;
#endif
};

2 抛异常

  实现抛出异常所需的处理可能包括以下步骤:

  1. 调用__cxa_allocate_exception来创建一个异常对象。
  2. 评估被抛出的表达式,并将其复制到由__cxa_allocate_exception返回的缓冲区中,可能使用复制构造函数。如果评估被抛出的表达式通过抛出异常退出,那么异常将传播而不是表达式本身。清理代码必须确保在刚刚分配的异常对象上调用__cxa_free_exception。(如果复制构造函数本身通过抛出异常退出,将调用 terminate()。)
  3. 调用__cxa_throw将异常传递给运行时库。

2.1 创建异常对象

  抛出异常时需要存储对象,而对象必须存储在具体的内存空间中。这个存储空间必须在堆栈unwind时必须保证其生命周期,并且必须是线程安全的。因此,异常对象的存储空间通常将在堆中分配,尽管实现可能提供紧急缓冲区以支持在低内存条件下抛出bad_alloc异常。
  内存由__cxa_allocate_exception分配,传递了要抛出的异常对象的大小(不包括__cxa_exception头部的大小),并返回指向异常对象临时空间的指针。如果可能的话,它将在堆上分配异常内存。如果堆分配失败,实现可以使用其他备份机制。
  C++ 运行时库应为每个潜在任务分配至少4K字节的静态紧急缓冲区,最多64KB。该缓冲区仅在异常对象动态分配失败的情况下使用。它应以 1KB 块的形式分配。任何时候最多有 16 个任务可以使用紧急缓冲区,最多4个嵌套异常,每个异常对象(包括Header)的大小最多为1KB。其他线程将被阻塞,直到16个线程之一取消分配其紧急缓冲存储。紧急缓冲区的接口是实现定义的,并且仅由异常库使用。
  如果在这些约束条件下__cxa_allocate_exception无法分配异常对象,它将调用terminate()终止程序。

void * __cxa_allocate_exception(size_t thrown_size);

  一旦空间被分配,throw 表达式必须根据 C++ 标准指定的抛出值初始化异常对象。临时空间将由__cxa_free_exception释放,该函数传递了前一个__cxa_allocate_exception返回的地址。

void __cxa_free_exception(void *thrown_exception);

  这些函数是线程安全的(在多线程环境中),并且在达到允许使用紧急缓冲区的最大线程数后,可能会阻塞线程。

2.2 抛异常

cxa_throw
在使用throw参数值构造异常对象后,生成的代码调用__cxa_throw运行时库函数。这个例程永远不会返回。

void __cxa_throw(void *thrown_exception, std::type_info *tinfo, void (*dest)(void *));

参数包括:

  • 抛出异常对象的地址(指向头部之后的抛出值,如上所述)。
  • 一个std::type_info指针,给出抛出参数的静态类型作为一个std::type_info指针,用于将潜在的捕获点与抛出的异常匹配。
  • 一个最终用于销毁对象的析构函数指针。
    __cxa_throw例程将执行以下操作:
  • 从抛出异常对象地址获取__cxa_exception头部,可以如下计算:
    __cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
    
  • 将当前的unexpected_handlerterminate_handler保存在 __cxa_exception头部中。
  • tinfodest参数保存在__cxa_exception头部中。
  • 在unwind头部中设置exception_class字段。这是一个64位值,表示 ASCII 字符串 “XXXXC++\0”,其中 "XXXX"是一个供应商相关的字符串。C++中64 位值的低 4 字节将是 “C++\0”。
  • 增加未捕获异常标志。
  • 在系统unwind库中调用_Unwind_RaiseException。它的参数是__cxa_throw自身作为参数接收的指向抛出异常的指针。
  • __Unwind_RaiseException开始堆栈unwind过程。在特殊情况下,例如无法找到处理程序,_Unwind_RaiseException可能会返回。在这种情况下,假设没有处理程序处理异常,__cxa_throw将调用terminate

__Unwind_RaiseException
  抛出一个异常,传递给定的异常对象,该对象应该已经设置了其exception_classexception_cleanup字段。异常对象已由特定于语言的运行时分配,并具有特定于语言的格式,除非它必须包含一个_Unwind_Exception结构体。_Unwind_RaiseException不返回,除非发现错误条件(例如异常没有处理程序、堆栈格式不正确等)。在这种情况下,将返回一个_Unwind_Reason_Code值。可能的情况包括:

  • _URC_END_OF_STACK:unwind在阶段1中遇到了堆栈的末尾,没有找到处理程序。unwind运行时不会修改堆栈。在这种情况下,C++运行时通常会调用uncaught_exception
  • _URC_FATAL_PHASE1_ERROR:unwind在阶段1中遇到了意外错误,例如堆栈损坏。unwind运行时不会修改堆栈。在这种情况下,C++运行时通常会调用terminate

  如果unwind时在阶段2中遇到意外错误,它应该向其调用者返回_URC_FATAL_PHASE2_ERROR。在 C++ 中,这通常是__cxa_throw,后者将调用terminate()

注意:unwind运行时很可能已经修改了堆栈(例如,从中弹出了帧)或者寄存器上下文,或者落脚点代码可能已经破坏了它们。因此,_Unwind_RaiseException的调用者无法对其堆栈或寄存器的状态做出任何假设。

3 捕获异常

3.1 两阶段

UnWind
  为了捕获异常,抛出异常后需要对当前函数的调用栈进行展开。堆栈展开有两个主要原因:

  1. 异常,由支持异常的语言定义(例如C++);
  2. “强制”展开(例如由longjmp或线程终止引起)。

  异常机制是语言无关的,为了和其他语言交互,将C++语言相关的部分抽象出来即personality routine

  在抛出异常的情况下,堆栈在异常传播时被展开,但是每个堆栈帧的personality routine都知道它是否想要捕获异常或将其传递是预期内的。因此,这个选择被委托给了personality routinepersonality routine被期望对任何类型的异常(“本地”或“外部”)都能正确地做出反应。
  在“强制展开”期间,外部代理驱动展开。例如,这可以是longjmp。这个外部代理,而不是每个personality routine决定何时停止展开。personality routine没有关于展开是否会继续的,而是否进行展开是由的_UA_FORCE_UNWIND标志表示。
  为了适应这些差异,提出了两种不同的处理程序。_Unwind_RaiseException执行异常展开,受personality routine控制。另一方面,_Unwind_ForcedUnwind执行展开,但给外部代理拦截调用personality routine的时机。这是通过使用代理personality routine完成的,该代理拦截对personality routine的调用,让外部代理覆盖堆栈帧personality routine的默认设置。
  因此,不需要每个personality routine了解可能导致展开的任何可能的外部代理。例如,C++personality routine只需要处理C++异常(可能伪装外部异常),但不需要了解关于代表longjmp或pthread取消执行的展开的任何特定信息。

UnWind流程
  异常时的Unwind处理分为两阶段过程:

  • 在搜索阶段,框架重复调用personality routine,使用_UA_SEARCH_PHASE标志,首先针对当前PC和寄存器状态,然后在每一步展开帧到新PC,直到personality routine报告在所有帧中成功(在查询的帧中找到处理程序)或失败(所有帧中都没有处理程序)。它实际上不会恢复展开的状态,personality routine必须通过API访问状态;
  • 如果搜索阶段报告失败,例如因为未找到处理程序,它将调用terminate()而不是开始第二阶段。
  • 如果搜索阶段报告成功,框架将重新启动清理阶段。同样,它重复调用personality routine,使用_UA_CLEANUP_PHASE标志,首先针对当前PC和寄存器状态,然后在每一步展开帧到新PC,直到到达具有已识别处理程序的帧为止。在那一点上,它恢复寄存器状态,并将控制传递给用户landing pad。
      这两个阶段都使用展开库和personality routine,因为给定处理程序的有效性和将控制传递给它的机制是语言相关的,但定位和恢复先前堆栈帧的方法是语言无关的。

两阶段异常处理模型并不是严格必要的来实现C++语言语义,但它确实提供了一些好处。例如,第一阶段允许异常处理机制在堆栈展开开始之前解除异常,这允许恢复式异常处理(纠正异常条件并在引发点恢复执行)。虽然C++不支持恢复式异常处理,但其他语言支持,而两阶段模型允许C++与这些语言在堆栈上共存。

  请注意,即使采用了两阶段模型,对于单个异常,可能会多次执行每个阶段,就好像异常被抛出了多次一样。例如,由于无法确定给定的catch子句是否会重新抛出异常,而不执行它,异常传播实际上在每个catch子句处停止,并且如果需要重新开始,则重新开始第1阶段。对于析构函数(清理代码)不需要此过程,因此阶段1可以安全地一次性处理所有仅包含析构函数的帧,并在下一个封闭的catch子句处停止。
  例如,如果展开的前两个帧仅包含清理代码,而第三个帧包含一个C++的catch子句,则第1阶段中的personality routine不会指示它在前两个帧中找到了处理程序。它必须在第三个帧中这样做,因为无法确定异常将如何传播出第三个帧,例如通过在C++中重新抛出异常或引发新异常。
  堆栈展开库在堆栈上运行两次遍历,如下所示:

  1. 恢复当前堆栈帧中的程序计数器(PC)。
  2. 使用展开表,在该PC处查找关于如何处理在该PC处发生的异常的信息,特别是获取该地址范围内personality routine的地址。
  3. 调用personality routine(见第2.5.2节)。personality routine将确定在堆栈的该级别是否找到了适当的处理程序(在第一次遍历中),并确定从landing pad调用哪个特定处理程序(在第二次遍历中),以及传递给landing pad的参数(见第3.5.2节)。personality routine将此信息传递回展开库。

  在第二阶段,展开库跳转到与调用对应的landing pad以展开的堆栈级别。由personality routine指示设置landing pad参数。landing pad执行补偿代码(由后端生成),以恢复适当的寄存器和堆栈状态。
  然后,前端生成的一些清理代码可能会执行,对应于try块的退出。例如,try块中的局部自动变量将在此处被销毁。
  异常处理程序可能会选择并执行与C++ catch子句和其他处理程序对应的用户定义代码。生成的代码可能类似于switch语句,其中switch值由运行时基于异常类型确定,并作为landing pad参数传递。
  一旦运行时确定执行将转到处理程序,展开库就认为展开过程对于其完成了。处理程序仍然可以重新抛出当前异常或不同的异常,但在两种情况下都将发生新的展开过程。否则,在处理程序中的代码执行完毕后,执行将在定义此处理程序的try块的末尾恢复。
  如果可能的处理程序都不匹配正在抛出的异常,则运行时会选择一个不匹配任何switch语句的switch值。在这种情况下,控制流将通过所有switch语句,并转到其他清理代码,该清理代码将调用需要为当前堆栈帧调用的所有析构函数。例如,在函数的外部块的自动变量将在此处被销毁。这意味着处理程序必须在清理帧的过程中循环通过任何包围原始块的try块,尝试每一个switch值。
  在当前清理代码的末尾,控制被转移回展开库,以展开另一个堆栈帧。

3.2 异常处理表格

异常处理帧(Exception Handling Frame)
  异常处理帧eh_frame与DWARF调试信息中使用的展开帧非常相似。该帧包含了撤销当前帧并恢复先前帧状态所需的所有信息。每个编译单元中的函数都有一个异常处理帧,另外还有一个通用异常处理帧,其中定义了单元中所有函数共享的信息。
  然而,这种调用帧信息(CFI)的格式通常与平台相关。例如,ARM 定义了自己的格式。苹果有自己的紧凑展开信息格式。在 Windows 上,自 32 位 x86 以来,所有架构都使用另一种格式。LLVM 将生成目标所需的任何信息。
  eh_frame信息可参考Linux Standard Base Core Specification 3.0RC1月也可以使用命令objdump -sj .eh_frame <your_exe_file>查看具体的内容。

异常表(Exception Tables)
  异常表包含有关在函数代码的特定部分抛出异常时应采取的操作的信息。这通常被称为特定于语言的数据区域(LSDA)。LSDA 表的格式特定于自定义函数,但其中的大部分使用的是 __gxx_personality_v0所需的变体表。每个函数都有一个异常表,除了叶子函数和仅调用非抛出函数的函数。它们不需要异常表。
在这里插入图片描述

3.3 Personality Routine

Personality Routine是C++(或其他语言)运行时库中的函数,它充当系统展开库与特定语言异常处理语义之间的接口。它是针对由展开信息块描述的代码片段而言的,并且始终通过展开信息块中的指针引用。当程序抛出异常时,该函数会被调用来处理异常。这个函数的确切实现取决于编译器和操作系统,但它的名称通常是固定的,并且在链接时被引入到程序中。Personality Routine需要在合适的时机将控制权交给LandingPad。

_Unwind_Reason_Code (*__personality_routine)
	    (int version,
	     _Unwind_Action actions,
	     uint64 exceptionClass,
	     struct _Unwind_Exception *exceptionObject,
	     struct _Unwind_Context *context);

  gcc和clang中该符号为__gxx_personality_v0

4 LLVM/libstdc++ 实现

4.1 __cxa_allocate_exception

  __cxa_allocate_exception实现比较简单,主要是通过rt内置的allocate函数申请内存,然后初始化内存。唯一需要注意的返回的对象指针并不是allocate的内存开头,而是经过偏移的,所以后面读取对象的时候都要偏移回去。

void *__cxa_allocate_exception(size_t thrown_size) throw() {
    size_t actual_size = cxa_exception_size_from_exception_thrown_size(thrown_size);

    // Allocate extra space before the __cxa_exception header to ensure the
    // start of the thrown object is sufficiently aligned.
    size_t header_offset = get_cxa_exception_offset();
    char *raw_buffer =
        (char *)__aligned_malloc_with_fallback(header_offset + actual_size);
    if (NULL == raw_buffer)
        std::terminate();
    __cxa_exception *exception_header =
        static_cast<__cxa_exception *>((void *)(raw_buffer + header_offset));
    ::memset(exception_header, 0, actual_size);
    return thrown_object_from_cxa_exception(exception_header);
}

4.2 __cxa_throw

  下面是llvm/libstdc++的实现,具体代码在llvm-project/libcxxabi/src/cxa_exception.cpp中。

void
#ifdef __USING_WASM_EXCEPTIONS__
// In Wasm, a destructor returns its argument
__cxa_throw(void *thrown_object, std::type_info *tinfo, void *(_LIBCXXABI_DTOR_FUNC *dest)(void *)) {
#else
__cxa_throw(void *thrown_object, std::type_info *tinfo, void (_LIBCXXABI_DTOR_FUNC *dest)(void *)) {
#endif
  __cxa_eh_globals* globals = __cxa_get_globals();
  globals->uncaughtExceptions += 1; // Not atomically, since globals are thread-local

  __cxa_exception* exception_header = __cxa_init_primary_exception(thrown_object, tinfo, dest);
  exception_header->referenceCount = 1; // This is a newly allocated exception, no need for thread safety.

#if __has_feature(address_sanitizer)
  // Inform the ASan runtime that now might be a good time to clean stuff up.
  __asan_handle_no_return();
#endif

#ifdef __USING_SJLJ_EXCEPTIONS__
    _Unwind_SjLj_RaiseException(&exception_header->unwindHeader);
#else
    _Unwind_RaiseException(&exception_header->unwindHeader);
#endif
    //  This only happens when there is no handler, or some unexpected unwinding
    //     error happens.
    failed_throw(exception_header);
}

  该函数接受的参数中描述了需要抛出的异常的对象地址和typeinfo信息,以及cleanup的函数指针。从上面的流程中能够看到执行步骤分别为:

  1. 通过cxa_get_globals接口获取TLS全局EH对象。在第一次调用时会创建__cxa_eh_globals表格并创建一个对喜爱嗯,该接口中使用平台相关的Thread实现;
  2. 然后调整__cxa_eh_globals和异常对象的引用技术,由于前者存储在TLS上,因此不需要atomic;
  3. 调用具体的抛异常函数_Unwind_RaiseException,如果返回了说明失败,否则不会进行下一步;
  4. 处理抛出异常失败的情况,调用一些处理函数后直接__terminate

  具体调用的_Unwind_SjLj_RaiseException还是_Unwind_RaiseException根据具体的配置而言。_Unwind_RaiseException的实现比较简单,首先就是调用__unw_getcontext获取unwind的context,然后分别调用两阶段的unwind函数。

_LIBUNWIND_EXPORT _Unwind_Reason_Code
_Unwind_RaiseException(_Unwind_Exception *exception_object) {
  _LIBUNWIND_TRACE_API("_Unwind_RaiseException(ex_obj=%p)",
                       (void *)exception_object);
  unw_context_t uc;
  unw_cursor_t cursor;
  __unw_getcontext(&uc);

  // Mark that this is a non-forced unwind, so _Unwind_Resume()
  // can do the right thing.
  exception_object->private_1 = 0;
  exception_object->private_2 = 0;

  // phase 1: the search phase
  _Unwind_Reason_Code phase1 = unwind_phase1(&uc, &cursor, exception_object);
  if (phase1 != _URC_NO_REASON)
    return phase1;

  // phase 2: the clean up phase
  return unwind_phase2(&uc, &cursor, exception_object);
}

4.3 捕获异常

4.3.1 两阶段

  从上面能够看到捕获异常时栈展开的两阶段是由unwind_phase1unwind_phase2完成的。
阶段1:搜索匹配的栈
  原来的代码中有大量_LIBUNWIND_TRACE_UNWINDING相关的内容,下面的代码是删除_LIBUNWIND_TRACE_UNWINDING相关内容的代码。

static _Unwind_Reason_Code
unwind_phase1(unw_context_t *uc, unw_cursor_t *cursor, _Unwind_Exception *exception_object) {
  __unw_init_local(cursor, uc);

  // Walk each frame looking for a place to stop.
  while (true) {
    // Ask libunwind to get next frame (skip over first which is
    // _Unwind_RaiseException).
    int stepResult = __unw_step(cursor);
    if (stepResult == 0) {
      _LIBUNWIND_TRACE_UNWINDING(
          "unwind_phase1(ex_obj=%p): __unw_step() reached "
          "bottom => _URC_END_OF_STACK",
          (void *)exception_object);
      return _URC_END_OF_STACK;
    } else if (stepResult < 0) {
      return _URC_FATAL_PHASE1_ERROR;
    }

    // See if frame has code to run (has personality routine).
    unw_proc_info_t frameInfo;
    unw_word_t sp;
    if (__unw_get_proc_info(cursor, &frameInfo) != UNW_ESUCCESS) {
      return _URC_FATAL_PHASE1_ERROR;
    }

    // If there is a personality routine, ask it if it will want to stop at
    // this frame.
    if (frameInfo.handler != 0) {
      _Unwind_Personality_Fn p =
          (_Unwind_Personality_Fn)(uintptr_t)(frameInfo.handler);
      _Unwind_Reason_Code personalityResult =
          (*p)(1, _UA_SEARCH_PHASE, exception_object->exception_class,
               exception_object, (struct _Unwind_Context *)(cursor));
      switch (personalityResult) {
      case _URC_HANDLER_FOUND:
        // found a catch clause or locals that need destructing in this frame
        // stop search and remember stack pointer at the frame
        __unw_get_reg(cursor, UNW_REG_SP, &sp);
        exception_object->private_2 = (uintptr_t)sp;
        return _URC_NO_REASON;

      case _URC_CONTINUE_UNWIND:
        // continue unwinding
        break;

      default:
        return _URC_FATAL_PHASE1_ERROR;
      }
    }
  }
  return _URC_NO_REASON;
}

  阶段1搜索匹配的栈帧:

  1. 首先cursorstep到表格中的下一项。cursor是libunwind中实现的一个游标,即AbstractUnwindCursor

    UnwindCursor contains all state (including all register values) during an unwind. This is normally stack-allocated inside a unw_cursor_t.

  2. 然后根据游标中存储的信息获取栈帧信息,如果获取失败就会退出;
  3. 最后检查当前栈帧是否包含personality routine处理程序,没有就会继续搜索,有的花将话就会调用并根据该程序的返回值举鼎下一步的行为。

阶段2:CleanUp

static _Unwind_Reason_Code
unwind_phase2(unw_context_t *uc, unw_cursor_t *cursor, _Unwind_Exception *exception_object) {
  __unw_init_local(cursor, uc);
  // uc is initialized by __unw_getcontext in the parent frame. The first stack
  // frame walked is unwind_phase2.
  unsigned framesWalked = 1;
#ifdef _LIBUNWIND_USE_CET
  unsigned long shadowStackTop = _get_ssp();
#endif
  // Walk each frame until we reach where search phase said to stop.
  while (true) {
    // Ask libunwind to get next frame (skip over first which is
    // _Unwind_RaiseException).
    int stepResult = __unw_step_stage2(cursor);
    if (stepResult == 0) {
      return _URC_END_OF_STACK;
    } else if (stepResult < 0) {
      return _URC_FATAL_PHASE2_ERROR;
    }

    // Get info about this frame.
    unw_word_t sp;
    unw_proc_info_t frameInfo;
    __unw_get_reg(cursor, UNW_REG_SP, &sp);
    if (__unw_get_proc_info(cursor, &frameInfo) != UNW_ESUCCESS) {
      return _URC_FATAL_PHASE2_ERROR;
    }
// In CET enabled environment, we check return address stored in normal stack
// against return address stored in CET shadow stack, if the 2 addresses don't
// match, it means return address in normal stack has been corrupted, we return
// _URC_FATAL_PHASE2_ERROR.
#ifdef _LIBUNWIND_USE_CET
    if (shadowStackTop != 0) {
      unw_word_t retInNormalStack;
      __unw_get_reg(cursor, UNW_REG_IP, &retInNormalStack);
      unsigned long retInShadowStack = *(
          unsigned long *)(shadowStackTop + __cet_ss_step_size * framesWalked);
      if (retInNormalStack != retInShadowStack)
        return _URC_FATAL_PHASE2_ERROR;
    }
#endif
    ++framesWalked;
    // If there is a personality routine, tell it we are unwinding.
    if (frameInfo.handler != 0) {
      _Unwind_Personality_Fn p =
          (_Unwind_Personality_Fn)(uintptr_t)(frameInfo.handler);
      _Unwind_Action action = _UA_CLEANUP_PHASE;
      if (sp == exception_object->private_2) {
        // Tell personality this was the frame it marked in phase 1.
        action = (_Unwind_Action)(_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME);
      }
       _Unwind_Reason_Code personalityResult =
          (*p)(1, action, exception_object->exception_class, exception_object,
               (struct _Unwind_Context *)(cursor));
      switch (personalityResult) {
      case _URC_CONTINUE_UNWIND:
        // Continue unwinding
        if (sp == exception_object->private_2) {
          // Phase 1 said we would stop at this frame, but we did not...
          _LIBUNWIND_ABORT("during phase1 personality function said it would "
                           "stop here, but now in phase2 it did not stop here");
        }
        break;
      case _URC_INSTALL_CONTEXT:
        // Personality routine says to transfer control to landing pad.
        // We may get control back if landing pad calls _Unwind_Resume().
        if (_LIBUNWIND_TRACING_UNWINDING) {
          unw_word_t pc;
          __unw_get_reg(cursor, UNW_REG_IP, &pc);
          __unw_get_reg(cursor, UNW_REG_SP, &sp);
        }

        __unw_phase2_resume(cursor, framesWalked);
        // __unw_phase2_resume() only returns if there was an error.
        return _URC_FATAL_PHASE2_ERROR;
      default:
        // Personality routine returned an unknown result code.
        return _URC_FATAL_PHASE2_ERROR;
      }
    }
  }
  // Clean up phase did not resume at the frame that the search phase
  // said it would...
  return _URC_FATAL_PHASE2_ERROR;
}

  阶段2逐个栈帧调用cleanup:

  1. 调用__unw_step_stage2调整当前的游标;
  2. 获取当前栈帧的基本信息,比如PC,寄存器等;
  3. 检查当前栈帧是否包含处理程序有的话调用,然后根据该程序的返回值执行下一步。

  需要注意的是阶段2和阶段1的起点相同,都是从调用__Unwind_RaiseException处开遍历。

4.3.2 Landing Pad

landing pad:A section of user code intended to catch, or otherwise clean up after, an exception. It gains control from the exception runtime via the personality routine, and after doing the appropriate processing either merges into the normal user code or returns to the runtime by resuming or raising a new exception.

  Landing pad是text section中的一段和exception相关的代码,它有三种:

  • cleanup clause:通常调用局部对象的析构函数或__attribute__((cleanup(…)))注册的callbacks,然后用_Unwind_Resume跳转回cleanup phase;
  • 捕获异常的catch clause:调用局部对象的析构函数,然后调用__cxa_begin_catch,执行catch代码,最后调用__cxa_end_catch
  • rethrow:调用catch clause中的局部对象的析构函数,然后调用__cxa_end_catch,接着用_Unwind_Resume跳转回cleanup phase。

5 异常编译后是什么样的

5.1 异常编译后是什么样的

  我们用下面的代码作为例子:

#include <cstdio>
#include <exception>

class UserDefine{
public:
  int a;
  UserDefine(){
    printf("run A cons\n");
  }
  ~UserDefine(){
    printf("run A des\n");
  }
};
class MyException : public std::exception{
public:
  int a;
  MyException(){
    printf("run MyException cons\n");
  }
  ~MyException(){
    printf("run MyException des\n");
  }
};

__attribute__((noinline)) void exceptionHanldeFunc1(){
  printf("the function %s is calling\n", __FUNCTION__);
}

__attribute__((noinline)) void exceptionHanldeFunc2(){
  printf("the function %s is calling\n", __FUNCTION__);
}

int main(int argc, char **argv){
    try{
      UserDefine a;
      throw MyException();
      printf("a %d", static_cast<int>(a.a));
    } catch(MyException &e){
      exceptionHanldeFunc2();
    } catch(...){
      exceptionHanldeFunc2();
    }

    return 0;
}

  下面是main函数反汇编的结果,代码含义看下面的注释:

00000000000011e0 <main>:
    11e0:	53                   	push   %rbx
    11e1:	48 8d 3d 62 0e 00 00 	lea    0xe62(%rip),%rdi        # 204a <_IO_stdin_used+0x4a>
    11e8:	e8 83 fe ff ff       	call   1070 <puts@plt>
    11ed:	bf 08 00 00 00       	mov    $0x8,%edi
    11f2:	e8 59 fe ff ff       	call   1050 <__cxa_allocate_exception@plt>      ;;调用cxa_allocate_exceptionallocate异常
    11f7:	48 8d 0d 92 2b 00 00 	lea    0x2b92(%rip),%rcx        # 3d90 <_ZNSt9exceptionD2Ev@GLIBCXX_3.4>  ;;由于自定义的MyException是空类因此这里直接调用的std::exception的构造函数
    11fe:	48 89 08             	mov    %rcx,(%rax)              ;;将异常对象的地址、typeinf等放到寄存器上
    1201:	48 8d 35 60 2b 00 00 	lea    0x2b60(%rip),%rsi        # 3d68 <_ZTVN10__cxxabiv120__si_class_type_infoE@CXXABI_1.3>
    1208:	48 8b 15 c1 2d 00 00 	mov    0x2dc1(%rip),%rdx        # 3fd0 <_ZNSt9exceptionD2Ev@GLIBCXX_3.4>
    120f:	48 89 c7             	mov    %rax,%rdi
    1212:	e8 79 fe ff ff       	call   1090 <__cxa_throw@plt>   ;;调用cxa_throw
    1217:	48 89 c3             	mov    %rax,%rbx
    121a:	48 8d 3d 34 0e 00 00 	lea    0xe34(%rip),%rdi        # 2055 <_IO_stdin_used+0x55>
    1221:	e8 4a fe ff ff       	call   1070 <puts@plt>
    1226:	48 89 df             	mov    %rbx,%rdi
    1229:	e8 12 fe ff ff       	call   1040 <__cxa_begin_catch@plt> ;;调用__cxa_begin_catch
    122e:	e8 8d ff ff ff       	call   11c0 <_Z20exceptionHanldeFunc2v> 
    1233:	e8 48 fe ff ff       	call   1080 <__cxa_end_catch@plt>   ;;调用__cxa_end_catch
    1238:	31 c0                	xor    %eax,%eax
    123a:	5b                   	pop    %rbx
    123b:	c3                   	ret
    123c:	0f 1f 40 00          	nopl   0x0(%rax)

5.2 LLVM-IR中的landing pad

21:                                               ; preds = %12
  %22 = landingpad { ptr, i32 }
          catch ptr @_ZTI11MyException
          catch ptr null
  %23 = extractvalue { ptr, i32 } %22, 0
  store ptr %23, ptr %7, align 8
  %24 = extractvalue { ptr, i32 } %22, 1
  store i32 %24, ptr %8, align 4
  br label %25

25:                                               ; preds = %21, %17
  call void @_ZN10UserDefineD2Ev(ptr noundef nonnull align 4 dereferenceable(4) %6) #8
  br label %26

6 参考文献

  • Exception Handling Tables
  • Linux Standard Base Core Specification 3.0RC1
  • Itanium C++ ABI: Exception Handling
  • Exception Handling in LLVM
  • C++ exception handling ABI
  • linux 栈回溯(x86_64 )
  • C++ 异常是如何实现的
  • Recon-2012-Skochinsky-Compiler-Internals

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1643446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android C++ 开发调试 LLDB 工具的使用

文章目录 调试环境准备基础命令Breakpoint CommandsWatchpoint CommandsExamining VariablesEvaluating ExpressionsExamining Thread StateExecutable and Shared Library Query Commands 参考&#xff1a; Android 中在进行 NDK 开发的时候&#xff0c;我们经常需要进行 C 代…

漏洞挖掘之某厂商OAuth2.0认证缺陷

0x00 前言 文章中的项目地址统一修改为: a.test.com 保护厂商也保护自己 0x01 OAuth2.0 经常出现的地方 1&#xff1a;网站登录处 2&#xff1a;社交帐号绑定处 0x02 某厂商绑定微博请求包 0x02.1 请求包1&#xff1a; Request: GET https://www.a.test.com/users/auth/weibo?…

88、动态规划-乘积最大子数组

思路&#xff1a; 首先使用递归来解&#xff0c;从0开始到N&#xff0c;每次都从index开始到N的求出最大值。然后再次递归index1到N的最大值&#xff0c;再求max。代码如下&#xff1a; // 方法一&#xff1a;使用递归方式找出最大乘积public static int maxProduct(int[] num…

局部性原理和磁盘预读

局部性原理 磁盘预读 \

Linux---软硬链接

软链接 我们先学习一下怎样创建软链接文件&#xff0c;指令格式为&#xff1a;ln -s 被链接的文件 生成的链接文件名 我们可以这样记忆&#xff1a;ln是link的简称&#xff0c;s是soft的简称。 我们在下面的图片中就是给test文件生成了一个软链接mytest&#xff1a; 我们来解…

【Linux—进程间通信】共享内存的原理、创建及使用

什么是共享内存 共享内存是一种计算机编程中的技术&#xff0c;它允许多个进程访问同一块内存区域&#xff0c;以此作为进程间通信&#xff08;IPC, Inter-Process Communication&#xff09;的一种方式。这种方式相对于管道、套接字等通信手段&#xff0c;具有更高的效率&…

【skill】onedrive的烦人问题

Onedrive的迷惑行为 安装Onedrive&#xff0c;如果勾选了同步&#xff0c;会默认把当前用户的数个文件夹&#xff08;桌面、文档、图片、下载 等等&#xff09;移动到安装时提示的那个文件夹 查看其中的一个文件的路径&#xff1a; 这样一整&#xff0c;原来的文件收到严重影…

孪生网络、匹配网络和原型网络:详解与区分

孪生网络、匹配网络和原型网络 孪生网络、匹配网络和原型网络&#xff1a;详解与区分孪生网络&#xff08;Siamese Networks&#xff09;核心概念工作原理 匹配网络&#xff08;Matching Networks&#xff09;核心概念工作原理 原型网络&#xff08;Prototypical Networks&…

环形链表知识点

目录 判断链表中是否有环快慢指针步数问题 判断链表中是否有环 题目&#xff1a;给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 解决方法&#xff1a;使用快慢指针 如果两个快慢指针相遇&#xff0c;则有环。 如果没有相遇&#xff0c;则没有环。 但是这个原理…

Linux——守护进程化(独立于用户会话的进程)

目录 前言 一、进程组ID与会话ID 二、setsid() 创建新会话 三、daemon 守护进程 前言 在之前&#xff0c;我们学习过socket编程中的udp通信与tcp通信&#xff0c;但是当时我们服务器启动的时候&#xff0c;都是以前台进程的方式启动的&#xff0c;这样很不优雅&#xff0c…

【LinuxC语言】setitimer与getitimer函数

文章目录 前言一、setitimer() 函数二、getitimer() 函数三、示例代码总结 前言 在Linux系统下&#xff0c;编写程序时经常需要使用定时器来实现一些定时任务、超时处理等功能。setitimer() 和 getitimer() 函数是两个用于操作定时器的重要函数。它们可以帮助我们设置定时器的…

第19章 基于质量特性的测试技术

一、功能性测试 &#xff08;一&#xff09;测试方法 等价类边界值法因果图法判定表法场景法 &#xff08;二&#xff09;用例 1、正常用例 2、异常用例 &#xff08;三&#xff09;完备性 1、功能覆盖率 2、X1-A/B 功能覆盖率X&#xff1a;软件实际功能覆盖文档中所有…

【Linux 网络】网络基础(一)(局域网、广域网、网络协议、TCP/IP结构模型、网络传输、封装和分用)-- 详解

一、计算机网络的发展背景 1、网络的定义 网络是指将多个计算机或设备通过通信线路、传输协议和网络设备连接起来&#xff0c;形成一个相互通信和共享资源的系统。 &#xff08;1&#xff09; 独立模式 独立模式 &#xff1a; 计算机之间相互独立。 &#xff08;2&#xff09;…

VMvare如何更改虚拟机内共享文件夹的挂载点

更改虚拟机内共享文件夹的路径 进入目录 /etc/init.d ,并找到vmware-tools文件 里面有配置项 vmhgfs_mnt"/mnt/hgfs" 将引号内的内容更改为你需要挂载的路径,重启即可 注意挂载的路径不能是 “/”&#xff0c;必须根目录下的某个文件夹&#xff0c;或者其子文件夹 …

在线OJ——链表经典例题详解

引言&#xff1a;本篇博客详细讲解了关于链表的三个经典例题&#xff0c;分别是&#xff1a;环形链表&#xff08;简单&#xff09;&#xff0c;环形链表Ⅱ&#xff08;中等&#xff09;&#xff0c;随机链表的复制&#xff08;中等&#xff09;。当你能毫无压力地听懂和成功地…

面试中算法(使用栈实现队列)

使用栈来模拟一个队列&#xff0c;要求实现队列的两个基本操作:入队、出队。 栈的特点&#xff1a;先入后出&#xff0c;出入元素都是在同一端&#xff08;栈顶&#xff09;。 队列的特点&#xff1a;先入先出&#xff0c;出入元素是在两端&#xff08;队头和队尾)。 分析&…

深度学习:基于Keras,使用长短期记忆神经网络模型LSTM和RMSProp优化算法进行销售预测分析

前言 系列专栏&#xff1a;【机器学习&#xff1a;项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学习模型、处理非…

springboot+vue课程作业成绩可视化大屏分析系统

教师的登录功能。 教师需要可以拥有每学期新增课程的功能。 新增的课程有作业成绩&#xff0c;考勤成绩&#xff0c;考试成绩&#xff0c;实验成绩&#xff0c;其中作业成绩是平时作业1到作业8的平均成绩&#xff0c;最后根据占比得出学期的总评成绩。&#xff08;参考我发的表…

Shell编程debug

debug调试 debug方法 sh -x显示脚本执行过程set命令设置开始debug和结束debug的位置显示脚本某一部分执行过程&#xff0c;解决复杂脚本故障 示例&#xff1a; sh -x 显示脚本执行过程 set显示脚本的部分执行过程 set -x 开始调试&#xff0c;从这里开始显示脚本的详细执行过…

【C++】模板初阶:泛型编程的起点

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…