C 語法上使用 goto 或是 setjmp/longjmp 實現例外處理。goto 只能做本地跳轉,亦即在同一函式內部進行跳轉。setjmp/longjmp 則能做到非本地跳轉。setjmp 會保留當前進程的狀態,以供之後的 longjmp 回到 setjmp 所在位置。setjmp/longjmp 並非以 stack unwind 實現1)

    • Deep Wizardry: Stack UnwindingAll setjmp is doing is saving a bunch of registers including rsp (the stack pointer) and the return address into the jmp_buf array that was passed as a parameter.
    • setjmp 主要保存被調用方保存 (callee-save) 暫存器、棧指針和返回位址於 jmp_buf。jmp_buf 可以是 unsigned char 陣列,大小足夠保存前述資訊即可。
    • longjmp 從傳入的 jmp_buf 中回復暫存器和棧指針,載入 setjmp 設置的返回位址。

C++ 語法上使用 try/catch/throw 實現例外處理,C++ 內部主要使用 table-driven 的方式實現。簡單來講,編譯和鏈結時期會建立一張表,一段範圍的計數器值會對應到例外處理語句。當執行時期發生例外時,運行期函式庫會表跳轉至對應的例外處理語句。

術語 stack unwinding,又稱棧回溯 (棧回退),此為一搜尋例外處理代碼的過程。當例外被丟出時,棧會以 callee 到 caller 的方向消退,以搜尋例外處理代碼處理該例外。棧回溯的過程中,當脫離某一個棧的範圍時,針對配置在該棧上的物件,運行期函式庫會呼叫其解構子。棧回溯的效果就如同除了例外處理代碼 (即 catch) 所在的函式以外,往下曾經被調用過的函式沒有作用,彷彿未被調用。

    • The objects allocated on the stack are "unwound" when the scope is exited (here the scope is of the function func.) This is done by the compiler inserting calls to destructors of automatic (stack) variables.
    • DWARF2 contains .debug_frame information for helping debuggers figure out how to unwind frames (i.e. how to restore the stack to the previous frame from any instruction executing using the current frame.

系列文章

代碼

  • libgcc: unwind-dw2.c
    • 關於 CIE 和 FDE,參考 debugger
    • uw_frame_state_for: 在 _Unwind_RaiseException 被調用。將當前 context 的 caller 更新至 fs (frame state)。
      /* Given the _Unwind_Context CONTEXT for a stack frame, look up the FDE for
         its caller and decode it into FS.  This function also sets the
         args_size and lsda members of CONTEXT, as they are really information
         about the caller's frame.  */
       
      static _Unwind_Reason_Code
      uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs)
      {
        // 尋找 caller 的 fde。
        fde = _Unwind_Find_FDE (context->ra + _Unwind_IsSignalFrame (context) - 1,
      			  &context->bases);
       
        // 找到 fde 基於的 cie,並執行 cie 中的指令。
        cie = get_cie (fde);
        insn = extract_cie_info (cie, context, fs);
       
        /* First decode all the insns in the CIE.  */
        end = (const unsigned char *) next_fde ((const struct dwarf_fde *) cie);
        execute_cfa_program (insn, end, context, fs);
       
       
        // 執行 fde 中指令,更新 fs。
        execute_cfa_program (insn, end, context, fs);
      }
    • uw_update_context: 在 _Unwind_RaiseException 被調用。將 fs (frame state) 所表示 context 的 caller,更新至 context。
      /* CONTEXT describes the unwind state for a frame, and FS describes the FDE
         of its caller.  Update CONTEXT to refer to the caller as well.  Note
         that the args_size and lsda members are not updated here, but later in
         uw_frame_state_for.  */
       
      static void
      uw_update_context (struct _Unwind_Context *context, _Unwind_FrameState *fs)
      {
        // 主要由 uw_update_context_1 更新 context。
        uw_update_context_1 (context, fs);
       
        if (fs->regs.reg[DWARF_REG_TO_UNWIND_COLUMN (fs->retaddr_column)].how
            == REG_UNDEFINED)
          /* uw_frame_state_for uses context->ra == 0 check to find outermost
             stack frame.  */
          context->ra = 0;
        else
          {  
            // 將 context 的 caller 的 return address 更新至 context->ra。
            // 至此,context 完全更新到原本 context 的 caller 的內容。
            context->ra = __builtin_extract_return_addr
      	(_Unwind_GetPtr (context, fs->retaddr_column));
          }
      }
      • image.slidesharecdn.com_hes2011-joakley-sbratus-exploiting-the-hard-working-dwarf-110415112206-phpapp01_95_hes2011-james-oakley-and-sergey-bratusexploitingthehardworkingdwarf-50-728.jpg
      • C++ 的 throw 會被轉成 __cxa_allocate_exception__cxa_throw
        • libstdc++: eh_alloc.cceh_throw.cc
        • libgcc: unwind.inc
        • __cxa_throw_Unwind_RaiseException
          extern "C" void
          __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo,
          			 void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
          {
            _Unwind_RaiseException (&header->exc.unwindHeader);
           
            // Some sort of unwinding error.  Note that terminate is a handler.
            __cxa_begin_catch (&header->exc.unwindHeader);
            std::terminate ();
          }
      • C++ 的 catch 會被轉成 __cxa_begin_catch__cxa_end_catch__cxa_begin_catch__cxa_end_catch 又被稱作 landing pad。
      • 調用函式之後,有底下幾種情況:
        • 正常返回。
        • 丟出例外,有 catch 可以處理該例外,跳到 __cxa_begin_catch__cxa_end_catch。處理例外之後,正常返回。
        • 丟出例外,沒有 catch 可以處理該例外,跳到 _Unwind_Resume
      • 有 try-catch 的函式,其開頭會有底下匯編指示符:
        • .cfi_personality: 定義 personality function,一般為 __gxx_personality_v0
        • .cfi_lsda: 定義 LSDA (Language Specific Data Area)。 __gxx_personality_v0 透過 LSDA 知道其所屬函式是否可以處理當前的例外。
      • 有 try-catch 的函式,在緊鄰其結尾處,會有一個 .gcc_except_table 段。_Unwind_RaiseException 調用 __gxx_personality_v0 檢查 .gcc_except_table 段中的 LSDA 得知該函式是否可以處理當前的例外。
      • libstdc++: eh_personality.cc 定義 __gxx_personality_v0
        _Unwind_Reason_Code __gxx_personality_v0 (
                             int version, _Unwind_Action actions, uint64_t exceptionClass,
                             _Unwind_Exception* unwind_exception, _Unwind_Context* context)
        • version/exceptionClass: 語言/ABI/編譯器相關版本。
        • actions: 當前 personality 是執行第一查找 (lookup) 或是第二輪清理 (cleanup)。
        • unwind_exception: 當前處理的例外。
        • context: 當前棧相關資訊,如: LSDA。
      • _Unwind_RaiseException 調用 personality function,檢查當前棧是否可以處理目前的例外。personality function 接受 _UA_SEARCH_PHASE 表示此為第一輪查找,返回 _URC_HANDLER_FOUND 表示當前棧可以處理目前的例外,返回 _URC_CONTINUE_UNWIND 表示繼續棧回朔。
        _Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE
        _Unwind_RaiseException(struct _Unwind_Exception *exc)
        {
          /* Phase 1: Search.  Unwind the stack, calling the personality routine
             with the _UA_SEARCH_PHASE flag set.  Do not modify the stack yet.  */
          while (1)
            {
              _Unwind_FrameState fs;
         
              /* Set up fs to describe the FDE for the caller of cur_context.  The
        	 first time through the loop, that means __cxa_throw.  */
              code = uw_frame_state_for (&cur_context, &fs);
         
              if (code == _URC_END_OF_STACK)
        	/* Hit end of stack with no handler found.  */
        	return _URC_END_OF_STACK;
         
              if (code != _URC_NO_REASON)
        	/* Some error encountered.  Usually the unwinder doesn't
        	   diagnose these and merely crashes.  */
        	return _URC_FATAL_PHASE1_ERROR;
         
              /* Unwind successful.  Run the personality routine, if any.  */
              if (fs.personality)
        	{
        	  code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class,
        				    exc, &cur_context);
        	  if (code == _URC_HANDLER_FOUND)
        	    break;
        	  else if (code != _URC_CONTINUE_UNWIND)
        	    return _URC_FATAL_PHASE1_ERROR;
        	}
         
              /* Update cur_context to describe the same frame as fs.  */
              uw_update_context (&cur_context, &fs);
            }
        • 對於註解的理解,可以參考底下的 GDB 輸出。_Unwind_RaiseException 第一次進入迴圈之時,其 caller 即為 __cxa_throw
          (gdb) bt
          #0  __gxx_personality_v0 (version=1, actions=1, exceptionClass=0, 
              unwind_exception=0x555555756040 <completed>, context=0x7fffffffdc20) at mycppabi.cpp:76
          #1  0x00007ffff7bce28b in _Unwind_RaiseException () from /lib/x86_64-linux-gnu/libgcc_s.so.1
          #2  0x0000555555554a99 in __cxa_throw (thrown_exception=0x555555756060 <exception_buff>, 
              tinfo=0x555555755d88 <typeinfo for Exception>, dest=0x0) at mycppabi.cpp:54
          #3  0x0000555555554901 in raise () at throw.cpp:8
          #4  0x000055555555490f in try_but_dont_catch () at throw.cpp:14
          #5  0x0000555555554977 in catchit () at throw.cpp:25
          #6  0x0000555555554a12 in seppuku () at throw.cpp:37
          #7  0x00005555555548d8 in main () at main.c:5
      • 一但 personality function 返回_URC_HANDLER_FOUND_Unwind_RaiseException 調用 _Unwind_RaiseException_Phase2。personality function 接受 _UA_CLEANUP_PHASE 表示此為第二輪清理。
        static _Unwind_Reason_Code
        _Unwind_RaiseException_Phase2(struct _Unwind_Exception *exc,
        			      struct _Unwind_Context *context,
        			      unsigned long *frames_p)
        {
          while (1)
            {
              _Unwind_FrameState fs;
              int match_handler;
         
              code = uw_frame_state_for (context, &fs);
         
              /* Identify when we've reached the designated handler context.  */
              match_handler = (uw_identify_context (context) == exc->private_2
        		       ? _UA_HANDLER_FRAME : 0);
         
              if (code != _URC_NO_REASON)
        	/* Some error encountered.  Usually the unwinder doesn't
        	   diagnose these and merely crashes.  */
        	return _URC_FATAL_PHASE2_ERROR;
         
              /* Unwind successful.  Run the personality routine, if any.  */
              if (fs.personality)
        	{
        	  code = (*fs.personality) (1, _UA_CLEANUP_PHASE | match_handler,
        				    exc->exception_class, exc, context);
        	  if (code == _URC_INSTALL_CONTEXT)
        	    break;
        	  if (code != _URC_CONTINUE_UNWIND) 
        	    return _URC_FATAL_PHASE2_ERROR;
        	}
         
              /* Don't let us unwind past the handler context.  */
              gcc_assert (!match_handler);
         
              uw_update_context (context, &fs);
              frames++;
            }
      • personality function 於第一輪查找,應返回 _URC_HANDLER_FOUND 表示可處理例外的 catch 已被找到; 第二輪清理,應返回 _URC_INSTALL_CONTEXT 表示應當恢復執行。此時必須要指定於哪一個 catch (即 landing pad) 恢復執行。
      • __gxx_personality_v0 透過 LSDA 知道其所屬函式是否可以處理當前的例外。
        _Unwind_Reason_Code __gxx_personality_v0 (
                             int version, _Unwind_Action actions, uint64_t exceptionClass,
                             _Unwind_Exception* unwind_exception, _Unwind_Context* context)
        {
            if (actions & _UA_SEARCH_PHASE)
            {
                printf("Personality function, lookup phase\n");
                return _URC_HANDLER_FOUND;
            } else if (actions & _UA_CLEANUP_PHASE) {
                printf("Personality function, cleanup\n");
                const uint8_t* lsda = (const uint8_t*)_Unwind_GetLanguageSpecificData(context);
                uintptr_t ip = _Unwind_GetIP(context) - 1;
                uintptr_t funcStart = _Unwind_GetRegionStart(context);
                uintptr_t ipOffset = ip - funcStart;
                return _URC_INSTALL_CONTEXT;
            } else {
                printf("Personality function, error\n");
                return _URC_FATAL_PHASE1_ERROR;
            }
        }
          • _Unwind_GetLanguageSpecificData: 取得當前棧的 LSDA。我們可以在 LSDA 找到 landing pad。
          • _Unwind_GetIP: 取得當前 PC 位址。基本上就是調用丟出例外函式之後所在的位址。
            (lldb) expr -f hex -- ip
            (uintptr_t) $4 = 0x000000010000085c
            (lldb) dis -a ip -m
               11  	    try {
            ** 12  	        raise();
            
                0x100000854 <+4>:   subq   $0x30, %rsp
                0x100000858 <+8>:   callq  0x100000820               ; raise at throw.cpp:6
                0x10000085d <+13>:  jmp    0x100000862               ; <+18> at throw.cpp:13
          • _Unwind_GetRegionStart: 取得當前棧的起始位址。可以於 gdb 反匯編該位址,看到函式 try_but_dont_catch
      • image.slidesharecdn.com_hes2011-joakley-sbratus-exploiting-the-hard-working-dwarf-110415112206-phpapp01_95_hes2011-james-oakley-and-sergey-bratusexploitingthehardworkingdwarf-46-728.jpg
      • personality function 讀取緊鄰函式之後,.gcc_except_table 段中的 LSDA,以得到該函式 landing pad 相關訊息。
        .LFE1:
                .globl  __gxx_personality_v0
                .section        .gcc_except_table,"a",@progbits
                .align 4
        .LLSDA1:
                .byte   0xff    # @LPStart format (omit)
                .byte   0x9b    # @TType format (indirect pcrel sdata4)
                .uleb128 .LLSDATT1-.LLSDATTD1   # @TType base offset
        .LLSDATTD1:
                .byte   0x1     # call-site format (uleb128)
                .uleb128 .LLSDACSE1-.LLSDACSB1  # Call-site table length
        .LLSDACSB1:
                .uleb128 .LEHB0-.LFB1   # region 0 start
                .uleb128 .LEHE0-.LEHB0  # length
                .uleb128 .L8-.LFB1      # landing pad
                .uleb128 0x1    # action
                .uleb128 .LEHB1-.LFB1   # region 1 start
                .uleb128 .LEHE1-.LEHB1  # length
                .uleb128 0      # landing pad
                .uleb128 0      # action
                .uleb128 .LEHB2-.LFB1   # region 2 start
                .uleb128 .LEHE2-.LEHB2  # length
                .uleb128 .L9-.LFB1      # landing pad
                .uleb128 0      # action
                .uleb128 .LEHB3-.LFB1   # region 3 start
                .uleb128 .LEHE3-.LEHB3  # length
                .uleb128 0      # landing pad
                .uleb128 0      # action
        .LLSDACSE1:
                .byte   0x1     # Action record table
                .byte   0
                .align 4
                .long   DW.ref._ZTI14Fake_Exception-.
        .LLSDATT1:
                .text
                .size   _Z18try_but_dont_catchv, .-_Z18try_but_dont_catchv
                .section        .rodata
      • try_but_dont_catch 為例,LSDA 將其分成數個區域 (region)。每一個區域都有對應的 call site table entry。
        void try_but_dont_catch() {
            try {
        /* region 0 begin */
                raise();
        /* region 0 end */
            } catch(Fake_Exception&) {
        /* region 2 begin */
                printf("Caught a Fake_Exception!\n");
        /* region 2 end */
            }
        /* region 1 begin */
            printf("try_but_dont_catch handled the exception\n");
        /* region 1 end */
        }
      • personality function 讀取緊鄰函式之後,.gcc_except_table 段中的 LSDA。透過 _Unwind_SetIP 設置要執行的 landing pad (即 catch)。這裡因為沒有透過 _Unwind_SetGR 將例外傳給 landing pad,landing pad 只能告知 _Unwind 無法處理該例外。當前實現,_Unwind 會陷入尋找可以處理例外的 landing pad 的無窮迴圈。
        // lsda 是 LSDA 基地址,加上 LSDA header 和 call site table header,得到 call site table。
        const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header)
                                            + sizeof(LSDA_Call_Site_Header);
        for (size_t i=0; i < cs_in_table; ++i)
        {
            const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)];
            LSDA_Call_Site cs(offset);
         
            if (cs.cs_lp)
            {
                uintptr_t func_start = _Unwind_GetRegionStart(context);
                _Unwind_SetIP(context, func_start + cs.cs_lp);
                break;
            }
        }
    • C++ exceptions under the hood 13: setting the context for a landing pad
      while (lsda < lsda_cs_table_end)
      {
          LSDA_CS cs(&lsda);
       
          if (cs.lp)
          {
              int r0 = __builtin_eh_return_data_regno(0);
              int r1 = __builtin_eh_return_data_regno(1);
       
              _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception));
              // Note the following code hardcodes the exception type;
              // we'll fix that later on
              _Unwind_SetGR(context, r1, (uintptr_t)(1));
       
              uintptr_t func_start = _Unwind_GetRegionStart(context);
              _Unwind_SetIP(context, func_start + cs.lp);
              break;
          }
      }
        • 使用 _Unwind_SetGR 傳參給 personality function。第一個參數傳入例外,第二個參數傳入例外的型別。此處我們傳入的型別使得 catch(Fake_Exception&) 實際上變成 catch(…)
      • LSDA 基本描述 try-catch 的範圍。
        void foo() {
            L0:
                try {
                    do_something();
            L1:
                } catch (const Exception1& ex) {
                    ...
                } catch (const Exception2& ex) {
                    ...
                } catch (const ExceptionN& ex) {
                    ...
                } catch (...) {
                }
            L2:
        }
        • call site table entry 依序由底下欄位組成:
          • start: try block 起始位址在函式中的偏移量,即 L0 – addr_of(foo)。
          • len: try block 的長度,即 L1 - L0。
          • lp: landing pad 起始位址在函式中的偏移量,即 L1 – addr_of(foo)。
          • action: 例外型別相關資訊。
      • 當 ip (透過 _Unwind_GetIP 取得) 落在 start 和 start + len 之間,我們可以知道當前棧可以處理例外。
        while (lsda < lsda_cs_table_end)
        {
            LSDA_CS cs(&lsda);
         
            // If there's no LP we can't handle this exception; move on
            if (not cs.lp) continue;
         
            uintptr_t func_start = _Unwind_GetRegionStart(context);
         
            // Calculate the range of the instruction pointer valid for this
            // landing pad; if this LP can handle the current exception then
            // the IP for this stack frame must be in this range
            uintptr_t try_start = func_start + cs.start;
            uintptr_t try_end = func_start + cs.start + cs.len;
         
            // Check if this is the correct LP for the current try block
            if (throw_ip < try_start) continue;
            if (throw_ip > try_end) continue;
         
            // We found a landing pad for this exception; resume execution
            int r0 = __builtin_eh_return_data_regno(0);
            int r1 = __builtin_eh_return_data_regno(1);
         
            _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception));
            // Note the following code hardcodes the exception type;
            // we'll fix that later on
            _Unwind_SetGR(context, r1, (uintptr_t)(1));
         
            _Unwind_SetIP(context, func_start + cs.lp);
            break;
        }
      • .gcc_except_table 段中有 call site table,action table 和 type table。type table 紀錄型別資訊。
        .LLSDACSE1:
                .byte   0x1     # Action record table
                .byte   0
                .align 4
                .long   DW.ref._ZTI14Fake_Exception-.
        .LLSDATT1:              # Type table 結尾。
    • 其中,DW.ref._ZTI14Fake_Exception-. 紀錄著 _ZTI14Fake_Exception 型別資訊所在位址。
      _ZTI14Fake_Exception:
              .quad   _ZTVN10__cxxabiv117__class_type_infoE+16
              .quad   _ZTS14Fake_Exception
              .weak   _ZTS14Fake_Exception
              .section        .rodata._ZTS14Fake_Exception,"aG",@progbits,_ZTS14Fake_Exception,comdat
              .align 16
              .type   _ZTS14Fake_Exception, @object
              .size   _ZTS14Fake_Exception, 17
      • LSDA 標頭紀錄 type table 的偏移 (@TType base offset)。
        .LLSDA1:
                .byte   0xff    # @LPStart format (omit)
                .byte   0x9b    # @TType format (indirect pcrel sdata4)
                .uleb128 .LLSDATT1-.LLSDATTD1   # @TType base offset
      • call site table entry 紀錄 action table 的索引 (非零值減去 1 才是真正的索引。0 表示沒有 action)。
        .LLSDACSB1:
                .uleb128 .LEHB0-.LFB1   # region 0 start
                .uleb128 .LEHE0-.LEHB0  # length
                .uleb128 .L8-.LFB1      # landing pad
                .uleb128 0x1    # action
      • 對於帶有 catch 的 landing pad,其 action table entry 紀錄 type table 的反向索引 (即要乘上 -1)。
        .LLSDACSE1:
                .byte   0x1     # Action record table
                .byte   0
                .align 4
                .long   DW.ref._ZTI14Fake_Exception-. # catch 可以處理例外的型別。
        .LLSDATT1:
      • __cxa_throw 會將例外的型別傳遞給 _Unwind_RaiseException
        extern "C" __cxa_refcounted_exception*
        __cxxabiv1::
        __cxa_init_primary_exception(void *obj, std::type_info *tinfo,
        			     void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
        _GLIBCXX_NOTHROW
        {
          __cxa_refcounted_exception *header
            = __get_refcounted_exception_header_from_obj (obj);
          header->exc.exceptionType = tinfo;
         
          return header;
        }
         
        extern "C" void
        __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo,
        			 void (_GLIBCXX_CDTOR_CALLABI *dest) (void *))
        {
          __cxa_refcounted_exception *header =
            __cxa_init_primary_exception(obj, tinfo, dest);
         
          _Unwind_RaiseException (&header->exc.unwindHeader);
        }
      • _Unwind_RaiseException 調用 personality function 檢視 LSDA 中的型別,和當前例外的型別對照,決定 landing pad 是否能處理該例外。
      • 描述如何檢視 .gcc_except_table 中的 call site table,action table 和 type table 以找到 landing pad 可以處理的例外型別。
      • action table entry 的 type index 如果為 0,表示該 landing pad 負責清理 (例如執行解構子),最後調用 _Unwind_Resume

外部連結

登录