1. 程式人生 > >自己動手編寫一個Linux偵錯程式系列之4 ELF檔案格式與DWARF除錯格式

自己動手編寫一個Linux偵錯程式系列之4 ELF檔案格式與DWARF除錯格式

目錄

在上一節中,你已經聽說了DWARF除錯格式,它是程式的除錯資訊,是一種可以更好理解原始碼的方式,而不只是解析程式。今天我們將討論原始碼級除錯資訊的細節,以準備在本教程後面的部分中使用它。

系列索引

  1. 準備工作
  2. 斷點的設定
  3. 暫存器和記憶體
  4. ELF檔案格式和DWARVF除錯格式
  5. 原始碼和訊號
  6. 原始碼級單步
  7. 原始碼級斷點
  8. 堆疊解除
  9. 處理變數
  10. 高階主題

ELF檔案格式與DWARF格式簡介

ELF和DWARF是你可能沒有聽說過的兩個概念資訊,但可能已經使用很長時間了。 ELF(可執行和可連結格式)是Linux世界中使用最廣泛的物件檔案格式; 它指定了一種儲存二進位制檔案的所有不同部分的方式,如程式碼,靜態資料,除錯資訊和字串。 它還告訴載入程式如何取得二進位制並準備好執行,這涉及二進位制檔案的不同部分應該放置在記憶體中,哪些部分需要根據其他資訊(重定位)等的位置來修復。 我不會在這些帖子中覆蓋更多ELF,但如果你有興趣,可以看看這個漂亮的

資訊圖表標準

DWARF是ELF最常用的除錯資訊格式。它不一定與ELF相關,但兩者是一起發展的,在開發中一起使用也非常好。該格式允許編譯器告訴偵錯程式程式原始碼如何與將執行的二進位制檔案相互關係。該資訊分為不同的ELF部分,每個部分都有自己的資訊來中繼。以下是定義的不同部分,取自於非常詳細的DWARF除錯格式介紹

  • .debug_abbrev .debug_info部分中使用的縮寫
  • .debug_aranges 記憶體地址和編譯之間的對映
  • .debug_frame 呼叫幀資訊
  • .debug_info 包含DWARF除錯資訊項(DIE)的核心DWARF資料
  • .debug_line
     行號程式
  • .debug_loc 位置說明
  • .debug_macinfo 巨集描述
  • .debug_pubnames 全域性物件和函式的查詢表
  • .debug_pubtypes 全域性型別的查詢表
  • .debug_ranges DIE引用的地址範圍
  • .debug_str .debug_info使用的字串表
  • .debug_types 型別說明

我們對.debug_line.debug_info部分最感興趣,所以讓我們看看一些DWARF的簡單程式。

1

2

3

4

5

6

int main() {

long 

= 3;

long = 2;

long = + b;

= 4;

}

DWARF debug_line表資訊

如果你使用編譯器(gcc 或 clang)的-g選項編譯此程式,並通過dwarfdump執行結果,則應該看到類似於行號的部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

.debug_line: line number info for a single cu

Source lines (from CU-DIE at .debug_info offset 0x0000000b):

NS new statement, BB new basic block, ET end of text sequence

PE prologue end, EB epilogue begin

IS=val ISA number, DI=val discriminator value

<pc>        [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"

0x00400670  [   10] NS uri: "/home/simon/play/MiniDbg/examples/variable.cpp"

0x00400676  [   2,10] NS PE

0x0040067e  [   3,10] NS

0x00400686  [   4,14] NS

0x0040068a  [   4,16]

0x0040068e  [   4,10]

0x00400692  [   57] NS

0x0040069a  [   61] NS

0x0040069c  [   61] NS ET

第一部分描述部分是關於如何理解下面顯示列表的一些資訊 - 表資訊主行號資料從0x00400670開始。本質上,它是將一個程式碼記憶體地址對映到一些檔案中的行和列號。 NS表示該地址標誌著新語句的開始,這通常用於設定斷點或步進。 PE標記函式開始的結尾,這有助於設定函式入口斷點。 ET標示翻譯單元的結尾。實際資訊上並不是像這樣編碼的;真正的編碼是一種非常節省空間的程式,可以執行這些程式來建立這個行資訊。

那麼說,我們想在variable.cpp的第4行設定一個斷點,我們該怎麼做?我們查詢與該檔案相對應的條目,然後查詢相關的行條目,查詢與之對應的地址,並在其中設定斷點。在我們的例子中,這是這個條目:

1

0x00400686  [   4,14] NS

所以我們要在地址0x00400686設定一個斷點。你可以用你已經寫過的偵錯程式手工完成,如果你想嘗試一下。

相反的工作也是如此。如果我們有一個記憶體位置 - 例如一個程式計數器值,並且想要找出原始碼中的哪個位置,我們只需在行表資訊中找到最接近的對映地址,並從中獲取行。

DWARF debug_info資訊

.debug_info部分是DWARF的核心。它給了我們有關我們的程式中存在的型別,函式,變數,希望和想要得到的資訊。本節的基本單位是DWARF資訊條目(DWARF Information Entry),簡稱為DIE。 DIE包含一個標籤,告訴您正在表示什麼樣的原始碼級實體,後面是一系列適用於該實體的屬性。這是上面釋出的簡單示例程式的.debug_info部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

.debug_info

COMPILE_UNIT<header overall offset = 0x00000000>:

0><0x0000000b>  DW_TAG_compile_unit

DW_AT_producer              clang version 3.9.1 (tags/RELEASE_391/final)

DW_AT_language              DW_LANG_C_plus_plus

DW_AT_name                  /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_stmt_list             0x00000000

DW_AT_comp_dir              /super/secret/path/MiniDbg/build

DW_AT_low_pc                0x00400670

DW_AT_high_pc               0x0040069c

LOCAL_SYMBOLS:

1><0x0000002e>    DW_TAG_subprogram

DW_AT_low_pc                0x00400670

DW_AT_high_pc               0x0040069c

DW_AT_frame_base            DW_OP_reg6

DW_AT_name                  main

DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_decl_line             0x00000001

DW_AT_type                  <0x00000077>

DW_AT_external              yes(1)

2><0x0000004c>      DW_TAG_variable

DW_AT_location              DW_OP_fbreg -8

DW_AT_name                  a

DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_decl_line             0x00000002

DW_AT_type                  <0x0000007e>

2><0x0000005a>      DW_TAG_variable

DW_AT_location              DW_OP_fbreg -16

DW_AT_name                  b

DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_decl_line             0x00000003

DW_AT_type                  <0x0000007e>

2><0x00000068>      DW_TAG_variable

DW_AT_location              DW_OP_fbreg -24

DW_AT_name                  c

DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_decl_line             0x00000004

DW_AT_type                  <0x0000007e>

1><0x00000077>    DW_TAG_base_type

DW_AT_name                  int

DW_AT_encoding              DW_ATE_signed

DW_AT_byte_size             0x00000004

1><0x0000007e>    DW_TAG_base_type

DW_AT_name                  long int

DW_AT_encoding              DW_ATE_signed

DW_AT_byte_size             0x00000008

第一個DIE表示一個編譯單元(CU),它基本上是一個原始檔,其中包含所有#includes,並且這樣解析。以下是它們的含義註釋的屬性:

1

2

3

4

5

6

7

8

9

10

11

12

DW_AT_producer   clang version 3.9.1 (tags/RELEASE_391/final)    <-- The compiler which produced

this binary

DW_AT_language   DW_LANG_C_plus_plus                             <-- The source language

DW_AT_name       /super/secret/path/MiniDbg/examples/variable.cpp  <-- The name of the file which

this CU represents

DW_AT_stmt_list  0x00000000                                      <-- An offset into the line table

which tracks this CU

DW_AT_comp_dir   /super/secret/path/MiniDbg/build                  <-- The compilation directory

DW_AT_low_pc     0x00400670                                      <-- The start of the code for

this CU

DW_AT_high_pc    0x0040069c                                      <-- The end of the code for

this CU

其他DIE遵循類似的方案,您可以直觀地看出不同屬性的含義。

現在我們可以嘗試用我們新發現的DWARF知識解決一些實際問題。

使用 DWARF 分析函式

如果我們有一個程式計數器值,並想獲取PC所在函式的資訊。一個簡單的演算法是:

1

2

3

4

5

for each compile unit:

if the pc is between DW_AT_low_pc and DW_AT_high_pc:

for each function in the compile unit:

if the pc is between DW_AT_low_pc and DW_AT_high_pc:

return function information

這可以用於許多情況,但是在成員函式和行內函數存在的情況下,事情會變得更加困難。 例如,使用行內函數,一旦找到範圍包含我們的PC的函式,我們將需要對該DIE的子項進行遞迴,以檢視是否存在更好匹配的行內函數。我不會在我的偵錯程式程式碼中處理行內函數,但如果你喜歡,你可以新增對此的支援。

如何在函式上設定斷點

再次申明,如果想要支援成員函式,名稱空間等特性可能需要更高階的做法。 對於簡單的函式,您可以在不同的編譯單元中迭代函式,直到找到具有正確名稱的函式。 如果您的編譯器足夠填寫.debug_pubnames部分,您可以更有效地執行此操作。

一旦找到該函式,您可以在DW_AT_low_pc給定的記憶體地址上設定一個斷點。 但是,在函式開始時會中斷,但最好在使用者程式碼開始時中斷。 由於行表資訊可以指定指定函式開頭結束的記憶體地址,因此您可以直接在行表中查詢DW_AT_low_pc的值,然後繼續閱讀,直到找到標記為函式開頭結束的條目。 有些編譯器不會輸出這個資訊,所以另外一個選擇是在該函式的第二行條目給出的地址上設定一個斷點。

假設我們要在我們的示例程式中設定一個斷點。 我們搜尋main函式,並得到這個DIE:

1

2

3

4

5

6

7

8

9

1><0x0000002e>    DW_TAG_subprogram

DW_AT_low_pc                0x00400670

DW_AT_high_pc               0x0040069c

DW_AT_frame_base            DW_OP_reg6

DW_AT_name                  main

DW_AT_decl_file             0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp

DW_AT_decl_line             0x00000001

DW_AT_type                  <0x00000077>

DW_AT_external              yes(1)

這告訴我們,函式從0x00400670開始。 如果我們線上表中檢視,我們得到這個條目:

1

0x00400670  [   10] NS uri: "/super/secret/path/MiniDbg/examples/variable.cpp"

我們想跳過開頭,所以我們先讀一個條目:

1

0x00400676  [   2,10] NS PE

Clang在這個條目中包含了程式碼開頭結束標誌,所以我們知道在這裡停下來,並在地址0x00400676上設定一個斷點。

如何讀取變數的內容

讀取變數可能非常複雜。 變數是一個難以捉摸的東西,可以在整個函式中存在,可以放在暫存器中,放在記憶體中,還可以被優化,隱藏在角落裡。幸運的是,我們簡單的例子是,很簡單。 如果我們想要讀取變數a的內容,我們來看看它的DW_AT_location屬性:

1

DW_AT_location              DW_OP_fbreg -8

這表示區域性變數的記憶體在距離堆疊幀基址的-8的偏移處。 要找出這個基址的位置,我們來看看包含函式的DW_AT_frame_base屬性。

1

DW_AT_frame_base            DW_OP_reg6

在x86上的reg6是棧幀指標暫存器,由System V x86_64 ABI指定。現在我們讀幀指標的內容,從中減去8,我們已經找到了變數。如果我們想弄明白這個問題,我們需要看看它的型別:

1

2

3

2><0x0000004c>      DW_TAG_variable

DW_AT_name                  a

DW_AT_type                  <0x0000007e>

如果我們在除錯資訊中查詢這個型別,就會得到這個DIE:

1

2

3

4

1><0x0000007e>    DW_TAG_base_type

DW_AT_name                  long int

DW_AT_encoding              DW_ATE_signed

DW_AT_byte_size             0x00000008

這告訴我們型別是一個8位元組(64位)的有符號整數型別,因此我們可以繼續將這些位元組解釋為int64_t並將其顯示給使用者。

當然,型別可以比這個複雜得多,因為它們必須能夠表達諸如c++型別之類的東西,但這給了你一個關於它們如何工作的基本概念。

回到該棧幀的基址,Clang編譯器可以比較好的跟蹤到幀指標暫存器的幀基址。 最近版本的GCC傾向於喜歡DW_OP_call_frame_cfa,它涉及解析.eh_frame ELF部分,這是一個完全不同的文章,在這裡我就不詳述。 如果你使用GCC的DWARF 2版本而不是更新的版本,命令是gcc -gdwarf-2 <原始碼>那麼它將傾向於輸出位置列表,這更容易閱讀:

1

2

3

4

5

DW_AT_frame_base            <loclist at offset 0x00000000 with 4 entries follows>

low-off : 0x00000000 addr  0x00400696 high-off  0x00000001 addr 0x00400697>DW_OP_breg7+8

low-off : 0x00000001 addr  0x00400697 high-off  0x00000004 addr 0x0040069a>DW_OP_breg7+16

low-off : 0x00000004 addr  0x0040069a high-off  0x00000031 addr 0x004006c7>DW_OP_breg6+16

low-off : 0x00000031 addr  0x004006c7 high-off  0x00000032 addr 0x004006c8>DW_OP_breg7+8

上面列表根據程式計數器的位置給出不同的位置。 這個例子是說,如果PC在DW_AT_low_pc處於0x0的偏移量的情況下,那麼棧幀基地址是從暫存器7中儲存的值加偏移量8,如果它在0x10x4之間,那麼它的偏移距離一樣都是16,等等。

總結一下

這節包含了很多DWARF資訊需要好好吸收一下才行。不要擔心!有個好訊息,就是在接下來的幾個章節中,我們將有一個庫幫我們完成最麻煩的工作。瞭解了DWARF的概念,特別是在出現問題或希望支援一些DWARF庫的情況下,仍然有用。

如果您想了解更多關於DWARF的資訊,那麼你可以在此獲取標準文件。 在撰寫本文時,DWARF 5剛剛被髮布,但DWARF 4更受歡迎。

說明

自己動手實踐一下

本節內容是整個系列最枯燥的一章,全篇都是在講述DWARF除錯格式的內容。我們可以使用編譯器gcc或者clang編譯原始碼時在生成的可執行檔案中產生除錯資訊,並使用DWARF相關的工具dwarfdump檢視和解析可執行檔案ELF檔案格式中的除錯資訊。 使用gcc的命令可以生成dwarf格式的除錯資訊

  • gcc -g <原始碼> 編譯生成dwarf除錯格式的資訊 原始碼使用的是文章的例子。

    1

    2

    3

    4

    5

    6

    int main() {

    long = 3;

    long = 2;

    long = + b;

    = 4;

    }

    使用gcc編譯之後,可以使用readelf檢視可執行檔案中的Seciton資訊

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

[email protected]:~/Desktop/test# gcc -g test.c

[email protected]:~/Desktop/test# readelf -S a.out

There are 35 section headers, starting at offset 0x1390:

Section Headers:

[Nr] Name              Type             Address           Offset

Size              EntSize          Flags  Link  Info  Align

0]                   NULL             0000000000000000  00000000

0000000000000000  0000000000000000           0     0     0

1] .interp           PROGBITS         0000000000400238  00000238

000000000000001c  0000000000000000   A       0     0     1

2] .note.ABI-tag     NOTE             0000000000400254  00000254

0000000000000020  0000000000000000   A       0     0     4

3] .note.gnu.build-i NOTE             0000000000400274  00000274

0000000000000024  0000000000000000   A       0     0     4

4] .gnu.hash         GNU_HASH         0000000000400298  00000298

000000000000001c  0000000000000000   A       5     0     8

5] .dynsym           DYNSYM           00000000004002b8  000002b8

0000000000000048  0000000000000018   A       6     1     8

6] .dynstr           STRTAB           0000000000400300  00000300

0000000000000038  0000000000000000   A       0     0     1

7] .gnu.version      VERSYM           0000000000400338  00000338

0000000000000006  0000000000000002   A       5     0     2

8] .gnu.version_r    VERNEED          0000000000400340  00000340

0000000000000020  0000000000000000   A       6     1     8

9] .rela.dyn         RELA             0000000000400360  00000360

0000000000000018  0000000000000018   A       5     0     8

[10] .rela.plt         RELA             0000000000400378  00000378

0000000000000030  0000000000000018   A       5    12     8

[11] .init             PROGBITS         00000000004003a8  000003a8

000000000000001a  0000000000000000  AX       0     0     4

[12] .plt              PROGBITS         00000000004003d0  000003d0

0000000000000030  0000000000000010  AX       0     0     16

[13] .text             PROGBITS         0000000000400400  00000400

00000000000001a2  0000000000000000  AX       0     0     16

[14] .fini             PROGBITS         00000000004005a4  000005a4

0000000000000009  0000000000000000  AX       0     0     4

[15] .rodata           PROGBITS         00000000004005b0  000005b0

0000000000000004  0000000000000004  AM       0     0     4

[16] .eh_frame_hdr     PROGBITS         00000000004005b4  000005b4

0000000000000034  0000000000000000   A       0     0     4

[17] .eh_frame         PROGBITS         00000000004005e8  000005e8

00000000000000f4  0000000000000000   A       0     0     8

[18] .init_array       INIT_ARRAY       0000000000600e10  00000e10

0000000000000008  0000000000000000  WA       0     0     8

[19] .fini_array       FINI_ARRAY       0000000000600e18  00000e18

0000000000000008  0000000000000000  WA       0     0     8

[20] .jcr              PROGBITS         0000000000600e20  00000e20

0000000000000008  0000000000000000  WA       0     0     8

[21] .dynamic          DYNAMIC          0000000000600e28  00000e28

00000000000001d0  0000000000000010  WA       6     0     8

[22] .got              PROGBITS         0000000000600ff8  00000ff8

0000000000000008  0000000000000008  WA       0     0     8

[23] .got.plt          PROGBITS         0000000000601000  00001000

0000000000000028  0000000000000008  WA       0     0     8

[24] .data             PROGBITS         0000000000601028  00001028

0000000000000010  0000000000000000  WA       0     0     8

[25] .bss              NOBITS           0000000000601038  00001038

0000000000000008  0000000000000000  WA       0     0     1

[26] .comment          PROGBITS         0000000000000000  00001038

000000000000005d  0000000000000001  MS       0     0     1

[27] .debug_aranges    PROGBITS         0000000000000000  00001095

0000000000000030  0000000000000000           0     0     1

[28] .debug_info       PROGBITS         0000000000000000  000010c5

0000000000000082  0000000000000000           0     0     1

[29] .debug_abbrev     PROGBITS         0000000000000000  00001147

0000000000000053  0000000000000000           0     0     1

[30] .debug_line       PROGBITS         0000000000000000  0000119a

000000000000003d  0000000000000000           0     0     1

[31] .debug_str        PROGBITS         0000000000000000  000011d7

0000000000000071  0000000000000001  MS       0     0     1

[32] .shstrtab         STRTAB           0000000000000000  00001248

0000000000000148  0000000000000000           0     0     1

[33] .symtab           SYMTAB           0000000000000000  00001c50

0000000000000678  0000000000000018          34    50     8

[34] .strtab           STRTAB           0000000000000000  000022c8

0000000000000224  0000000000000000           0     0     1

可以看出其種有譯文中最重要的兩個Section,.debug_line.debug_info

  • gcc -gdwarf-2 <原始碼> 編譯生成 DWARF 2 版本除錯格式的資訊 與上面的命令類似,只是格式版本略有不同

使用dwarfdump可以檢視生成的可執行檔案的除錯資訊

  • dwarfdump -a <程式> 檢視程式中所有debug開頭的除錯資訊 由於資訊量比較大,就不貼圖了
  • dwarfdump -l <程式> 檢視程式中除錯資訊的debugline資訊圖片描述
  • dwarfdump -i <程式> 檢視程式中除錯資訊的debuginfo資訊圖片描述
  • dwarfdump -p <程式> 檢視程式中除錯資訊的debug_pubnames資訊圖片描述