App崩溃现场取变量名和其实际值对应关系(不只是寄存器)

3,919 阅读4分钟

一、背景

前段时间群里有同学提说希望能在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 baseAT_location中如果使用的fbreg即取此frame base的值
  • AT_name:方法、参数、变量等的名称
  • AT_location:参数/变量的内存地址,上例中:
    • 参数arg为:fbreg - 20
      • 表明arg的存放地址在当前函数的AT_frame_base偏移-20处,myFunction函数的AT_frame_basex29,则参数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_pcAT_high_pc数值相吻合

  • 观察地址100006780处对局部变量local的赋值,其寻址方式为[sp, 24],也与AT_location的内容相吻合

三、结论

综上可知,通过分析.dSYM文件中的__DWARF__debug_info节中的具体信息,能够在运行时(特别是崩溃时)得到方法内变量名对应的实际存放位置(内存地址),根据需要dump出来相应内存的内容最后放到崩溃日志中即可实现原始需求。

注:因为涉及符号文件解析,可能有两个方案来实现:

  • App中带上符号文件,崩溃时实时解析
  • 将整个栈区内容dump下来,发到服务器上做具体解析 应该都只能用在内测版上。

注:此文只做了基本方案调研,工程化上还有很多需要考虑的点,可能还得实现或改造一个DWARF解析器,不在本文讨论范围之内。

参考资料

最后,欢迎关注我的微信公众号,有空多多交流