開發(fā)一個Linux調(diào)試器(四):Elves和dwarves
到目前為止,你已經(jīng)偶爾聽到了關(guān)于 dwarves、調(diào)試信息、一種無需解析就可以理解源碼方式。今天我們會詳細(xì)介紹源碼級的調(diào)試信息,作為本指南后面部分使用它的準(zhǔn)備。
系列文章索引
隨著后面文章的發(fā)布,這些鏈接會逐漸生效。
- 準(zhǔn)備環(huán)境
- 斷點(diǎn)
- 寄存器和內(nèi)存
- Elves 和 dwarves
- 源碼和信號
- 源碼級逐步執(zhí)行
- 源碼級斷點(diǎn)
- 調(diào)用棧展開
- 讀取變量
- 下一步
ELF 和 DWARF 簡介
ELF 和 DWARF 可能是兩個你沒有聽說過,但可能大部分時間都在使用的組件。ELF(Executable and Linkable Format,可執(zhí)行和可鏈接格式)是 Linux 系統(tǒng)中使用最廣泛的目標(biāo)文件格式;它指定了一種存儲二進(jìn)制文件的所有不同部分的方式,例如代碼、靜態(tài)數(shù)據(jù)、調(diào)試信息以及字符串。它還告訴加載器如何加載二進(jìn)制文件并準(zhǔn)備執(zhí)行,其中包括說明二進(jìn)制文件不同部分在內(nèi)存中應(yīng)該放置的地點(diǎn),哪些位需要根據(jù)其它組件的位置固定(重分配)以及其它。在這些博文中我不會用太多篇幅介紹 ELF,但是如果你感興趣的話,你可以查看這個很好的信息圖或該標(biāo)準(zhǔn)。
DWARF是通常和 ELF 一起使用的調(diào)試信息格式。它不一定要綁定到 ELF,但它們兩者是一起發(fā)展的,一起工作得很好。這種格式允許編譯器告訴調(diào)試器最初的源代碼如何和被執(zhí)行的二進(jìn)制文件相關(guān)聯(lián)。這些信息分散到不同的 ELF 部分,每個部分都銜接有一份它自己的信息。下面不同部分的定義,信息取自這個稍有過時但非常重要的 DWARF 調(diào)試格式簡介:
- .debug_abbrev .debug_info 部分使用的縮略語
- .debug_aranges 內(nèi)存地址和編譯的映射
- .debug_frame 調(diào)用幀信息
- .debug_info 包括 DWARF 信息條目(DWARF Information Entries)(DIEs)的核心 DWARF 數(shù)據(jù)
- .debug_line 行號程序
- .debug_loc 位置描述
- .debug_macinfo 宏描述
- .debug_pubnames 全局對象和函數(shù)查找表
- .debug_pubtypes 全局類型查找表
- .debug_ranges DIEs 的引用地址范圍
- .debug_str .debug_info 使用的字符串列表
- .debug_types 類型描述
我們最關(guān)心的是 .debug_line 和 .debug_info 部分,讓我們來看一個簡單程序的 DWARF 信息。
- int main() {
- long a = 3;
- long b = 2;
- long c = a + b;
- a = 4;
- }
DWARF 行表
如果你用 -g 選項(xiàng)編譯這個程序,然后將結(jié)果傳遞給 dwarfdump 執(zhí)行,在行號部分你應(yīng)該可以看到類似這樣的東西:
- .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 [ 1, 0] 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 [ 5, 7] NS
- 0x0040069a [ 6, 1] NS
- 0x0040069c [ 6, 1] NS ET
前面幾行是一些如何理解 dump 的信息 - 主要的行號數(shù)據(jù)從以 0x00400670 開頭的行開始。實(shí)際上這是一個代碼內(nèi)存地址到文件中行列號的映射。NS 表示地址標(biāo)記一個新語句的開始,這通常用于設(shè)置斷點(diǎn)或逐步執(zhí)行。PE 表示函數(shù)序言(LCTT 譯注:在匯編語言中,function prologue 是程序開始的幾行代碼,用于準(zhǔn)備函數(shù)中用到的棧和寄存器)的結(jié)束,這對于設(shè)置函數(shù)斷點(diǎn)非常有幫助。ET 表示轉(zhuǎn)換單元的結(jié)束。信息實(shí)際上并不像這樣編碼;真正的編碼是一種非常節(jié)省空間的排序程序,可以通過執(zhí)行它來建立這些行信息。
那么,假設(shè)我們想在 variable.cpp 的第 4 行設(shè)置斷點(diǎn),我們該怎么做呢?我們查找和該文件對應(yīng)的條目,然后查找對應(yīng)的行條目,查找對應(yīng)的地址,在那里設(shè)置一個斷點(diǎn)。在我們的例子中,條目是:
- 0x00400686 [ 4,14] NS
假設(shè)我們想在地址 0x00400686 處設(shè)置斷點(diǎn)。如果你想嘗試的話你可以在已經(jīng)編寫好的調(diào)試器上手動實(shí)現(xiàn)。
反過來也是如此。如果我們已經(jīng)有了一個內(nèi)存地址 - 例如說,一個程序計(jì)數(shù)器值 - 想找到它在源碼中的位置,我們只需要從行表信息中查找最接近的映射地址并從中抓取行號。
DWARF 調(diào)試信息
.debug_info 部分是 DWARF 的核心。它給我們關(guān)于我們程序中存在的類型、函數(shù)、變量、希望和夢想的信息。這部分的基本單元是 DWARF 信息條目(DWARF Information Entry),我們親切地稱之為 DIEs。一個 DIE 包括能告訴你正在展現(xiàn)什么樣的源碼級實(shí)體的標(biāo)簽,后面跟著一系列該實(shí)體的屬性。這是我上面展示的簡單事例程序的 .debug_info 部分:
- .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),實(shí)際上是一個包括了所有 #includes 和類似語句的源文件。下面是帶含義注釋的屬性:
- DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final) <-- 產(chǎn)生該二進(jìn)制文件的編譯器
- DW_AT_language DW_LANG_C_plus_plus <-- 原編程語言
- DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp <-- 該 CU 表示的文件名稱
- DW_AT_stmt_list 0x00000000 <-- 跟蹤該 CU 的行表偏移
- DW_AT_comp_dir /super/secret/path/MiniDbg/build <-- 編譯目錄
- DW_AT_low_pc 0x00400670 <-- 該 CU 的代碼起始
- DW_AT_high_pc 0x0040069c <-- 該 CU 的代碼結(jié)尾
其它的 DIEs 遵循類似的模式,你也很可能推測出不同屬性的含義。
現(xiàn)在我們可以根據(jù)新學(xué)到的 DWARF 知識嘗試和解決一些實(shí)際問題。
當(dāng)前我在哪個函數(shù)?
假設(shè)我們有一個程序計(jì)數(shù)器值然后想找到當(dāng)前我們在哪一個函數(shù)。一個解決該問題的簡單算法:
- 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
這對于很多目的都有效,但如果有成員函數(shù)或者內(nèi)聯(lián)(inline),就會變得更加復(fù)雜。假如有內(nèi)聯(lián),一旦我們找到其范圍包括我們的程序計(jì)數(shù)器(PC)的函數(shù),我們需要遞歸遍歷該 DIE 的所有孩子檢查有沒有內(nèi)聯(lián)函數(shù)能更好地匹配。在我的代碼中,我不會為該調(diào)試器處理內(nèi)聯(lián),但如果你想要的話你可以添加該功能。
如何在一個函數(shù)上設(shè)置斷點(diǎn)?
再次說明,這取決于你是否想要支持成員函數(shù)、命名空間以及類似的東西。對于簡單的函數(shù)你只需要迭代遍歷不同編譯單元中的函數(shù)直到你找到一個合適的名字。如果你的編譯器能夠填充 .debug_pubnames 部分,你可以更高效地做到這點(diǎn)。
一旦找到了函數(shù),你可以在 DW_AT_low_pc 給定的內(nèi)存地址設(shè)置一個斷點(diǎn)。不過那會在函數(shù)序言處中斷,但更合適的是在用戶代碼處中斷。由于行表信息可以指定序言的結(jié)束的內(nèi)存地址,你只需要在行表中查找 DW_AT_low_pc 的值,然后一直讀取直到被標(biāo)記為序言結(jié)束的條目。一些編譯器不會輸出這些信息,因此另一種方式是在該函數(shù)第二行條目指定的地址處設(shè)置斷點(diǎn)。
假如我們想在我們示例程序中的 main 函數(shù)設(shè)置斷點(diǎn)。我們查找名為 main 的函數(shù),獲取到它的 DIE:
- < 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)
這告訴我們函數(shù)從 0x00400670 開始。如果我們在行表中查找這個,我們可以獲得條目:
- 0x00400670 [ 1, 0] NS uri: "/super/secret/path/MiniDbg/examples/variable.cpp"
我們希望跳過序言,因此我們再讀取一個條目:
- 0x00400676 [ 2,10] NS PE
Clang 在這個條目中包括了序言結(jié)束標(biāo)記,因此我們知道在這里停止,然后在地址 0x00400676 處設(shè)一個斷點(diǎn)。
我如何讀取一個變量的內(nèi)容?
讀取變量可能非常復(fù)雜。它們是難以捉摸的東西,可能在整個函數(shù)中移動、保存在寄存器中、被放置于內(nèi)存、被優(yōu)化掉、隱藏在角落里,等等。幸運(yùn)的是我們的簡單示例是真的很簡單。如果我們想讀取變量 a 的內(nèi)容,我們需要看它的 DW_AT_location 屬性:
- DW_AT_location DW_OP_fbreg -8
這告訴我們內(nèi)容被保存在以棧幀基(base of the stack frame)偏移為 -8 的地方。為了找到棧幀基,我們查找所在函數(shù)的 DW_AT_frame_base 屬性。
- DW_AT_frame_base DW_OP_reg6
從 System V x86_64 ABI 我們可以知道 reg6 在 x86 中是幀指針寄存器。現(xiàn)在我們讀取幀指針的內(nèi)容,從中減去 8,就找到了我們的變量。如果我們知道它具體是什么,我們還需要看它的類型:
- < 2><0x0000004c> DW_TAG_variable
- DW_AT_name a
- DW_AT_type <0x0000007e>
如果我們在調(diào)試信息中查找該類型,我們得到下面的 DIE:
- < 1><0x0000007e> DW_TAG_base_type
- DW_AT_name long int
- DW_AT_encoding DW_ATE_signed
- DW_AT_byte_size 0x00000008
這告訴我們該類型是 8 字節(jié)(64 位)有符號整型,因此我們可以繼續(xù)并把這些字節(jié)解析為 int64_t 并向用戶顯示。
當(dāng)然,類型可能比那要復(fù)雜得多,因?yàn)樗鼈円軌虮硎绢愃?C++ 的類型,但是這能給你它們?nèi)绾喂ぷ鞯幕菊J(rèn)識。
再次回到幀基(frame base),Clang 可以通過幀指針寄存器跟蹤幀基。最近版本的 GCC 傾向于使用 DW_OP_call_frame_cfa,它包括解析 .eh_frame ELF 部分,那是一個我不會去寫的另外一篇完全不同的文章。如果你告訴 GCC 使用 DWARF 2 而不是最近的版本,它會傾向于輸出位置列表,這更便于閱讀:
- 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
位置列表取決于程序計(jì)數(shù)器所處的位置給出不同的位置。這個例子告訴我們?nèi)绻绦蛴?jì)數(shù)器是在 DW_AT_low_pc 偏移量為 0x0 的位置,那么幀基就在和寄存器 7 中保存的值偏移量為 8 的位置,如果它是在 0x1 和 0x4 之間,那么幀基就在和相同位置偏移量為 16 的位置,以此類推。
休息一會
這里有很多的信息需要你的大腦消化,但好消息是在后面的幾篇文章中我們會用一個庫替我們完成這些艱難的工作。理解概念仍然很有幫助,尤其是當(dāng)出現(xiàn)錯誤或者你想支持一些你使用的 DWARF 庫所沒有實(shí)現(xiàn)的 DWARF 概念時。
如果你想了解更多關(guān)于 DWARF 的內(nèi)容,那么你可以從這里獲取其標(biāo)準(zhǔn)。在寫這篇博客時,剛剛發(fā)布了 DWARF 5,但更普遍支持 DWARF 4。