调试器工作原理探究系列第三篇
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
本文是调试器工作原理探究系列的第三篇,在阅读前请先确保已经读过本系列的第一和第二篇。
本篇主要内容
在本文中我将向大家解释关于调试器是如何在机器码中寻找C
函数以及变量的,以及调试器使用了何种数据能够在C源代码的行号和机器码中来回映射。
调试信息
现代的编译器在转换高级语言程序代码上做得十分出色,能够将源代码中漂亮的缩进、嵌套的控制结构以及任意类型的变量全都转化为一长串的比特流——这就是机器码。这么做的唯一目的就是希望程序能在目标CPU上尽可能快的运行。大多数的C代码都被转化为一些机器码指令。变量散落在各处——在栈空间里、在寄存器里,甚至完全被编译器优化掉。结构体和对象甚至在生成的目标代码中根本不存在——它们只不过是对内存缓冲区中偏移量的抽象化表示。
那么当你在某些函数的入口处设置断点时,调试器如何知道该在哪里停止目标进程的运行呢?当你希望查看一个变量的值时,调试器又是如何找到它并展示给你呢?答案就是——调试信息。
调试信息是在编译器生成机器码的时候一起产生的。它代表着可执行程序和源代码之间的关系。这个信息以预定义的格式进行编码,并同机器码一起存储。许多年以来,针对不同的平台和可执行文件,人们发明了许多这样的编码格式。由于本文的主要目的不是介绍这些格式的历史渊源,而是为您展示它们的工作原理,所以我们只介绍一
种最重要的格式,这就是DWARF。作为Linux以及其他类Unix平台上的ELF可执行文件的调试信息格式,如今的DWARF可以说是无处不在。
ELF文件中的DWARF格式
根据维基百科上的词条解释,DWARF是同ELF可执行文件格式一同设计出来的,尽管在理论上DWARF也能够嵌入到其它的对象文件格式中。
DWARF是一种复杂的格式,在多种体系结构和操作系统上经过多年的探索之后,人们才在之前的格式基础上创建了DWARF。它肯定是很复杂的,因为它解决了一个非常棘手的问题——为任意类型的高级语言和调试器之间提供调试信息,支持任意一种平台和应用程序二进制接口(ABI)。要完全解释清楚这个主题,本文就显得太微不足道了。说实话,我也不理解其中的所有角落。本文我将采取更加实践的方法,只介绍足量的DWARF相关知识,能够阐明实际工作中调试信息是如何发挥其作用的就可以了。
ELF文件中的调试段
首先,让我们看看DWARF格式信息处在ELF
文件中的什么位置上。
ELF可以为每个目标文件定义任意多个段(section)。而Section header表中则定义了实际存在有哪些段,以及它们的名称。不同的工具以各自特殊的方式来处理这些不同的段,比如链接器只寻找它关注的段信息,而调试器则只关注其他的段。
我们通过下面的C代码构建一个名为traceprog2的可执行文件来做下实验。
C
1 2 3 4 5 6 7 8 9 1#include
void do_stuff(int my_arg) {
int my_local = my_arg + 2; int i;
for (i = 0; i < my_local; ++i) printf("i = %d\n", i);
}
1 1
1 2
1 3
1 4
1 5
1 6 int main() {
do_stuff(2); return 0;
}
通过objdump –h导出ELF
可执行文件中的段头信息,我们注意
到其中有几个段的名字是以.debug_打头的,这些就是DWARF格式的调试段:
C
1 226 .debug_aranges 00000020 00000000 00000000 00001037
CONTENTS, READONLY, DEBUGGING
3
4
5
6
7
8
9
1 0
1 1
1 2
1 3
1 4
1 5
1 6 27 .debug_pubnames 00000028 00000000 00000000 00001057
CONTENTS, READONLY, DEBUGGING
28 .debug_info 000000cc 00000000 00000000 0000107f
CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 0000008a 00000000 00000000 0000114b
CONTENTS, READONLY, DEBUGGING
30 .debug_line 0000006b 00000000 00000000 000011d5
CONTENTS, READONLY, DEBUGGING
31 .debug_frame 00000044 00000000 00000000 00001240
CONTENTS, READONLY, DEBUGGING
32 .debug_str 000000ae 00000000 00000000 00001284
CONTENTS, READONLY, DEBUGGING
33 .debug_loc 00000058 00000000 00000000 00001332
CONTENTS, READONLY, DEBUGGING
每行的第一个数字表示每个段的大小,而最后一个数字表示距离ELF文件开始处的偏移量。调试器就是利用这个信息来从可执行文件中读取相关的段信息。现在,让我们通过一些实际的例子来看看如何在DWARF中找寻有用的调试信息。
定位函数
当我们在调试程序时,一个最为基本的操作就是在某些函数中设置断点,期望调试器能在函数入口处将程序断下。要完成这个功能,调试器必须具有某种能够从源代码中的函数名称到机器码中该函数的起始指令间相映射的能力。
这个信息可以通过从DWARF中的.debug_info段获取到。在我们继续之前,先说点背景知识。DWARF的基本描述实体被称为调试信息表项(Debugging Information Entry —— DIE),每个DIE有一个标签——包含它的类型,以及一组属性。各个DIE之间通过兄弟和孩子结点互相链接,属性值可以指向其他的DIE。
我们运行
Shell
objdump –dwarf=info traceprog2