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

前言

不知道大家有没有用过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_variableDW_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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 一、符号表 1、概念:debugger Symbols 的简称。符号表就是指在Xcode项目编译后,在编译生成的....
    codeTao阅读 18,746评论 2 17
  • 目的 探索iOS Crash分类及捕获流程 了解Crash文件结构及段含义 了解Mach-o文件结构 分析Cras...
    lltree阅读 1,293评论 0 8
  • 从开发中常见的调试场景开始 打开IDE在某个方法中设置断点,切换到其他源文件后运行程序 运行到断点时,程序停止,I...
    毅个天亮阅读 901评论 0 0
  • 一:介绍 1、什么是符号表? 符号表是内存地址与函数名、文件名、行号的映射表。 符号表元素如下所示: <起始地址>...
    平凡之路561阅读 1,248评论 1 1
  • 1. 为什么崩溃日志需要解析 如图所示是崩溃日志线程回溯信息,其中的调用堆栈都是二进制地址,而不是可读的函数名称因...
    落叶情思阅读 1,550评论 0 1