DWARF 文件初探——提取轻量符号表

发布于 2023-11-21 03:14:58 字数 5741 浏览 26 评论 0

不知道大家有没有用过 bugly,bugly 提供了一种从 dSYM 文件中抽取轻量符号表的功能,生成的符号表更加小巧,而且保留了地址和符号的映射关系,日志解析后仍然可以精确到行号。可读符号表解压打开后如下:

Symbol table:

笔者在 2018 年做技术项目时第一次接触 DWARF 文件,当时做了简单的调研后没有继续在深入研究。直到今年才开始思考 bugly 提供的 buglySymboliOS.jar 到底是怎么工作的。前段时间对 DWARF 做了一些调研,发现虽然相关文章很多,但是同质相近的内容较多,且大多数停留在概念介绍上,真正操刀实战的文章少之又少。因此,笔者抽出部分时间将这段时间遇到的问题以及解决方式整理总结成文章,供感兴趣的人参考。(由于调研深度和广度有限,文中如有错误请及时指出。)

什么是 DWARF 文件

首先来介绍下什么是 DWARF 文件,DWARF(Debugging With Attributed RecordFormats)是记录应用的调试信息的文件,目前最新版本是 V5。在 iOS 中,我们将 Build Settings -> Debug Information Format 修改为 DWARF With dSYM File 即可将调试信息从可执行文件中剥离到 dSYM 文件中。

一旦可执行文件被剥离了 DWARF 文件,那么原则上可执行文件中内部的符号地址映射就不存在了(这里需要注意下,近期发现如果我们本地修改为 release 编译虽然能生成 dSYM 文件,但是可执行文件中依旧保留了符号表,如果有相关实验不要被此误导)。尽管我们可以通过 OC 的存储特性来还原这种映射关系,但是这已经脱离了 DWARF 的范围了。

DWARF 简介

介绍 DWARF 的文章非常多,概念和用处都介绍的非常详尽。大家可以随意搜索 DWARF 等关键字了解相关内容,笔者在这里不想摘抄相关内容。通过 MachOView 打开 DWARF 后会发现其外层依旧是 Mach-O 格式。其中 debug_info 这个 section 中存储了主要的调试信息。大家可以通过 dwarfdump 命令来了解下其中存储的内容。 dwarfdump --debug-info xxxx.app.dSYM/Contents/Resources/DWARF/xxxx

在输入 demo 产生的 DWARF 文件其打印片段如下:

0x0008ec6f:   DW_TAG_subprogram

以上是 dwarfdump 命令帮我们提取并格式化输出的信息,便于我们理解。debug_info 中的数据是树形结构存储的。首先我们需要先了解几个名词,这几个名词也是在文章和本文所介绍的 libdwarf 开源库中经常提及到的概念。

  • DIE

调试信息项(Debugging InformationEntry——DIE),在上文打印的信息中 DW_TAG_subprogram DW_TAG_variable 等都可以称为一个 DIE。一般来说"DW_TAG_*"开头的都是一个 DIE,DIE 作为树的节点,彼此可能存在父子兄弟关系。在上文中打印片段中, DW_TAG_subprogram 就包含多个 DW_TAG_formal_parameter 以及多个 DW_TAG_variable 子节点。

  • Attribute

顾名思义-属性,作为 DIE 的描述信息。例如

DW_TAG_subprogram

代表的就是 DW_TAG_subprogram 这个 DIE 有 DW_AT_low_pc DW_AT_high_pc DW_AT_frame_base 等属性。其中 DW_AT_low_pc 代表这个 DIE 的起始地址, DW_AT_high_pc 代表这个 DIE 的结束地址,地址前闭后开[)。 DW_AT_decl_file 代表这个函数所处的文件。 DW_AT_decl_line 代表这个函数从哪一行开始定义。

  • CU

编译单元,一般来说一个文件就是一个编译单元。

  • subprogram

子程序,一般指我们在文件中编写的函数方法等。

  • variable

变量定义

那 debug_info 在文件中是如何存储的呢?首先我们来看个图片。

由于画图工具的限制没有列出每个字节对应的格式化输出。在二进制中 debug_info 中存储的是连续的不定长的带有层级的数据。这里的解析 libdwarf 帮我们做了解析,否则这是一个很大的工作量。

如何提取轻量符号表

提取轻量符号表就是确定每个文件每个函数每一行代码的汇编指令区间。开源库 libdwarf 提供了解析 DWARF 的能力。

思考历程

  • 符号地址映射

通过 MachOView 我们能清楚的看到,DWARF 文件中保存了符号表结构(Symbol Table)。因此笔者最初的想法是通过 Symbol Table 来实现提取轻量符号表。但是有个很现实的问题摆在面前,在 Symbol Table 中只存储了函数和地址的映射,并没有行号信息。也就是说 Symbol Table 无法精确到行级别的指令区分。因此 bugly 肯定不是这样处理的。

  • 如何精确到行号?

从上文打印的 debug_info 信息片段中,我们可以看出 DW_TAG_subprogram 以及部分子节点都存在 DW_AT_decl_line 或者 DW_AT_call_line 的属性。理想情况下,假设一棵树每个节点都存在行号属性和地址范围,那是不是意味着我们能知道每个函数的每一行的指令区间了?但实际上肯定不是这么简单的,因为在实践后就会发现大量的 DW_TAG_variable 没有提取出地址区间。如果存在如下代码

BOOL hasVtable = [self hasVTable:baseType];

那么 debug_info 中只存储了变量定义 DW_TAG_variable 这个 DIE,在 DW_TAG_variableDW_AT_location 中记录的是当前这个变量位于哪个寄存器的偏移位置。没有找到这一行代码的指令区间。因此需要换一种方式来思考问题了。

  • 柳暗花明——line

在 DWARF 文件中,可以看到存在一个 debug_line 的 section,这里存储的是行信息。因此尝试 dump 看下行信息都包括哪些内容。

 dwarfdump --debug-line xxxo.app.dSYM/Contents/Resources/DWARF/xxx

打印片段如下:

include_directories[  1] = "SwiftDemo"

从打印片段中,我们很容易就能看出每个文件的每一行代码都存储了起始地址以及行号。也就是说到这一步,我们可以提取出起始地址、行号、文件名。回顾下 bugly 的轻量符号表

Symbol table:

我们缺少函数名信息。到这一步就比较好处理了,回顾下上文,我们通过 DW_TAG_subprogram 可以获取到这个函数的文件名、函数名、函数起始地址、函数终止地址。因此获取到行信息后,可以查看当前这个行的起始地址位于同文件下那个函数的指令区间内,即可得知函数名。

遇到的坑

1、数据获取失败

有时获取地址通过 dwarf_formaddr 函数并不能获取到数据,如果失败需要尝试 dwarf_formudata、dwarf_formsdata 等函数。

   res = dwarf_formaddr(attr,&uval,errp);

2、DW_AT_high_pc

实践发现 DW_AT_high_pc 中存储的并不是结束地址,而是当前这个 DIE 的地址长度,即 size。

3、dwarf_line_srcfileno

从函数名来看,仿佛是获取该行的文件编号,但是实际调用上会发现调用报错。libdwarf 类似的情况还很多,经常函数调用报错但是却缺少错误信息,这对开发和调试来说很不方便。

总结及展望

初见 debug_info 的存储有点像 AST 的感觉,从 DWARF 文件中我们也能找出源码中的蛛丝马迹,甚至能根据 DWARF 还原出源码中的部分内容。例如变量定义,在 DWARF 文件中变量的名字、变量的类型都有介绍,形参类型等也有介绍。那么是不是某些基于抽象语法树的技术方案可以考虑能否用 DWARF 解析来实现呢?

参考文献

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

心在旅行

暂无简介

0 文章
0 评论
21 人气
更多

推荐作者

花开柳相依

文章 0 评论 0

zyhello

文章 0 评论 0

故友

文章 0 评论 0

对风讲故事

文章 0 评论 0

Oo萌小芽oO

文章 0 评论 0

梦明

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文