自己动手编写一个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指定。现在我们读帧指针的内容,从中减去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信息