-
-
[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB
-
发表于: 2017-10-20 22:14 9446
-
在上一节中,你已经听说了DWARF调试格式,它是程序的调试信息,是一种可以更好理解源码的方式,而不只是解析程序。今天我们将讨论源代码级调试信息的细节,以准备在本教程后面的部分中使用它。
ELF和DWARF是你可能没有听说过的两个概念信息,但可能已经使用很长时间了。 ELF(可执行和可链接格式)是Linux世界中使用最广泛的对象文件格式; 它指定了一种存储二进制文件的所有不同部分的方式,如代码,静态数据,调试信息和字符串。 它还告诉加载程序如何取得二进制并准备好执行,这涉及二进制文件的不同部分应该放置在内存中,哪些部分需要根据其他信息(重定位)等的位置来修复。 我不会在这些帖子中覆盖更多ELF,但如果你有兴趣,可以看看这个漂亮的信息图表或标准。
DWARF是ELF最常用的调试信息格式。它不一定与ELF相关,但两者是一起发展的,在开发中一起使用也非常好。该格式允许编译器告诉调试器程序源代码如何与将执行的二进制文件相互关系。该信息分为不同的ELF部分,每个部分都有自己的信息来中继。以下是定义的不同部分,取自于非常详细的DWARF调试格式介绍:
我们对.debug_line
和.debug_info
部分最感兴趣,所以让我们看看一些DWARF的简单程序。
如果你使用编译器(gcc 或 clang
)的-g
选项编译此程序,并通过dwarfdump
运行结果,则应该看到类似于行号的部分:
第一部分描述部分是关于如何理解下面显示列表的一些信息 - 表信息主行号数据从0x00400670
开始。本质上,它是将一个代码内存地址映射到一些文件中的行和列号。 NS
表示该地址标志着新语句的开始,这通常用于设置断点或步进。 PE
标记函数开始的结尾,这有助于设置函数入口断点。 ET
标示翻译单元的结尾。实际信息上并不是像这样编码的;真正的编码是一种非常节省空间的程序,可以执行这些程序来建立这个行信息。
那么说,我们想在variable.cpp的第4行设置一个断点,我们该怎么做?我们查找与该文件相对应的条目,然后查找相关的行条目,查找与之对应的地址,并在其中设置断点。在我们的例子中,这是这个条目:
所以我们要在地址0x00400686
设置一个断点。你可以用你已经写过的调试器手工完成,如果你想尝试一下。
相反的工作也是如此。如果我们有一个内存位置 - 例如一个程序计数器值,并且想要找出源代码中的哪个位置,我们只需在行表信息中找到最接近的映射地址,并从中获取行。
.debug_info
部分是DWARF的核心。它给了我们有关我们的程序中存在的类型,函数,变量,希望和想要得到的信息。本节的基本单位是DWARF信息条目(DWARF Information Entry),简称为DIE。 DIE包含一个标签,告诉您正在表示什么样的源代码级实体,后面是一系列适用于该实体的属性。这是上面发布的简单示例程序的.debug_info
部分:
第一个DIE表示一个编译单元(CU),它基本上是一个源文件,其中包含所有#includes
,并且这样解析。以下是它们的含义注释的属性:
其他DIE遵循类似的方案,您可以直观地看出不同属性的含义。
现在我们可以尝试用我们新发现的DWARF知识解决一些实际问题。
如果我们有一个程序计数器值,并想获取PC所在函数的信息。一个简单的算法是:
这可以用于许多情况,但是在成员函数和内联函数存在的情况下,事情会变得更加困难。 例如,使用内联函数,一旦找到范围包含我们的PC的函数,我们将需要对该DIE的子项进行递归,以查看是否存在更好匹配的内联函数。我不会在我的调试器代码中处理内联函数,但如果你喜欢,你可以添加对此的支持。
再次申明,如果想要支持成员函数,命名空间等特性可能需要更高级的做法。 对于简单的函数,您可以在不同的编译单元中迭代函数,直到找到具有正确名称的函数。 如果您的编译器足够填写.debug_pubnames
部分,您可以更有效地执行此操作。
一旦找到该函数,您可以在DW_AT_low_pc
给定的内存地址上设置一个断点。 但是,在函数开始时会中断,但最好在用户代码开始时中断。 由于行表信息可以指定指定函数开头结束的内存地址,因此您可以直接在行表中查找DW_AT_low_pc
的值,然后继续阅读,直到找到标记为函数开头结束的条目。 有些编译器不会输出这个信息,所以另外一个选择是在该函数的第二行条目给出的地址上设置一个断点。
假设我们要在我们的示例程序中设置一个断点。 我们搜索main函数,并得到这个DIE:
这告诉我们,函数从0x00400670
开始。 如果我们在线表中查看,我们得到这个条目:
我们想跳过开头,所以我们先读一个条目:
Clang在这个条目中包含了代码开头结束标志,所以我们知道在这里停下来,并在地址0x00400676
上设置一个断点。
读取变量可能非常复杂。 变量是一个难以捉摸的东西,可以在整个函数中存在,可以放在寄存器中,放在内存中,还可以被优化,隐藏在角落里。幸运的是,我们简单的例子是,很简单。 如果我们想要读取变量a的内容,我们来看看它的DW_AT_location
属性:
这表示局部变量的内存在距离堆栈帧基址的-8
的偏移处。 要找出这个基址的位置,我们来看看包含函数的DW_AT_frame_base
属性。
在x86上的reg6
是栈帧指针寄存器,由System V x86_64 ABI指定。现在我们读帧指针的内容,从中减去8,我们已经找到了变量。如果我们想弄明白这个问题,我们需要看看它的类型:
如果我们在调试信息中查找这个类型,就会得到这个DIE:
这告诉我们类型是一个8字节(64位)的有符号整数类型,因此我们可以继续将这些字节解释为int64_t
并将其显示给用户。
当然,类型可以比这个复杂得多,因为它们必须能够表达诸如c++类型之类的东西,但这给了你一个关于它们如何工作的基本概念。
回到该栈帧的基址,Clang编译器可以比较好的跟踪到帧指针寄存器的帧基址。 最近版本的GCC倾向于喜欢DW_OP_call_frame_cfa
,它涉及解析.eh_frame
ELF部分,这是一个完全不同的文章,在这里我就不详述。 如果你使用GCC的DWARF 2版本而不是更新的版本,命令是gcc -gdwarf-2 <源码>
那么它将倾向于输出位置列表,这更容易阅读:
上面列表根据程序计数器的位置给出不同的位置。 这个例子是说,如果PC在DW_AT_low_pc
处于0x0
的偏移量的情况下,那么栈帧基地址是从寄存器7中存储的值加偏移量8,如果它在0x1
到0x4
之间,那么它的偏移距离一样都是16,等等。
这节包含了很多DWARF信息需要好好吸收一下才行。不要担心!有个好消息,就是在接下来的几个章节中,我们将有一个库帮我们完成最麻烦的工作。了解了DWARF的概念,特别是在出现问题或希望支持一些DWARF库的情况下,仍然有用。
如果您想了解更多关于DWARF的信息,那么你可以在此获取标准文档。 在撰写本文时,DWARF 5刚刚被发布,但DWARF 4更受欢迎。
原文:https://blog.tartanllama.xyz/writing-a-linux-debugger-elf-dwarf/
翻译:lantie@15PB, 15PB信息安全教育,http://www.15pb.com.cn
本节内容是整个系列最枯燥的一章,全篇都是在讲述DWARF调试格式的内容。我们可以使用编译器gcc
或者clang
编译源码时在生成的可执行文件中产生调试信息,并使用DWARF相关的工具dwarfdump
查看和解析可执行文件ELF文件格式中的调试信息。
使用gcc
的命令可以生成dwarf格式的调试信息
可以看出其种有译文中最重要的两个Section,.debug_line
和.debug_info
使用dwarfdump
可以查看生成的可执行文件的调试信息
int main() { long a = 3; long b = 2; long c = a + b; a = 4; }
.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
0x00400686 [ 4,14] NS
.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
DW_AT_producer clang version 3.9.1 (tags/RELEASE_391/final) <-- The compiler which produced this binary DW_AT_language DW_LANG_C_plus_plus <-- The source language DW_AT_name /super/secret/path/MiniDbg/examples/variable.cpp <-- The name of the file which this CU represents DW_AT_stmt_list 0x00000000 <-- An offset into the line table which tracks this CU DW_AT_comp_dir /super/secret/path/MiniDbg/build <-- The compilation directory DW_AT_low_pc 0x00400670 <-- The start of the code for this CU DW_AT_high_pc 0x0040069c <-- The end of the code for this CU
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
< 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)
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!