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