Dwarf 格式介绍

2022-11-18 14:10:24 浏览数 (1)

本篇介绍

在软件调试中,一种有效的方法是用打断点,这样可以实时看到堆栈,变量,寄存器的变化,那调试器是如何完成源代码和执行指令的关联呢?本篇来解答这个问题。

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

代码语言:javascript复制
<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] = '';
22: return (char *) memcpy (result, s, len);
23: }

对应的DIE如下:

代码语言:javascript复制
<1>: DW_TAG_base_type
       DW_AT_name = int
       DW_AT_byte_size = 4
       DW_AT_encoding = signed
<2>: DW_TAG_typedef
       DW_AT_name = size_t
       DW_AT_type = <3>
<3>: DW_TAG_base_type
       DW_AT_name = unsigned int 
       DW_AT_byte_size = 4 
       DW_AT_encoding = unsigned
<4>: DW_TAG_base_type
       DW_AT_name = long int
       DW_AT_byte_size = 4
       DW_AT_encoding = signed
<5>: DW_TAG_subprogram
       DW_AT_sibling = <10>
       DW_AT_external = 1
       DW_AT_name = strndup
       DW_AT_prototyped = 1
       DW_AT_type = <10>
       DW_AT_low_pc = 0
       DW_AT_high_pc = 0x7b
<6>: DW_TAG_formal_parameter
       DW_AT_name = s
       DW_AT_type = <12>
       DW_AT_location = (DW_OP_fbreg: 0)
<7>: DW_TAG_formal_parameter
       DW_AT_name = n
       DW_AT_type = <2>
       DW_AT_location = (DW_OP_fbreg: 4)
<8>: DW_TAG_variable
       DW_AT_name = result
       DW_AT_type = <10>
       DW_AT_location = (DW_OP_fbreg: -28)
<9>: DW_TAG_variable
       DW_AT_name = len
       DW_AT_type = <2>
       DW_AT_location =
           (DW_OP_fbreg: -24)
<10>: DW_TAG_pointer_type
       DW_AT_byte_size = 4
       DW_AT_type = <11>
<11>: DW_TAG_base_type
       DW_AT_name = char
       DW_AT_byte_size = 1
       DW_AT_encoding = signed char
<12>: DW_TAG_pointer_type
       DW_AT_byte_size = 4
       DW_AT_type = <13>
<13>: DW_TAG_const_type
       DW_AT_type = <11>

编译单元

dwarf将每一个源文件当作一个编译单元。编译单元DIE是该文件内类型,函数等DIE的共同父类。编译单元DIE包括文件名,程序语言,dwarf的提供商,还有相对于Dwarf数据的偏移。

数据编码

由于Dwarf 将代码表示成了DIE树,就有很多重复信息,因此就需要一些优化手段。目前有以下优化手段 :

DIE树序列化和反序列化

本质上就是将一棵树序列化成一个线性结构,这样就可以避免存储树的结构信息。这就变成了一道leetcode题了,如何将n叉树转成一个线性结构,然后如何再转回来。Dwarf里的实现思路如下:

代码语言:javascript复制
struct TreeNode {
    int value;
    std::vector<TreeNode*> child;
    
    TreeNode(int v):value(v) {
        
    }
};

struct FlatNode {
    int value;
    bool hasChild;
    
    FlatNode(int v, bool c): value(v), hasChild(c) {
        
    }
};


void FlatTree(TreeNode* root, std::list<FlatNode*> &flatList) {
    if (root == nullptr) {
        return;
    }
    if (root->child.empty()) {
        flatList.push_back(new FlatNode(root->value, false));
    } else {
        flatList.push_back(new FlatNode(root->value, true));
        for (auto child: root->child) {
            FlatTree(child, flatList);
        }
    }
    flatList.push_back(nullptr);
}


TreeNode* DeFlatTree(std::list<FlatNode*> &flatList) {
    if (flatList.empty()) {
        return nullptr;
    }
    std::stack<TreeNode*> nodeStack;
    TreeNode *root = new TreeNode(flatList.front()->value);
    nodeStack.push(root);
    flatList.pop_front();
    
    for(auto node : flatList) {
        if (node != nullptr) {
            nodeStack.top()->child.push_back(new TreeNode(node->value));
            if (node->hasChild) {
                nodeStack.push(new TreeNode(node->value));
            }
        } else {
            nodeStack.pop();
        }
    }
    return root;
}

字段缩写

由于每个DIE都有属性字段和属性值,可是属性字段大多是一样的,如果可以将属性字段抽取出来,只在DIE里存放值,那么就可以节省不少空间。实现如下, 对于这样的DIE

代码语言:javascript复制
<6>: DW_TAG_formal_parameter
       DW_AT_name = s
       DW_AT_type = <12>
       DW_AT_location =
(DW_OP_fbreg: 0)

在字段缩写表里添加一项:

代码语言:javascript复制
Abbrev 5:
DW_TAG_formal_parameter    [no children]
   DW_AT_name         DW_FORM_string
   DW_AT_decl_file    DW_FORM_data1
   DW_AT_decl_line    DW_FORM_data1
   DW_AT_type         DW_FORM_ref4
   DW_AT_location     DW_FORM_block1

这样原先的DIE就可以写成如下形式:

代码语言:javascript复制
abbreviation 5
”s”
file 1
line 41
type DIE offset
location (fbreg   0)
terminating NUL

行号表

dwarf的行号表包含指令内存地址和源代码行号的映射。这样就可以支持源码级别的打断点,那这个表是如何存储的呢?如果是每个指令对应一套行号信息,那么这个表会非常大。dwarf是依据FSM(finite state machine)的状态记录的。在编译器层面,语法分析器会将程序抽象成一个个的状态,一个合法的程序最终一定会走到一个可接收的状态上。这样每个状态对应一行记录,这样就可能对应了n条指令。如下所示:

image.png

宏信息

当代码中包含宏时,调试器处理起来会比较麻烦。Dwarf专门存放了宏信息,这样可以方便调试器显示调用宏的参数,甚至将宏转成对应的源代码。

调用栈信息

调用约定是调用函数时候参数的传递规则,是通过寄存器还是调用栈,顺序是从左到右还是从右到左。通过编译选项(-fomit-frame-pointer)也可以决定是否使用栈寄存器(FP),而栈回溯就是依赖于FP值找到上级调用栈。

在Dwarf中也记录了详细的CFI(Call Frame Information), 这样编译器用CFI就可以回栈。类似于行号表,CFI也是一个基于指令序列的表,按地址每行一条记录,第一列是虚拟地址,后面几列是寄存器的值。

可变长度的数据

在Dwarf中很多地方都会用到int,可是有的场景int值范围比较小,也就是可能只用1个字节保存数据,3个字节都没用到。Dwarf就提供了一个压缩能力,可以只使用一个字节保存数值,这样剩余的3字节就可以节省下来了。

dwarf 信息节

将dwarf信息按内容分段,这样就可以去重,目前有的段如下: .debug_abbrev - .debug_info 中的缩写信息 .debug_aranges - 地址到编译单元的查找表 .debug_frame - 调用栈信息 .debug_info - 主要的dwarf信息 .debug_line - 行信息 .debug_loc - 位置信息 .debug_macinfo - 宏信息 .debug_pubnames - 函数名字到编译单元的查找表 .debug_pubtypes - 类型名字到编译单元的查找表 .debug_ranges - 地址范围 .debug_str - .debug_info 中的字符串 .debug_types - 类型描述

如果需要查看dwarf信息,可以使用libdwarf,dwarfdump,甚至readelf 也可以直接读取dwarf信息。

代码语言:javascript复制
readelf 
-w[lLiaprmfFsoRt] or
  --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
               =frames-interp,=str,=loc,=Ranges,=pubtypes,
               =gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
               =addr,=cu_index]
                         Display the contents of DWARF2 debug sections
  --dwarf-depth=N        Do not display DIEs at depth N or greater
  --dwarf-start=N        Display DIEs starting with N, at the same depth
                         or deeper

0 人点赞