一、背景
二、方案
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寄存器
- 参数arg为:fbreg - 20
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解析器,不在本文讨论范围之内。