当前位置:网站首页>C++异常实现机制
C++异常实现机制
2020-11-09 23:48:00 【shzwork】
、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++异常的实现原理,当然其他语言的异常捕获机制也是同样的思想实现异常处理的。
版权声明
本文为[shzwork]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4000302/blog/4710394
边栏推荐
- 剑指offer之打印二叉搜索树中第k小的结点
- November 09, 2020: talk about the similarities and differences between the bulon filter and the cuckoo filter?
- Fear of reconstruction? I'm too late to tell you how to refactor. Now I'm here
- SQL case conversion, remove the space before and after
- AQS source code in-depth analysis of conditional queue
- Prometheus installation configuration
- 商品后台系统优化
- 飞鸽传书局域网找不到其他人的问题解决
- 当我们开发一个接口时需要注意些什么
- crm系统的成本一般是多少?
猜你喜欢
What is the SRM system? SRM supplier management system functions
Python prompt attributeerror or depreciation warning: This module was degraded solution
Top 5 Chinese cloud manufacturers in 2018: Alibaba cloud, Tencent cloud, AWS, telecom, Unicom
Quick for imx6ull development board c program call shell
Visit 2020 PG Technology Conference
LinkedList源码简析
京淘项目day10
11.9
Realization of commodity backstage system
IP地址SSL证书
随机推荐
Kubernetes-18: installation and use of dashboard
11.9
[QT] subclass QObject + movetothread to realize multithreading
Python中[:]与[::]的用法
Hengxun Technology: the way to deal with server downtime
2018中国云厂商TOP5:阿里云、腾讯云、AWS、电信、联通 ...
Baishan cloud technology is selected as the top 100 Internet enterprises in China in 2020
The number of more than half of the array is printed by the sword
算法模板整理(一)
Top 5 Chinese cloud manufacturers in 2018: Alibaba cloud, Tencent cloud, AWS, telecom, Unicom
input 与 button 的问题 (空隙/不等高/对不齐)及 解决办法
那么当下的行情投资者该怎么办呢? 现在新的投资风口来了!
day84:luffy:优惠活动策略&用户认证&购物车商品的勾选/结算
Win7 + vs2015 + cuda10.2 configuration tensorrt7.0
nodejs篇-手写koa中间件
【CentOS7操作系统安全加固系列】第(2)篇
PLSQL Developer常用设置
Guest interview: Wang Jian
剑指offer之打印二叉搜索树中第k小的结点
恒讯科技浅谈:出现服务器宕机的处理方式