一、背景
前段时间群里有同学提说希望能在App
的崩溃日志中得到崩溃现场当前方法中各个变量名和其当前值(而不是只有寄存器),于是去调研了一下.dSYM
文件格式,发现理论上是可行的。
二、方案
1. .dSYM 文件基本概念
.dSYM
文件是Xcode
在编译iOS
工程过程中产生的符号文件,一般用于崩溃日志解析——将崩溃栈中的指令地址转换为实际代码文件及其对应行号。
以下命令可以显示
.dSYM
文件中各个段的大小:$ size -m xxx.dSYM/Contents/Resources/DWARF/xxx
我们感兴趣的是__DWARF
段中的__debug_info
节。
2. __debug_info 数据
__debug_info
节中存放了各个函数的起始、结束地址及函数中各局部变量的变量名、类型、内存地址(相对于fp或其他寄存器)信息。
以一个简单的测试方法为例:
- (void)myFunction:(int) arg {
int local = arg + 5;
int i;
for (i = 0; i < local; ++i)
printf("i = %d\n", i);
}
编译出.dSYM
文件后,运行以下命令可以导出__debug_info
信息:
$ dwarfdump --debug-info ./testDwarf.app.dSYM/Contents/Resources/DWARF/testDwarf
其中与-[ViewController myFunction:]
方法相关的部分如下:
0x0004005f: TAG_subprogram [122] *
AT_low_pc( 0x0000000100006760 ) //方法代码起始地址
AT_high_pc( 0x00000074 ) //方法代码长度
AT_frame_base( reg29 ) //指明此方法的frame base是x29(也就是fp),后面会用到
AT_object_pointer( {0x00040078} )
AT_name( "-[ViewController myFunction:]" ) //当前测试方法名
AT_decl_file( "/Users/jz/bsl/Tests/testDwarf/testDwarf/ViewController.m" ) //文件路径
AT_decl_line( 22 ) //行号
AT_prototyped( true )
0x00040078: TAG_formal_parameter [123]
AT_location( fbreg -8 )
AT_name( "self" )
AT_type( {0x000400bb} ( const ViewController* ) )
AT_artificial( true )
0x00040084: TAG_formal_parameter [123]
AT_location( fbreg -16 )
AT_name( "_cmd" )
AT_type( {0x000400c5} ( SEL ) )
AT_artificial( true )
0x00040090: TAG_formal_parameter [124]
AT_location( fbreg -20 ) //AT_location字段表明此变量(参数 arg)的内存地址在当前函数的 AT_frame_base 偏移 -20 处,myFunction函数的AT_frame_base 为 x29,则参数arg的实际存放地址为 $x29 - 20
AT_name( "arg" ) //参数 arg 变量名
AT_decl_file( "/Users/jz/bsl/Tests/testDwarf/testDwarf/ViewController.m" )
AT_decl_line( 22 )
AT_type( {0x000400d8} ( int ) ) //具体类型信息,见下个代码片断
0x0004009e: TAG_variable [125]
AT_location( breg31 +24 ) //局部变量 local 的存放位置为 breg31 + 24 == x31 + 24,其中:x31也就是sp
AT_name( "local" ) //局部变量local
AT_decl_file( "/Users/jz/bsl/Tests/testDwarf/testDwarf/ViewController.m" )
AT_decl_line( 23 )
AT_type( {0x000400d8} ( int ) ) //具体类型信息,见下个代码片断
0x000400ac: TAG_variable [125]
AT_location( breg31 +20 )
AT_name( "i" )
AT_decl_file( "/Users/jz/bsl/Tests/testDwarf/testDwarf/ViewController.m" )
AT_decl_line( 24 )
AT_type( {0x000400d8} ( int ) )
//arg和local的具体类型信息都指向 0x000400d8
0x000400d8: TAG_base_type [5]
AT_name( "int" )
AT_encoding( DW_ATE_signed )
AT_byte_size( 0x04 )
其中重点关注以下字段(详见上面代码片断中的注释):
-
AT_low_pc
:此方法代码开始地址 -
AT_high_pc
:此方法代码长度 -
AT_frame_base
:方法的frame base
,AT_location
中如果使用的fbreg
即取此frame base
的值 -
AT_name
:方法、参数、变量等的名称 -
AT_location
:参数/变量的内存地址,上例中:- 参数
arg
为:fbreg - 20
- 表明
arg
的存放地址在当前函数的AT_frame_base
偏移-20
处,myFunction
函数的AT_frame_base
为x29
,则参数arg
的实际存放地址为$x29 - 20
- 表明
- 局部变量
local
为:breg31 + 24
- 表明
local
的存放地址为breg31 + 24 == $x31 + 24
,其中:x31
也就是sp
寄存器
- 表明
- 参数
3. 数据验证
下面验证一下实际的汇编指令是否与上面的__debug_info
中的字段数据相吻合。
-
执行以下命令可以将二进制反汇编为汇编语言:
$ objdump -d ./testDwarf.app/testDwarf 复制代码
-[ViewController myFunction:]: 100006760: ff 03 01 d1 sub sp, sp, #64 100006764: fd 7b 03 a9 stp x29, x30, [sp, #48] 100006768: fd c3 00 91 add x29, sp, #48 10000676c: a0 83 1f f8 stur x0, [x29, #-8] 100006770: a1 03 1f f8 stur x1, [x29, #-16] 100006774: a2 c3 1e b8 stur w2, [x29, #-20] 100006778: a2 c3 5e b8 ldur w2, [x29, #-20] 10000677c: 42 14 00 11 add w2, w2, #5 100006780: e2 1b 00 b9 str w2, [sp, #24] //注:此处是对变量local的赋值,可对应上图中变量 local 的 AT_location( breg31 +24 ) 字段 100006784: ff 17 00 b9 str wzr, [sp, #20] 100006788: e8 17 40 b9 ldr w8, [sp, #20] 10000678c: e9 1b 40 b9 ldr w9, [sp, #24] 100006790: 1f 01 09 6b cmp w8, w9 100006794: aa 01 00 54 b.ge #52 100006798: e8 17 40 b9 ldr w8, [sp, #20] 10000679c: e0 03 08 aa mov x0, x8 1000067a0: e9 03 00 91 mov x9, sp 1000067a4: 20 01 00 f9 str x0, [x9] 1000067a8: 00 00 00 b0 adrp x0, #4096 1000067ac: 00 cc 19 91 add x0, x0, #1651 1000067b0: fd 00 00 94 bl #1012 1000067b4: e0 13 00 b9 str w0, [sp, #16] 1000067b8: e8 17 40 b9 ldr w8, [sp, #20] 1000067bc: 08 05 00 11 add w8, w8, #1 1000067c0: e8 17 00 b9 str w8, [sp, #20] 1000067c4: f1 ff ff 17 b #-60 1000067c8: fd 7b 43 a9 ldp x29, x30, [sp, #48] 1000067cc: ff 03 01 91 add sp, sp, #64 1000067d0: c0 03 5f d6 ret
-
观察
-[ViewController myFunction:]
方法的起始、结束地址,与__debug_info
中的AT_low_pc
和AT_high_pc
数值相吻合 -
观察地址
100006780
处对局部变量local
的赋值,其寻址方式为[sp, 24]
,也与AT_location
的内容相吻合
三、结论
综上可知,通过分析.dSYM
文件中的__DWARF
段__debug_info
节中的具体信息,能够在运行时(特别是崩溃时)得到方法内变量名对应的实际存放位置(内存地址),根据需要dump
出来相应内存的内容最后放到崩溃日志中即可实现原始需求。
注:因为涉及符号文件解析,可能有两个方案来实现:
- App中带上符号文件,崩溃时实时解析
- 将整个栈区内容dump下来,发到服务器上做具体解析 应该都只能用在内测版上。
注:此文只做了基本方案调研,工程化上还有很多需要考虑的点,可能还得实现或改造一个DWARF解析器,不在本文讨论范围之内。
参考资料
推荐👇:
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:789143298 ,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
申请即送:
-
BAT大厂面试题、独家面试工具包,
-
资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,
作者:zhangjiezhi_
链接:https://juejin.im/post/6883160410736820231
网友评论