前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dwarf 格式介绍

Dwarf 格式介绍

作者头像
一只小虾米
发布2022-11-18 14:10:24
1.1K0
发布2022-11-18 14:10:24
举报
文章被收录于专栏:Android点滴分享Android点滴分享

本篇介绍

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

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] = '\0';
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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本篇介绍
  • Dwarf 的出现
    • stabs
      • COFF
        • OMF
          • IEEE-695
            • Dwarf
              • DIE
                • 描述数据和类型
                  • 可执行代码
                    • 编译单元
                      • 数据编码
                        • DIE树序列化和反序列化
                        • 字段缩写
                      • 行号表
                        • 宏信息
                          • 调用栈信息
                            • 可变长度的数据
                              • dwarf 信息节
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档