原文地址:https://monoinfinito.wordpress.com/2013/03/26/c-exceptions-under-the-hood-8-two-phase-handling/
上一章以添加一个_Unwind_能够调用的personality函数而结束。它没做什么,但它在那里。我们已经实现的ABI现在可以抛出异常,捕捉也已经完成一半,但需要正确选择catch块(着陆垫)的personality函数目前有点傻。我们通过尝试理解personality函数接受什么参数开始新的一章,下次我们将向__gxx_personality_v0添加某些真实的行为:在调用__gxx_personality_v0时,我们应该说“是的,这个栈帧确实可以处理这个异常”。
我们已经说过我们不在乎我们小ABI的版本或exceptionClass。现在让我们也忽略上下文:我们将仅处理抛出函数上的第一个栈帧;注意这意味着紧靠着抛出函数之上的函数必须有一个try/catch块,否则一切都被打破。这也意味着这个catch将忽略其异常规范,有效地将它变成一个catch(…)。我们如何让_Unwind_知道我们希望处理当前的异常?
_Unwind_Reason_Code是personality函数的返回值;这告诉_Unwind_我们是否找到一个着陆垫来处理该异常。让我们实现我们的personality函数返回_URC_HANDLER_FOUND,然后看会发生什么:
|
|
看到了吗?我们告诉_Unwind_我们找到了一个处理句柄,它再次调用personality函数!那里发生了什么?
记得活动参数吗?这是_Unwind告诉我们它期望什么的方式,这是因为异常捕捉分两阶段处理:查找与清理(或者_UA_SEARCH_PHASE与_UA_CLEANUP_PHASE)。让我们再次重温异常抛出与捕捉的方法:
- __cxa_throw/__cxa_allocate_exception将创建异常,并通过调用_Unwind_RaiseException把它转发给底层unwind库
- Unwind将使用CFI来了解栈上有哪些函数(即知道如何启动栈回滚)
- 每个函数有一个LSDA(语言特定数据区)部分,加上称为.gcc_except_table的部分
- Unwind将尝试定位用于该异常的着陆垫:
- Unwind以_UA_SEARCH_PHASE以及指向当前栈帧的上下文指针调用personality函数。
- 通过分析LSDA,personality函数检查当前栈帧是否能处理抛出的异常。
- 如果可以处理该异常,它返回_URC_HANDLER_FOUND。
- 如果不能处理该异常,它将返回_URC_CONTINUE_UNWIND,然后Unwind尝试下一个栈帧。
- 如果没有找到着陆垫,调用缺省异常处理句柄(通常是std::terminate)。
- 如果找到一个着陆垫:
- Unwind将再次迭代栈,以_UA_CLEANUP_PHASE调用personality函数。
- 这个personality函数将再次检查它是否能处理当前异常:
- 如果这个帧可以处理这个异常,那么它将运行一个由LSDA描述的清理函数,并告诉Unwind继续下一个帧(实际上这是非常重要的一步:清理函数将运行在这个栈帧里分配对象的析构函数)!
- 如果这个帧不能处理这个异常,不运行任何清理代码:告诉Unwind我们希望在这个着陆垫上恢复执行。
这里有两个重要的信息需要注意:
- 运行两阶段异常处理过程意味着在没有找到句柄时,缺省异常处理句柄可以获得最初异常的栈追踪(如果我们一边处理一边回滚栈,将得不到栈追踪,或者我们需要持有它的一个拷贝)!
- 运行_UA_CLEANUP_PHASE并第二次调用每个帧,即使我们已经知道这个帧将处理这个异常,也是重要的:personality函数将利用这个机会为在这个作用域上构建的对象运行所有的析构函数。正是这使得RAII成为一个异常安全的习惯用法。
现在我们理解了catch查找阶段如何工作,我们可以继续实现我们的personality函数。