原文地址:https://monoinfinito.wordpress.com/2013/02/05/c-exceptions-under-the-hood/
每个人都知道良好的异常处理是困难的。在异常“生命期”的每个层面,出现这种情况的原因有许多:编写异常安全的代码是困难的,异常可能从不期望的位置抛出(双关语),理解设计不良的异常架构是复杂的,因为幕后发生了许多巫术,它是慢的;因为不正确地抛出异常可能导致调用不可原谅的std::terminate,它是危险的。虽然每个曾经与“异常”程序斗争的人可能知道这,造成这种混乱的原因并不广为人知。
我们要问自己的第一个问题是,这一切是如何工作的。这是应该长系列的第一篇文章,在这个系列里我将讨论在C++里,异常在幕后是如何实现的(实际上在x86平台上使用gcc编译C++,不过这也可能适用于其他平台)。在这些文章中,将详细解释抛出与捕捉异常的过程,但对那些没有耐心的人这里有小的一个文章摘要:在gcc/x86中如何抛出异常:
- 当我们编写一条throw语句时,编译器把它翻译为对libstdc++的一对调用:分配异常,然后通过调用libstdc开始栈回滚过程。
- 对每条catch语句,编译器将在这个方法主体后写下一些特殊信息,一张这个方法可以捕捉的异常表以及清理表(稍后再解释清理表)。
- 随着回滚器穿过栈,它将调用libstdc++提供的一个特殊函数(称为personality例程),这个函数检查在栈里的每个函数可以捕捉哪些异常。
- 如果没有找到与这个异常相符的捕捉,调用std::terminate。
- 如果找到相符的捕捉,回滚器现在在栈顶开始启动。
- 随着回滚器第二次穿过栈,它将要求personality例程为这个方法执行清理。
- 这个personality例程将检查当前方法上的清理表。如果有任何清理活动要运行,它将“跳转”到当前栈帧并运行清理代码。这将为在当前作用域里分配的每个对象运行析构函数。
- 一旦回滚器到达可以处理这个异常的栈帧,它将跳转到合适的catch语句里。
- 在完成catch语句的执行时,将调用一个清理函数来释放该异常持有的内存。
这看起来已经相当复杂,而我们甚至还没开始;对异常处理的所有复杂性而言,这是一个短的、不准确的描述。
为了学习发生在幕后的所有细节,在下一篇文章我们将开始实现我们自己的迷你libstdlibc++。但不是全部,仅是处理异常的部分。实际上甚至还不是这些全部,仅是使得简单throw/catch语句工作所需的最低要求。将需要一些汇编,但不是很有趣。恐怕需要很多耐心。
如果你实在好奇且希望开始阅读有关异常处理的实现,那么你可以从这里开始,在后续几篇文章里我们将实现一个完整的规范。我将尝试使得这些文章更有指导性,更容易遵循,期望下次开始我们的ABI时再见!
免责声明:我一点也不精通抛出异常时的魔术。这个系列将试图揭开面纱,在过程中学习一些东西,同时我希望其中一些是正确的,毫无疑问有许多不太准确的细节。如果你认为我该改正什么,请告诉我。