以Itanium C++ ABI为例子,其为类unix操作系统普遍使用的c++ABI标准

参考

https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#base-framework

https://maskray.me/blog/2020-12-12-c++-exception-handling-abi#%E4%B8%AD%E6%96%87%E7%89%88

一.BASE ABI

所有语言和实现通用的接口;感觉写的很混乱就做个参考吧

1.符合psABI架构的处理器的运行时库需要提供的接口

_Unwind_RaiseException,
_Unwind_Resume,
_Unwind_DeleteException,
_Unwind_GetGR,
_Unwind_SetGR,
_Unwind_GetIP,
_Unwind_SetIP,
_Unwind_GetRegionStart,
_Unwind_GetLanguageSpecificData,
_Unwind_ForcedUnwind

另外,定义了两种数据类型(_Unwind_Context 和 _Unwind_Exception),用于连接调用运行时(如 C++ 运行时)与上述例程的接口。所有例程和接口的行为都如同是用 extern "C" 定义的一样,特别是这些名称不会被修饰。作为此接口的一部分,所有定义的名称都以 “Unwind“ 作为前缀。

最后,编译器会在需要异常处理的栈帧的展开描述符中存储一个语言和供应商特定的 personality 函数。展开器(unwinder)会调用 personality 函数来处理特定语言的任务,例如识别处理特定异常的栈帧。

2.Data Structures详解

Reason Codes

typedef enum {
_URC_NO_REASON = 0,
_URC_FOREIGN_EXCEPTION_CAUGHT = 1,
_URC_FATAL_PHASE2_ERROR = 2,
_URC_FATAL_PHASE1_ERROR = 3,
_URC_NORMAL_STOP = 4,
_URC_END_OF_STACK = 5,
_URC_HANDLER_FOUND = 6,
_URC_INSTALL_CONTEXT = 7,
_URC_CONTINUE_UNWIND = 8
} _Unwind_Reason_Code;

Exception Header

typedef void (*_Unwind_Exception_Cleanup_Fn)
(_Unwind_Reason_Code reason,
struct _Unwind_Exception *exc);
struct _Unwind_Exception {
uint64 exception_class; //特定语言和实现的标识符, It allows a personality routine to distinguish between native and foreign exceptions
_Unwind_Exception_Cleanup_Fn exception_cleanup; //当异常对象被删除会调用此
uint64 private_1; // 存储内部信息
uint64 private_2; // 存储内部信息
};
展开器(unwinder)在处理异常时,通过这个头部来识别和管理异常对象,确保在异常处理完毕后正确地释放资源。

Unwind Context

3.Throwing an Exception1

查找异常处理器:先查找是否有栈帧可以处理异常(阶段 1)。

展开栈与清理:如果找到处理器,则沿着调用栈展开并执行清理操作(阶段 2)。

此函数是两阶段的结合。

_Unwind_RaiseException 是一个函数,用于抛出一个异常。它会接收一个异常对象作为参数,这个异常对象必须包含一个已设置的 exception_class 字段和 exception_cleanup 字段。这些字段在抛出异常之前由特定语言的运行时设置。

异常对象通常是由特定语言的运行时(如 C++ 的运行时)分配和初始化的。尽管这个异常对象的结构可以根据不同的语言有所不同,但它必须包含一个 _Unwind_Exception 结构体,作为异常处理过程的基础部分。

只有在发生错误时, _Unwind_RaiseException 才会返回。

_Unwind_Reason_Code _Unwind_RaiseException
( struct _Unwind_Exception *exception_object );

可能遇到的部分错误码:

_URC_END_OF_STACK:展开器在阶段 1 中遇到了堆栈的末尾,但没有找到处理程序。展开运行时不会修改堆栈。在这种情况下,C++运行时通常会调用 uncaught_exception()。
_URC_FATAL_PHASE1_ERROR:展开器在阶段 1 中遇到意外错误,例如堆栈损坏。展开运行时不会修改堆栈。在这种情况下,C++ 运行时通常会调用 terminate()。
如果展开器在第 2 阶段遇到意外错误,则应将 _URC_FATAL_PHASE2_ERROR 返回给其调用方。 在 C++ 中,这通常是 __cxa_throw,它将调用 terminate()。

3.Throwing an Exception2

此部分主要用于资源清理。而非异常处理,只有一个阶段,那就是栈展开,没有search操作

具体细节,用时再说

typedef _Unwind_Reason_Code (*_Unwind_Stop_Fn)
(int version,
_Unwind_Action actions,
uint64 exceptionClass,
struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context,
void *stop_parameter );

_Unwind_Reason_Code _Unwind_ForcedUnwind
( struct _Unwind_Exception *exception_object,
_Unwind_Stop_Fn stop,
void *stop_parameter );

3.1_Unwind_Resume

对异常处理的第二阶段的补充,用于恢复异常的传播,如多个catch捕捉异常

void _Unwind_Resume (struct _Unwind_Exception *exception_object);

4.Exception Object Management

4.1_Unwind_DeleteException

这个函数会调用 exception_object 结构体中的 exception_cleanup 函数指针,执行特定语言或实现的清理操作,以确保异常对象中可能占用的资源(如动态分配的内存)被正确释放。

void _Unwind_DeleteException
(struct _Unwind_Exception *exception_object);

5.Context Management

用于返回栈帧与寄存器信息

6. Personality Routine

对底层baseABI的封装,通过展开信息块的指针来引用。

Transferring Control to a Landing Pad:通过上下文保存的指针
_Unwind_Reason_Code (*__personality_routine)
(int version,
_Unwind_Action actions, //action
uint64 exceptionClass, //类型
struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context);

二 C++ABI

1. Data Structures

Caught Exception Stack

struct __cxa_eh_globals {
__cxa_exception * caughtExceptions;//当前线程引发和捕获的异常列表
unsigned int uncaughtExceptions; //未捕获的异常计数
};

C++ Exception Objects

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;
};

2.过程(算法)

Throwing an Exception

1.Allocating the Exception Object

https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#base-personality

调用:void *__cxa_allocate_exception(size_t thrown_size);

+---------------------+----------------------+
| __cxa_exception 标头 | 实际的异常对象数据 |
+---------------------+----------------------+
^ ^
| |
| +-- 通过 __cxa_allocate_exception 返回的指针
|
+-- 标头起始地址,通常通过从异常对象指针减去一个固定偏移量得到

2.Throwing the Exception Object

https://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html#base-personality

调用: void __cxa_throw (void *thrown_exception, std::type_info *tinfo, void (*dest) (void *) );This routine never returns.

1./*获得标头*/
__cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
2./*将当前 unexpected_handler 和 terminate_handler 保存在 __cxa_exception 标头中。*/
3./*将 tinfo 和 dest 参数保存在 __cxa_exception 标头中。*/
4./*在 unwind 标头中设置 exception_class 字段。这是一个 64 位值,表示 ASCII 字符串 “XXXXC++\0”,其中 “XXXX” 是与供应商相关的字符串。也就是说,对于符合此 ABI 的实现,此 64 位值的低序 4 字节将为“C++\0”。*/
5./*递增 uncaught_exception 标志。*/
//简化实现
void __cxa_throw(void *thrown, std::type_info *tinfo, void (*destructor)(void *)) {
__cxa_exception *hdr = (__cxa_exception *)thrown - 1;
hdr->exceptionType = tinfo; hdr->destructor = destructor;
hdr->unexpectedHandler = std::get_unexpected();
hdr->terminateHandler = std::get_terminate();
hdr->unwindHeader.exception_class = ...;
__cxa_get_globals()->uncaughtExceptions++;
_Unwind_RaiseException(&hdr->unwindHeader);
// Failed to unwind, e.g. the .eh_frame FDE is absent.
__cxa_begin_catch(&hdr->unwindHeader);
std::terminate();
}
3._Unwind_RaiseException

此过程是baseabi的throw的两阶段。会调用此函数

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

phase1

会通过光标来寻找__personality_routine,找到则进行调用,根据返回值是否找到hander来进行下一步操作(返回,或者继续展开),若返回的话那么_Unwind_Exception的private_2则会记住栈指针,指向具体的catch块的位置,以便于正确使用变量。

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.
bool handlerNotFound = true;
while (handlerNotFound) {
// 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_ojb=%p): __unw_step() reached "
"bottom => _URC_END_OF_STACK",
(void *)exception_object);
return _URC_END_OF_STACK;
} else if (stepResult < 0) {
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): __unw_step failed => "
"_URC_FATAL_PHASE1_ERROR",
(void *)exception_object);
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) {
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): __unw_get_proc_info "
"failed => _URC_FATAL_PHASE1_ERROR",
(void *)exception_object);
return _URC_FATAL_PHASE1_ERROR;
}

// When tracing, print state information.
if (_LIBUNWIND_TRACING_UNWINDING) {
char functionBuf[512];
const char *functionName = functionBuf;
unw_word_t offset;
if ((__unw_get_proc_name(cursor, functionBuf, sizeof(functionBuf),
&offset) != UNW_ESUCCESS) ||
(frameInfo.start_ip + offset > frameInfo.end_ip))
functionName = ".anonymous.";
unw_word_t pc;
__unw_get_reg(cursor, UNW_REG_IP, &pc);
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): pc=0x%" PRIxPTR ", start_ip=0x%" PRIxPTR
", func=%s, lsda=0x%" PRIxPTR ", personality=0x%" PRIxPTR "",
(void *)exception_object, pc, frameInfo.start_ip, functionName,
frameInfo.lsda, frameInfo.handler);
}

// If there is a personality routine, ask it if it will want to stop at
// this frame.
if (frameInfo.handler != 0) {
__personality_routine p =
(__personality_routine)(uintptr_t)(frameInfo.handler);
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): calling personality function %p",
(void *)exception_object, (void *)(uintptr_t)p);
_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
handlerNotFound = false;
__unw_get_reg(cursor, UNW_REG_SP, &sp);
exception_object->private_2 = (uintptr_t)sp;
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): _URC_HANDLER_FOUND",
(void *)exception_object);
return _URC_NO_REASON;

case _URC_CONTINUE_UNWIND:
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): _URC_CONTINUE_UNWIND",
(void *)exception_object);
// continue unwinding
break;

default:
// something went wrong
_LIBUNWIND_TRACE_UNWINDING(
"unwind_phase1(ex_ojb=%p): _URC_FATAL_PHASE1_ERROR",
(void *)exception_object);
return _URC_FATAL_PHASE1_ERROR;
}
}
}
return _URC_NO_REASON;
}

phase2

**初始化光标 (cursor)**:

  • 函数首先通过 __unw_init_local(cursor, uc) 初始化光标,以便在当前上下文中遍历堆栈帧。光标用于跟踪堆栈展开过程中的当前栈帧。

遍历栈帧

  • 函数进入一个循环,调用 __unw_step(cursor) 来遍历堆栈帧。如果返回值为 0,表示已经到达堆栈的底部,函数返回 _URC_END_OF_STACK,表示堆栈展开已经完成。
  • 如果 __unw_step(cursor) 返回负值,表示在移动到下一个栈帧时发生了错误,函数返回 _URC_FATAL_PHASE2_ERROR,表示在第二阶段遇到致命错误。

获取栈帧信息

  • 对于每个栈帧,函数调用 __unw_get_proc_info(cursor, &frameInfo) 来获取与该栈帧相关联的处理信息(如程序计数器、栈指针等)。如果获取信息失败,函数返回 _URC_FATAL_PHASE2_ERROR

**检查栈帧中的 personality routine**:

  • 如果当前栈帧有 personality routine(即 frameInfo.handler 不为 0),函数将调用它来决定如何处理该栈帧。
  • 如果栈指针 sp 等于 exception_object->private_2,表示这个栈帧是在第一阶段中标记为需要处理的栈帧,函数会向 personality routine 传递 _UA_HANDLER_FRAME 标志,指示这是一个需要处理的帧。

处理 personality routine 的返回值

  • personality routine
    

    可能返回以下几种结果:

    - **_URC_CONTINUE_UNWIND**:继续展开堆栈,函数将继续遍历下一个栈帧。
    - **_URC_INSTALL_CONTEXT**:`personality routine` 指示需要转移控制权到异常处理器,函数调用 `__unw_resume(cursor)` 将控制权转移到处理器。如果 `__unw_resume` 返回,表示发生了错误,函数返回 `_URC_FATAL_PHASE2_ERROR`。

    **错误处理**:

    - 如果在遍历栈帧的过程中发生任何无法处理的错误,函数会记录错误信息并返回 `_URC_FATAL_PHASE2_ERROR`。

    ```C
    static _Unwind_Reason_Code
    unwind_phase2(unw_context_t *uc, unw_cursor_t *cursor, _Unwind_Exception *exception_object) {
    __unw_init_local(cursor, uc);

    _LIBUNWIND_TRACE_UNWINDING("unwind_phase2(ex_ojb=%p)",
    (void *)exception_object);

    // 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(cursor);
    if (stepResult == 0) {
    _LIBUNWIND_TRACE_UNWINDING(
    "unwind_phase2(ex_ojb=%p): __unw_step() reached "
    "bottom => _URC_END_OF_STACK",
    (void *)exception_object);
    return _URC_END_OF_STACK;
    } else if (stepResult < 0) {
    _LIBUNWIND_TRACE_UNWINDING(
    "unwind_phase2(ex_ojb=%p): __unw_step failed => "
    "_URC_FATAL_PHASE1_ERROR",
    (void *)exception_object);
    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) {
    _LIBUNWIND_TRACE_UNWINDING(
    "unwind_phase2(ex_ojb=%p): __unw_get_proc_info "
    "failed => _URC_FATAL_PHASE1_ERROR",
    (void *)exception_object);
    return _URC_FATAL_PHASE2_ERROR;
    }

    // When tracing, print state information.
    if (_LIBUNWIND_TRACING_UNWINDING) {
    char functionBuf[512];
    const char *functionName = functionBuf;
    unw_word_t offset;
    if ((__unw_get_proc_name(cursor, functionBuf, sizeof(functionBuf),
    &offset) != UNW_ESUCCESS) ||
    (frameInfo.start_ip + offset > frameInfo.end_ip))
    functionName = ".anonymous.";
    _LIBUNWIND_TRACE_UNWINDING("unwind_phase2(ex_ojb=%p): start_ip=0x%" PRIxPTR
    ", func=%s, sp=0x%" PRIxPTR ", lsda=0x%" PRIxPTR
    ", personality=0x%" PRIxPTR,
    (void *)exception_object, frameInfo.start_ip,
    functionName, sp, frameInfo.lsda,
    frameInfo.handler);
    }

    // If there is a personality routine, tell it we are unwinding.
    if (frameInfo.handler != 0) {
    __personality_routine p =
    (__personality_routine)(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
    _LIBUNWIND_TRACE_UNWINDING(
    "unwind_phase2(ex_ojb=%p): _URC_CONTINUE_UNWIND",
    (void *)exception_object);
    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:
    _LIBUNWIND_TRACE_UNWINDING(
    "unwind_phase2(ex_ojb=%p): _URC_INSTALL_CONTEXT",
    (void *)exception_object);
    // 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);
    _LIBUNWIND_TRACE_UNWINDING("unwind_phase2(ex_ojb=%p): re-entering "
    "user code with ip=0x%" PRIxPTR
    ", sp=0x%" PRIxPTR,
    (void *)exception_object, pc, sp);
    }
    __unw_resume(cursor);
    // __unw_resume() only returns if there was an error.
    return _URC_FATAL_PHASE2_ERROR;
    default:
    // Personality routine returned an unknown result code.
    _LIBUNWIND_DEBUG_LOG("personality function returned unknown result %d",
    personalityResult);
    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;
    }
4.异常处理
  • 对于被search phase标记的栈帧,landing pad调用__cxa_begin_catch,然后执行catch block中的代码,最后调用__cxa_end_catch销毁exception object

3.关于文件结构

此部分描述了如何识别personality routine

四 调试

以2024羊城杯为例子

异常对象的空间分配与抛出

初始全0

 __cxa_allocate_exception@plt;
0x586110: 0x0000000000000000 0x0000000000000000
........................
0x586190: 0x00000000905f2923 0x0000000000000f21
__cxa_throw@plt;
调用: __cxa_get_globals@plt获得exception stack存储在:0x7f9ec5b5f3a0
调用: __cxa_init_primary_exception@plt 来初始化__cxa_exception,结果如图:可以看出都被初始化。

image-20240903211022609

搜查阶段

调用:_Unwind_RaiseException@plt
之后查找存在personality,则调用,可观察到此函数的第五个参数为一个链条这个也许就是
(struct _Unwind_Context *)(cursor)

image-20240903212603434

image-20240903212615565

调用三次之后,终于在调用__gxx_personality_v0时找到处理函数,private被赋值,为:0x7ffffc2572d0

image-20240903213114934