1. 程式人生 > >開發一個 Linux 偵錯程式(四):Elves 和 dwarves

開發一個 Linux 偵錯程式(四):Elves 和 dwarves

https://linux.cn/article-8719-1.html

到目前為止,你已經偶爾聽到了關於 dwarves、除錯資訊、一種無需解析就可以理解原始碼方式。今天我們會詳細介紹原始碼級的除錯資訊,作為本指南後面部分使用它的準備。

系列文章索引

隨著後面文章的釋出,這些連結會逐漸生效。

  1. 準備環境
  2. 斷點
  3. 暫存器和記憶體
  4. Elves 和 dwarves
  5. 原始碼和訊號
  6. 原始碼級逐步執行
  7. 原始碼級斷點
  8. 呼叫棧展開
  9. 讀取變數
  10. 下一步

ELF 和 DWARF 簡介

ELF 和 DWARF 可能是兩個你沒有聽說過,但可能大部分時間都在使用的元件。ELF(

Executable and Linkable Format,可執行和可連結格式)是 Linux 系統中使用最廣泛的目標檔案格式;它指定了一種儲存二進位制檔案的所有不同部分的方式,例如程式碼、靜態資料、除錯資訊以及字串。它還告訴載入器如何載入二進位制檔案並準備執行,其中包括說明二進位制檔案不同部分在記憶體中應該放置的地點,哪些位需要根據其它元件的位置固定(重分配)以及其它。在這些博文中我不會用太多篇幅介紹 ELF,但是如果你感興趣的話,你可以檢視這個很好的資訊圖該標準

DWARF是通常和 ELF 一起使用的除錯資訊格式。它不一定要繫結到 ELF,但它們兩者是一起發展的,一起工作得很好。這種格式允許編譯器告訴偵錯程式最初的原始碼如何和被執行的二進位制檔案相關聯。這些資訊分散到不同的 ELF 部分,每個部分都銜接有一份它自己的資訊。下面不同部分的定義,資訊取自這個稍有過時但非常重要的

DWARF 除錯格式簡介

  • .debug_abbrev .debug_info 部分使用的縮略語
  • .debug_aranges 記憶體地址和編譯的對映
  • .debug_frame 呼叫幀資訊
  • .debug_info 包括 DWARF 資訊條目(DWARF Information Entries)(DIEs)的核心 DWARF 資料
  • .debug_line 行號程式
  • .debug_loc 位置描述
  • .debug_macinfo 巨集描述
  • .debug_pubnames
     全域性物件和函式查詢表
  • .debug_pubtypes 全域性型別查詢表
  • .debug_ranges DIEs 的引用地址範圍
  • .debug_str .debug_info 使用的字串列表
  • .debug_types 型別描述

我們最關心的是 .debug_line 和 .debug_info 部分,讓我們來看一個簡單程式的 DWARF 資訊。

 
  1. int main() {
  2. long a = 3;
  3. long b = 2;
  4. long c = a + b;
  5. a = 4;
  6. }

DWARF 行表

如果你用 -g 選項編譯這個程式,然後將結果傳遞給 dwarfdump 執行,在行號部分你應該可以看到類似這樣的東西:

 
  1. .debug_line: line number info for a single cu
  2. Source lines (from CU-DIE at .debug_info offset 0x0000000b):
  3.  
  4. NS new statement, BB new basic block, ET end of text sequence
  5. PE prologue end, EB epilogue begin
  6. IS=val ISA number, DI=val discriminator value
  7. <pc> [lno,col] NS BB ET PE EB IS= DI= uri: "filepath"
  8. 0x00400670 [ 1, 0] NS uri: "/home/simon/play/MiniDbg/examples/variable.cpp"
  9. 0x00400676 [ 2,10] NS PE
  10. 0x0040067e [ 3,10] NS
  11. 0x00400686 [ 4,14] NS
  12. 0x0040068a [ 4,16]
  13. 0x0040068e [ 4,10]
  14. 0x00400692 [ 5, 7] NS
  15. 0x0040069a [ 6, 1] NS
  16. 0x0040069c [ 6, 1] NS ET

前面幾行是一些如何理解 dump 的資訊 - 主要的行號資料從以 0x00400670 開頭的行開始。實際上這是一個程式碼記憶體地址到檔案中行列號的對映。NS 表示地址標記一個新語句的開始,這通常用於設定斷點或逐步執行。PE 表示函式序言(LCTT 譯註:在組合語言中,function prologue 是程式開始的幾行程式碼,用於準備函式中用到的棧和暫存器)的結束,這對於設定函式斷點非常有幫助。ET 表示轉換單元的結束。資訊實際上並不像這樣編碼;真正的編碼是一種非常節省空間的排序程式,可以通過執行它來建立這些行資訊。

那麼,假設我們想在 variable.cpp 的第 4 行設定斷點,我們該怎麼做呢?我們查詢和該檔案對應的條目,然後查詢對應的行條目,查詢對應的地址,在那裡設定一個斷點。在我們的例子中,條目是:

 
  1. 0x00400686 [ 4,14] NS

假設我們想在地址 0x00400686 處設定斷點。如果你想嘗試的話你可以在已經編寫好的偵錯程式上手動實現。

反過來也是如此。如果我們已經有了一個記憶體地址 - 例如說,一個程式計數器值 - 想找到它在原始碼中的位置,我們只需要從行表資訊中查詢最接近的對映地址並從中抓取行號。

DWARF 除錯資訊

.debug_info 部分是 DWARF 的核心。它給我們關於我們程式中存在的型別、函式、變數、希望和夢想的資訊。這部分的基本單元是 DWARF 資訊條目(DWARF Information Entry),我們親切地稱之為 DIEs。一個 DIE 包括能告訴你正在展現什麼樣的原始碼級實體的標籤,後面跟著一系列該實體的屬性。這是我上面展示的簡單事例程式的 .debug_info 部分:

 
  1. .debug_info
  2.  
  3. COMPILE_UNIT<header overall offset = 0x00000000>:
  4. < 0><0x0000000b> DW_TAG_compile_unit
  5. DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final)
  6. DW_AT_language DW_LANG_C_plus_plus
  7. DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp
  8. DW_AT_stmt_list 0x00000000
  9. DW_AT_comp_dir /super/secret/path/MiniDbg/build
  10. DW_AT_low_pc 0x00400670
  11. DW_AT_high_pc 0x0040069c
  12.  
  13. LOCAL_SYMBOLS:
  14. < 1><0x0000002e> DW_TAG_subprogram
  15. DW_AT_low_pc 0x00400670
  16. DW_AT_high_pc 0x0040069c
  17. DW_AT_frame_base DW_OP_reg6
  18. DW_AT_name main
  19. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  20. DW_AT_decl_line 0x00000001
  21. DW_AT_type <0x00000077>
  22. DW_AT_external yes(1)
  23. < 2><0x0000004c> DW_TAG_variable
  24. DW_AT_location DW_OP_fbreg -8
  25. DW_AT_name a
  26. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  27. DW_AT_decl_line 0x00000002
  28. DW_AT_type <0x0000007e>
  29. < 2><0x0000005a> DW_TAG_variable
  30. DW_AT_location DW_OP_fbreg -16
  31. DW_AT_name b
  32. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  33. DW_AT_decl_line 0x00000003
  34. DW_AT_type <0x0000007e>
  35. < 2><0x00000068> DW_TAG_variable
  36. DW_AT_location DW_OP_fbreg -24
  37. DW_AT_name c
  38. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  39. DW_AT_decl_line 0x00000004
  40. DW_AT_type <0x0000007e>
  41. < 1><0x00000077> DW_TAG_base_type
  42. DW_AT_name int
  43. DW_AT_encoding DW_ATE_signed
  44. DW_AT_byte_size 0x00000004
  45. < 1><0x0000007e> DW_TAG_base_type
  46. DW_AT_name long int
  47. DW_AT_encoding DW_ATE_signed
  48. DW_AT_byte_size 0x00000008

第一個 DIE 表示一個編譯單元(CU),實際上是一個包括了所有 #includes 和類似語句的原始檔。下面是帶含義註釋的屬性:

 
  1. DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final) <-- 產生該二進位制檔案的編譯器
  2. DW_AT_language DW_LANG_C_plus_plus <-- 原程式語言
  3. DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp <-- 該 CU 表示的檔名稱
  4. DW_AT_stmt_list 0x00000000 <-- 跟蹤該 CU 的行表偏移
  5. DW_AT_comp_dir /super/secret/path/MiniDbg/build <-- 編譯目錄
  6. DW_AT_low_pc 0x00400670 <-- 該 CU 的程式碼起始
  7. DW_AT_high_pc 0x0040069c <-- 該 CU 的程式碼結尾

其它的 DIEs 遵循類似的模式,你也很可能推測出不同屬性的含義。

現在我們可以根據新學到的 DWARF 知識嘗試和解決一些實際問題。

當前我在哪個函式?

假設我們有一個程式計數器值然後想找到當前我們在哪一個函式。一個解決該問題的簡單演算法:

 
  1. for each compile unit:
  2. if the pc is between DW_AT_low_pc and DW_AT_high_pc:
  3. for each function in the compile unit:
  4. if the pc is between DW_AT_low_pc and DW_AT_high_pc:
  5. return function information

這對於很多目的都有效,但如果有成員函式或者內聯(inline),就會變得更加複雜。假如有內聯,一旦我們找到其範圍包括我們的程式計數器(PC)的函式,我們需要遞迴遍歷該 DIE 的所有孩子檢查有沒有行內函數能更好地匹配。在我的程式碼中,我不會為該偵錯程式處理內聯,但如果你想要的話你可以新增該功能。

如何在一個函式上設定斷點?

再次說明,這取決於你是否想要支援成員函式、名稱空間以及類似的東西。對於簡單的函式你只需要迭代遍歷不同編譯單元中的函式直到你找到一個合適的名字。如果你的編譯器能夠填充 .debug_pubnames 部分,你可以更高效地做到這點。

一旦找到了函式,你可以在 DW_AT_low_pc 給定的記憶體地址設定一個斷點。不過那會在函式序言處中斷,但更合適的是在使用者程式碼處中斷。由於行表資訊可以指定序言的結束的記憶體地址,你只需要在行表中查詢 DW_AT_low_pc 的值,然後一直讀取直到被標記為序言結束的條目。一些編譯器不會輸出這些資訊,因此另一種方式是在該函式第二行條目指定的地址處設定斷點。

假如我們想在我們示例程式中的 main 函式設定斷點。我們查詢名為 main 的函式,獲取到它的 DIE:

 
  1. < 1><0x0000002e> DW_TAG_subprogram
  2. DW_AT_low_pc 0x00400670
  3. DW_AT_high_pc 0x0040069c
  4. DW_AT_frame_base DW_OP_reg6
  5. DW_AT_name main
  6. DW_AT_decl_file 0x00000001 /super/secret/path/MiniDbg/examples/variable.cpp
  7. DW_AT_decl_line 0x00000001
  8. DW_AT_type <0x00000077>
  9. DW_AT_external yes(1)

這告訴我們函式從 0x00400670 開始。如果我們在行表中查詢這個,我們可以獲得條目:

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

我們希望跳過序言,因此我們再讀取一個條目:

 
  1. 0x00400676 [ 2,10] NS PE

Clang 在這個條目中包括了序言結束標記,因此我們知道在這裡停止,然後在地址 0x00400676 處設一個斷點。

我如何讀取一個變數的內容?

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

 
  1. DW_AT_location DW_OP_fbreg -8

這告訴我們內容被儲存在以棧幀基(base of the stack frame)偏移為 -8 的地方。為了找到棧幀基,我們查詢所在函式的 DW_AT_frame_base 屬性。

 
  1. DW_AT_frame_base DW_OP_reg6

System V x86_64 ABI 我們可以知道 reg6 在 x86 中是幀指標暫存器。現在我們讀取幀指標的內容,從中減去 8,就找到了我們的變數。如果我們知道它具體是什麼,我們還需要看它的型別:

 
  1. < 2><0x0000004c> DW_TAG_variable
  2. DW_AT_name a
  3. DW_AT_type <0x0000007e>

如果我們在除錯資訊中查詢該型別,我們得到下面的 DIE:

 
  1. < 1><0x0000007e> DW_TAG_base_type
  2. DW_AT_name long int
  3. DW_AT_encoding DW_ATE_signed
  4. DW_AT_byte_size 0x00000008

這告訴我們該型別是 8 位元組(64 位)有符號整型,因此我們可以繼續並把這些位元組解析為 int64_t 並向用戶顯示。

當然,型別可能比那要複雜得多,因為它們要能夠表示類似 C++ 的型別,但是這能給你它們如何工作的基本認識。

再次回到幀基(frame base),Clang 可以通過幀指標暫存器跟蹤幀基。最近版本的 GCC 傾向於使用 DW_OP_call_frame_cfa,它包括解析 .eh_frame ELF 部分,那是一個我不會去寫的另外一篇完全不同的文章。如果你告訴 GCC 使用 DWARF 2 而不是最近的版本,它會傾向於輸出位置列表,這更便於閱讀:

 
  1. DW_AT_frame_base <loclist at offset 0x00000000 with 4 entries follows>
  2. low-off : 0x00000000 addr 0x00400696 high-off 0x00000001 addr 0x00400697>DW_OP_breg7+8
  3. low-off : 0x00000001 addr 0x00400697 high-off 0x00000004 addr 0x0040069a>DW_OP_breg7+16
  4. low-off : 0x00000004 addr 0x0040069a high-off 0x00000031 addr 0x004006c7>DW_OP_breg6+16
  5. low-off : 0x00000031 addr 0x004006c7 high-off 0x00000032 addr 0x004006c8>DW_OP_breg7+8

位置列表取決於程式計數器所處的位置給出不同的位置。這個例子告訴我們如果程式計數器是在 DW_AT_low_pc 偏移量為 0x0 的位置,那麼幀基就在和暫存器 7 中儲存的值偏移量為 8 的位置,如果它是在 0x1 和 0x4 之間,那麼幀基就在和相同位置偏移量為 16 的位置,以此類推。

休息一會

這裡有很多的資訊需要你的大腦消化,但好訊息是在後面的幾篇文章中我們會用一個庫替我們完成這些艱難的工作。理解概念仍然很有幫助,尤其是當出現錯誤或者你想支援一些你使用的 DWARF 庫所沒有實現的 DWARF 概念時。

如果你想了解更多關於 DWARF 的內容,那麼你可以從這裡獲取其標準。在寫這篇部落格時,剛剛釋出了 DWARF 5,但更普遍支援 DWARF 4。


via: https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/