自己动手编写一个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信息

linux的静态编译elf无法调试,[翻译]自己动手编写一个Linux调试器系列之4 ELF文件格式与DWARF调试格式 by lantie@15PB...相关推荐

  1. Linux下静态编译的一个TIP

    Linux下静态编译的一个TIP | 素包子 Linux下静态编译的一个TIP 2010年3月28日 baoz 阅读评论 linux下静态编译好处很多,一来是可以跨发行版(debian redhat ...

  2. 编写一个 Linux 内核模块

    编写一个 Linux 内核模块 作者:解琛 时间:2020 年 8 月 16 日 编写一个 Linux 内核模块 一.实验环境 二.Linux 内核模块相关命令 三.程序架构 四.编写一个内核模块 4 ...

  3. 用C语言编写一个Linux下的简单shell程序

    这是一个简单的C程序,展示了如何进行系统调用执行logout cd ls pwd pid rm mkdir mv cp等命令,这是一个简单的命令解释程序shell,其源代码如下: #include & ...

  4. linux 驱动编译静态,Linux驱动静态编译和动态编译方法详解

    内核源码树的目录下都有两个文档Kconfig和Makefile.分布到各目录的Kconfig构成了一个分布式的内核配置数据库,每个Kconfig分别描述了所属目录源文档相关的内核配置菜单.在内核配置m ...

  5. 实战!手把手教你如何编写一个Linux驱动并写一个支持物联网的LED演示demo

    目录 一.开发环境 二. 准备工作: 1. 创建一个项目工程目录 2. 创建输出与目标目录 3.头文件目录 4. 建立源代码src目录 5. 使用git管理你的项目 三.编写LED驱动 三.一 准备工 ...

  6. 操作系统课程设计——Shell编程(用c编写一个Linux的外壳Shell)

    文章目录 前言 功能与展示 功能列表 功能展示 依赖库安装 具体实现 Shell工作流程 外部命令工作流程 内置命令工作流程 管道功能与I/O重定向的实现 alias功能的一些思考 Shell的编译与 ...

  7. linux的静态编译elf无法调试,macos-运行arm-elf-gcc编译代码时出现段错误

    使用MacPorts,我刚刚在我的MacBook Pro上安装了arm-elf-gcc.这项工作完美无缺,并且一切运行正常. 但是,在用C和C编译了一个简单的hello world测试程序并尝试在目标 ...

  8. linux gcc 静态编译,GCC 程序编译的静态链接和动态链接

    (给Linux爱好者加星标,提升Linux技能)转自:Mr_Bluyee 在链接阶段中,所有对应于源文件的 .o 文件.'-l' 选项指定的库文件.无法识别的文件名(包括指定的.o目标文件和.a库文件 ...

  9. linux下静态编译mupdf,在Qt中调用Mupdf库进行pdf显示

    2018.5.10 更新内存对齐说明 感谢知乎网友@孤独子狮指出QImage处需要考虑内存对齐的问题.因为本人缺乏跨平台.图形库开发经验,所以在调试成功后就没有深入探究. 主要修改了QImage的构造 ...

最新文章

  1. Spark集群搭建【Spark+Hadoop+Scala+Zookeeper】
  2. LaTex 使用特殊章节符号 (§)
  3. pxe方式安装gentoo
  4. Catch a cold, will be back later
  5. Angular2学习笔记——NgModule
  6. SVM熟练到精通2:SVM目标函数的dual优化推导
  7. apt来安装mysql5.7,linux系统ubuntu18.04安装mysql 5.7
  8. 4.0之后的hibernate获取sessionFactory
  9. ASP.NET Core DI 手动获取注入对象
  10. e-r模型教案高中计算机,E-R模型实例答案.ppt
  11. 最新!中国内地高校ESI排名出炉:342所大学上榜!
  12. 文件或目录损坏且无法读取怎么办,文件或目录损坏且无法读取寻回方法
  13. rocketmq client端源码分析(2)-consumer实现
  14. 微信小程序 实现拨打电话
  15. 全球海温数据NOAA Extended Reconstructed Sea Surface Temperature (SST) V5的时间解释
  16. 计算机网络:移动IP
  17. MarkDown超级教程 Obsidian版_11.4
  18. 分享一个非常好的壁纸网站http://www.itoobz.com
  19. python 判断平闰年的方法
  20. 《ANSYS CFX 14.0超级学习手册》——1.4 CFD软件结构及常用的CFD软件

热门文章

  1. puppeteer执行js_使用Node.js和Puppeteer与表单和网页进行交互– 2
  2. r语言descstats_一条命令轻松绘制CNS顶级配图-ggpubr
  3. php 当前ip_php获取本机ip(远程IP地址)
  4. Java SimpleTimeZone toString()方法与示例
  5. C# Winform 窗体美化(二、LayeredSkin 界面库)
  6. 如何在使用ASPMVC4的分部视图中获取数据展示
  7. struts处理中文乱码问题总结
  8. zoj 1091 Knight Moves
  9. Android--快速接入微信支付
  10. C++ SVM Opencv3.4实现人脸检测