• 硬體提供 trap 指令供 debugger 實現斷點。針對 CISC 架構,trap 指令長度為硬體指令中最短者。
      • 描述除錯器基本程式邏輯。
      • Also, no other method can terminate the process (Task Manager, Process Explorer, Kill utility…). Attempting to kill the process from these utilities will, however, schedule the terminating process. Thus, the debugger would receive EXIT_PROCESS_DEBUG_EVENT as the next event!
        • 對目標進程下 kill 指令並不會立刻生效,控制權會先交還給除錯器,由除錯器處理 EXIT_PROCESS_DEBUG_EVENT 例外。
      • When a process is being debugged, the debugger always receives the exception before the debuggee gets it. You must have seen "First-chance exception at 0x00412882 in SomeModule:…" while debugging your Visual C++ module. This is referred to as First Chance Exception.
        • 除錯器會攔截到目標進程的所有例外。因此當目標進程觸發例外時,Windows 會將該例外丟給除錯器,此即 first chance exception。除錯器可以選擇處理或是不處理 first chance exception。如果除錯器選擇不處理該例外,則轉交由目標進程處理。如果目標進程仍然不處理該例外,則除錯器將會再次看到該例外,此即 second chance exception。First chance exception 是否代表問題發生,端視目標進程是否正確處理該例外; 若 second chance exception 發生,一般代表問題發生,因為在沒有除錯器的情況下,該例外會導致程序崩潰。
      • EXCEPTION_BREAKPOINT 和 EXCEPTION_SINGLE_STEP 是除錯器關注的焦點。
      • 處理斷點。
      • 顯示暫存器、調用棧和源代碼。除錯器和 GUI 交互。

硬體支援

處理器一般提供以下功能供上層軟體除錯:

  • A special instruction that halts execution.
    • 提供 trap 指令供除錯器插入斷點。硬體必須提供此基本功能。
  • A special mode of the processor to execute a single instruction.
    • 提供單步模式供除錯器設置單步執行。
  • Page protection mechanisms.
    • 如果無硬體支援 data breakpoint,需提供頁機制,透過頁錯誤達成該功能。
  • Exception or fault detection mechanisms.
    • 提供例外處理機制。
  • In some case, special debug registers.
    • 提供硬體暫存器供除錯器使用。

作業系統

Linux 提供 ptrace,Windows 提供 Win32 Debug API

除錯資訊

針對匯編語言,除錯訊息應該要能帶入以下資訊。

  • 源代碼資訊
    • 目錄、檔名、巨集名稱…
  • 符號資訊
    • 全域符號、本地符號
    • 匯編中的符號一般沒有型別資訊
  • 控制流資訊
  • 指令對映
  • 段資訊
  • 巨集資訊

STABS

      • 編譯器在編譯源代碼時,會將除錯訊息輸出至匯編,再經由匯編器和鏈結器輸出至最終執行檔。
      • 編譯器以匯編指示 .stab 輸出除錯訊息輸出至匯編。匯編指示基本有底下三種:
        • .stabs: string
          • .stabs "string",type,other,desc,value
        • .stabn: number
          • .stabn type,other,desc,value
        • .stabd: dot
          • .stabd type,other,desc
      • 除錯 symbol 有底下基本 type (Appendix A Table of Stab Types):
          • Before any other stabs occur, there must be a stab specifying the source file. This information is contained in a symbol of stab type N_SO; the string field contains the name of the file. The value of the symbol is the start address of the portion of the text section corresponding to that file.
          • An N_SLINE symbol represents the start of a source line. The desc field contains the line number and the value contains the code address for the start of that source line.
          • The N_LBRAC and N_RBRAC stabs that describe the block scope of a procedure are located after the N_FUN stab that represents the procedure itself.
      • 描述上述 "string" 欄位格式。
    • 鏈結器將目的檔中所有的 .stab 和 .stabstr 段合併成一份 .stab 和 .stabstr 段。
        • 匯編器將除錯訊息放在底下兩個段中:
          • .stab
            • 匯編中每一個除錯訊息皆以一個固定大小的資料結構存放在 .stab 段。
          • .stabstr
            • 除錯訊息的 string 欄位內容皆存放在 .stabstr 段。
$ cat hello.c
main()
{
    printf("Hello world");
}
$ gcc -gstabs -S hello.c
        .file   "hello.c"
        .stabs  "hello.c",100,0,2,.Ltext0
        .text
.Ltext0:
        .stabs  "gcc2_compiled.",60,0,0,0
        .stabs  "int:t(0,1)=r(0,1);-2147483648;2147483647;",128,0,0,0
$ objdump --stabs hello
a.out:     file format elf32-i386
 
Contents of .stab section:
 
Symnum n_type n_othr n_desc n_value  n_strx String
 
-1     HdrSym 0      118    00000f24 1     
0      SO     0      2      0804841d 1      hello.c
1      OPT    0      0      00000000 9      gcc2_compiled.
2      LSYM   0      0      00000000 24     int:t(0,1)=r(0,1);-2147483648;2147483647;

DWARF

    • 除錯資訊是以樹節點 (DIE, Debugging Information Entry) 構成的一個樹狀結構,以 prefix order 儲存成 table 放在段中。
      • DIE 帶有 tag 和數個 attribute。
  • CU (compilation unit) DIE 是根節點,有底下屬性:
    • DW_AT_low_pc
    • DW_AT_high_pc
    • [DW_AT_low_pc, DW_AT_high_pc) 為 CU 的範圍。
  • Line number table
    • 存放代碼和對應二進制位址關係
$ cat hello.c 
int main() {
  int i = 0;
 
  return 0;
}
$ gcc -g hello.c
# 觀察 DWAFT 資訊。
$ objdump --dwarf=info a.out
a.out:     file format elf32-i386
 
Contents of the .debug_info section:
 
  Compilation Unit @ offset 0x0:
   Length:        0x4f (32-bit)
   Version:       4
   Abbrev Offset: 0x0
   Pointer Size:  4
 <0><b>: Abbrev Number: 1 (DW_TAG_compile_unit)
    <c>   DW_AT_producer    : (indirect string, offset: 0x22): GNU C 4.8.2 -mtune=generic -march=i686 -g -fstack-protector      
    <10>   DW_AT_language    : 1        (ANSI C)
    <11>   DW_AT_name        : (indirect string, offset: 0x0): hello.c  
    <15>   DW_AT_comp_dir    : (indirect string, offset: 0x8): /home/chenwj/project/misc        
    <19>   DW_AT_low_pc      : 0x80483ed        
    <1d>   DW_AT_high_pc     : 0x14     
    <21>   DW_AT_stmt_list   : 0x0      
 <1><25>: Abbrev Number: 2 (DW_TAG_subprogram)
    <26>   DW_AT_external    : 1        
    <26>   DW_AT_name        : (indirect string, offset: 0x5e): main    
    <2a>   DW_AT_decl_file   : 1        
    <2b>   DW_AT_decl_line   : 1        
    <2c>   DW_AT_type        : <0x4b>   
    <30>   DW_AT_low_pc      : 0x80483ed        
    <34>   DW_AT_high_pc     : 0x14     
    <38>   DW_AT_frame_base  : 1 byte block: 9c         (DW_OP_call_frame_cfa)
    <3a>   DW_AT_GNU_all_call_sites: 1  
    <3a>   DW_AT_sibling     : <0x4b>   
 <2><3e>: Abbrev Number: 3 (DW_TAG_variable)
    <3f>   DW_AT_name        : i        
    <41>   DW_AT_decl_file   : 1        
    <42>   DW_AT_decl_line   : 2        
    <43>   DW_AT_type        : <0x4b>   
    <47>   DW_AT_location    : 2 byte block: 91 74      (DW_OP_fbreg: -12)
 <2><4a>: Abbrev Number: 0
 <1><4b>: Abbrev Number: 4 (DW_TAG_base_type)
    <4c>   DW_AT_byte_size   : 4        
    <4d>   DW_AT_encoding    : 5        (signed)
    <4e>   DW_AT_name        : int      
 <1><52>: Abbrev Number: 0

CIE & FDE

目的是要從當前棧回復調用棧的狀態。掌握此原則之後,較好理解底下的內容。
    • 先閱讀 DWARF Debugging Information Format 第 6.4 節 Call Frame Information。
      • CFI (Call Frame Information) 的目的是重建 callee-save 暫存器於一個調用棧的值,其方式是透過底下的表加以重建。LOC 基本可以想成是當前棧的 PC 值。CFA 是計算開棧前,棧指針的位址 (即一個調用棧的頂部)。RN 代表該虛擬暫存器在一個調用棧的值如何復現,基本上透過 CFA 作為基地址加以計算。
        LOC CFA R0 R1 ... RN
        L0
        L1                   // 執行完 L0 之後的狀態
        ...
        LN
        • CFA 一行,是計算指向 caller 棧的 CFA。RN 基本上是以 CFA 作為基地址加以計算。
      • CIE (Common Information Entry): 用來建立上表中的第一列。
      • FDE (Frame Description Entry): 描述如何從表中的第一列,依序產生其它列。
      • LEB128 (Little Endian Base 128): 一種壓縮方式,可以以少量 byte 表示任意整數。Call Frame Instructions 的操作數基本以 LEB128 表示。

image.slidesharecdn.com_hes2011-joakley-sbratus-exploiting-the-hard-working-dwarf-110415112206-phpapp01_95_hes2011-james-oakley-and-sergey-bratusexploitingthehardworkingdwarf-24-728.jpg

  • 在一個編譯單元中,每一個函式對應一個 FDE。FDE 中相同的部分,抽取出來作為 CIE。
  • CIE 中的 return_address_register 代表哪一個虛擬暫存器存放返回地址。
  • CIE 中的 initial_instructions 用來表示如何重建上述表的第一行。
  • FDE 中的 initial location 和 address range 決定此 FDE 對應到哪一個函式。
  • FDE 中的 augmentation 代表平台或語言相關訊息。
  • FDE 中的 instructions 用來表示如何重建上述表中,除第一行外的其它行。

底下是 DWARF Debugging Information Format 的 Appendix 5。

  • 執行 foo 前五條指令之後,棧的佈局。
     high  -------------- <---- old R7 (CFA)
          |      R1      |
           -------------- 8
          |    old R6    |
           -------------- 4
          |      R4      |
           -------------- <---- new R7/R6
      low
    • new R6 為棧底。new R7 繼續增長。
  • foo 代碼。
           ;; start prologue
    foo    sub   R7, R7, <fsize>      ; Allocate frame
    foo+4  store R1, R7, (<fsize>-4)  ; Save the return address
    foo+8  store R6, R7, (<fsize>-8)  ; Save R6
    foo+12 add   R6, R7, 0            ; R6 is now the Frame ptr
    foo+16 store R4, R6, (<fsize>-12) ; Save a preserve reg.
           ;; This subroutine does not change R5
           ...
           ;; Start epilogue (R7 has been returned to entry value)
    foo+64 load  R4, R6, (<fsize>-12) ; Restore R4
    foo+68 load  R6, R7, (<fsize>-8)  ; Restore R6
    foo+72 load  R1, R7, (<fsize>-4)  ; Restore return address
    foo+76 add   R7, R7, <fsize>      ; Deallocate frame
    foo+80 jump  R                    ; Return
    foo+84
  • 用於復原 foo caller 棧狀態的表。
    Loc    CFA        R0 R1 R2 R3 R4  R5 R6 R7 R8
    foo    [R7]+0     s  u  u  u  s   s  s  s  r1 # 尚未執行第一條指令。caller 調用 foo 時,R8 (PC) 被存在 R1。
    foo+4  [R7]+fsize s  u  u  u  s   s  s  s  r1 # 執行完 foo    指令。CFA 為當前 R7 + fsize。
    foo+8  [R7]+fsize s  u  u  u  s   s  s  s  c4 # 執行完 foo+4  指令。R1 存在 CFA - 4。
    foo+12 [R7]+fsize s  u  u  u  s   s  c8 s  c4 # 執行完 foo+8  指令。R6 存在 CFA - 8。
    foo+16 [R6]+fsize s  u  u  u  s   s  c8 s  c4 # 執行完 foo+12 指令。當前棧底由 R7 改為 R6。CFA 為當前 R6 + fsize。
    foo+20 [R6]+fsize s  u  u  u  c12 s  c8 s  c4 # 執行完 foo+16 指令。R4 存在 CFA - 12。
    foo+64 [R6]+fsize s  u  u  u  c12 s  c8 s  c4 # 此時棧頂 R7 已回復到 R6 所在位置。
    foo+68 [R6]+fsize s  u  u  u  s   s  c8 s  c4 # 執行完 foo+64 指令。R4 回復至調用前的狀態。
    foo+72 [R7]+fsize s  u  u  u  s   s  s  s  c4 # 執行完 foo+68 指令。R6 回復至 old R6。當前棧底由 R6 改為 R7。
    foo+76 [R7]+fsize s  u  u  u  s   s  s  s  r1 # 執行完 foo+72 指令。R1 回復至調用前的狀態。
    foo+80 [R7]+0     s  u  u  u  s   s  s  s  r1 # 執行完 foo+74 指令。CFA 為當前 R7。
    • CIE 用於復原表中的第一行。
      cie+13 DW_CFA_def_cfa (7, 0)  ; CFA = [R7]+0
      cie+16 DW_CFA_same_value (0)  ; R0 not modified (=0)
      cie+18 DW_CFA_undefined (1)   ; R1 scratch
      cie+20 DW_CFA_undefined (2)   ; R2 scratch
      cie+22 DW_CFA_undefined (3)   ; R3 scratch
      cie+24 DW_CFA_same_value (4)  ; R4 preserve
      cie+26 DW_CFA_same_value (5)  ; R5 preserve
      cie+28 DW_CFA_same_value (6)  ; R6 preserve
      cie+30 DW_CFA_same_value (7)  ; R7 preserve
      cie+32 DW_CFA_register (8, 1) ; R8 is in R1
    • FDE 用於復原表中剩餘的其它行。
      fde+16 DW_CFA_advance_loc(1)            ; foo+4
      fde+17 DW_CFA_def_cfa_offset(<fsize>/4) ; CFA = [R7] + <fsize>/4 (4)
      fde+19 DW_CFA_advance_loc(1)            ; foo+8
      fde+20 DW_CFA_offset(8,1)               ; R8 = CFA - 1 (4)
      fde+22 DW_CFA_advance_loc(1)            ; foo+12
      fde+23 DW_CFA_offset(6,2)               ; R6 = CFA - 2 (4)
      fde+25 DW_CFA_advance_loc(1)            ; foo+16
      fde+26 DW_CFA_def_cfa_register(6)       ; CFA = [R6] + <fsize>/4 (4)
      fde+28 DW_CFA_advance_loc(1)            ; foo+20
      fde+29 DW_CFA_offset(4,3)               ; R4 = CFA - 3 (4)
      fde+31 DW_CFA_advance_loc(11)           ; foo+64
      fde+32 DW_CFA_restore(4)                ; R4 回復至調用前的狀態。 
      fde+33 DW_CFA_advance_loc(1)            ; foo+68
      fde+34 DW_CFA_restore(6)                ; R6 回復至調用前的狀態。
      fde+35 DW_CFA_def_cfa_register(7)       ; R6 回復至 old R6。當前棧底由 R6 改為 R7。
      fde+37 DW_CFA_advance_loc(1)            ; foo+68
      fde+38 DW_CFA_restore(8)                ; R8 回復至調用前的狀態。
      fde+39 DW_CFA_advance_loc(1)            ; foo+72
      fde+40 DW_CFA_def_cfa_offset(0)         ; CFA = [R7]+0
$ gcc -g test.c -S -o test.s
main:
.LFB0:
        .file 1 "test.c"
        .loc 1 1 0
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16   ; (a)
        .cfi_offset 6, -16       ; (b)
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6  ; (c)
        .loc 1 1 0
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8        ; (d)
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
$ gcc -g test.c
$ objdump -d --start-address=0x5fa --stop-address=0x605 a.out
 
00000000000005fa <main>:
 5fa:	55                   	push   %rbp
 5fb:	48 89 e5             	mov    %rsp,%rbp
 5fe:	b8 00 00 00 00       	mov    $0x0,%eax
 603:	5d                   	pop    %rbp
 604:	c3                   	retq   
 
$ objdump --dwarf=frames a.out
 
00000030 0000000000000014 00000000 CIE
  Version:               1
  Augmentation:          "zR"
  Code alignment factor: 1
  Data alignment factor: -8
  Return address column: 16
  Augmentation data:     1b
 
  DW_CFA_def_cfa: r7 (rsp) ofs 8
  DW_CFA_offset: r16 (rip) at cfa-8
  DW_CFA_nop
  DW_CFA_nop
 
00000088 000000000000001c 0000005c FDE cie=00000030 pc=00000000000005fa..0000000000000605     
  DW_CFA_advance_loc: 1 to 00000000000005fb
  DW_CFA_def_cfa_offset: 16                  (a)
  DW_CFA_offset: r6 (rbp) at cfa-16          (b)
  DW_CFA_advance_loc: 3 to 00000000000005fe
  DW_CFA_def_cfa_register: r6 (rbp)          (c)
  DW_CFA_advance_loc: 6 to 0000000000000604
  DW_CFA_def_cfa: r7 (rsp) ofs 8             (d)
  DW_CFA_nop
  DW_CFA_nop
  DW_CFA_nop

除錯器

以下描述除錯器內部資料結構。

  • 斷點可以分為邏輯斷點 (logical breakpoints) 和物理斷點 (physical breakpoints)。
    • 邏輯斷點: 對映到使用者設置的斷點,通常是針對特定代碼行數。計算條件斷點的成立與否,通常是於邏輯斷點進行判斷。
    • 物理斷點: 實際需要寫入斷點指令的位址。
    • 邏輯斷點與物理斷點是多對一映射。

術語

      • hardware breakpoint
        • 硬體暫存器提供的中斷,如 x86 debug register。將欲設置斷點的位址寫入硬體暫存器,之後對該位址的讀/寫/執行,皆會觸發中斷。
        • the main strength of hardware breakpoints is that you can use them to halt on non-execution accesses to memory locations.
      • software breakpoint
        • 硬體提供陷阱指令供除錯器插入中斷,如 INT 3
    • instruction breakpoint
    • conditional breakpoint, data breakpoint, watchpoint
      • 當條件成立,或是變數值改變,觸發斷點。
      • 一般透過 hardware breakpoint 實現。若硬體不支援,則採 page protection 機制達成。

外部連結

登录