C+异常原理:以一个小程序为例


C+异常原理:以一个小程序为例

文章插图
阿里妹导读
作者在调查某个 bug 时涉及到 C++ 异常 , 借此机会以本文把 C++ 异常机制梳理清楚供大家参考 。
最近我们在调查某个 bug 涉及到 C++ 异常 。平时较少用 C++ 异常 , 借此机会把 C++ 异常机制梳理清楚 。互联网上现有的资料不多 , 大多过于深奥 。因此写下这篇文档备忘 。
C++ 异常的实现机制有 SJLJ、Dwarf CFI、EHABI 。具体选择哪种实现和操作系统及体系结构相关 。它是 C++ ABI 的一部分 。这里我们仅关注 Dwarf CFI , 它是 linux 在 x86_64 和 arm64 上的默认实现 。?
完整的 C++ 异常机制需要编译器生成的代码、C++ 运行时(libstdc++ 或 libc++)、unwind 库分工协作完成 。本文为了描述浅显易懂 , 并不区分它们三者 。
测试程序
我们从下面的小程序出发 , 分析 C++ 异常的实现原理 。这个程序演示了几个关键点:
1.f() 分配异常对象并抛出来;
2.向上回溯栈帧 , 沿途析构 g() 栈上的对象;
3.mAIn() 匹配到 catch 语句 , 处理异常 。?
抛出异常
为了方便描述 , 我们下面以 C 语法描述编译器为异常生成的代码 。(小技巧:在 CompilerExplorer 网站能看到各种编译器生成的汇编代码 。)?
让我们先看抛异常的 f() 函数 。它抛出了类型为 E 的异常 , 除此以外没有其它功能 。
这些 __cxa 开头的函数是由 C++ 运行时库提供的 。
__cxa_allocate_exception() 从堆上分配异常对象和其它内部数据结构 。
__cxa_throw() 会向上回溯栈帧 , 依次回溯到 g() 和 main() 。
传播异常
我们再来看 g() 。g() 没有 catch 语句 , 异常会继续向上传播 。但是在此之前还有一个栈上对象 a , 因此回溯栈桢时需要在此停留 , 以析构 a 对象 。
这里引出一个概念:着陆场(landing pad) 。下面代码中第 9~10 行是 f() 正常返回的执行路径 。若 f() 抛异常 , 则会跳转到第 15 行 。这里称为着陆场 。这里第 15 行析构了a 对象 , 第 16 行继续向上回溯到 main() 。
捕获异常
最后来看 main() 。main() 中有 catch 语句 , 第二个 catch 语句匹捕获到 E 类型的异常 。
其它细节
前面埋了个包袱:__cxa_throw() 是回溯栈帧和找到着陆场呢?已知 PC 指针位置 , 这些信息编译时确定的 。编译时产生 .eh_frame 和 .gcc_except_table 段 , 运行时借助这两张表可以找到上层栈帧和着陆场的位置 。详细的描述过于复杂 , 请参考本文末尾的链接 。
【C+异常原理:以一个小程序为例】找到着陆场后 , 在运行时依次根据捕获的异常类型来匹配 catch 语句 , 这里用到了 C++ RTTI 信息 。若匹配不到合适的 catch 语句 , 则继续向上回溯栈帧传播异常 。
参考资料:
1、?Itanium C++ ABI: Exception Handling?:https://itanium-cxx-abi.Github.io/cxx-abi/abi-eh.html
2、?Exception Handling ABI for the Arm Architecture?:https://github.com/ARM-software/abi-aa/blob/844a79fd4c77252a11342709e3b27b2c9f590cf1/ehabi32/ehabi32.rst
3、?libunwind LLVM Unwinder?:https://github.com/llvm/llvm-project/blob/main/libunwind/docs/index.rst
4、?Linux 栈回溯(x86_64)?:https://zhuanlan.zhihu.com/p/302726082
5、?.eh_frame?:https://www.airs.com/blog/archives/460
6、?.gcc_except_table?:https://www.airs.com/blog/archives/464




    推荐阅读