一、背景

前段时间群里提到希望能在App的崩溃日志中得到崩溃现场当前方法中各个变量名和其当前值(而不是只有寄存器),于是去调研了一下.dSYM文件格式,发现理论上是可行的。
想了解其他相关的可以联系我,同时可以进群提问。642363427

二、方案

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解析器,不在本文讨论范围之内。