C++中异常机制的实现机制详解

前言

异常就是运行时出现出现的不正常(没说一样),例如系统运行时耗尽了内存或遇到意外的非法输入。本文详细介绍了关于C++中异常机制实现机制的相关内容,下面话不多说了,来一起看看详细的介绍吧。

1、C函数的调用和返回

要理解C++异常机制实现之前,首先要了解一个函数的调用和返回机制,这里面就要涉及到ESP和EBP寄存器。我们先看一下函数调用和返回的流程。

下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码
假设执行函数前堆栈指针ESP为NN
push p2 ;参数2入栈, ESP -= 4h , ESP = NN - 4h
push p1 ;参数1入栈, ESP -= 4h , ESP = NN - 8h
call test ;压入返回地址 ESP -= 4h, ESP = NN - 0Ch
{
push ebp ;保护先前EBP指针, EBP入栈, ESP-=4h, ESP = NN - 10h
mov ebp, esp ;设置EBP指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch为NN-4h,即参数2的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h为NN-8h,即参数1的位置
sub esp, 8 ;局部变量所占空间ESP-=8, ESP = NN-18h
...
add esp, 8 ;释放局部变量, ESP+=8, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}

函数栈架构主要承载着以下几个部分:

  1、传递参数:通常,函数的调用参数总是在这个函数栈框架的最顶端。

  2、传递返回地址:告诉被调用者的 return 语句应该 return 到哪里去,通常指向该函数调用的下一条语句(代码段中的偏移)。

  3、存放调用者的当前栈指针:便于清理被调用者的所有局部变量、并恢复调用者的现场。

  4、存放当前函数内的所有局部变量:记得吗?刚才说过所有局部和临时变量都是存储在栈上的。  

  

2、C++函数调用

首先澄清一点,这里说的 “C++ 函数”是指:

  1、该函数可能会直接或间接地抛出一个异常:即该函数的定义存放在一个 C++ 编译(而不是传统 C)单元内,并且该函数没有使用“throw()”异常过滤器

  2、该函数的定义内使用了 try 块。

以上两者满足其一即可。为了能够成功地捕获异常和正确地完成栈回退(stack unwind),编译器必须要引入一些额外的数据结构和相应的处理机制。我们首先来看看引入了异常处理机制的栈框架大概是什么样子:

  

由图2可见,在每个 C++ 函数的栈框架中都多了一些东西。仔细观察的话,你会发现,多出来的东西正好是一个 EXP 类型的结构体。进一步分析就会发现,这是一个典型的单向链表式结构:

  piPrev 成员指向链表的上一个节点,它主要用于在函数调用栈中逐级向上寻找匹配的 catch 块,并完成栈回退工作。

  piHandler 成员指向完成异常捕获和栈回退所必须的数据结构(主要是两张记载着关键数据的表:“try”块表:tblTryBlocks 及“栈回退表”:tblUnwind)。

  nStep 成员用来定位 try 块,以及在栈回退表中寻找正确的入口。

需要说明的是:编译器会为每一个“C++ 函数”定义一个 EHDL 结构,不过只会为包含了“try”块的函数定义 tblTryBlocks 成员。此外,异常处理器还会为每个线程维护一个指向当前异常处理框架的指针。该指针指向异常处理器链表的链尾,通常存放在某个 TLS 槽或能起到类似作用的地方。

3、栈回退(stack unwind)

“栈回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。下面我们就来具体看看编译器是如何实现栈回退机制的:

  

图中的“FuncUnWind”函数内,所有真实代码均以黑色和蓝色字体标示,编译器生成的代码则由灰色和橙色字体标明。此时,在图里给出的 nStep 变量和 tblUnwind 成员作用就十分明显了。

nStep 变量用于跟踪函数内局部对象的构造、析构阶段。再配合编译器为每个函数生成的 tblUnwind 表,就可以完成退栈机制。表中的 pfnDestroyer 字段记录了对应阶段应当执行的析构操作(析构函数指针);pObj 字段则记录了与之相对应的对象 this 指针偏移。将 pObj 所指的偏移值加上当前栈框架基址(EBP),就是要代入 pfnDestroyer 所指析构函数的 this 指针,这样即可完成对该对象的析构工作。而 nNextIdx 字段则指向下一个需要析构对象所在的行(下标)。

在发生异常时,异常处理器首先检查当前函数栈框架内的 nStep 值,并通过 piHandler 取得 tblUnwind[] 表。然后将 nStep 作为下标带入表中,执行该行定义的析构操作,然后转向由 nNextIdx 指向的下一行,直到 nNextIdx 为 -1 为止。在当前函数的栈回退工作结束后,异常处理器可沿当前函数栈框架内 piPrev 的值回溯到异常处理链中的上一节点重复上述操作,直到所有回退工作完成为止。

值得一提的是,nStep 的值完全在编译时决定,运行时仅需执行若干次简单的整形立即数赋值(通常是直接赋值给CPU里的某个寄存器)。此外,对于所有内部类型以及使用了默认构造、析构方法(并且它的所有成员和基类也使用了默认方法)的类型,其创建和销毁均不影响 nStep 的值。

注意:如果在栈回退的过程中,由于析构函数的调用而再次引发了异常(异常中的异常),则被认为是一次异常处理机制的严重失败。此时进程将被强行禁止。为防止出现这种情况,应在所有可能抛出异常的析构函数中使用“std::uncaught_exception()”方法判断当前是否正在进行栈回退(即:存在一个未捕获或未完全处理完毕的异常)。如是,则应抑制异常的再次抛出。

4、异常捕获

一个异常被抛出时,就会立即引发 C++ 的异常捕获机制:

  

在上一小节中,我们已经看到了 nStep 变量在跟踪对象构造、析构方面的作用。实际上 nStep 除了能够跟踪对象创建、销毁阶段以外,还能够标识当前执行点是否在 try 块中,以及(如果当前函数有多个 try 块的话)究竟在哪个 try 块中。这是通过在每一个 try 块的入口和出口各为 nStep 赋予一个唯一 ID 值,并确保 nStep 在对应 try 块内的变化恰在此范围之内来实现的。

在具体实现异常捕获时,首先,C++ 异常处理器检查发生异常的位置是否在当前函数的某个 try 块之内。这项工作可以通过将当前函数的 nStep 值依次在 piHandler 指向tblTryBlocks[] 表的条目中进行范围为 [nBeginStep, nEndStep) 的比对来完成。

例如:若图4 中的 FuncB 在 nStep == 2 时发生了异常,则通过比对 FuncB 的 tblTryBlocks[] 表发现 2∈[1, 3),故该异常发生在 FuncB 内的第一个 try 块中。

其次,如果异常发生的位置在当前函数中的某个 try 块内,则尝试匹配该 tblTryBlocks[] 相应条目中的 tblCatchBlocks[] 表。tblCatchBlocks[] 表中记录了与指定 try 块配套出现的所有 catch 块相关信息,包括这个 catch 块所能捕获的异常类型及其起始地址等信息。

若找到了一个匹配的 catch 块,则复制当前异常对象到此 catch 块,然后跳转到其入口地址执行块内代码。

否则,则说明异常发生位置不在当前函数的 try 块内,或者这个 try 块中没有与当前异常相匹配的 catch 块,此时则沿着函数栈框架中 piPrev 所指地址(即:异常处理链中的上一个节点)逐级重复以上过程,直至找到一个匹配的 catch 块或到达异常处理链的首节点。对于后者,我们称为发生了未捕获的异常,对于 C++ 异常处理器而言,未捕获的异常是一个严重错误,将导致当前进程被强制结束。

5、抛出异常

接下来讨论整个 C++ 异常处理机制中的最后一个环节,异常的抛出:

  

在编译一段 C++ 代码时,编译器会将所有 throw 语句替换为其 C++ 运行时库中的某一指定函数,这里我们叫它 __CxxRTThrowExp(与本文提到的所有其它数据结构和属性名一样,在实际应用中它可以是任意名称)。该函数接收一个编译器认可的内部结构(我们叫它 EXCEPTION 结构)。这个结构中包含了待抛出异常对象的起始地址、用于销毁它的析构函数,以及它的 type_info 信息。对于没有启用 RTTI 机制(编译器禁用了 RTTI 机制或没有在类层次结构中使用虚表)的异常类层次结构,可能还要包含其所有基类的 type_info 信息,以便与相应的 catch 块进行匹配。

在图中的深灰色框图内,我们使用 C++ 伪代码展示了函数 FuncA 中的 “throw myExp(1);” 语句将被编译器最终翻译成的样子。实际上在多数情况下,__CxxRTThrowExp 函数即我们前面曾多次提到的“异常处理器”,异常捕获和栈回退等各项重要工作都由它来完成。

__CxxRTThrowExp 首先接收(并保存)EXCEPTION 对象;然后从 TLS:Current ExpHdl 处找到与当前函数对应的 piHandler、nStep 等异常处理相关数据;并按照前文所述的机制完成异常捕获和栈回退。由此完成了包括“抛出”->“捕获”->“回退”等步骤的整套异常处理机制。

 6、总结

以上就是C++异常的实现原理,当然其他语言的异常捕获机制也是同样的思想实现异常处理的。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • C++中异常处理的基本思想及throw语句抛出异常的使用

    异常处理基本思想 C++的异常处理的基本思想大致可以概括为传统错误处理机制.通过函数返回值来处理错误. 1)C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理.上层调用者可以再适当的位置设计对不同类型异常的处理. 2)异常是专门针对抽象编程中的一系列错误处理的,C++中不能借助函数机制,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试,如图 3)异常超

  • 解析C++编程中的bad_cast异常

    由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常. 语法 catch (bad_cast) statement 备注 bad_cast 的接口为: class bad_cast : public exception { public: bad_cast(const char * _Message = "bad cast"); bad_cast(const bad_cast &); virtual ~bad_cast(); }; 以下代码包

  • C++编程异常处理中try和throw以及catch语句的用法

    若要在 C++ 中实现异常处理,你可以使用 try.throw 和 catch 表达式. 首先,使用 try 块将可能引发异常的一个或多个语句封闭起来. throw 表达式发出信号,异常条件(通常是错误)已在 try 块中发生.你可以使用任何类型的对象作为 throw 表达式的操作数.该对象一般用于传达有关错误的信息.大多数情况下,建议你使用 std::exception 类或标准库中定义的派生类之一.如果其中的类不合适,建议你从 std::exception 派生自己的异常类. 若要处理可能引

  • C++的try块与异常处理及调试技术实例解析

    本文以示例形式简述了C++ try块的异常处理与调试技术,有助于读者复习并加深对try块的了解. 一.格式: 抛出异常throw 异常类型例如throw runtime_error("Data must refer to same ISBN"); try{ program-statements }catch(exception-specifier) { handler-statement; }catch(exception-specifier) { handler-statement;

  • C++运行时获取类型信息的type_info类与bad_typeid异常

    type_info 类 type_info 类描述编译器在程序中生成的类型信息.此类的对象可以有效存储指向类型的名称的指针. type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值.类型的编码规则和排列顺序是未指定的,并且可能因程序而异. 必须包含 <typeinfo> 标头文件才能使用 type_info 类. type_info 类的接口是: class type_info { public: virtual ~type_info(); size_t hash_co

  • C++ 异常处理 catch(...)介绍

    如果要想使一个catch block能抓获多种数据类型的异常对象的话,怎么办?C++标准中定义了一种特殊的catch用法,那就是" catch(-)". 感性认识 1.catch(-)到底是一个什么样的东东,先来个感性认识吧!看例子先: 复制代码 代码如下: int main() { try { cout << "在 try block 中, 准备抛出一个异常." << endl; //这里抛出一个异常(其中异常对象的数据类型是int,值为1

  • C++ 异常的详细介绍

    C++ 异常的详解 程序有时会遇到运行阶段错误,导致程序无法正常执行下去.c++异常为处理这种情况提供了一种功能强大的而灵活的工具.异常是相对比较新的C++功能,有些老编译器可能没有实现.另外,有些编译器默认关闭这种特性,我们可能需要使用编译器选项来启用它. 一.异常机制的使用 异常提供了将控制程序的一个部分传递到另一部分的途径.对异常的处理有3个组成部分: 引发异常 使用处理程序捕获异常 使用try块 示例代码: #include "stdafx.h" #include <io

  • c++异常处理机制示例及详细讲解

    这两天我写了一个测试c++异常处理机制的例子,感觉有很好的示范作用,在此贴出来,给c++异常处理的初学者入门.本文后附有c++异常的知识普及,有兴趣者也可以看看. 下面的代码直接贴到你的console工程中,可以运行调试看看效果,并分析c++的异常机制. 复制代码 代码如下: #include "stdafx.h" #include<stdlib.h> #include<crtdbg.h> #include <iostream> // 内存泄露检测机

  • C++之异常处理详解

    程序中的错误分为编译时的错误和运行时的错误.编译时的错误主要是语法错误,比如:句尾没有加分号,括号不匹配,关键字错误等,这类错误比较容易修改,因为编译系统会指出错误在第几行,什么错误.而运行时的错误则不容易修改,因为其中的错误是不可预料的,或者可以预料但无法避免的,比如内存空间不够,或者在调用函数时,出现数组越界等错误.如果对于这些错误没有采取有效的防范措施,那么往往会得不到正确的运行结果,程序不正常终止或严重的会出现死机现象.我们把程序运行时的错误统称为异常,对异常处理称为异常处理.C++中所

  • 了解C++编程中指定的异常和未经处理的异常

    noexcept C++11:指定函数是否可能会引发异常. 语法 ReturnType FunctionName(params) noexcept; ReturnType FunctionName(params) noexcept(noexcept(expression); 参数 表达式 计算结果是 True 或 False 的常量表达式.无条件版本相当于 noexcept(true). 备注 noexcept(及其同义词 noecept(true))指定函数绝不会引发异常,或允许从异常直接或间

随机推荐