linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...
自己動(dòng)手編寫一個(gè)Linux調(diào)試器系列之4 ELF文件格式與DWARF調(diào)試格式 by lantie@15PB
在上一節(jié)中,你已經(jīng)聽說了DWARF調(diào)試格式,它是程序的調(diào)試信息,是一種可以更好理解源碼的方式,而不只是解析程序。今天我們將討論源代碼級(jí)調(diào)試信息的細(xì)節(jié),以準(zhǔn)備在本教程后面的部分中使用它。
系列索引準(zhǔn)備工作
斷點(diǎn)的設(shè)置
寄存器和內(nèi)存
ELF文件格式和DWARVF調(diào)試格式
源碼和信號(hào)
源碼級(jí)單步
源碼級(jí)斷點(diǎn)
堆棧解除
處理變量
高級(jí)主題
ELF文件格式與DWARF格式簡(jiǎn)介
ELF和DWARF是你可能沒有聽說過的兩個(gè)概念信息,但可能已經(jīng)使用很長(zhǎng)時(shí)間了。 ELF(可執(zhí)行和可鏈接格式)是Linux世界中使用最廣泛的對(duì)象文件格式; 它指定了一種存儲(chǔ)二進(jìn)制文件的所有不同部分的方式,如代碼,靜態(tài)數(shù)據(jù),調(diào)試信息和字符串。 它還告訴加載程序如何取得二進(jìn)制并準(zhǔn)備好執(zhí)行,這涉及二進(jìn)制文件的不同部分應(yīng)該放置在內(nèi)存中,哪些部分需要根據(jù)其他信息(重定位)等的位置來修復(fù)。 我不會(huì)在這些帖子中覆蓋更多ELF,但如果你有興趣,可以看看這個(gè)漂亮的信息圖表或標(biāo)準(zhǔn)。
DWARF是ELF最常用的調(diào)試信息格式。它不一定與ELF相關(guān),但兩者是一起發(fā)展的,在開發(fā)中一起使用也非常好。該格式允許編譯器告訴調(diào)試器程序源代碼如何與將執(zhí)行的二進(jìn)制文件相互關(guān)系。該信息分為不同的ELF部分,每個(gè)部分都有自己的信息來中繼。以下是定義的不同部分,取自于非常詳細(xì)的DWARF調(diào)試格式介紹:
.debug_abbrev .debug_info部分中使用的縮寫
.debug_aranges 內(nèi)存地址和編譯之間的映射
.debug_frame 調(diào)用幀信息
.debug_info 包含DWARF調(diào)試信息項(xiàng)(DIE)的核心DWARF數(shù)據(jù)
.debug_line 行號(hào)程序
.debug_loc 位置說明
.debug_macinfo 宏描述
.debug_pubnames 全局對(duì)象和函數(shù)的查找表
.debug_pubtypes 全局類型的查找表
.debug_ranges DIE引用的地址范圍
.debug_str .debug_info使用的字符串表
.debug_types 類型說明
我們對(duì).debug_line和.debug_info部分最感興趣,所以讓我們看看一些DWARF的簡(jiǎn)單程序。
int main() {
long a = 3;
long b = 2;
long c = a + b;
a = 4;
}
DWARF debug_line表信息
如果你使用編譯器(gcc 或 clang)的-g選項(xiàng)編譯此程序,并通過dwarfdump運(yùn)行結(jié)果,則應(yīng)該看到類似于行號(hào)的部分:
.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
[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
第一部分描述部分是關(guān)于如何理解下面顯示列表的一些信息 - 表信息主行號(hào)數(shù)據(jù)從0x00400670開始。本質(zhì)上,它是將一個(gè)代碼內(nèi)存地址映射到一些文件中的行和列號(hào)。 NS表示該地址標(biāo)志著新語句的開始,這通常用于設(shè)置斷點(diǎn)或步進(jìn)。 PE標(biāo)記函數(shù)開始的結(jié)尾,這有助于設(shè)置函數(shù)入口斷點(diǎn)。 ET標(biāo)示翻譯單元的結(jié)尾。實(shí)際信息上并不是像這樣編碼的;真正的編碼是一種非常節(jié)省空間的程序,可以執(zhí)行這些程序來建立這個(gè)行信息。
那么說,我們想在variable.cpp的第4行設(shè)置一個(gè)斷點(diǎn),我們?cè)撛趺醋?#xff1f;我們查找與該文件相對(duì)應(yīng)的條目,然后查找相關(guān)的行條目,查找與之對(duì)應(yīng)的地址,并在其中設(shè)置斷點(diǎn)。在我們的例子中,這是這個(gè)條目:
0x00400686 [ 4,14] NS
所以我們要在地址0x00400686設(shè)置一個(gè)斷點(diǎn)。你可以用你已經(jīng)寫過的調(diào)試器手工完成,如果你想嘗試一下。
相反的工作也是如此。如果我們有一個(gè)內(nèi)存位置 - 例如一個(gè)程序計(jì)數(shù)器值,并且想要找出源代碼中的哪個(gè)位置,我們只需在行表信息中找到最接近的映射地址,并從中獲取行。
DWARF debug_info信息
.debug_info部分是DWARF的核心。它給了我們有關(guān)我們的程序中存在的類型,函數(shù),變量,希望和想要得到的信息。本節(jié)的基本單位是DWARF信息條目(DWARF Information Entry),簡(jiǎn)稱為DIE。 DIE包含一個(gè)標(biāo)簽,告訴您正在表示什么樣的源代碼級(jí)實(shí)體,后面是一系列適用于該實(shí)體的屬性。這是上面發(fā)布的簡(jiǎn)單示例程序的.debug_info部分:
.debug_info
COMPILE_UNIT:
< 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),它基本上是一個(gè)源文件,其中包含所有#includes,并且這樣解析。以下是它們的含義注釋的屬性:
DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final)
this binary
DW_AT_language DW_LANG_C_plus_plus
DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp
this CU represents
DW_AT_stmt_list 0x00000000
which tracks this CU
DW_AT_comp_dir /super/secret/path/MiniDbg/build
DW_AT_low_pc 0x00400670
this CU
DW_AT_high_pc 0x0040069c
this CU
其他DIE遵循類似的方案,您可以直觀地看出不同屬性的含義。
現(xiàn)在我們可以嘗試用我們新發(fā)現(xiàn)的DWARF知識(shí)解決一些實(shí)際問題。
使用 DWARF 分析函數(shù)
如果我們有一個(gè)程序計(jì)數(shù)器值,并想獲取PC所在函數(shù)的信息。一個(gè)簡(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
這可以用于許多情況,但是在成員函數(shù)和內(nèi)聯(lián)函數(shù)存在的情況下,事情會(huì)變得更加困難。 例如,使用內(nèi)聯(lián)函數(shù),一旦找到范圍包含我們的PC的函數(shù),我們將需要對(duì)該DIE的子項(xiàng)進(jìn)行遞歸,以查看是否存在更好匹配的內(nèi)聯(lián)函數(shù)。我不會(huì)在我的調(diào)試器代碼中處理內(nèi)聯(lián)函數(shù),但如果你喜歡,你可以添加對(duì)此的支持。
如何在函數(shù)上設(shè)置斷點(diǎn)
再次申明,如果想要支持成員函數(shù),命名空間等特性可能需要更高級(jí)的做法。 對(duì)于簡(jiǎn)單的函數(shù),您可以在不同的編譯單元中迭代函數(shù),直到找到具有正確名稱的函數(shù)。 如果您的編譯器足夠填寫.debug_pubnames部分,您可以更有效地執(zhí)行此操作。
一旦找到該函數(shù),您可以在DW_AT_low_pc給定的內(nèi)存地址上設(shè)置一個(gè)斷點(diǎn)。 但是,在函數(shù)開始時(shí)會(huì)中斷,但最好在用戶代碼開始時(shí)中斷。 由于行表信息可以指定指定函數(shù)開頭結(jié)束的內(nèi)存地址,因此您可以直接在行表中查找DW_AT_low_pc的值,然后繼續(xù)閱讀,直到找到標(biāo)記為函數(shù)開頭結(jié)束的條目。 有些編譯器不會(huì)輸出這個(gè)信息,所以另外一個(gè)選擇是在該函數(shù)的第二行條目給出的地址上設(shè)置一個(gè)斷點(diǎn)。
假設(shè)我們要在我們的示例程序中設(shè)置一個(gè)斷點(diǎn)。 我們搜索main函數(shù),并得到這個(gè)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開始。 如果我們?cè)诰€表中查看,我們得到這個(gè)條目:
0x00400670 [ 1, 0] NS uri: "/super/secret/path/MiniDbg/examples/variable.cpp"
我們想跳過開頭,所以我們先讀一個(gè)條目:
0x00400676 [ 2,10] NS PE
Clang在這個(gè)條目中包含了代碼開頭結(jié)束標(biāo)志,所以我們知道在這里停下來,并在地址0x00400676上設(shè)置一個(gè)斷點(diǎn)。
如何讀取變量的內(nèi)容
讀取變量可能非常復(fù)雜。 變量是一個(gè)難以捉摸的東西,可以在整個(gè)函數(shù)中存在,可以放在寄存器中,放在內(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)存在距離堆棧幀基址的-8的偏移處。 要找出這個(gè)基址的位置,我們來看看包含函數(shù)的DW_AT_frame_base屬性。
DW_AT_frame_base DW_OP_reg6
在x86上的reg6是棧幀指針寄存器,由System V x86_64 ABI指定。現(xiàn)在我們讀幀指針的內(nèi)容,從中減去8,我們已經(jīng)找到了變量。如果我們想弄明白這個(gè)問題,我們需要看看它的類型:
< 2><0x0000004c> DW_TAG_variable
DW_AT_name a
DW_AT_type <0x0000007e>
如果我們?cè)谡{(diào)試信息中查找這個(gè)類型,就會(huì)得到這個(gè)DIE:
< 1><0x0000007e> DW_TAG_base_type
DW_AT_name long int
DW_AT_encoding DW_ATE_signed
DW_AT_byte_size 0x00000008
這告訴我們類型是一個(gè)8字節(jié)(64位)的有符號(hào)整數(shù)類型,因此我們可以繼續(xù)將這些字節(jié)解釋為int64_t并將其顯示給用戶。
當(dāng)然,類型可以比這個(gè)復(fù)雜得多,因?yàn)樗鼈儽仨毮軌虮磉_(dá)諸如c++類型之類的東西,但這給了你一個(gè)關(guān)于它們?nèi)绾喂ぷ鞯幕靖拍睢?/p>
回到該棧幀的基址,Clang編譯器可以比較好的跟蹤到幀指針寄存器的幀基址。 最近版本的GCC傾向于喜歡DW_OP_call_frame_cfa,它涉及解析.eh_frame ELF部分,這是一個(gè)完全不同的文章,在這里我就不詳述。 如果你使用GCC的DWARF 2版本而不是更新的版本,命令是gcc -gdwarf-2 那么它將傾向于輸出位置列表,這更容易閱讀:
DW_AT_frame_base
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ù)程序計(jì)數(shù)器的位置給出不同的位置。 這個(gè)例子是說,如果PC在DW_AT_low_pc處于0x0的偏移量的情況下,那么棧幀基地址是從寄存器7中存儲(chǔ)的值加偏移量8,如果它在0x1到0x4之間,那么它的偏移距離一樣都是16,等等。
總結(jié)一下
這節(jié)包含了很多DWARF信息需要好好吸收一下才行。不要擔(dān)心!有個(gè)好消息,就是在接下來的幾個(gè)章節(jié)中,我們將有一個(gè)庫幫我們完成最麻煩的工作。了解了DWARF的概念,特別是在出現(xiàn)問題或希望支持一些DWARF庫的情況下,仍然有用。
如果您想了解更多關(guān)于DWARF的信息,那么你可以在此獲取標(biāo)準(zhǔn)文檔。 在撰寫本文時(shí),DWARF 5剛剛被發(fā)布,但DWARF 4更受歡迎。
說明
自己動(dòng)手實(shí)踐一下
本節(jié)內(nèi)容是整個(gè)系列最枯燥的一章,全篇都是在講述DWARF調(diào)試格式的內(nèi)容。我們可以使用編譯器gcc或者clang編譯源碼時(shí)在生成的可執(zhí)行文件中產(chǎn)生調(diào)試信息,并使用DWARF相關(guān)的工具dwarfdump查看和解析可執(zhí)行文件ELF文件格式中的調(diào)試信息。
使用gcc的命令可以生成dwarf格式的調(diào)試信息
gcc -g 編譯生成dwarf調(diào)試格式的信息
源碼使用的是文章的例子。int main() {
long a = 3;
long b = 2;
long c = a + b;
a = 4;
}使用gcc編譯之后,可以使用readelf查看可執(zhí)行文件中的Seciton信息
root@ubuntu:~/Desktop/test# gcc -g test.c
root@ubuntu:~/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
可以看出其種有譯文中最重要的兩個(gè)Section,.debug_line和.debug_info
gcc -gdwarf-2 編譯生成 DWARF 2 版本調(diào)試格式的信息
與上面的命令類似,只是格式版本略有不同
使用dwarfdump可以查看生成的可執(zhí)行文件的調(diào)試信息
dwarfdump -a 查看程序中所有debug開頭的調(diào)試信息
由于信息量比較大,就不貼圖了
dwarfdump -l 查看程序中調(diào)試信息的debugline信息
dwarfdump -i 查看程序中調(diào)試信息的debuginfo信息
dwarfdump -p 查看程序中調(diào)試信息的debug_pubnames信息
總結(jié)
以上是生活随笔為你收集整理的linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言省2全民,C语言省试题(2-数据类
- 下一篇: android 时间管理app,时间管理