首页
论坛
课程
招聘
[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB
2017-10-20 22:14 3451

[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB

2017-10-20 22:14
3451

自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB

目录

 

在上一节中,你已经听说了DWARF调试格式,它是程序的调试信息,是一种可以更好理解源码的方式,而不只是解析程序。今天我们将讨论源代码级调试信息的细节,以准备在本教程后面的部分中使用它。


系列索引

  1. 准备工作
  2. 断点的设置
  3. 寄存器和内存
  4. ELF文件格式和DWARVF调试格式
  5. 源码和信号
  6. 源码级单步
  7. 源码级断点
  8. 堆栈解除
  9. 处理变量
  10. 高级主题

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
<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

第一部分描述部分是关于如何理解下面显示列表的一些信息 - 表信息主行号数据从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<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

第一个DIE表示一个编译单元(CU),它基本上是一个源文件,其中包含所有#includes,并且这样解析。以下是它们的含义注释的属性:

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

其他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            <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

上面列表根据程序计数器的位置给出不同的位置。 这个例子是说,如果PC在DW_AT_low_pc处于0x0的偏移量的情况下,那么栈帧基地址是从寄存器7中存储的值加偏移量8,如果它在0x10x4之间,那么它的偏移距离一样都是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格式的调试信息

  • 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信息
    图片描述

《0day安全 软件漏洞分析技术(第二版)》第三次再版印刷预售开始!

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回