前言
不知道大家有没有用过bugly,bugly提供了一种从dSYM文件中抽取轻量符号表的功能,生成的符号表更加小巧,而且保留了地址和符号的映射关系,日志解析后仍然可以精确到行号。
可读符号表解压打开后如下:
Symbol table:
5ef0 5f00 viewDidLoad ViewController.swift:10
5f00 5f18 viewDidLoad ViewController.swift:0
5f18 5f2c viewDidLoad ViewController.swift:11
5f2c 5f48 viewDidLoad ViewController.swift:11
5f48 5f4c viewDidLoad ViewController.swift:0
5f4c 5f68 viewDidLoad ViewController.swift:11
笔者在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
DW_AT_low_pc (0x000000010000b4b8)
DW_AT_high_pc (0x000000010000b6ac)
DW_AT_frame_base (DW_OP_reg29 W29)
DW_AT_object_pointer (0x0008ec8d)
DW_AT_name ("+[WBOCTest getTypeName:]")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (199)
DW_AT_prototyped (true)
DW_AT_type (0x0008f1a3 "NSString*")
0x0008ec8d: DW_TAG_formal_parameter
DW_AT_location (DW_OP_fbreg -24)
DW_AT_name ("self")
DW_AT_type (0x0008f1cc "const Class")
DW_AT_artificial (true)
0x0008ec9a: DW_TAG_formal_parameter
DW_AT_location (DW_OP_fbreg -32)
DW_AT_name ("_cmd")
DW_AT_type (0x0008e2d3 "SEL")
DW_AT_artificial (true)
0x0008eca7: DW_TAG_formal_parameter
DW_AT_location (DW_OP_fbreg -40)
DW_AT_name ("typeOffset")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (199)
DW_AT_type (0x000000000000101f "uintptr_t")
0x0008ecb6: DW_TAG_variable
DW_AT_location (DW_OP_fbreg -48)
DW_AT_name ("linkedit")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (201)
DW_AT_type (0x0008f1d1 "const segment_command_64*")
0x0008ecc5: DW_TAG_variable
DW_AT_location (DW_OP_fbreg -56)
DW_AT_name ("linkBase")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (203)
DW_AT_type (0x000000000000101f "uintptr_t")
0x0008ecd4: DW_TAG_variable
DW_AT_location (DW_OP_fbreg -64)
DW_AT_name ("exeHeader")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (204)
DW_AT_type (0x000000000000101f "uintptr_t")
0x0008ece3: DW_TAG_variable
DW_AT_location (DW_OP_fbreg -72)
DW_AT_name ("typeAddress")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (205)
DW_AT_type (0x000000000000101f "uintptr_t")
以上是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_AT_low_pc (0x000000010000b4b8)
DW_AT_high_pc (0x000000010000b6ac)
DW_AT_frame_base (DW_OP_reg29 W29)
DW_AT_object_pointer (0x0008ec8d)
DW_AT_name ("+[WBOCTest getTypeName:]")
DW_AT_decl_file ("/Users/a58/SwiftVTHook/SwiftDemo/SwiftDemo/WBOCTest.m")
DW_AT_decl_line (199)
DW_AT_prototyped (true)
DW_AT_type (0x0008f1a3 "NSString*")
代表的就是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_variable
的DW_AT_location
中记录的是当前这个变量位于哪个寄存器的偏移位置。没有找到这一行代码的指令区间。因此需要换一种方式来思考问题了。
- 柳暗花明——line
在DWARF文件中,可以看到存在一个debug_line的section,这里存储的是行信息。因此尝试dump看下行信息都包括哪些内容。
dwarfdump --debug-line xxxo.app.dSYM/Contents/Resources/DWARF/xxx
打印片段如下:
include_directories[ 1] = "SwiftDemo"
file_names[ 1]:
name: "SwiftMethodTableModel.h"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
file_names[ 2]:
name: "SwiftMethodTableModel.m"
dir_index: 1
mod_time: 0x00000000
length: 0x00000000
Address Line Column File ISA Discriminator Flags
------------------ ------ ------ ------ --- ------------- -------------
0x000000010000bbac 15 0 1 0 0 is_stmt
0x000000010000bbb8 15 35 1 0 0 is_stmt prologue_end
0x000000010000bbd0 15 0 1 0 0 is_stmt
0x000000010000bbe8 15 35 1 0 0 is_stmt prologue_end
0x000000010000bc08 17 0 1 0 0 is_stmt
0x000000010000bc14 17 37 1 0 0 is_stmt prologue_end
0x000000010000bc24 17 0 1 0 0 is_stmt
0x000000010000bc34 17 37 1 0 0 is_stmt prologue_end
0x000000010000bc48 19 0 1 0 0 is_stmt
0x000000010000bc54 19 37 1 0 0 is_stmt prologue_end
0x000000010000bc64 19 0 1 0 0 is_stmt
0x000000010000bc74 19 37 1 0 0 is_stmt prologue_end
0x000000010000bc88 21 0 1 0 0 is_stmt
0x000000010000bc94 21 37 1 0 0 is_stmt prologue_end
0x000000010000bca4 21 0 1 0 0 is_stmt
0x000000010000bcb4 21 37 1 0 0 is_stmt prologue_end
0x000000010000bcc8 10 0 2 0 0 is_stmt
0x000000010000bcdc 10 17 2 0 0 is_stmt prologue_end
0x000000010000bcf8 28 0 1 0 0 is_stmt
0x000000010000bd04 28 35 1 0 0 is_stmt prologue_end
0x000000010000bd1c 28 0 1 0 0 is_stmt
0x000000010000bd34 28 35 1 0 0 is_stmt prologue_end
0x000000010000bd54 30 0 1 0 0 is_stmt
0x000000010000bd60 30 35 1 0 0 is_stmt prologue_end
0x000000010000bd78 30 0 1 0 0 is_stmt
0x000000010000bd90 30 35 1 0 0 is_stmt prologue_end
0x000000010000bdb0 14 0 2 0 0 is_stmt
0x000000010000bdc8 14 17 2 0 0 is_stmt prologue_end
0x000000010000bdf4 14 17 2 0 0 is_stmt end_sequence
从打印片段中,我们很容易就能看出每个文件的每一行代码都存储了起始地址以及行号。也就是说到这一步,我们可以提取出起始地址、行号、文件名。回顾下bugly的轻量符号表
Symbol table:
5ef0 5f00 viewDidLoad ViewController.swift:10
我们缺少函数名信息。到这一步就比较好处理了,回顾下上文,我们通过DW_TAG_subprogram
可以获取到这个函数的文件名、函数名、函数起始地址、函数终止地址。因此获取到行信息后,可以查看当前这个行的起始地址位于同文件下那个函数的指令区间内,即可得知函数名。
遇到的坑
1、数据获取失败
有时获取地址通过dwarf_formaddr函数并不能获取到数据,如果失败需要尝试dwarf_formudata、dwarf_formsdata等函数。
res = dwarf_formaddr(attr,&uval,errp);
if(res == DW_DLV_OK) {
*val = uval;
return;
}
res = dwarf_formudata(attr, &uval, errp);
if(res == DW_DLV_OK) {
*val = uval;
return;
}
Dwarf_Signed sival = 0;
res = dwarf_formsdata(attr, &sival, errp);
if(res == DW_DLV_OK) {
*val = sival;
return;
}
2、DW_AT_high_pc
实践发现DW_AT_high_pc
中存储的并不是结束地址,而是当前这个DIE的地址长度,即size。
3、dwarf_line_srcfileno
从函数名来看,仿佛是获取该行的文件编号,但是实际调用上会发现调用报错。libdwarf类似的情况还很多,经常函数调用报错但是却缺少错误信息,这对开发和调试来说很不方便。
总结及展望
初见debug_info的存储有点像AST的感觉,从DWARF文件中我们也能找出源码中的蛛丝马迹,甚至能根据DWARF还原出源码中的部分内容。例如变量定义,在DWARF文件中变量的名字、变量的类型都有介绍,形参类型等也有介绍。那么是不是某些基于抽象语法树的技术方案可以考虑能否用DWARF解析来实现呢?
下篇文章介绍
《如何通过Mach-O扫描Swift无用代码》
之前在简书平台上介绍过《从Mach-O角度谈谈Swift和OC的存储差异》和《一种Swift Hook新思路——从Swift的虚函数表说起》,通过Mach-O扫描OC无用类方案可能大家已经很熟悉了,但是Swift类如果调用的话并没有放到classref这个section中,那如何识别一个Swift类被调用呢?
作者简书:https://www.jianshu.com/u/739b677928f7
参考文献
https://github.com/avast/libdwarf
https://blog.csdn.net/wuhui_gdnt/article/details/7283483/
https://gohalo.me/post/program-c-gdb-dwarf-format-introduce.html
https://stackoverflow.com/questions/9719266/use-and-meaning-of-dw-at-location
https://stackoverflow.com/questions/45295190/dw-at-location-dw-op-fbreg-dw-op-addr