C++ 异常处理机制的实现

本文深入探讨了C++的异常处理机制,包括结构化异常处理、函数与栈的关系、C++异常处理的实现细节,以及异常处理器的注册和栈展开过程。文章通过实例解释了VC++如何利用Windows的异常处理机制实现C++的异常处理,强调了栈帧、EXCEPTION_REGISTRATION结构体和异常处理器在其中的角色,并讨论了函数调用、栈展开、catch块的匹配和异常对象的处理。
摘要由CSDN通过智能技术生成

介绍

相对于传统语言,C++ 的革命性特征之一,就是它对异常处理的支持。传统异常处理技术有缺陷并且易于出错,而 C++ 提供了一个非常优秀的替代方案。它将正常流程代码与错误处理代码清晰的隔离出来,使得程序更加健壮,易于维护。这篇文章将讨论编译器是如何实现异常处理的。假定读者已经对异常处理机制及其语法已经有了大致的了解。我用 VC++ 实现了本文中介绍的异常处理库。要将异常处理器替换成我的 VC++ 实现方式,调用下面的函数:

install_my_handler();

在此之后,程序中发生的任何异常——从抛出一个异常到栈展开、调用catch块、然后恢复执行——都被我的异常处理库处理。

和C++的其他特性一样,C++标准没有关于应该如何实现异常处理的任何说明。这意味着每个编译器厂商都可以自由选择它认为合适的任何实现方式。我将介绍VC++是如何实现这一特性的。对使用其他编译器或者操作系统的开发者[1],它应该是一个很好的学习资料。VC++基于Windows操作系统的结构化异常处理(SEH),构建了它的异常处理支持[2]。

结构化异常处理——概述

在此讨论中,我将考虑那些显式抛出或者被 0 除、访问空指针等操作导致的异常。当异常发生时,将产生中断,然后控制权转移给操作系统。操作系统调用异常处理器,它将从产生异常的函数开始检查函数调用序列,然后执行栈展开,并转移控制权。我们可以写自己的异常处理器,然后注册到操作系统;这样当异常事件发生时,操作系统会调用它。

为了注册,Windows定义了一个特殊的结构体,叫做EXCEPTION_REGISTRATION:

struct EXCEPTION_REGISTRATION

{

   EXCEPTION_REGISTRATION *prev;

   DWORD handler;

};

要注册你自己的异常处理器,就需要创建这个结构体,然后将它的地址存储到寄存器 FS 所指位置的0偏移处,如下伪汇编代码所示:

mov FS:[0], exc_regp

字段 prev 表明 EXCEPTION_REGISTRATION 结构体是一个链表结构。当我们注册 EXCEPTION_REGISTRATION 结构体时,我们需要将前一个注册的结构体地址存入prev字段。

那么异常回调函数是什么样子呢?Windows要求异常处理器的函数签名如下,它在EXCPT.h中定义:

EXCEPTION_DISPOSITION (*handler)(

    _EXCEPTION_RECORD *ExcRecord,

    void * EstablisherFrame,

    _CONTEXT *ContextRecord,

    void * DispatcherContext);

目前你可以忽略所有的参数和返回值类型。下面的程序向操作系统注册异常处理器,并通过除以0产生了一个异常。这个异常被异常处理器捕捉到。处理器没做多余的工作,只是输出了一条信息,然后退出程序。

#include <iostream>

 

#include <windows.h>

 

using std::cout;

using std::endl;

 

 

struct EXCEPTION_REGISTRATION

{

   EXCEPTION_REGISTRATION *prev;

   DWORD handler;

};

 

 

EXCEPTION_DISPOSITION myHandler(

    _EXCEPTION_RECORD *ExcRecord,

    void * EstablisherFrame,

    _CONTEXT *ContextRecord,

    void * DispatcherContext)

{

cout << "In the exception handler" << endl;

cout << "Just a demo. exiting..." << endl;

exit(0);

return ExceptionContinueExecution; //will not reach here

}

 

int g_div = 0;

 

void bar()

{

//initialize EXCEPTION_REGISTRATION structure

EXCEPTION_REGISTRATION reg, *preg = &reg;

reg.handler = (DWORD)myHandler;

//get the current head of the exception handling chain

DWORD prev;

_asm

{

mov EAX, FS:[0]

mov prev, EAX

}

reg.prev = (EXCEPTION_REGISTRATION*) prev;

//register it!

_asm

{

mov EAX, preg

mov FS:[0], EAX

}

 

//generate the exception

int j = 10 / g_div;  //Exception. Divide by 0.

}

 

int main()

{

bar();

return 0;

}

 

/*-------output-------------------

In the exception handler

Just a demo. exiting...

---------------------------------*/

请注意,Windows有一个严格的规则要求:EXCEPTION_REGISTRATION 结构体应该放在栈上,并且它的内存地址应该比前一个节点要小。如果Windows发现不满足这个规则,将会结束进程。

函数与栈

栈是一块连续的内存区域,用来存储函数的局部变量。具体来说,每个函数都对应着一个栈帧(stack frame),用来存储这个函数的所有局部变量,以及函数内表达式产生的中间值。请注意下图是一个典型的示例。真实情况下,编译器为了达到快速访问的目的,可能会把部分或者全部变量存储到寄存器中。栈是处理器级别的概念。处理器提供内部寄存器和操作寄存器的特殊指令。

图2展示了函数foo调用函数bar,而bar调用函数widget时栈的典型情况。注意,此时栈是向下增长的。这意味着,后压入栈的变量地址会比先压入栈的变量地址要小。

编译器使用EBP寄存器来标识当前活动栈帧。在这个例子中,widget函数将被执行,因此EBP寄存器指向widget的栈帧,如图所示。函数通过偏移这个帧指针来获取局部变量。在编译阶段,编译器将局部变量的名称绑定到相对于帧指针的一个固定偏移值。例如,widget函数的一个局部变量,会通过栈指针向下偏移固定字节数来访问,称作EBP-24。

图中也展示了ESP寄存器,它是栈指针,指向栈的最后一个数据。在这个例子中,ESP指向widget帧的尾部。下一个帧会在这个位置创建。

处理器支持两种栈操作:压栈和出栈。如:

pop EAX

它的意思是从ESP指向的位置读取4个字节,然后ESP增加4(记住,这里栈是向下增长的)。同样地,

push EBP

它的意思是ESP递减4,然后将EBP寄存器的值写入到ESP指向的位置。

当编译器编译函数时,它会在函数开始的地方加入一些代码,称作初始化段(prologue),它负责创建并初始化函数的栈帧。同样地,编译器在函数尾部也添加一些代码,称作清理段(epilogue),它负责弹出当前函数的栈帧。

编译器一般会为初始化段生成如下的代码序列:

Push EBP      ; save current frame pointer on stack

Mov EBP, ESP  ; Activate the new frame

Sub ESP, 10   ; Subtract. Set ESP at the end of the frame

第一行语句把当前帧指针EBP保存到栈上。第二行语句通过修改调用函数帧位置的EBP寄存器,激活被调用函数的帧。第三行语句通过把ESP减去函数所创建的所有局部变量与中间值的大小,将ESP寄存器移动到当前帧的尾部。在编译阶段,编译器知道函数的所有局部变量的类型与大小,因此它能够计算出帧的大小。

清理段的工作与初始化段相反,它把当前帧从栈上移除:

Mov ESP, EBP    

Pop EBP         ; activate caller's frame

Ret             ; return to the caller

它将调用函数保存的帧指针恢复到ESP(即被调用函数帧指针指向的位置),将它弹出到EBP&#

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值