linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...
自己動手編寫一個Linux調試器系列之4 ELF文件格式與DWARF調試格式 by lantie@15PB
在上一節中,你已經聽說了DWARF調試格式,它是程序的調試信息,是一種可以更好理解源碼的方式,而不只是解析程序。今天我們將討論源代碼級調試信息的細節,以準備在本教程后面的部分中使用它。
系列索引準備工作
斷點的設置
寄存器和內存
ELF文件格式和DWARVF調試格式
源碼和信號
源碼級單步
源碼級斷點
堆棧解除
處理變量
高級主題
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的簡單程序。
int main() {
long a = 3;
long b = 2;
long c = a + b;
a = 4;
}
DWARF debug_line表信息
如果你使用編譯器(gcc 或 clang)的-g選項編譯此程序,并通過dwarfdump運行結果,則應該看到類似于行號的部分:
.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
第一部分描述部分是關于如何理解下面顯示列表的一些信息 - 表信息主行號數據從0x00400670開始。本質上,它是將一個代碼內存地址映射到一些文件中的行和列號。 NS表示該地址標志著新語句的開始,這通常用于設置斷點或步進。 PE標記函數開始的結尾,這有助于設置函數入口斷點。 ET標示翻譯單元的結尾。實際信息上并不是像這樣編碼的;真正的編碼是一種非常節省空間的程序,可以執行這些程序來建立這個行信息。
那么說,我們想在variable.cpp的第4行設置一個斷點,我們該怎么做?我們查找與該文件相對應的條目,然后查找相關的行條目,查找與之對應的地址,并在其中設置斷點。在我們的例子中,這是這個條目:
0x00400686 [ 4,14] NS
所以我們要在地址0x00400686設置一個斷點。你可以用你已經寫過的調試器手工完成,如果你想嘗試一下。
相反的工作也是如此。如果我們有一個內存位置 - 例如一個程序計數器值,并且想要找出源代碼中的哪個位置,我們只需在行表信息中找到最接近的映射地址,并從中獲取行。
DWARF debug_info信息
.debug_info部分是DWARF的核心。它給了我們有關我們的程序中存在的類型,函數,變量,希望和想要得到的信息。本節的基本單位是DWARF信息條目(DWARF Information Entry),簡稱為DIE。 DIE包含一個標簽,告訴您正在表示什么樣的源代碼級實體,后面是一系列適用于該實體的屬性。這是上面發布的簡單示例程序的.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
第一個DIE表示一個編譯單元(CU),它基本上是一個源文件,其中包含所有#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遵循類似的方案,您可以直觀地看出不同屬性的含義。
現在我們可以嘗試用我們新發現的DWARF知識解決一些實際問題。
使用 DWARF 分析函數
如果我們有一個程序計數器值,并想獲取PC所在函數的信息。一個簡單的算法是:
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><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開始。 如果我們在線表中查看,我們得到這個條目:
0x00400670 [ 1, 0] NS uri: "/super/secret/path/MiniDbg/examples/variable.cpp"
我們想跳過開頭,所以我們先讀一個條目:
0x00400676 [ 2,10] NS PE
Clang在這個條目中包含了代碼開頭結束標志,所以我們知道在這里停下來,并在地址0x00400676上設置一個斷點。
如何讀取變量的內容
讀取變量可能非常復雜。 變量是一個難以捉摸的東西,可以在整個函數中存在,可以放在寄存器中,放在內存中,還可以被優化,隱藏在角落里。幸運的是,我們簡單的例子是,很簡單。 如果我們想要讀取變量a的內容,我們來看看它的DW_AT_location屬性:
DW_AT_location DW_OP_fbreg -8
這表示局部變量的內存在距離堆棧幀基址的-8的偏移處。 要找出這個基址的位置,我們來看看包含函數的DW_AT_frame_base屬性。
DW_AT_frame_base DW_OP_reg6
在x86上的reg6是棧幀指針寄存器,由System V x86_64 ABI指定?,F在我們讀幀指針的內容,從中減去8,我們已經找到了變量。如果我們想弄明白這個問題,我們需要看看它的類型:
< 2><0x0000004c> DW_TAG_variable
DW_AT_name a
DW_AT_type <0x0000007e>
如果我們在調試信息中查找這個類型,就會得到這個DIE:
< 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 那么它將傾向于輸出位置列表,這更容易閱讀:
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
上面列表根據程序計數器的位置給出不同的位置。 這個例子是說,如果PC在DW_AT_low_pc處于0x0的偏移量的情況下,那么棧幀基地址是從寄存器7中存儲的值加偏移量8,如果它在0x1到0x4之間,那么它的偏移距離一樣都是16,等等。
總結一下
這節包含了很多DWARF信息需要好好吸收一下才行。不要擔心!有個好消息,就是在接下來的幾個章節中,我們將有一個庫幫我們完成最麻煩的工作。了解了DWARF的概念,特別是在出現問題或希望支持一些DWARF庫的情況下,仍然有用。
如果您想了解更多關于DWARF的信息,那么你可以在此獲取標準文檔。 在撰寫本文時,DWARF 5剛剛被發布,但DWARF 4更受歡迎。
說明
自己動手實踐一下
本節內容是整個系列最枯燥的一章,全篇都是在講述DWARF調試格式的內容。我們可以使用編譯器gcc或者clang編譯源碼時在生成的可執行文件中產生調試信息,并使用DWARF相關的工具dwarfdump查看和解析可執行文件ELF文件格式中的調試信息。
使用gcc的命令可以生成dwarf格式的調試信息
gcc -g 編譯生成dwarf調試格式的信息
源碼使用的是文章的例子。int main() {
long a = 3;
long b = 2;
long c = a + b;
a = 4;
}使用gcc編譯之后,可以使用readelf查看可執行文件中的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
可以看出其種有譯文中最重要的兩個Section,.debug_line和.debug_info
gcc -gdwarf-2 編譯生成 DWARF 2 版本調試格式的信息
與上面的命令類似,只是格式版本略有不同
使用dwarfdump可以查看生成的可執行文件的調試信息
dwarfdump -a 查看程序中所有debug開頭的調試信息
由于信息量比較大,就不貼圖了
dwarfdump -l 查看程序中調試信息的debugline信息
dwarfdump -i 查看程序中調試信息的debuginfo信息
dwarfdump -p 查看程序中調試信息的debug_pubnames信息
總結
以上是生活随笔為你收集整理的linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言省2全民,C语言省试题(2-数据类
- 下一篇: android 时间管理app,时间管理