C++编译器怎么实现异常处理

对于 VC ++实现异常处理的深入探讨
 
导论
 
相比较其他传统的语言, C ++的一个变革的特征是支持异常处理。相对于传统语言的不清楚容易错误的错误处理机制, C ++的异常处理是一个非常好的替代。在正常的代码和错误处理代码之间清楚的分割使得程序非常整洁和宜于维护。本文讨论编译器怎么实现异常处理。假设读者熟悉异常处理的语法。 本文包含一个异常处理的 VC ++的库来替代 VC ++的异常处理,使用这个函数:
 
install_my_handler();
 
在这以后,程序中发生的任何异常(包含抛出异常到 stack unwinding ,调用 catch 块和继续执行)都使用我自己的异常处理库。
 
译者注:当异常出现时,正常的执行流被中断,异常处理机制开始在当前范围寻找匹配的处理函数。如果找不到,把当前函数从栈中弹出,在调用者中继续寻找。这个过程称为 stack unwinding
 
像其他 C ++特征一样, C ++的标准并没有指定异常处理的实现机制。这使得 C ++实现者可以使用任何实现机制。我将讲述 VC++ 怎么实现的。 VC ++把异常处理置于 SHE Structured exception hangling )的上面。 SHE windows 操作系统提供的结构化的异常处理。
 
SHE 导论
 
在本讨论中,我将考虑那些显式的异常。例如被 0 除,空指针访问等。当异常出现,中断会产生,控制被转到 OS OS 调用异常处理,检查从异常发出的函数开始的函数调用顺序,执行 stack unwinding 和控制转移。我们可以开发自己的异常处理函数,在 OS 中注册。 OS 就会在异常事件时调用它们。
 
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 要求异常处理的信号,定义在 excp.h 文件中:
 
EXCEPTION_DISPOSITION (*handler)(
    _EXCEPTION_RECORD *ExcRecord,
    void * EstablisherFrame,
    _CONTEXT *ContextRecord,
    void * DispatcherContext);
 
现在你可以忽略所有的参数和返回类型。下面的程序在 OS 中注册了一个异常处理句柄并将产生一个被 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, 在调用函数时 , 进入函数以后第一句应该是 push ebp ,然后 mov ebp,esp, 所以 ebp 一般都指向当前函数进入时的栈顶,而且指向的内容是上一层调用函数进入时栈顶 , 如此向外,最后找到 0 ,就是系统的入口,这样一个函数使用的那些栈应该就是一帧 ) 来保存所有的函数局部对象和函数表达式产生的临时对象 ( 译注: 1.C++ 里对象的意义很广泛,不只是 class, 结构,简单类型也是对象 2. 临时对象,学过编译原理的话应该很清楚,举个例子,比如有一个函数是 int myfun(); 你在函数中这样写 int ret = myfun(); 这样 myfun() 返回的结果就放到了 ret 里,如果你写成 myfun(); 而不理它的返回值,你虽然不理它,但是仍然会有它返回值存放的地方,这就是一个临时的对象,在 vc 的调试环境了,看 auto 变量的页面就可以看到临时的变量 ) 。请注意上面描述只是很典型的情况。而实际上,编译器可能会储存所有或部分的变量到寄存器里,以便获得更快的执行速度 ( 译注:编译器优化 ) 。堆栈是一个处理器 (CPU) 级就支持的概念 ( 译注:之所以这么说,因为汇编代码里就有 push pop ). 处理器提供内部的寄存器和特殊的指令来实现堆栈处理
图 2 显示了一个典型的栈,这是当函数foo调用函数bar,然后bar 调用函数widget以后的栈的内容。请注意栈是向下增长的(译注:平时我在纸上画栈都是低地址在上,所以作者的这个图看起来感觉有点怪,但是看懂应该没有问题),这意味着下一个将要入栈的元素所在的地址将比前一个元素的地址更小(低)。
编译器用 ESP 寄存器来鉴别当前的栈帧,在上图所示的情况下, widget 时正在执行的函数 , EBP 寄存器指向了 widget 的栈帧 ( 就是函数进入时, push ebp 以后的栈顶位置 ) 。函数访问局部变量都是用局部变量所在的位置相对于帧顶的偏移量。编译器在编译的时候就把所有的局部变量从名字变成固定的相对于帧顶的偏移,例如, widget 函数访问它的一个局部变量就用 ebp-24 来指明它的位置
上图也显示了 ESP 寄存器,在图示的情况下,它指向了堆栈的最后一项,也就是处于 widget 帧的尾部,下一帧将从这个位置开始
处理器支持两种栈操作 :push pop:
pop EAX
意味着从 esp 所在的位置读 4 个字节到 eax 中,然后把 esp 增加 4(32 位的处理器下 ) 。同样的 ,
push EBP
 
意味着把 esp 4 ,然后把 ebp 的内容写到 esp 指向的地方。
当编译器编译一个函数, 它在函数头部添加一些创建和初始化函数栈帧的代码,同样,在函数结尾加上从堆栈里弹出栈帧的代码。
典型的,编译器在函数头部生成
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 寄存器到当前帧的尾部,就是把 esp 减去本函数内的局部对象的总长度。编译器在编译的时候就知道有多少的局部对象和每个对象的长度,所以能够清楚地知道一个函数的帧的确切长度
在函数结束时把当前帧从堆栈中弹出
Mov ESP, EBP   
Pop EBP         ; activate caller's frame
Ret             ; return to the caller
恢复 ESP EBP ,然后执行 RET
当处理器执行 RET 指令时,实际上类似执行了一条 pop eip, 把栈里保存的 EIP 弹出,然后跳到 EIP 处开始执行。相反, call 指令执行的时候先把当前的 EIP 推入堆栈,然后 jmp 到相应的地址,
3 显示了运行时堆栈的更多详细信息。如图所示,函数参数也是函数帧的一部分,调用函数者把参数推入堆栈,然后函数返回否执行
Add ESP, args_size
或者,采用另一种 RET 指令,如下
Ret 24
相当于返回后,执行了 ADD ESP , 24
注意没有进程里的每隔线程都有它自己的栈
 
 
C++和异常
再回头来说我们在第一节里说到的 EXCEPTION_REGISTRATION结构,这个结构是用来注册操作系统的异常回调函数的,当异常发生时,该函数将被调用。
VC++扩展了异常回调函数得语法,增加了两个新的参数:
struct EXCEPTION_REGISTRATION
{
   EXCEPTION_REGISTRATION *prev;
   DWORD handler;
   int   id;
   DWORD ebp;
};
VC ++ 里,除了一些例外,在每个函数的头部生成 EXCEPTION_REGISTRATION 结构的局部变量 ( 作者注:编译器可能在一个函数里根本不生成任何异常相关的代码,如果函数里没有 try 或者所有的局部对象都没有可供调用的析构函数 ( 译注:当异常产生时,要把堆栈顶到 catch 到异常那点之间的局部变量都释放掉,这是异常处理一个重要责任。如果这些局部变量都不需要去特别释放,比如 int 变量或简单结构变量,只要把堆栈指针指过来就可以了,异常处理在这一段里就是没有必要的,因此编译器识别了这样的情况,作个一定的优化 )) 。上面结构的 ebp 和前面说的堆栈帧里的 ebp 指针是重叠在一起的 ( 译注:请参看 C++ 编译器怎么实现异常处理 2 ) ,函数在开始时把这个结构创建在它的堆栈上,同时把这个结构注册到操作系统 ( 译注:就是把 FS:[0] 的位置改成这个结构的指针);在函数的结尾恢复调用者在系统注册的 EXCEPTION_REGISTRATION 结构 ( 译注:就是把 FS:[0] 改成结构里的 prev) ,我将在下一节讨论 EXCEPTION_REGISTRATION 结构里的 id 域的重要性。
VC++ 编译一个函数,它生成函数的两套数据:
a) 异常的回调函数
b) 一个包含重要信息的数据结构,这些重要信息比如,各个 catch 块,它们的地址,关心的异常的种类等等,我将在下一节更多的谈论它
 4 显示了如果考虑异常处理,程序运行时堆栈的情况。 Widget 的异常回调是在异常链的头部 ( 异常链的头部就是 FS:[0] 指向的内容 ) FS:[0] 的内容就是在 Widget 头部定义的 EXCEPTION_REGISTRATION 结构的指针。当异常产生时,异常处理把 Widget 的函数信息结构的地址 ( 就是 EXCEPTION_REGISTRATION 的指针 ) 传递给 __CxxFrameHandler 函数, __CxxFrameHandler 函数检查这个数据结构,察看函数里是否有 catch 块对当前的异常感兴趣,如果没有找到,函数返回 ExceptionContinueSearch 给操作系统,操作系统获得异常处理链里的下一个节点,再调用这个节点的异常处理函数(这个节点应该是在本函数的调用者定义的 )
这个动作一直持续到异常处理找到一个对这个异常感兴趣的 catch 块,找到了匹配的 catch 块以后,程序不再返回到操作系统。但是在它调用 catch 块前 ( 在函数信息结构里有这个异常回调函数的指针,看图 4), 异常处理必须执行堆栈释放:清除这个函数之前的所有函数的堆栈帧。清除堆栈帧的动作有点复杂,异常处理必须找到所有在异常产生时还没有结束的函数,找到每个函数所有的局部变量,调用每个变量的析构函数。我将在以后的章节里更详细的讨论这点。
异常处理是这样一个任务,当异常处理时清除异常处理所在的函数帧。这个操作是从 FS:[0] 指向的位置,也就是异常处理链头开始的, 然后沿着链一个节点一个节点的通知它所在的堆栈将要被回收,收到通知的节点,调用所在帧的所有局部对象的析构然后返回,这个过程一直延续到抛出的异常被捕获到。
因为 catch 块也是某个函数的一部分,所以它也用了所在函数的函数帧来保存数据。因此异常处理的一个 catch 块进入的时候会激活所在函数的那一帧 ( 译注:就是会把 ebp 改到那个函数去,而 esp 是不动的,简单的说,在 catch 函数块里执行的时候, ebp 是指向所在函数的帧顶,而 esp 可能在非常远的堆栈顶端)。同时,每个能能捕获异常的 catch ( 就是异常种类和 catch 块要捕获的异常种类一致 ) 实际上只有一个参数,就是这个异常对象的拷贝或者是异常对象的引用拷贝。 catch 块知道怎么从函数信息结构中拷贝这个异常对象,编译器已经产生了足够多的信息(译注:就是根据传入的 id ,来判断怎么复制异常 )
当拷贝了异常对象同时激活了函数帧以后,异常处理就开始调用 catch 块, catch 块的返回告诉异常处理在 try-catch 后面跟着执行 ( 译注:当然也可以在 catch 块里接着 throw) 。注意,在这一时刻,即使堆栈恢复已经存在,而且函数帧都已经清理,但是这些地址仍然在堆栈上存在 ( 译注:就是说在进入 catch 块函数时,仍然是用程序的堆栈,不过多压了好多东西进去,而在栈顶之前的一些东西都已经被清理了,但是它们仍然在堆栈上占据着空间,这个时间是检查错误来源的好时机,这也是我研究异常处理详细机制的目的 ) ,这是因为异常处理仍然像其他函数一样的执行,它也使用程序堆栈来存放它的局部变量, cantch 块存放局部变量的帧在异常产生时调用的最后一个函数的堆栈帧的上边 ( 地址更低 ), catch 块返回时,需要删除这个异常对象的拷贝 ( 译注:如果异常对象是在栈里,自动就删除了,如果是在堆了,需要用 delete 或者 free 明确删除 ) 。在 catch 块的结尾,异常处理回收了包括自己的帧在内的所有已经清理的堆栈帧,实际上就是把 esp 指向当前函数帧的结尾 ( 译注:前面说过在 catch 块里, ebp catch 所在函数的 ebp 比如是 0x12ff58 ,而 esp 在很远的地方比如 0x12f9bc ,而实际上 ,esp ebp 之间的大部分东西都已经被删除了,现在把 esp 改回来,回到函数正常运行的状态,比如 0x12ff80,0x12ff58-0x12ff80 实际上是拥有 catch 块的那个函数的堆栈帧的范围 ) ,完成这点以后,跳到 try-catch 块的下一条语句继续执行。但是 catch 块是怎么知道函数堆栈帧的尾在哪里的 ? 这本来是没有办法知道的,这就是为什么编译器要在函数的堆栈帧的开头保存一个 esp 的原因。参看图 4 ,堆栈帧指针 EBP 减去 16 就是 esp 所在的位置。
 catch 块自己可能会产生一个新的异常或者把当前异常重新抛出。异常不得不监视这种情况然后采取适当的操作。如果异常处理抛出一个新的异常,前一个异常就被删除。如果 catch 块决定再次抛出捕获的异常,前一个异常就被往后传递。
有一个需要注意的地方:因为每个线程都有自己的堆栈,所以都有它自己的一套异常处理链 ( FS:[0] 处指向的 EXCEPTION_REGISTRATION 结构开始 )
 
 
C++ 和异常 2
 5 显示了函数信息 (funinfo) 结构的内容。请注意结构使用的名字可能和 VC++ 编译器使用的实际名字不一样,而且我在图中只显示了有关的成员,结构中的 unwind table 成员我将在下一节讲到。
当异常产生时,异常处理不得不寻找函数中的 catch 块,首先它要知道函数里这个产生异常的语句是不是被一个 try 块所包含。如果函数根本就没有 try 块,异常处理直接就从函数里返回,否则,异常处理就从所有的 try 块里找出那个包含了这条语句的块。
首先  ,让我们来看看怎么来找这个关联的 try 块。在编译的时候,编译器赋给每个 try 块一个起始 ID 和一个结束 ID ,通过 funcinfo 结构,很容易找到这些 ID 。参看图 5 。编译器为函数里的每一个 try 块生成 tryblock 数据结构。
在以前的章节里,我已经说过了 VC++ 扩展的 EXCEPTION_REGISTRATION 结构,这个结构里就有一个成员叫 ID 。这个结构是放在函数帧上的,参看图四。当异常产生的时候,异常处理从堆栈里获得这个结构里的 ID 成员,然后检测这个 ID 是不是等于 try 块的两个 ID 之一,或者值的范围在起始和结束 ID 之间。如果上述条件满足,那么产生异常的语句就是在这个 try 块里的,如果不是,异常处理就查找 tryblocktable 里的下一个 try 块。
谁在堆栈里写下这些值?应该把这个值赋成多少?编译器在函数的不同位置填写恰当的语句来更新当前帧的 ID ,通过这样的手段来反映当前的运行状态 ( 译注 :
比如,一段代码 :
BOOL E1(FARPROC p)
{
try{ return (*p)(); }
catch(...) { printf("exception/r/n"); return FALSE; }
}
编译出来时这样的
var_10 = dword ptr -10h                   ; 数据定义
var_C = dword ptr -0Ch                   ; 数据定义
var_4 = dword ptr -4                        ; 数据定义
push ebp               ;caller's ebp
mov ebp, esp             
push 0FFFFFFFFh           ; 这就是 id
push offset loc_401320    ;handle
mov eax, large fs:0      
push eax                  ;prev ,堆栈里这四项组成了结构 EXCEPTION_REGISTRATION
mov large fs:0, esp       ; 然后将现在的 EXCEPTION_REGISTRATION 注册到 fs:0
push ecx
push ebx
push esi
push edi
mov [ebp+var_4], 0        ;这是把 id 0xffffffff 变成 0, 这就是作者说的函数中恰当的位置
mov [ebp+var_10], esp     ; 保留 esp, 见图四
call [ebp+arg_0]          ; 调用函数
mov ecx, [ebp+var_C]     
mov large fs:0, ecx       ; 恢复 fs:0 的值为 prev, 同时调用函数 (*p)() 的返回值是放在 EAX 中的,所以用的是 ECX 寄存器
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp                    ; 恢复 EBP
retn
)
例如:编译器会在 try 函数块进入的地方添加一条语句,把 try 函数块的起始 ID 写到函数的堆栈帧里。
当异常处理处理异常时找到了一个 try 函数块包含产生异常的语句,它能通过 trycatch 表检查这个 try 函数块是否有 catch 块能捕获这个产生的异常。请注意在嵌套的 try 函数块里,内层的 try 函数块产生的异常也在外层的 try 函数块的范围里。这样的情况下,异常处理先找内层的 catch 块,如果没有找到,才去找外层的 try 函数块。在生成 tryblock 表时, VC++ 把内层的 tryblock 结构放在外层的 tryblock 之前。
异常处理怎么知道(从 catch 块的结构中 ) 一个 catch 块能不能捕获当前的异常呢?它是通过比较 catch 块的参数,那个异常对象的种类来做到这点的。
catch 块能捕获的异常 H E 是完全相同的类型,产生的异常就会被捕获。因此,异常处理不得不动态比较参数的类型。但是,一般来说, C 或者是类似 C 的语言并不能很容易的在运行时决定参数的类型 ( 译注: C 就和汇编差不多,看着就知道长度,谁知道在源码里它是什么类型)。为此,定义个一个叫 type_info 的类,这个定义写在标准头文件 <typeinfo> 里,用来描述变量运行时的类型。 catchblock 结构的第二个成员 ( 5 )就是一个指向 type_info 结构的指针,代表了 catch 块参数的运行时的类型。 type_info 类重载了 == 操作符,用来判断两个类型是不是完全相同的类型。因此,所有的异常处理都要做这个比较 ( 调用 == 操作符重载函数 ) 来确认 catchblock 参数的 type_info 和产生的异常的 type_info 是否相等,从而判断当前的 catch 块能不能捕获当前异常。
异常处理从 funcinfo 结构里知道了 catchblock 的参数,但是怎么知道当前异常的 type_info 呢,当编译器遇到这样的语句
throw E();
它为这个抛出的异常创建一个 excpt_info 结构,参看图 6 。请注意名字可能和 VC++ 编译器使用的有所不同,而且我只列出了有关的项。如图所示,异常的 type_info 可以通过 excpt_info 结构来访问。有些时候,异常处理要销毁异常 ( catch 块完成 ) ,也可能需要拷贝异常 ( 在调用 catch 块之前 ) ,为了帮助异常处理完成这些任务,编译器产生异常的析构函数,拷贝构造函数和取异常对象的大小的函数(通过 excpt_info 结构 )
 
如果 catch 块的参数是一个基类,产生的异常是它的派生类,异常处理仍然应该在异常产生时调用这个 catch 块。然而,比较这两个种类 ( 基类和派生类 ) 将返回 false ,因为这两个类型本来不是同一种类型。不管是 type_info 类提供的成员函数还是操作符,都不能判断两个类一个是不是另一个的子类。但是,在这样的情况下,异常处理却确实能捕获到这样的异常。这是怎么做到的呢?实际上,编译器为这个异常产生了更多的信息。如果异常类是从别的类派生的,那么 etypeinfo_table( 在结构 excpt_info 结构里 ) 包含了 etype_info( 扩展的 type_info ,我命名的 ) 指针指向所有的父类,这样异常处理比较 catch 块的 type_info catch 块参数的所有的 type_info( 自己和自己的所有基类的 type_info) 。只要有一个匹配成功, catch 块就会被执行。
在我总结这一节之前,至少还有一个问题,就是异常处理是怎么知道当前产生的异常的 excpt_info 在哪里?我将在下面回答这个问题。
VC++ throw 语句编译成和下面类似的语句:
//throw E(); //compiler generates excpt_info structure for E.
E e = E(); //create exception on the stack
_CxxThrowException(&e, E_EXCPT_INFO_ADDR);
_CxxThrowException 把控制权传递给操作系统 ( 通过软件中断,参看函数 RaiseException) ,同时传递两个参数。操作系统在准备调用异常回调函数时,把这两个函数打包到结构 _EXCEPTION_RECORD 里。接着操作系统找到 FS:[0] 处的异常处理链头的第一个 EXCEPTION_REGISTRATION 结构,调用结构里的 handle 。指向 EXCEPTION_REGISTRATION 的指针也就是异常处理的第二个参数。再提一下,在 VC++ 里每一个函数在自己的那一帧堆栈上创建它自己的 EXCEPTION_REGISTRATION 结构,同时把这个结构注册到系统。第二个参数对异常处理很重要,通过它可以找到像 ID 这样的成员 ( 没有 ID 就不能确定 catch ) 。这个参数也能使异常处理知道函数的堆栈帧 ( 这对清除本帧变量很有用 ), 同时也能往下找出更多的 EXCEPTION_REGISTRATION 节点(这对清除堆栈很有用 ) 。第一个参数是一个指向 _EXCEPTION_RECORD 结构的指针,通过它异常处理能找到异常对象的指针和 excpt_info 结构。异常处理的返回值定义在 EXCPT.H
( 译注 :
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution,
 ExceptionContinueSearch,
ExceptionNestedException,
 ExceptionCollidedUnwind }
 EXCEPTION_DISPOSITION; )
EXCEPTION_DISPOSITION (*handler)(
    _EXCEPTION_RECORD *ExcRecord,
    void * EstablisherFrame, 
    _CONTEXT *ContextRecord,
    void * DispatcherContext);
你能忽略最后的两个参数。异常处理的返回值是一个枚举 (EXCEPTION_DISPOSITION 类型 ) 。在前面我们已经说到,如果异常处理不能找到 catch 块,它就返回 ExceptionContinueSearch 给操作系统。其他的不太重要的信息在结构 _EXCEPTION_RECORD 里,这个结构定义在 WINNT.H :
struct _EXCEPTION_RECORD
{
    DWORD ExceptionCode;
    DWORD ExceptionFlags; 
    _EXCEPTION_RECORD *ExcRecord;
    PVOID   ExceptionAddress; 
    DWORD NumberParameters;
    DWORD ExceptionInformation[15]; 
} EXCEPTION_RECORD; 
ExceptionInformation 数组的个数和入口的种类由 ExceptionCode 决定。如果 ExceptionCode 表示异常是个 C++ 异常 (ExceptionCode 0x06d7363, 当通过 throw 来产生异常就会出现这样的情况 ) ,那么 ExceptionInformation 数组包含了指向异常对象和 excpt_info 结构的指针。而其他的异常,基本上都没有入口。其他的异常有除 0 ,访问拒绝等,都能在 WINNT.H 里找到它们对应的值。
异常处理通过 EXCEPTION_RECORD 结构的 ExceptionFlags 成员来决定异常时采取什么动作。如果这个值是 EH_UNWINDING ( 定义在 except.inc ) ,提示异常处理清除堆栈正在进行,这时,异常处理应该清除了函数堆栈帧然后返回。清除函数帧包括这样的动作,找到所有的在异常发生时还没有释放的局部变量,然后调用它们的析构函数。这点下一节继续讨论。否则,异常处理不得不在函数里继续查找匹配的 catch 块,然后调用找到的 catch
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值