本篇介绍
在软件调试中,一种有效的方法是用打断点,这样可以实时看到堆栈,变量,寄存器的变化,那调试器是如何完成源代码和执行指令的关联呢?本篇来解答这个问题。
Dwarf 的出现
在从源代码编译成机器指令的时候,中间也会涉及到多次优化,为了方便调试,就需要建立源代码和机器指令的关联,这个关键结构需要简单,而且解析效率高,dwarf就是这样的结构。 在出现Dwarf之前,也有一些其他的结构,比如stabs,COFF,PE-COFF,OMF,IEEE-695,下面分别介绍下。
stabs
最开始出现的是stabs,stabs将程序信息以字符串的信息记录,因此实现起来比较简单,不过stabs并没有成为标准,也没有友好的文档,倒是有一些组织会在此基础上加一些扩展,所以目前也在一些系统上使用。
COFF
COFF(Common object file format)源自Unix system V Release V3, 这个格式最大的问题是每个架构上的格式都不一样,比如在IBM RS/6000 上是XCOFF,在MIPS和Alpha上是ECOFF,在Windows上是PE-COFF。虽然都有友好的文档,不过这些格式都没有成为标准。
OMF
OMF(Object Module Format)用于 CP/M, DOS 和OS/2 系统上,定义了用于调试器公开的名字和行号信息,不过这些对于调试器来都是最初级的支持。
IEEE-695
IEEE-695 是由Microtec Research 和HP在1980's 针对嵌入式环境开发出来的,并于1990年成为了IEEE标准。各种调试格式是基于块结构,和其他格式比起来,可以更好的表现代码结构。由于后续Microtrc Research和HP基于该结构做的C 和代码优化等都没有公开,因此该标准也很少更新。逐渐这个格式仅仅用于一些小处理器上。
Dwarf
Dwarf(Debugging With Arbitrary Record Formats)是由Brain Russell 博士在1988年贝尔实验室开发出来的,主要是为了Unix System V Release 4上的 C和sdb调试器。1992年 Programming Languages Special Interest Group(PLSIG) 将 这时候的Dwarf 命名为 Dwraf 第一版并标准化,虽然这时候的结构还不够紧凑,不过还是可以广泛应用于一些小的处理器。 1993年 PLSIG优化了Dwarf格式体积,并且支持了C ,并作为Dwarf第二版的草稿,可惜的是并没有正式发布。原因是这一年摩托罗拉88000 处理器上被爆出了致命漏洞,于是停止了88000的支持,导致了使用88000处理器开发电脑的Open88 公司倒闭,而该公司又是PLSIG的最重要赞助商,进而多米诺骨牌就倒在了PLSIG上,这个组织也消失了。这时候Dwarf 2 就被各个组织针对不同处理器加扩展,有步入COFF结局的趋势。
在1999年,让dwarf更好支持HP/Intel IA-64架构和解决C ABI的兼容性问题,Brain担任了Dwarf委员会的主席,并开始开发Dwarf 第三版,在2005年dwarf 第三版正式发布。
2007开始Dwarf 第四版的开发,添加了对VLIM架构的支持,并可以进一步压缩调试数据,在2010年正式发布。目前最新的是第五版。
当前大多数程序语言都是基于块结构,每个实体可以包含其他实体,同时也可以被其他实体包含,而每个类或函数定义都可以看成一个实体。这样编译器内部就可以讲代码用树结构(抽象语法树)来表示。 Dwarf也使用了同样的模型,也是基于块结构,也将一个程序表示成一棵树,数的节点可以表示类型,变量,函数等。这样的格式就方便扩展了,调试器只处理认识的并忽略不认识的类型就行。这样Dwarf就可以支持任何架构上的任何语言。
尽管Dwarf 主要是和E LF一块使用的,但是实际上不依赖于文件格式,也可以用于其他文件格式。
DIE
DIE(Debugging Information Entry) 是dwarf中基础的描述实体。每个DIE有一个tag,指定了DIE描述的类型,还有一列属性。DIE也可以包含其他DIE。举一个例子,经典的helloworld DIE结构如下:
image.png
这个结构是非正式的,后面可以看到正式结构。
DIE大体可以分位2类,一类是描述数据和类型,一类是描述函数和其他可执行代码的。
描述数据和类型
大多数程序语言包含了内置的数据类型,也支持自定义的数据类型。Dwarf需要支持各种语言,因此就提供了一种可以支持各种语言的数据抽象。
举一个例子,int变量在32位的机器上就是4字节,在16位的机器上就是2字节,那在Dwarf中的表示如下:
代码语言:javascript复制DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 4
DW_AT_encoding = signed
DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 2
DW_AT_encoding = signed
那如果实际类型就是2字节,如何在32位的机器上表示呢?这时候就需要指定偏移,比如放到高16位或者低16位:
代码语言:javascript复制 DW_TAG_base_type
DW_AT_name = word
DW_AT_byte_size = 4
DW_AT_bit_size = 16
DW_AT_bit_offset = 0
DW_AT_encoding = signed
可以通过DIE组合描述变量,比如int x 就可以如下表示:
代码语言:javascript复制<1>: DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 4
DW_AT_encoding = signed
<2>: DW_TAG_variable
DW_AT_name = x
DW_AT_type = <1>
可以看到DW_TAG_variable
的DW_AT_type
字段引用了标号为1的DW_TAG_base_type
。
如果要表示int *px,结果如下:
代码语言:javascript复制<1>: DW_TAG_variable
DW_AT_name = px
DW_AT_type = <2>
<2>: DW_TAG_pointer_type
DW_AT_byte_size = 4
DW_AT_type = <3>
<3>: DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 4
DW_AT_encoding = signed
通过这种形式就可以支持比较复杂的类型,比如const char **argc
:
<1>: DW_TAG_variable
DW_AT_name = argv
DW_AT_type = <2>
<2>: DW_TAG_pointer_type
DW_AT_byte_size = 4
DW_AT_type = <3>
<3>: DW_TAG_pointer_type
DW_AT_byte_size = 4
DW_AT_type = <4>
<4>: DW_TAG_const_type
DW_AT_type = <5>
<5>: DW_TAG_base_type
DW_AT_name = char
DW_AT_byte_size = 1
DW_AT_encoding = unsigned
数组类型在DIE中也有自己的属性,比如是列优先还是行优先,数组索引可以用sub-range 类型表示,拥有上下边界属性,这样就可以用于C那样0作为最低索引的场景,也可以用于Pascal,Ada那样不做限制的场景。
在Dwarf中也拥有start,union,class,interface类型,这样就可以表示编程语言中的复合类型。比如DIE是这样表示class类型的,有名字,大小,可见行等属性。对于C/C 中针对比特位定义的类型,在DIE中用偏移就可以表示了。
那变量的位置在DIE中是如何表示的呢?对于变量声明,直接用文件,行号,列号就可以了,对于变量存储位置就会复杂一些了,函数内变量就依赖于函数的栈基址(ebp)了,对于全局变量,就依赖于数据段地址了,类变量还需要考虑到在类中的偏移。DIE提供了一个字段告诉如何计算偏移,是参考寄存器,还是栈基址,还是数据段等,参考如下:
代码语言:javascript复制fig7.c:
1: int a;
2: void foo()
3: {
4: register int b;
5: int c;
6: }
<1>: DW_TAG_subprogram DW_AT_name = foo
<2>: DW_TAG_variable DW_AT_name = b
DW_AT_type = <4>
DW_AT_location = (DW_OP_reg0)
<3>: DW_TAG_variable
DW_AT_name = c
DW_AT_type = <4>
DW_AT_location =
(DW_OP_fbreg: -12)
<4>: DW_TAG_base_type
DW_AT_name = int
DW_AT_byte_size = 4
DW_AT_encoding = signed
<5>: DW_TAG_variable DW_AT_name = a
DW_AT_type = <4>
DW_AT_external = 1 DW_AT_location = (DW_OP_addr: 0)
可执行代码
DIE使用subprogram DIE 来表示函数,拥有名字,源文件位置,外部可见行等属性。同时也会包含地址范围,低地址一班就是函数的入口地址。DIE不关心调用约定,因此函数参数的DIE顺序基本和参数列表的顺序一致。下面是一个例子:
代码语言:javascript复制strndup.c:
1: #include "ansidecl.h"
2: #include <stddef.h>
3:
4: extern size_t strlen (const char*);
5: extern PTR malloc (size_t);
6: extern PTR memcpy (PTR, const PTR, size_t); 7:
8: char *
9: strndup (const char *s, size_t n)
10: {
11: char *result;
12: size_t len = strlen (s);
13:
14: if (n < len)
15: len = n;
16:
17: result = (char *) malloc (len 1);
18: if (!result)
19: return 0;
20:
21: result[len] = '