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, ;局部变量所占空间ESP-=, ESP = NN-18h
...
add esp, ;释放局部变量, ESP+=, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=, ESP = NN-0Ch
ret ;ret返回,弹出返回地址,ESP+=, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=,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++异常的实现原理,当然其他语言的异常捕获机制也是同样的思想实现异常处理的。
C++异常实现机制的更多相关文章
- 【 转】__try,__except,__finally,__leave异常模型机制
转自:http://blog.csdn.net/wwl33695/article/details/8686458 导读: 从本篇文章开始,将全面阐述__try,__except,__finally,_ ...
- Atitit.跨语言异常转换机制 java c# php到js的异常转换
Atitit.跨语言异常转换机制 java c# php到js的异常转换 1. bizEx 直接抓取,然后js catchEX1 2. Chkec runtimeEx1 3. Other异常..J ...
- Javascript的异常捕获机制
這個異常處理機制,之前也只是,但是沒有怎麼用過,用了一次后發現還可以, 和java,python的異常處理機制也很相似 Javascript的异常捕获机制 1.1 基本的try…catch语句 ES3 ...
- Java异常控制机制和异常处理原则【转】
原文:https://www.jianshu.com/p/15872cba211d Java异常控制机制又被称为“违例控制机制”. 捕获程序错误最理想的时机是在编译阶段,这样可以彻底避免错误的代码运行 ...
- struts2 进阶--异常捕获机制
在SpringMvc中有自己的异常处理机制,struts2当然会有此功能,主要是在struts.xml中配置: <bean type="com.opensymphony.xwork2. ...
- spring 异常管理机制
三.异常处理的几种实现: 3.1.在经典的三层架构模型中,通常都是这样来进行异常处理的: A.持久层一般抛出的是RuntiomeException类型的异常,一般不处理,直接向上抛出. B.业务层一般 ...
- eggjs异常捕获机制
1. try catch捕获异步链中的方法 2. ctx.runInBackground(scope)捕获跳出异步链的方法 // 旧代码 class HomeController extends Co ...
- C# 异常捕获机制(Try Catch Finally)
一.C#的异常处理所用到关键字 try 用于检查发生的异常,并帮助发送任何可能的异常. catch 以控制权更大的方式处理错误,可以有多个catch子句. finally 无论是否引发了异常,fina ...
- c#异常重试机制
有时候我们碰到程序异常了,想让程序继续重新执行,进行重试,这时候就需要有一个合适的方法来进行操作: 自己写代码控制太麻烦了,也容易出错.这时候当然是站在巨人的肩膀上, https://github.c ...
随机推荐
- deep_learning_Function_ Matplotlib 3D 绘图函数 plot_surface 的 rstride 和 cstride 参数
今晚开始接触 Matplotlib 的 3D 绘图函数 plot_surface,真的非常强大,图片质量可以达到出版级别,而且 3D 图像可以旋转 ,可以从不同角度来看某个 3D 立体图,但是我发现各 ...
- CPU排行
早期的时候,在某个网站上面收录下来的关于CPU排行的信息,发出来让更多的人看到,希望能够帮助到一些人!
- 7.Nginx_Keepalived高可用配置
2. 利用keepalived实现高可靠配置(HA) 2.1. 高可靠概念 HA(High Available):高可用性集群,是保证业务连续性的有效解决方案,一般有两个或两个以上的节点,且分为活动节 ...
- JSON跨域读取那点事(JSONP跨域访问)
最近在码一个小项目,需要远程读取json.因为需求很少,如果引用jquery使用其getjson方法就显得很浪费嘛= = 这篇文章很详细的解释了JSON跨域读取的前世今生,把原理讲得很透彻.特此分享. ...
- CNN for NLP
卷积神经网络在自然语言处理任务中的应用.参考链接:Understanding Convolutional Neural Networks for NLP(2015.11) Instead of ima ...
- 安装keras和解决python3退格显示^H的问题
我的是centos7的版本,然后之前是安装好了3.5,默认安装了2.7 看了几篇博文和手上的书,发现安装没有那么麻烦,只需要 pip install tensorflow pip install ke ...
- JS 四种条件控制语句
// 1.if...else if (true) { console.log("TRUE1"); } else { console.log("TRUE2"); ...
- BZOJ1821 部落划分[最小生成树]
方法一:套路性的,二分距离,然后把距离点对距离小于答案的边都联通起来,然后看集合数量超过k说明答案小,增大,否则减小. 方法二:贪心,类kruskal.n个点,k个连通块,则需要有效连接(同一个块内的 ...
- $'\r': command not found
在Linux下执行程序最省事的方式就是将系统的执行流程封装成一个shell脚本,上传到linux环境中后就可以直接执行了,但是今天在具体实施的时候出现了错误 $'\r': command not fo ...
- 【51nod 1245】Binomial Coefficients Revenge
题目大意 C(M,N) = M! / N! / (M - N)! (组合数).给出M和质数p,求C(M,0), C(M,1)......C(M,M)这M + 1个数中,有多少数不是p的倍数,有多少是p ...