Daily C/C++ C++异常机制
这篇文章需要一些前置的知识,是函数栈相关的。推荐先去看看《深入理解计算机系统》和《程序员的自我修养》
首先要明白,我们在调用函数的时候,会有一个抽象的概念叫栈帧。里面储存了我们在调用函数期间需要用到的各种信息,栈帧存储在栈中,递归调用的层数越多,栈帧也就会叠的越多,就有可能导致栈溢出,即stackoverflow
栈帧中储存的信息主要有函数的返回地址,函数参数,函数的局部变量等(其实如果观察过汇编的同学可以发现,我们在使用栈的过程中一般都会有一个rbp的寄存器,用来储存之前的栈指针。这个就是我们函数的基址(base pointer),我们在使用的栈帧中的信息的时候,一般都会利用这个指针加上偏移来寻找,同时最后退栈的时候也只需要简单的将这个指针的值赋给栈指针。在编译的时候,我们也可以显示的取消这个机制,让编译器自己去计算相关的值,这样可以省出来一个寄存器,但一般不推荐这样做)
还有一个相关的术语叫做调用约定,这个是用来约定参数的顺序,以及退栈时是由调用者还是函数来清理栈。具体可以去看《程序员的自我修养》一书
图来自于参考文章
C++为了处理异常,在栈中增加了额外的信息。考虑RAII,在退栈的时候我们要保证对象被正确析构。同时我们也需要一个结构来让我们在发现异常时可以逐级向上查找try catch块,从而用来处理异常。
图来自于参考文章
这是一个单链表的结构,可以看到piPrev
就是指向前一个节点
而对应的piHandler
就是用来处理异常的一个数据结构
他会指向两个表,在图中我们可以看到,分别是TRYBLOCK
和UNWINDTBL
nstep
用来定位try块,可以让我们找到正确的try catch block
这里要注意的是最下面的那个piHandler
,他会对每一个线程都维护一个处理当前异常框架的指针,相当与栈顶指针,这个指针一般存放于某个TLS槽。即Thread Local Storage
然后我们看C++是怎么实现回退机制的
可以看到,我们会在执行函数的过程中维护nStep的值,然后在发生异常的时候,我们可以根据nStep的值在栈回退表中找到对应的槽。pfnDestroyer
就是对应的析构函数,pObj
就是对应的对象,而nNextIdx
则是需要析构的下一个对象。
我们首先根据nStep找到条目,然后执行析构函数,然后根据nNextIdx找到下一个对象,直到资源都被释放
pObj
其实记录了对象的偏移,即当前对象的this指针与基址(rbp)的偏移,这样我们得到了这个对象,就可以调用对应的析构函数
仔细观察一下nStep的值,我们也可以很快的得出计算nStep的算法。在一个块中,每遇到一个新的对象,我们在会回退表中添加一个新的条目,并将这个对象对应的信息填进去。同时将nStep的值赋给nNextIdx,并更新当前的nStep值。要注意的是,更新nStep值的时候,并不是之前的nStep值加一,而是之前nStep的最值加一。因为nStep的值实际上代表了我们储存的条目的索引,nStep就是类似的一个栈顶,区别我们回退表的条目只能增加,不能减少。而我们在利用这个栈顶来得到下一个需要析构的对象的id。
然后考虑我们的try catch表
这个涉及到nStep的另一个用途,他不仅可以帮助我们维护对象的生命周期,同时还可以帮我们判断当前执行到了具体哪一步。我们在try catch表中维护try的范围,其实就是nStep的范围。然后当遇到异常时,我们就可以遍历tryBlock表,判断异常是在那个try中出现的。然后执行对应的catch
根据结构我们也可以看到,我们记录了nBeginStep
和nEndStep
,用来判断nStep是否在这个try之间,然后记录对应的CatchBlock,而对应的CatchBlock则记录了异常的类型piType
和Catch的入口pCatchBlockEntry
最后就是异常的抛出
C++会将异常的抛出换成图中的形式,即调用一个内部函数,然后传入对应异常的结构体信息
这个函数会保存我们传入的异常对象,然后在当前线程的TSL中找到对应的异常处理结构,然后用nStep进行判断并执行catch中的处理,栈回退等操作
遇到异常时,我们就会逐级的向上寻找,直到找到有对应的try catch块来捕获这个异常,或者一直退栈直到程序退出。
异常的机制差不多就是这些,我更推荐去看看参考文章,里面还分析了异常相关的运行效率等。
文章评论