精华内容
下载资源
问答
  • 详细分析C++ 异常处理

    2021-01-19 23:43:31
    C++ 异常处理涉及到三个关键字:try、catch、throw。 throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。 catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字...
  • c++异常处理

    2015-08-11 00:24:07
    文件里简单的描述了c++编程中经常出现的异常,及其简单的处理方法 使您可以轻松的学会c++异常处理
  • C++ 异常处理

    2020-01-13 17:18:11
    C++ 异常处理 文章目录C++ 异常处理1. 异常关键字2. 异常处理实例3. C++ 标准异常4. 自定义异常5. 被遗弃的标准 1. 异常关键字 异常是程序在 执行期间 产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如...

    C++ 异常处理

    1. 异常关键字

    异常是程序在 执行期间 产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

    异常提供了一种 转移程序控制权 的方式。
    C++ 异常处理涉及到三个关键字:try、catch、throw。

    关键字描述
    throw当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
    catch在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
    trytry关键字覆盖范围中的代码称为保护代码,这些代码可能通过 throw 关键字抛出异常,它后面通常跟着一个或多个 catch 块。

    异常必须被抛出,才可以被捕获。也就是说如果想要 try / catch,必须有 throw。

    使用 try / catch 语句的语法如下所示:

    try
    {
       // 可能通过 throw 抛出异常
    }
    catch( ExceptionName e1 )
    {
       //捕获 ExceptionName 类型的异常并处理
    }
    catch( ExceptionName e2 )
    {
        //捕获 ExceptionName 类型的异常并处理
    }
    

    2. 异常处理实例

    #include <iostream>
    using namespace std;
    
    double division(int a, int b)
    {
       if( b == 0 ){throw "Division by zero condition!";}
       return (a/b);
    }
    
    int main ()
    {
       int x = 50;
       int y = 0;
       double z = 0;
    
       try 
       {
         z = division(x, y);
         cout << z << endl;
       }
       catch (const char* msg) {cerr << msg << endl;}
    
       return 0;
    }
    

    在这里插入图片描述

    我们需要 保证抛出异常与捕获异常的类型相匹配,这里抛出了一个类型为 const char* 的异常,因此,当捕获该异常时,我们必须在 catch 块中使用 const char*;如果抛出的是 int ,那么捕获的时候也该是 int。

    如果类型不匹配,那么是无法捕获异常的,程序会调用 adort 函数 终止程序。

    catch(…) 表示捕捉任何类型的异常,不会存在类型不匹配的问题,缺点在于只能检测到出现异常,不能发现到底是哪里出现异常。

    前面说到,一旦出现抛出异常没被处理的问题,系统将自动调用 abort() 函数,实际上,它触发的是 terminate,我们可以自定义 terminate 的处理方法:

    #include<exception>  
    #include<iostream>  
    using namespace std;  
      
    void check(int y) 
    {  
         if(y==0) throw "exception";  
    }  
    
    void myTerminate() 
    {  
         cout<<"Unhandler exception!\n";  
         exit(-1);  
    }  
    
    int main()  
    {  
        terminate_handler preHandler=set_terminate(myTerminate);  
        int x=100,y=0;  
        try  
        {  
            check(y);  
            cout<<x/y;  
        }  
        catch(int &e) //no catch sentence matches the throw type  
        {  
            cout<<e<<endl;  
        }  
    
        return 0;  
    }  
    

    在这里插入图片描述
    异常类型不匹配,无法捕获异常,系统调用自定义 terminate 方法。

    如果注释掉:
    terminate_handler preHandler=set_terminate(myTerminate);

    在这里插入图片描述

    3. C++ 标准异常

    C++ 提供了一系列标准的异常,定义在 “《exception》” 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

    在这里插入图片描述

    异常描述
    std::exception该异常是所有标准 C++ 异常的父类。
    std::bad_alloc该异常可以通过 new 抛出。
    std::bad_cast该异常可以通过 dynamic_cast 抛出。
    std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
    std::bad_typeid该异常可以通过 typeid 抛出。
    std::logic_error理论上可以通过读取代码来检测到的异常。
    std::domain_error当使用了一个无效的数学域时,会抛出该异常。
    std::invalid_argument当使用了无效的参数时,会抛出该异常。
    std::length_error当创建了太长的 std::string 时,会抛出该异常。
    std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。
    std::runtime_error理论上不可以通过读取代码来检测到的异常。
    std::overflow_error当发生数学上溢时,会抛出该异常。
    std::range_error当尝试存储超出范围的值时,会抛出该异常。
    std::underflow_error当发生数学下溢时,会抛出该异常。

    4. 自定义异常

    用户可以自定义异常类型,异常类型并不受到限制,可以是内建数据类型如 int,double等,也可以是自定义的类,也可以从C++某个异常类继承下来。

    #include<exception>  
    #include<iostream>  
    using namespace std;  
      
    /*自定义异常类 myException,重载 exception 的 what 方法*/  
    class myException:public exception  
    {  
    public:  
       const char* what()const throw()  
       {  
            return "ERROR! Don't divide a number by integer zero.\n";  
       }      
    };  
    
    void check(int y)  
    {  
         if(y==0) throw myException();  
    }  
      
    
    int main()  
    {  
     
        int x=100,y=0;  
        try  
        {  
            check(y);  
            cout<<x/y;  
        }  
        catch(myException& e) 
        {  
    		cout << e.what() << endl;  
        }  
        return 0;  
    }  
    

    在这里插入图片描述

    what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。
    在这里插入图片描述

    如果你想捕获标准异常 bad_alloc 抛出的异常,可以这样做。

    catch(bad_alloc	& e) 
    {  
    	cout << e.what() << endl;  
    }  
    

    5. 被遗弃的标准

    throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明函数能够抛出的异常类型。有些文档中称为异常列表。例如:

    double func (char param) throw (int);

    如果希望能够抛出多种类型的异常,可以用逗号隔开:

    double func (char param) throw (int, char, exception);

    如果不希望限制异常类型,那么可以省略:

    double func (char param) throw ();

    **C++标准已经不建议这样使用 throw 关键字了,**因为各个编译器对 throw 的支持不同,有的直接忽略,不接受 throw 的限制,有的将 throw 作为函数签名,导致引用函数时可能会有问题。上面的代码在 GCC 下运行时会崩溃,在 VS 下运行时则直接忽略 throw 关键字对异常类型的限制,try 可以正常捕获到抛出的异常,程序并不会崩溃。

    参考 https://blog.csdn.net/u011068702/article/details/52012013
    参考 https://blog.csdn.net/makenothing/article/details/43273137
    参考 https://wizardforcel.gitbooks.io/w3school-cpp/content/Text/78.html

    展开全文
  • C++ ,VS2010平台编辑,下载后可以直接使用,异常处理(try_catch)
  • C++异常处理源码与安全性分析

    千次阅读 2021-12-10 21:21:42
    C++异常处理需要DWARF的支持,其业界实际标准是 IA-64 C++ABI[1],本文主要描述整个异常处理的流程以及在libgcc中的实现.关于基于DWARF的栈回溯可参考 [2], 关于C++异常处理其他分析可参考[3-8]。 一、异常处理代码...

    版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/lidan113lidan/article/details/121865210

    更多内容可关注微信公众号  

      C++异常处理需要DWARF的支持, 其业界实际标准是 IA-64 C++ABI[1],本文主要描述整个异常处理的流程以及在libgcc中的实现. 关于基于DWARF的栈回溯可参考 [2], 关于C++异常处理其他分析可参考[3-8]。


    一、异常处理代码举例

      先以一个简单的C++程序为例:

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3
    4 class x {
    5         public:
    6         x(void) {
    7             printf("x:x() called\n");
    8         }
    9         ~x(void) {
    10            printf("x:~x() called\n");
    11        }
    12 };
    13
    14 void test() {
    15         x a;
    16         throw "test";
    17 }
    18
    19 int main()
    20 {
    21         try {
    22                 test();
    23                 throw 1;
    24         }
    25         catch(int x) {                        //loc1
    26                 printf("Int: %d\n", x);
    27                 return 0;
    28         }
    29         catch(const char * s) {               //loc2
    30                 printf("String: %s\n", s);
    31                 return 0;
    32         }
    33         return 0;
    34 }

      编译与输出:

    tangyuan@ubuntu:~/compiler_test/gcc_test/aarch64/test_exception$ aarch64-linux-gnu-g++ -static main.cc -O0 -o main
    tangyuan@ubuntu:~/compiler_test/gcc_test/aarch64/test_exception$ ./main
    x:x() called
    x:~x() called
    String: test1

       在此函数中 main函数调用了test, test函数在创建了类实例a后主动抛出了异常, 最终结果是test的父函数main捕获到了此异常, 在异常处理之前实例a的析构函数先被执行.


    二、术语定义

      按照IA-64 C++ ABI的描述, 在抛出异常后的处理可以分为两个阶段: 1) 异常处理handler的搜索 2)逐级cleanup直到执行到handler, 这里先给出一些术语的定义:

       1. 异常处理中的handler代码片段:

          是最终捕获并处理异常的这段代码片段,如对于上面的代码来说:

          * throw 1;  的handler就是loc1的这一段代码片段

          * throw "test"; 的handler就是loc2的这一段代码片段

           一个包含多个catch的try/catch语句, 其多个catch case会在同一个handler代码片段中,运行时异常处理需为其传入参数来区分具体case。

       2. 异常处理中的cleanup代码片段:

          一个函数或block中可能有收尾工作要做,如test函数在返回前需要执行类实例a的析构, 这些为block做收尾工作的代码片段称为cleanup, test函数的cleanup代码片段中需要析构类实例a; (一个函数中如果存在类实例定义又存在函数调用的话,此函数中通常都会存在一段cleanup代码以确保子函数中抛出异常时此cleanup代码可被用来执行类实例的析构函数)。

       3. landing_pad: 

           landing_pad指的是一个cleanup或handler,或二者结合的一段代码片段:

           * 函数中的任何一地址最多只能对应此函数内的一个landing_pad:

              - 如果函数某地址处抛出了异常(可能是直接throw或子函数throw导致的)且此地址(在当前函数内)不需要执行cleanup/handler,则其landing_pad为空

              - 如果函数某地址处抛出了异常且此地址同时需要cleanup和handler, 则其landing_pad中会先执行cleanup再执行handler.

           * 函数中不同地址可能对应不同的landing_pad:

             - 如函数中若存在多个try { ... } [catch {..}], 那么不同try中的地址都有自己的landing_pad

       4. DWARF/.eh_frame:

           DWARF是一种调试文件格式, .eh_frame是加载到内存的节区,其中保存了代码中所有函数的CFI(Call Frame Infomation)信息,栈回溯中需要依赖此节区的内容(细节见[2]).

           注: 异常处理虽然是C++的标准, 但C编译输出的二进制中也可以存在.eh_frame段. C中虽然无法使用异常处理,但异常处理的栈回溯可以正常经过C代码(此时C代码编译时需开启-fexceptions支持)

       5. 异常处理过程中的两个阶段:

           在C++中可以通过throw抛出异常,异常的处理要经过两个阶段:

           1) phase1(search phase):  phase1要从throw语句所在函数开始逐级的unwind, 直到在某一级栈帧中找到了可以处理此throw抛出的异常的handler为止.

           2) phase2(cleanup phase): phase2 要再次从throw语句所在的函数开始逐级的unwind, 并依次执行每一级栈帧中的cleanup函数(若有),直到执行到handler为止(这里注意区分cleanup phase和cleanup函数)

            实际上两段式异常处理不是必须的,但这样会带来一定的好处[1]:

        A two-phase exception-handling model is not strictly necessary to implement C++ language semantics, but it does provide some benefits. For example, the first phase allows an exception-handling mechanism to dismiss an exception before stack unwinding begins, which allows resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised). While C++ does not support resumptive exception handling, other languages do, and the two-phase model allows C++ to coexist with those languages on the stack.

      6. LSDA(language-specifi data area): 

          异常相关一段数据(格式见四), 一个try/throw/catch 中是否有cleanup需要执行,是否有某类异常的handler等信息都记录在LSDA中.

      7. personality routine: 

          在异常处理phase1/phase2均会调用的栈回溯回调函数,phase1/2中每unwind到一个栈帧时都会调用此函数,随着传入参数的不同此函数的作用也不同: 

          * 在phase1: 此函数负责回溯每一级栈帧,直到在某个栈帧对应函数中找到当前抛出的异常类型的handler为止

          * 在phase2: 此函数同样回溯每一级栈帧,并逐级调用每一级中的cleanup函数,直到执行到handler函数为止

          personality routine的指针记录在CIE中(见[2]), 最常用的personality routine是 __gxx_personality_v0,本文的后续分析也基于此函数.

       需要注意的是: C++异常与windows的SEH异常不同,C++中只能支持主动触发的异常(也就是通过throw抛出的异常), 而windows SEH可以捕获如除零异常等。


    三、异常处理的流程和关键函数

      以上面的测试代码为例,其异常处理的流程如图:

         test函数中 throw "test"; 的整个异常处理流程为:

      test: throw "test"; => _Unwind_RaiseException => test:cleanup => test:_Unwind_Resume => main:handler => main:函数返回

        其中涉及到的关键函数/代码片段描述如下:

        1. _Unwind_RaiseException:

            test中的throw函数最终调用调用到libgcc的_Unwind_RaiseException, 此函数的作用:

            * 首先执行phase1, 逐级栈回溯并通过personality routine搜索到此异常类型最终的handler(即main:handler)

            * 之后调用_Unwind_RaiseException_Phase2开始执行phase2, phase2回溯到一个landing_pad即返回,这里第一个找到的是test:cleanup(此信息记录在LSDA中), 说明test函数有收尾工作需要做, 此函数返回.

            * _Unwind_RaiseException跳转到test:cleanup执行test函数的收尾工作

        2. test:cleanup:

            test:cleanup的代码是编译器生成的,其作用是执行test中的收尾工作,在这里则是调用x:~x()这个析构函数; 收尾工作完成后则需要再次调用_Unwind_Resume继续phase2.

        3) _Unwind_Resume:

             _Unwind_Resume中重新调用_Unwind_RaiseException_Phase2继续phase2 (1=>2=>3 的过程中通过指针传递了异常相关的全局数据)

            * phase2还是从test函数开始栈回溯,但由于调用_Unwind_Resume的代码并不属于try块之内,test在此PC位置没有cleanup代码片段, phase2继续栈回溯到其父函数main;

            * main函数调用test时是在一个try块中的, 此时phase2找到了此try块的handler, 并跳转到main:handler

        4. main:handler:

            main:handler执行此异常的处理(这里是一个printf), 执行完毕后main函数正常退出其try/catch块继续执行,直到main函数返回(_Unwind_xxx在跳转前会修复栈帧,确保跳转到main:handler时的上下文(callee-saved reg)和main函数中导致抛出异常位置的上下文相同(即main函数调用test函数的位置)。


    三、异常处理源码分析

    1. __cxa_allocate_exception

        抛出异常前需要先调用__cxa_allocate_exception函数分配一个全局结构体用于在栈回溯中传递信息,其定义如下:

    //./libstdc++-v3/libsupc++/eh_alloc.cc
    /*
      分配大小为thrown_size + __cxa_refcounted_exception的空间
      ---------------------------   <== 分配 thrown_size + exception 
      __cxa_refcounted_exception
      ---------------------------   <== 函数返回的是指向这里的指针
      thrown_size
      ---------------------------   
    */
    extern "C" void *
    __cxxabiv1::__cxa_allocate_exception(std::size_t thrown_size) _GLIBCXX_NOTHROW
    {
      void *ret;
    
      thrown_size += sizeof (__cxa_refcounted_exception);
      ret = malloc (thrown_size);
    
      if (!ret)
        ret = emergency_pool.allocate (thrown_size);
    
      if (!ret)
        std::terminate ();
    
      memset (ret, 0, sizeof (__cxa_refcounted_exception));
    
      return (void *)((char *)ret + sizeof (__cxa_refcounted_exception));
    }

    2. _Unwind_RaiseException

        throw语句最终调用_Unwind_RaiseException抛出异常,源码如下:

    //./gcc/config/aarch64/aach64.h, 指定此属性的函数在pro/epilogue中会push/pop栈帧寄存器x29
    #define LIBGCC2_UNWIND_ATTRIBUTE __attribute__((optimize ("no-omit-frame-pointer")))
    
    //./libgcc/unwind.inc
    /*  
        此函数最终实现异常抛出(throw), 其:
       * 首选执行phase1 search, 通过栈回溯确定当前异常类型最终的handler
       * 开始phase2 cleanup: 
          - 栈回溯找到一个landing_pad后则停止了, 设置上下文为栈回溯时landing_pad所在函数的上下文,并跳转到landing_pad
        landing_pad中如果需要继续栈回溯,则需在最后一条语句中调用 _Unwind_Resume.
      参数exc会在整个异常期间作为全局变量传递
     */
    _Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE _Unwind_RaiseException(struct _Unwind_Exception *exc)
    {
      struct _Unwind_Context this_context, cur_context;
      _Unwind_Reason_Code code;
      unsigned long frames;
    
      /* 
         此函数基于_Unwind_RaiseException的DWARF信息, 将_Unwind_RaiseException入口时各个已保存到栈中的寄存器的值初始化到this_context中(细节可参考[2]),
         需要注意的是由于当前函数中调用了_builtin_eh_return(见后), 故和_Unwind_Backtrace不同的是uw_init_context除了初始化callee-saved寄存器外,还同时
         初始化了x0-x3, x0-x3的内存位置后续可用来为_builtin_eh_return传递参数
      */
      uw_init_context (&this_context);
    
      /* 此上下文初始化一次即可,这里先复制一份用于phase1 search 的栈回溯 */
      cur_context = this_context;
    
      /* phase 1: search, 其作用是逐级栈回溯直到找到可以处理当前异常的那个handler */
      while (1)
      {
          _Unwind_FrameState fs;
        
          /* 根据cur_context->ra, 计算当前函数caller各个寄存器的回溯规则(细节参考[2]), 结果记录到fs中*/
          code = uw_frame_state_for (&cur_context, &fs);
    
          if (code == _URC_END_OF_STACK) return _URC_END_OF_STACK;    /* 如果已经到栈底了则直接返回 */
          if (code != _URC_NO_REASON) return _URC_FATAL_PHASE1_ERROR; /* 出错则返回error */
    
          /* 为context->ra所在函数(caller)执行personality routine, 查看其中是否有异常的handler(第一次遍历是从_Unwind_RaiseException的父函数开始的) */
          if (fs.personality)
          {
            /* personality routine的地址记录在CIE中,这里以__gxx_personality_v0函数为例, 此时传入参数_UA_SEARCH_PHASE代表这是phase1, search; */
            code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class, exc, &cur_context);
    
            if (code == _URC_HANDLER_FOUND) break;    /* 找到handler则直接break; 发生错误则返回error; 否则继续循环遍历下一个栈帧 */
            else if (code != _URC_CONTINUE_UNWIND)
                return _URC_FATAL_PHASE1_ERROR;
          }
          /* context->ra所在函数中未找到handler则将fs信息更新到cur_context中,此后cur_context记录当前栈帧函数的caller函数的信息, 再次循环则从caller的caller中查找handler */
          uw_update_context (&cur_context, &fs);     
      }
    
      /* 执行到这里说明异常handler已经找到, handler和context->ra在同一函数, context中记录其caller指向到context->ra时的各个寄存器信息 */
      exc->private_1 = 0;
      /* 将找到handler时的context->CFA记录到exc->praivate_2中, phase2栈回溯时再次遍历到此栈帧时则可以直接执行的handler并结束处理 
         context记录的是handler的calee函数入口的寄存器信息,这也是context->ra所在函数执行到context->ra时的寄存器信息
      */
      exc->private_2 = uw_identify_context (&cur_context);    
    
      /* 这里开始执行phase2 cleanup, 此过程也需要从最后一级栈帧开始一边unwind一边执行cleanup函数, 此时可以复用前面已经初始化的上下文 */
      cur_context = this_context;
    
      /* 此函数执行phase2, 其通过栈回溯找到某级函数的一个landing_pad(cleanup/handler),并将landding_pad和参数保存到cur_context->ra/reg[0/1]中 */
      code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);
    
      /* 返回_URC_INSTALL_CONTEXT代表上下文已经设置好, 后续真正执行landing_pad, 未返回此值代表出错 */
      if (code != _URC_INSTALL_CONTEXT)  return code;
    
    /* 这里调用的是 uw_install_context,为了便于分析直接将此宏展开
       uw_install_context的作用是恢复到this_context时的上下文(this_context记录的是callee入口时寄存器信息,同时也是caller调用callee前的寄存器信息), 并跳转到caller中的landing_pad代码执行,因此landing_pad执行时和之前执行到this_context->ra时拥有相同的寄存器上下文. 
       caller中landing_pad执行完毕后可以直接通过ret正常返回父函数, 也可以调用_Unwind_Resume继续处理异常,这取决于当前的landing_pad是cleanup函数还是handler.
       这里要恢复的寄存器上下文包括:
       * 当前的sp指向caller函数的栈顶
       * 所有callee-saved寄存器必须和caller调用callee时一致(callee可能是throw函数,也可能是任意一个内部调用了throw抛出异常的函数)
         注意:
         1) 这里不是和caller入口时一致,而是和caller调用callee时一致,因为callee保存的是此时的状态
         2) 跳转到landing_pad时必须保证所有callee-saved硬件寄存器全部恢复, 因为如A=>B=>C, 函数B中使用了x19,C中使用了x20, 那么函数B返回时会修复x19但不会修复x20,函数B自身的epilogue无法保证所有callee-saved寄存器全被修复。
       * 所有callee-used寄存器不必恢复,因为函数调用中这些寄存器被破坏是正常的,任何调用子函数的代码在子函数返回后都不应该期待这些寄存器还拥有原始值(但恢复应该也是没有错的).
       * caller执行landing_pad时的状态和其调用callee时的状态一致
    
       libgcc在_Unwind_RaiseException返回时是利用编译器为其生成的一系列pop指令恢复硬件寄存器的,这与libunwind实现不同, 后者是通过汇编代码主动将所有context中的寄存器pop到硬件寄存器中的.在libgcc中:
       * 由于 _Unwind_RaiseException => uw_init_context => __builtin_unwind_init 会导致所有callee-saved寄存器入栈, 故修改callee-saved寄存器实际上只需要修改当前函数栈上这些内存中的值即可, 当前函数_Unwind_RaiseException返回时会自动将这些内存值同步到硬件寄存器中.
       * 同时由于_Unwind_RaiseException => uw_install_context =>  __builtin_eh_return 会导致x0-x3寄存器入栈, 故修改当前函数栈上的x0-x3内存值同样可以在函数返回时将其同步到硬件寄存器中.
    */
    //  uw_install_context (&this_context, &cur_context, frames);
    
      /*
         将this_context中的寄存器值全部写入到current中的寄存器指针指向的内存单元中(current指向的内存即为epilouge中pop寄存器的内存单元),
         landing_pad属于this_context的caller函数,但caller函数的运行时环境是记录在this_context中的.
      */
      long offset = uw_install_context_1 ((this_context), (cur_context));   
      
      void *handler = uw_frob_return_addr ((this_context), (cur_context));        /* 获取返回地址,实际上就是landing_pad的地址 */
    
      _Unwind_DebugHook ((cur_context)->cfa, handler);    
    
      _Unwind_Frames_Extra (frames);                                              /* 为Intel CET修复SCS栈帧 */
      
      /* 异常返回, 此函数和正常函数返回一样在返回前都需要执行epilogue(也就是pop各个寄存器的代码), 区别在于:
         * 正常函数返回是 ret;
         * 此函数返回代码类似 sp = sp + offset; goto handler;
         故此函数最终恢复到landing_pad时的函数栈, 并跳转到landing_pad的代码执行
      */
      __builtin_eh_return (offset, handler);           
    }

    3. _Unwind_RaiseException_Phase2

    //./libgcc/unwind.inc
    /*
        此函数负责phase2的cleanup, 此函数并非完成整个phase2,而是在发现一个landing_pad(cleanup/handler)时就返回,返回前需要为此landing_pad修改context上下文. 
        整个phase2 是通过: _Unwind_RaiseException => [func:cleanup;/function:handler => _Unwind_Resume]* 循环完成的
    */
    static _Unwind_Reason_Code _Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc,
                      struct _Unwind_Context *context, unsigned long *frames_p)
    {
      _Unwind_Reason_Code code;
      unsigned long frames = 1;
    
      while (1)
      {
          _Unwind_FrameState fs;
          int match_handler;
    
          /* 通过栈回溯获取执行到context->ra时的上下文, 结果暂时保存在fs中*/
          code = uw_frame_state_for (context, &fs);
    
          /* 若当前栈帧就是phase1中找到handler的那个栈帧(context->cfa == exc->private_2), 
             则后面调用personality routine时添加flag _UA_HANDLER_FRAME,代表本次直接执行handler即可 */
          match_handler = (uw_identify_context (context) == exc->private_2 ? _UA_HANDLER_FRAME : 0);
    
          if (code != _URC_NO_REASON) return _URC_FATAL_PHASE2_ERROR;
    
          if (fs.personality)        
          {
             /* phase2 同样调用personality routine, 此时personality routine需要找到一个cleanup函数(或最后的handler),
               并将需要传递给cleanup/handler的参数写入 context->reg[0/1], context->ra中 */
             code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler, exc->exception_class, exc, context);
    
             if (code == _URC_INSTALL_CONTEXT) break;     /* 代表已经为一个landing_pad设置好了context,此函数直接返回 */
    
             if (code != _URC_CONTINUE_UNWIND) return _URC_FATAL_PHASE2_ERROR;    /* 出错则返回错误 */
          }
    
          gcc_assert (!match_handler);         /* 执行到handler所在栈帧后则栈回溯就结束了*/
    
          uw_update_context (context, &fs);    /* 当前栈帧中没有cleanup函数/handler需要执行,继续栈回溯上一帧 */
         
          _Unwind_Frames_Increment (context, frames);     /* 记录一共回溯了多少个栈帧,在Intel CET中需要根据其来更新影子栈SP */
      }
    
      *frames_p = frames;
      return code;
    }

    4. __gxx_personality_v0

    //./libstdc++-v3/libsupc++/eh_personallity.cc
    /*
       phase1/phase2均会执行此personality routine, 在两个阶段其作用不同:
       * phase1中负责确定context->ra上下文中是否有当前异常类型的handler, 如果有将其记录到ue_header全局结构体中(handler与context->ra属于同一函数),没有则返回继续回溯.
       * phase2中负责确定context->ra上下文中是否有需要执行的cleanup代码或是否已经回溯到了handler所在栈帧(的子函数), 如果是则更新当前context中 reg[0/1]/ra寄存器并返回, 没有则返回继续回溯.
    */
    extern "C" _Unwind_Reason_Code __gxx_personality_v0 (int version, _Unwind_Action actions,
                  _Unwind_Exception_Class exception_class,  struct _Unwind_Exception *ue_header, struct _Unwind_Context *context)
    {
      ......
      const unsigned char *language_specific_data;   /* 指向当前函数LSDA的指针 */
      const unsigned char *action_record;            /* action_record = 0, 代表此时找到的landing_pad是一个纯cleanup函数, 非0则可能是handler和cleanup的混合,也可能是单独的handler */
      _Unwind_Ptr landing_pad;                       /* context所在函数的landing_pad地址(若有),这是运行时获取的一个可执行代码地址 */
      int handler_switch_value;                      /* 一个整形值,来自LSDA的解析结果,此值最终会传递给landing_pad,以帮助landing_pad确定当前应该执行哪个catch分支 */
      ......
    
      /* 如果当前context是handler所在栈帧,则直接从phase1 personality routine保存的结果中获取handler相关信息,并写入context 即可 */
      if (actions == (_UA_CLEANUP_PHASE | _UA_HANDLER_FRAME) && !foreign_exception)
        {
          /* 恢复phase1 personality routine存入的handler信息 */
          restore_caught_exception(ue_header, handler_switch_value, language_specific_data, landing_pad);
          found_type = (landing_pad == 0 ? found_terminate : found_handler);
          goto install_context;        /* 将handler信息写入context */
        }
    
      /* 如果当前context不是handler所在栈帧, 则不论phase1还是phase2都需要获取当前函数的LSDA信息:
         * 在phase1中是用来确认当前函数是否有handler
         * 在phase2中是用来确认当前函数是否有cleanup函数
        这里pass了LSDA解析相关代码
      */
      language_specific_data = (const unsigned char *) _Unwind_GetLanguageSpecificData (context);
    
      if (! language_specific_data) CONTINUE_UNWINDING;    /* 当前函数没有LSDA则直接继续上一个栈帧(return _URC_CONTINUE_UNWIND) */
    
      p = parse_lsda_header (context, language_specific_data, &info);    /* 解析LSDA头 => info */
      ......
    
      /* 获取当前栈帧对应的函数的返回地址, 在fs初始化时会初始化context->lsda, context->lsda/ra记录的都是当前context caller的信息  */
      ip = _Unwind_GetIP (context);    
      landing_pad = 0;
      action_record = 0;
      handler_switch_value = 0;
      .......
      /* 查找当前函数返回地址(context->ra)所在的代码位置是否有landing_pad(cleanup/handler) */
      while (p < info.action_table)
      {
          _Unwind_Ptr cs_start, cs_len, cs_lp;
          _uleb128_t cs_action;
          p = read_encoded_value (0, info.call_site_encoding, p, &cs_start);
          .......
    
          if (cs_lp) landing_pad = info.LPStart + cs_lp;            /* 如果有landing_pad则记录其地址 */
          /*
            action = 0,代表landing_pad是context->ra所在位置需要的一个cleanup代码片段
            action !=0,代表landing_pad是context->ra所在位置的一个handler代码片段
          */
          if (cs_action)    action_record = info.action_table + cs_action - 1;
          goto found_something;
        }
      }
      .......
    
      /* 这里判断找到的是cleanup函数还是handler,如果是handler还需确定当前抛出的异常类型对应catch 的handler_switch_value */
    found_something:
      if (landing_pad == 0)  found_type = found_nothing;        /* 若context->ra所在函数未找到landing_pad则返回遍历下一个栈帧 */
      else if (action_record == 0)
          found_type = found_cleanup;                           /* action_record=0代表这是一个纯cleanup函数 */
      else                                   /* 否则是一个[cleanup +] handler(在landing_pad的最开始通常先执行cleanup,然后再执行handler的代码片段),此时需要根据LSDA信息
                                                确定本次throw的异常对应的编码(handler_switch_value),此编码会传递给handler, handler通过其来判断应该执行哪个catch case */
      {
          bool saw_handler = false;
          while (1)
          {
              p = read_sleb128 (p, &ar_filter);
              .......
    
              if (saw_handler)
              {  
                  handler_switch_value = ar_filter;            /* 确定 handler_switch_value */
                  found_type = found_handler;    
              }
              else
                  found_type = (saw_cleanup ? found_cleanup : found_nothing);
          }
      }
    
    do_something:
      if (found_type == found_nothing)  CONTINUE_UNWINDING;    /* 若最终什么也没发现,则继续unwind */
      if (actions & _UA_SEARCH_PHASE)                          /* 在phase1 search阶段, 只关注handler, 发现纯cleanup则继续unwind */
      {
          if (found_type == found_cleanup) CONTINUE_UNWINDING; /* search阶段发现cleanup则继续unwind (return _URC_CONTINUE_UNWIND) */
          ......
         
          save_caught_exception(ue_header, context, thrown_ptr,     /* 在phase1若发现了最终的handler,则保存到exc中以供后续phase2使用 */
                    handler_switch_value, language_specific_data, landing_pad, action_record);
          
          return _URC_HANDLER_FOUND;                           /* 不论如何,phase1 到此即返回了 */
      }
    
    
    install_context:            /* 若当前处于 phase2, 则发现landing_pad(cleanup/handler)时会走到install_context流程 */
    
      .......
      /* install_context主要是在context->reg[]中设置r0/r1参数分别为: 异常头_Unwind_Exception的指针和handler_switch_value
        并设置landing pad为返回地址(context->ra)并返回, 后续uw_install_context函数会将此上下文设置到硬件寄存器中并跳转到context->ra执行.
      */
      _Unwind_SetGR (context, __builtin_eh_return_data_regno (0), __builtin_extend_pointer (ue_header));
      _Unwind_SetGR (context, __builtin_eh_return_data_regno (1), handler_switch_value);
      _Unwind_SetIP (context, landing_pad);
      .......
      return _URC_INSTALL_CONTEXT;
    }

    5. uw_install_context_1

    /*
       此函数负责将landing_pad所在栈帧的上下文(target) install到当前上下文(current)上, 此install操作只是对current->reg[i]或current->reg[i]指向的内存内容的修改,其并没有修改任何硬件寄存器.
       uw_install_context_1的install只是将target中寄存器的值全部写入到_Unwind_RaiseException栈上各个寄存器对应的内存中,最终是_Unwind_RaiseException自身的epilogue将这些内存值恢复到硬件寄存器的.
       
       这里需要注意的是:
       1) target中记录的是landing_pad所在函数子函数的上下文, 因为只有callee上下文中才会记录caller调用到callee时各个寄存器的状态, caller的上下文中记录的是caller的caller执行到caller时的寄存器状态.
       2) 子函数(callee)中记录的caller上下文通常不会因为子函数执行到某条语句而改变(见附录举例),.cfi指令理论上恢复的是某寄存器上一个保存在栈中的值,但通常编译器的用法是用其来恢复当前函数caller在调用callee时的寄存器状态. 也就是通常来说在一个函数不同位置其CFA或寄存器值只是获取方法不同, 而真正获取到的都应该是caller调用callee时的CFA和register.
    */
    static long uw_install_context_1 (struct _Unwind_Context *current, struct _Unwind_Context *target)
    {
      long i;
      _Unwind_SpTmp sp_slot;
    
      /* 
         遍历所有target时的寄存器状态,将其记录到current中, 后续current返回时即可将这些寄存器值写入到硬件寄存器中
         (libgcc中通过uw_install_context_1 caller的epilogue将 current->regs 写入到硬件寄存器,
         这里只负责将target->regs中寄存器的值复制到 current->regs指向的内存中)
      */
      for (i = 0; i < __LIBGCC_DWARF_FRAME_REGISTERS__; ++i)
      {
    
          /* c为最后一级栈帧中某寄存器在context中的指针(如若当前uw_install_context_1的caller是_Unwind_RaiseException,
             那么*c是_Unwind_RaiseException栈帧中的一个地址).
             t为当前要执行landing_pad的函数子函数context中某寄存器的指针.
          */
          void *c = (void *) (_Unwind_Internal_Ptr) current->reg[i];
          void *t = (void *) (_Unwind_Internal_Ptr)target->reg[i];
     
          /* current->reg[x]中必须记录的是一个指针,此地址在epilogue的会用来恢复其对应硬件寄存器的值
             current->reg[x]中存的若直接是寄存器值,那么epilogue默认无法将其写入硬件寄存器.
            而target->reg[x]是指针还是值就无所谓了,最终都是将其值写入到current对应的内存中.
          */
          gcc_assert (current->by_value[i] == 0);
    
          if (target->by_value[i] && c)
          {
             ......
             memcpy (c, &t, sizeof (_Unwind_Word));         /* 若target->reg[x]中记录的是寄存器的值,那么这个值直接复制到 current->reg[x]指向的内存中即可 */
          }
          else if (t && c && t != c)
             memcpy (c, t, dwarf_reg_size_table[i]);        /* 若target->reg[x]也是一个指针,则将其指向内存中的值复制到current->reg[x]指向的内存中即可 */
      }
      ......
      return target->cfa- current->cfa + target->args_size; /* 此函数返回的是target与current两个CFA的差值,后续用来修正sp */
    }

    6. _Unwind_Resume

    //./libgcc/unwind.inc
    /*
       _Unwind_Resume的作用是基于当前栈帧继续做栈回溯直到找到下一个landing_pad后跳转执行.
       需要注意的是, 如前面_Unwind_RaiseException 最终跳转到函数 A的landing_pad, 函数A需要resume则会再次调用_Unwind_Resume,
       此时_Unwind_Resume的caller是函数A, 在此过程中函数A在栈回溯中被遍历了两次:
      * 第一次来自_Unwind_RaiseException,其发现函数A中有cleanup/handler需要执行
      * 第二次来自_Unwind_Resume,其发现函数A中没有需要执行的landing_pad
      两次遍历结果不同是因为二者栈回溯时的context->ra 不同:
      * _Unwind_RaiseException回溯的通常是try {...} 中的地址, 在LSDA中会找到其cleanup/handler函数
      * _Unwind_Resume 回溯的是非try {...}中的地址, 故其不存在cleanup/handler
    */
    void LIBGCC2_UNWIND_ATTRIBUTE _Unwind_Resume (struct _Unwind_Exception *exc)
    {
      struct _Unwind_Context this_context, cur_context;
      _Unwind_Reason_Code code;
      unsigned long frames;
    
      /* 将当前callee-saved/x0-x3硬件寄存器的值初始化到 this_context上下文中 */
      uw_init_context (&this_context);
        
      cur_context = this_context;            /* 若执行uw_install_context,这里需要一个上下文备份 */
    
      if (exc->private_1 == 0)               /* 此值在_Unwind_RaiseException中设置为0 */
        code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);        /* 继续栈回溯查找landing_pad */
      else
        code = _Unwind_ForcedUnwind_Phase2 (exc, &cur_context, &frames);
    
      gcc_assert (code == _URC_INSTALL_CONTEXT);
    
      uw_install_context (&this_context, &cur_context, frames);        /* 恢复一个landing_pad所在函数的上下文, 并跳转到landing_pad执行 */
    }

    四、异常处理的安全性分析

       注: 这里的内容引自[9],笔者未亲自试验,若有错误之处还请指正.

       GCC编译的所有支持异常处理的二进制中都存在.eh_frame段, .eh_frame非调试段,是无法strip掉的. 且包含异常处理就意味着运行时一定存在DWARF的代码解释器(也就是如_Unwind_RaiseException等函数),  DWARF的bytecode实际上是另类的一种指令集,理论上是可以完成图灵完备的计算的, 同样也可以基于对这些bytecode的修改完成如对syscall、库函数的调用或ROP。 DWARF本质上应该是DOA(Date Oriented Attack) 攻击的一种。利用DWARF可以在不修改二进制可执行段和数据段的情况下完成木马程序的植入, 在[9]发表时(2011)尚无已知有效的检测方式(理论上如果只是木马注入应该是可以做检测的,但这会很麻烦).

       DWARF自身的语义可以:

    • 读取任意内存
    • 利用内存和和寄存器的值执行任意计算
    • 控制部分寄存器并影响程序的控制流

       DWARF自身的限制是没法直接写寄存器和内存(注意这里说的是没法直接写,但配合已有代码理论上总是可以完成写操作的),在GCC4.5.2中只支持64byte的栈. 作者[9]同时也提供可一个修改ELF中DWARF的工具katana,配合dwarfscript脚本可以方便的修改ELF中的DWARF内容.

       作者同时举例了DWARF的以下原语:

      1) 修改CFA:

    DW_CFA_offset r16 1                   //CFA = r16 + 1
    改为
    DW_CFA_offset r16 6                  //CFA = r16 + 6 

         通过修改DWARF可以很容易修改CFA的计算方式, 在知道某个栈帧大小的情况下可以通过改变一个偏移来绕过某一级别的异常处理, 即修改CFA可导致异常处理执行不同的landing_pad(但不能做到任意地址跳转)

         * 修改CFA会直接导致context->ra(也就是返回地址)的修改(ra通常是根据CFA计算的,见下)返回地址是用来决定在哪个函数中查找landing_pad用的, 故修改CFA可导致将控制流重定位到其他landing_pad

    //通常ra是通过*(CFA - x) 来获取的
    00000100 0000000000000024 00000064 FDE cie=000000a0 pc=00000000004007c0..00000000004008a4
       LOC           CFA      x19   x20   x29   ra    
    00000000004007c0 sp+0     u     u     u     u          
    00000000004007c4 sp+48    u     u     c-48  c-40  
    00000000004007cc sp+48    c-32  c-24  c-48  c-40  
    00000000004008a0 sp+0     u     u     u     u  

         * 但context->ra的修改并不会影响最终的函数返回, 在异常处理中其作用只是用来确定landing_pad, 而landing_pad返回到父函数的地址来自程序栈,这个值是DWARF修改不了的.

      2) 修改寄存器:

         通过修改DWARF可以轻易修改寄存器的值,如:

    DW_CFA_val_expression r16      //r16 = 0x600DF00D     //r16=0x600DF00D
    begin EXPRESSION
    DW_OP_constu 0x600DF00D
    end EXPRESSION

         此修改会直接被带入到landing_pad, 可惜大多数情况下landing_pad应该不会直接使用寄存器的原始值(除了参数x0-x3), 但如果landing_pad被修改为任意地址时(见后)此原语就很有用了(注: DWARF虽然不能修改任意内存,但应该还是可以做到栈溢出的任意1字节写的, 如作者提到的DW_CFA_offset_extened原语配合DW_CFA_val_expression原语等).

         修改DWARF虽然可以控制流,但通常只能将控制流重定向到某一个catch块中(或cleanup代码), 这个catch块甚至可以任何函数中的一个catch块,但其缺点是控制流无法走出catch块, 因为DWARF指令始终还是走正常的异常处理流程的,正常异常处理总是要跳转到某个landing_pad。

      为了走出catch块,作者还提出了修改LSDA数据, LSDA数据记录在.gcc_except_table中,如图:

        修改LSDA则代表可以修改landing_pad为任意地址,这就解决了DWARF无法走出catch块的问题. 一个backdoor的运行逻辑如下:

        1) 程序正常执行直到有异常抛出(throw)

        2) 修改LSDA和.eh_frame后可以让异常跳转到任意地址执行(同时也可以控制全部callee-saved寄存器,x0-x3,其他寄存器则取决于当前函数保存了什么)

        此过程中不需要修改程序中原有的任何代码段与数据段。

        在此基础上,由于运行时.eh_frame段的指针是可以修改的, 故运行时也也可以实现动态的插入DWARF指令(旧版本gcc中.eh_frame和.gcc_except_table是可写的)来实现任意代码执行


    参考资料:

    [1] C++ ABI for Itanium: Exception Handling

    [2] AARCH64平台的栈回溯_ashimida-CSDN博客

    [3] C++ exception handling ABI | MaskRay

    [4] Unwinding a Bug - How C++ Exceptions Work - shorne in japan

    [5] c++ 异常处理(1) - twoon - 博客园

    [6] c++ 异常处理(2) - twoon - 博客园

    [7] C++异常机制的实现方式和开销分析

    [8] https://monkeywritescode.blogspot.com/p/c-exceptions-under-hood.html

    [9] https://cs.dartmouth.edu/~sergey/battleaxe/hackito_2011_oakley_bratus.pdf

    [10] Katana

    展开全文
  • 举例说明自定义C++异常处理的实例 例1:自定义一个继承自excepton的异常类myException C++标准中,定义在中的任何异常类都派生自exception Class,本例也只是简单地由exception继承,在try段抛出一个异常并捕捉。...
  •  所谓异常处理,即让一个程序运行时遇到自己无法处理的错误时抛出一个异常,希望调用者可以发现处理问题.  异常处理的基本思想是简化程序的错误代码,为程序键壮性提供一个标准检测机制.  也许我们已经使用过异常...
  • 这两天我写了一个测试c++异常处理机制的例子,感觉有很好的示范作用,在此贴出来,给c++异常处理的初学者入门。本文后附有c++异常的知识普及,有兴趣者也可以看看。下面的代码直接贴到你的console工程中,可以运行...
  • C++ 异常处理 入门 异常:程序执行期间,可检测到的不正常情况。 例如:0作除数;数组下标越界;打开不存在的文件;远程机器连接超时;malloc失败等等。 程序的两种状态: 正常状态和异常状态,发生不正常情况后...
  • 异常处理是个十分深奥的主题,这里只是浅论其对C++性能的影响。  在VC++中,有多个异常处理模式,三个重要:  No exception handling (无异常处理)  C++ only (C++语言异常处理)  C++ 加SEH (C++语言加...
  • C++异常处理

    千次阅读 2020-05-24 18:38:13
    A A 异常类由标准库提供,不可以自定义 B C++异常处理机制具有为抛出异常前构造的所有局部对象自动调用析构函数的能力 C 若catch块采用异常类对象接收异常信息,则在抛出异常时将通过拷贝构造函数进行对象复制,...

    1-1 If you are not interested in the contents of an exception object, the catch block parameter may be omitted. T

    1-2catch (type p) acts very much like a parameter in a function. Once the exception is caught, you can access the thrown value from this parameter in the body of a catch block. T

    2-1One of the major features in C++ is ( ) handling,which is a better way of handling errors. D
    A data
    B pointer
    C test
    D exception

    2-2What is wrong in the following code? C

    vector<int> v;
      v[0] = 2.5;   
    

    A The program has a compile error because there are no elements in the vector.
    B The program has a compile error because you cannot assign a double value to v[0].
    C The program has a runtime error because there are no elements in the vector.
    D The program has a runtime error because you cannot assign a double value to v[0].

    2-3If you enter 1 0, what is the output of the following code? D

    #include "iostream"
    using namespace std;
    
    int main()
    
    {
      // Read two integers
      cout << "Enter two integers: ";
      int number1, number2;
      cin >> number1 >> number2;
      try
      {
        if (number2 == 0)
          throw number1;
        cout << number1 << " / " << number2 << " is "
          << (number1 / number2) << endl;
    
        cout << "C" << endl;
      }
      catch (int e)
      {
        cout << "A" ;
      }
    
      cout << "B" << endl;
    
      return 0;
    }
    

    A A
    B B
    C C
    D AB

    2-4The function what() is defined in __. A
    A exception
    B runtime_error
    C overflow_error
    D bad_exception

    2-5下列关于异常的描述中,错误的是()。 A
    A 编译错属于异常,可以抛出
    B 运行错属于异常
    C 硬件故障也可当异常抛出
    D 只要是编程者认为是异常的都可当异常抛出

    2-6下列关于异常类的说法中,错误的是。 A
    A 异常类由标准库提供,不可以自定义
    B C++的异常处理机制具有为抛出异常前构造的所有局部对象自动调用析构函数的能力
    C 若catch块采用异常类对象接收异常信息,则在抛出异常时将通过拷贝构造函数进行对象复制,异常处理完后才将两个异常对象进行析构,释放资源
    D 异常类对象抛出后,catch块会用类对象引用接收它以便执行相应的处理动作

    7-1 求平方根函数mySqrt的异常处理 (10分)
    改造下面的求平方根函数mySqrt,当x小于0时,输出错误信息:“Error: Can not take sqrt of negative number”;当x不小于0时,输出x的平方根。要求在main函数中采用C++的异常处理方法。

    double mySqrt(double x)
    { return sqrt(x); }
    

    输入格式:
    4

    输出格式:

    The sqrt of 4 is 2
    

    输入样例:
    -9
    输出样例:

    Error: Can not take sqrt of negative number
    
    #include<iostream>
    #include<cmath>
    double mySqrt(double x)
    { 
        if (x>=0)
            return sqrt(x); 
        else 
            return -1;
    }
    int main()
    {
        double x,y;
        std::cin>>x;
        y=mySqrt(x);
        if (-1==y)
            std::cout<<"Error: Can not take sqrt of negative number";
        else
            std::cout<<"The sqrt of "<<x<<" is "<<y;
        return 0;
    }
    
    展开全文
  • 标准C++异常处理的基本语法和语义 4. 实例剖析 5. C++的new 和delete 操作时的异常处理 6. Microsoft 对于的实现版本中的异常处理 7. 部分构造及placement delete 8. 自动删除,类属new 和delete、placement ...
  • C++异常处理底层机制详解

    千次阅读 2018-07-11 10:18:04
    C++异常机制的实现方式和开销分析白杨http://baiy.cn 在我几年前开始写《C++编码规范与指导》一文时,就已经规划着要加入这样一篇讨论 C++ 异常机制的文章了。没想到时隔几年以后才有机会把这个尾巴补完 :-)。还是...

    说明:本文转载。个人感觉写的非常好。

    C++异常机制的实现方式和开销分析

    白杨

    http://baiy.cn

     

    在我几年前开始写《C++编码规范与指导》一文时,就已经规划着要加入这样一篇讨论 C++ 异常机制的文章了。没想到时隔几年以后才有机会把这个尾巴补完 :-)。

    还是那句开场白:“在恰当的场合使用恰当的特性” 对每个称职的 C++ 程序员来说都是一个基本标准。想要做到这点,就必须要了解语言中每个特性的实现方式及其时空开销。异常处理由于涉及大量底层内容,向来是 C++ 各种高级机制中较难理解和透彻掌握的部分。本文将在尽量少引入底层细节的前提下,讨论 C++ 中这一崭新特性,并分析其实现开销:

     

    关于线程

    进程和线程的概念相信各位看官早已耳熟能详。在这里,我只想带大家回忆几点重要概念:
    1. 一个进程中可以同时包含多个线程。
       
    2. 我们通常认为线程是操作系统可识别的最小并发执行和调度单位(不要跟俺说还有 Green Thread 或者 Fiber,OS Kernel 不认识也不参与这些物件的调度)。
       
    3. 同一进程中的多个线程共享代码段(代码和常量)、数据段(静态和全局变量)和扩展段(堆存储),但是每个线程有自己的栈段。栈段又叫运行时栈,用来存放所有局部变量和临时变量(参数、返回值、临时构造的变量等)。这一条对下文中的某些概念来说是非常重要的 。但是请注意,这里提到的各个“段”都是逻辑上的说法,在物理上某些硬件架构或者操作系统可能不使用段式存储。不过没关系,编译器会保证这些逻辑概念和假设的前提条件对每个 C/C++ 程序员来说始终是成立的。
       
    4. 由于共享了除栈以外的所有内存地址段,线程不可以有自己的“静态”或“全局”变量,为了弥补这一缺憾,操作系统通常会提供一种称为 TLS(Thread Local Storage,即:“线程本地存储”)的机制。通过该机制可以实现类似的功能。TLS 通常是线程控制块(TCB)中的某个指针所指向的一个指针数组,数组中的每个元素称为一个槽(Slot),每个槽中的指针由使用者定义,可以指向任意位置(但通常是指向堆存储中的某个偏移)。

     

    函数的调用和返回

    接着我们来回顾下一个预备知识:编译器如何实现函数的调用和返回。一般来说,编译器会为当前调用栈里的每个函数建立一个栈框架(Stack Frame)。“栈框架”担负着以下重要任务:
    1. 传递参数:通常,函数的调用参数总是在这个函数栈框架的最顶端。
    2. 传递返回地址:告诉被调用者的 return 语句应该 return 到哪里去,通常指向该函数调用的下一条语句(代码段中的偏移)。
    3. 存放调用者的当前栈指针:便于清理被调用者的所有局部变量、并恢复调用者的现场。
    4. 存放当前函数内的所有局部变量:记得吗?刚才说过所有局部和临时变量都是存储在栈上的。

    最后再复习一点:栈是一种“后进先出”(LIFO)的数据结构,不过实际上大部分栈的实现都支持随机访问。

    下面我们来看个具体例子:

    假设有 FuncA、FuncB 和 FuncC 三个函数,每个函数均接收两个整形值作为其参数。在某线程上的某一时间段内,FuncA 调用了 FuncB,而 FuncB 又调用了 FuncC。则,它们的栈框架看起来应该像这样:


    图1 函数调用栈框架示例

    正如上图所示的那样,随着函数被逐级调用,编译器会为每一个函数建立自己的栈框架,栈空间逐渐消耗。随着函数的逐级返回,该函数的栈框架也将被逐级销毁,栈空间得以逐步释放。顺便说一句,递归函数的嵌套调用深度通常也是取决于运行时栈空间的剩余尺寸。

    这里顺便解释另一个术语:调用约定(calling convention)。调用约定通常指:调用者将参数压入栈中(或放入寄存器中)的顺序,以及返回时由谁(调用者还是被调用者)来清理这些参数等细节规程方面的约定。

    最后再说一句,这里所展示的函数调用乃是最“经典”的方式。实际情况是:在开启了优化选项后,编译器可能不会为一个内联甚至非内联的函数生成栈框架,编译器可能使用很多优化技术消除这个构造。不过对于一个 C/C++ 程序员来说,达到这样的理解程度通常就足够了。


     

    C++ 函数的调用和返回

    首先澄清一点,这里说的 “C++ 函数”是指:
    1. 该函数可能会直接或间接地抛出一个异常:即该函数的定义存放在一个 C++ 编译(而不是传统 C)单元内,并且该函数没有使用“throw()”异常过滤器。
    2. 或者该函数的定义内使用了 try 块。

    以上两者满足其一即可。为了能够成功地捕获异常和正确地完成栈回退(stack unwind),编译器必须要引入一些额外的数据结构和相应的处理机制。我们首先来看看引入了异常处理机制的栈框架大概是什么样子:


    图2 C++函数调用栈框架示例

    由图2可见,在每个 C++ 函数的栈框架中都多了一些东西。仔细观察的话,你会发现,多出来的东西正好是一个 EXP 类型的结构体。进一步分析就会发现,这是一个典型的单向链表式结构:

    • piPrev 成员指向链表的上一个节点,它主要用于在函数调用栈中逐级向上寻找匹配的 catch 块,并完成栈回退工作。

    • piHandler 成员指向完成异常捕获和栈回退所必须的数据结构(主要是两张记载着关键数据的表:“try”块表:tblTryBlocks 及“栈回退表”:tblUnwind)。

    • nStep 成员用来定位 try 块,以及在栈回退表中寻找正确的入口。

    需要说明的是:编译器会为每一个“C++ 函数”定义一个 EHDL 结构,不过只会为包含了“try”块的函数定义 tblTryBlocks 成员。此外,异常处理器还会为每个线程维护一个指向当前异常处理框架的指针。该指针指向异常处理器链表的链尾,通常存放在某个 TLS 槽或能起到类似作用的地方。

    最后,请再看一遍图2,并至少对其中的数据结构留下一个大体印象。我们会在后面多个小节中详细讨论它们。

    注意:为了简化起见,本文中描述的数据结构内,大多省略了一些与话题无关的成员。

     

    栈回退(Stack Unwind)机制

    “栈回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。

    受益于栈回退机制的引入,以及 C++ 类所支持的“资源申请即初始化”语意,使得我们终于能够彻底告别既不优雅也不安全的 setjmp/longjmp 调用,简便又安全地实现远程跳转了。我想这也是 C++ 异常处理机制在错误处理以外唯一一种合理的应用方式了。

    下面我们就来具体看看编译器是如何实现栈回退机制的:


    图3 C++ 栈回退机制

    图3中的“FuncUnWind”函数内,所有真实代码均以黑色和蓝色字体标示,编译器生成的代码则由灰色和橙色字体标明。此时,在图2里给出的 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()”方法判断当前是否正在进行栈回退(即:存在一个未捕获或未完全处理完毕的异常)。如是,则应抑制异常的再次抛出。

     

    异常捕获机制

    一个异常被抛出时,就会立即引发 C++ 的异常捕获机制:


    图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++ 异常处理器而言,未捕获的异常是一个严重错误,将导致当前进程被强制结束。

    注意:虽然在图4示例中的 tblTryBlocks[] 只有一个条目,这个条目中的 tblCatchBlocks[] 也只有一行。但是在实际情况中,这两个表中都允许有多条记录。意即:一个函数中可以有多个 try 块,每个 try 块后均可跟随多个与之配套的 catch 块。

    注意:按照标准意义上的理解,异常时的栈回退是伴随着异常捕获过程沿着异常处理链逐层向上进行的。但是有些编译器是在先完成异常捕获后再一次性进行栈回退的。无论具体实现使用了哪种方式,除非正在开发一个内存严格受限的嵌入式应用,通常我们按照标准语意来理解都不会产生什么问题。

    备注:实际上 tblCatchBlocks 中还有一些较为关键但被故意省略的字段。比如指明该 catch 块异常对象复制方式(传值(拷贝构造)或传址(引用或指针))的字段,以及在何处存放被复制的异常对象(相对于入口地址的偏移位置)等信息。

     
     

    异常的抛出

    接下来讨论整个 C++ 异常处理机制中的最后一个环节,异常的抛出:


    图5 C++ 异常抛出

    在编译一段 C++ 代码时,编译器会将所有 throw 语句替换为其 C++ 运行时库中的某一指定函数,这里我们叫它 __CxxRTThrowExp(与本文提到的所有其它数据结构和属性名一样,在实际应用中它可以是任意名称)。该函数接收一个编译器认可的内部结构(我们叫它 EXCEPTION 结构)。这个结构中包含了待抛出异常对象的起始地址、用于销毁它的析构函数,以及它的 type_info 信息。对于没有启用 RTTI 机制(编译器禁用了 RTTI 机制或没有在类层次结构中使用虚表)的异常类层次结构,可能还要包含其所有基类的 type_info 信息,以便与相应的 catch 块进行匹配。

    在图5中的深灰色框图内,我们使用 C++ 伪代码展示了函数 FuncA 中的 “throw myExp(1);” 语句将被编译器最终翻译成的样子。实际上在多数情况下,__CxxRTThrowExp 函数即我们前面曾多次提到的“异常处理器”,异常捕获和栈回退等各项重要工作都由它来完成。

    __CxxRTThrowExp 首先接收(并保存)EXCEPTION 对象;然后从 TLS:Current ExpHdl 处找到与当前函数对应的 piHandler、nStep 等异常处理相关数据;并按照前文所述的机制完成异常捕获和栈回退。由此完成了包括“抛出”->“捕获”->“回退”等步骤的整套异常处理机制。

     

    Windows 中的结构化异常处理

    Microsoft Windows 带有一种名为“结构化异常处理”的机制,非常著名的“内存访问违例”出错对话框就是该机制的一种体现。Windows 结构化异常处理与前文讨论的 C++ 异常处理机制有惊人的相似之处,同样使用类似的链式结构实现。对于 Windows 下的应用程序,只需使用 SetUnhandledExceptionFilter API 注册异常处理器;用 FS:[0] 替代前文所述的 TLS: Current ExpHdl 等很少的改动,即可将此两种错误处理机制合而为一。这样做的优势十分明显:
    • 由于可直接借助操作系统提供的机制,所以简化了 C++ 异常处理器的实现。
    • 使“catch (...)” 块得以捕获操作系统产生的异常(如:“内存访问违例”等等)。
    • 使操作系统的异常处理机制能够捕获所有 C++ 异常。

    实际上,大多数 Windows 下的 C++ 编译器的异常机制均使用这种方式实现。

     
     

    异常处理机制的开销分析

    至此,我们已完整地阐述了整套 C++ 异常处理机制的实现原理。我在本文的开头曾提到,作为一名 C++ 程序员,了解其某一特性的实现原理主要是为了避免错误地使用该特性。要达到这个目的,还要在了解实现原理的基础上进行一些额外的开销分析工作:
     
    特性时间开销空间开销
    EHDL无运行时开销每“C++函数”一个 EHDL 对象,其中的 tblTryBlocks[] 成员仅在函数中包含至少一个 try 块时使用。典型情况下小于 64 字节。

     

    C++栈框架极高的 O(1) 效率,每次调用时进行3次额外的整形赋值和一次 TLS 访问。每 调用两个指针和一个整形开销。典型情况下小于 16 字节。

     

    step 跟踪极高的 O(1) 效率每次进出 try 块或对象构造/析构一次整形立即数赋值。无(已记入 C++ 栈框架中的相应项目)。

     

    异常的抛出、捕获和栈回退异常的抛出是一次 O(1) 级操作。在单个函数中进行捕获和栈回退也均为 O(1) 操作。

    但异常捕获的总体成本为 O(m),其中 m 等于当前函数调用栈中,从抛出异常的位置到达匹配 catch 块之间所经过的函数调用中,包含 try 块(即:定义了有效 tblTryBlocks[])的函数个数。

    栈回退的总成本为 O(n),其中 n 等于当前函数调用栈中,从抛出异常的位置到达匹配 catch 块之间所经过的函数调用数。

    在异常处理结束前,需保存异常对象及其析构函数指针和相应的 type_info 信息。

    具体根据对象尺寸、编译器选项(是否开启 RTTI)及异常捕获器的参数传递方式(传值或传址)等因素有较大变化。典型情况下小于 256 字节。

     

    可以看出,在没有抛出异常时,C++ 的异常处理机制是十分有效的。在有异常被抛出后,可能会依当前函数调用栈的情形进行若干次整形比较(try块表匹配)操作,但这通常不会超过几十次。对于大多数 15 年前的 CPU 来说,整形比较也只需 1 时钟周期,所以异常捕获的效率还是很高的。栈回退的效率则与 return 语句基本相当。

    考虑到即使是传统的函数调用、错误处理和逐级返回机制也不是没有代价的。这些开销在绝大多数情形下仍可以接受。空间开销方面,每“C++ 函数”一个 EHDL 结构体的引入在某些极端情形下会明显增加目标文件尺寸和内存开销。但是典型情况下,它们的影响并不大,但也没有小到可以完全忽略的程度。如果正在为一个资源严格受限的环境开发应用程序,你可能需要考虑关闭异常处理和 RTTI 机制以节约存储空间。

    以上讨论的是一种典型的异常机制的实现方式,各具体编译器厂商可能有自己的优化和改进方案,但总体的出入不会很大。

     

    小节

    异常处理是 C++ 中十分有用的崭新特性之一。在绝大多数情况下,它们都有着优异的表现和令人满意的时空效率。异常处理本质上是另一种返回机制。但无论从软件工程、模块设计、编码习惯还是时空效率等角度来说,除了在有充分文档说明的前提下,偶尔可用来替代替代传统的 setjmp/longjmp 功能外,应保证只将其用于程序的错误处理机制中。

    此外,由于长跳转的使用既易于出错,又难于理解和维护。在编码过程中也应当尽量避免使用。关于异常的一般性使用说明,请参考:代码风格与版式:异常

     转载地址:http://www.baiy.cn/doc/cpp/inside_exception.htm


    展开全文
  • C++异常处理.ppt

    2014-11-17 10:01:36
    C++异常处理.ppt ,相信对学习c++的你来说一定有帮助
  • 主要介绍了C++异常处理的基本思想及throw类抛出异常的使用,也深入谈到了异常被抛出后的栈解旋unwinding过程,需要的朋友可以参考下
  • C++异常处理机制

    千次阅读 多人点赞 2018-04-05 17:38:49
      函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈。2.异常设计目的:  栈机制是一种...
  • C和C++中的异常处理

    2018-06-09 14:02:47
    3. 标准 C++异常处理的基本语法和语义 4. 实例剖析 EH 5. C++的 new 和 delete 操作时的异常处理 6. Microsoft 对于的实现版本中的异常处理 7. 部分构造及 placement delete 8. 自动删除,类属 new 和 delete、...
  • C++ 异常处理(try catch)

    千次阅读 2019-03-09 15:14:37
    C++ 异常处理机制会涉及 try、catch、throw 三个关键字。 程序错误 程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误: 1) 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码...
  • C++异常处理与输入输出

    千次阅读 2018-10-21 22:19:24
    C++异常处理与输入输出 文章目录C++异常处理与输入输出1. C++异常处理1.1 使用简单的错误分支处理abort()和exit()1.2 使用try-throw-catch处理机制1.3使用exception类,头文件exception2. C++输入与输出2.1使用...
  • C++异常处理总结

    2014-05-12 18:20:15
    主要针对c++异常处理过程中的各种机制的总结
  • C++ 异常处理 C++ 异常处理C++ 异常处理C++ 异常处理C++ 异常处理C++ 异常处理C++ 异常处理C++ 异常处理
  • C++异常处理的编程方法
  • C++异常处理的开销

    千次阅读 2018-08-06 23:07:54
    C++异常是C++有别于C的一大特性 ,异常处理机制给开发人员处理程序中可能...C++异常处理使用try、throw和catch三个关键词来完成,在程序执行过程中,异常处理流程大致如下:当函数体内某处发生异常(trow 异常)时,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 205,821
精华内容 82,328
关键字:

c++异常处理

c++ 订阅
友情链接: Classes.zip