iOS crash log分析实践 / 2019-09-02

通过分析线上的一个crash演示iOS crash log分析的一般方法。会涉及到otool,dwarfdump,atos,IDA等工具的简单使用及iOS 汇编分析。

通过分析线上的一个crash演示iOS crash log分析的一般方法。

我们收到的 iOS 的crash log一般都是如下形式(以arm64架构为例):

Thread 0 Crashed:
0   Project  0x10722957c 0x10461c000 + funcA
1   Project  0x10722958c 0x10461c000 + funcB
2   Project  0x107226b0c 0x10461c000 + funcC
3   Project  0x1073143f0 0x10461c000 + funcD
	库名称	实际运行地址  基地址  方法偏移地址  

其中每一行都表示一个函数调用,有如下的规则:

实际运行地址 = 基地址 + 方法偏移地址;

实际上,上方演示的crash log已经是符号化之后的样子,符号化之前就像下面这样:

Thread 0 Crashed:
0   Project  0x10722957c 0x10461c000 + 46191996
1   Project  0x10722958c 0x10461c000 + 46192012
2   Project  0x107226b0c 0x10461c000 + 46181132
3   Project  0x1073143f0 0x10461c000 + 47154160

关于符号化Crash log的内容,可以参考之前的笔记:iOS Crash log符号化

符号化之后的crash log将变得易于阅读,甚至能精确到Objective-C/C代码的哪一行。但对于一些C函数,特别是代码经过混淆之后的,即使符号化之后的crash log可能不会那么直观。在上述示例中,crash发生在funcA,但funcA是一个比较复杂的方法,具体是funcA哪里出问题了,还需要进一步使用工具分析。

分析工具

要分析crash,需要拿到二进制文件和对应的dSYM文件,然后使用各种辅助工具。

otool

otool是分析Mach-O很方便的工具。

otool -l Project.app/Project

结果示例片段:

Project.app/Project (architecture arm64):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777228   0  0x00   2    69   7928 0x00218085
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot 0x00000000
 initprot 0x00000000
   nsects 0
    flags 0x0
Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1272
  segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x0000000003580000
  fileoff 0
 filesize 56098816
  maxprot 0x00000005
 initprot 0x00000005
   nsects 15
    flags 0x0

这里的重要信息就是vmaddr 0x0000000100000000,后续计算时会用到。

dwarfdump

使用dwarfdump检查二进制文件和dSYM文件是否匹配。

$ dwarfdump --uuid Project.app/Project
UUID: 67AC6ECC-BFB2-3294-8728-CAF4CD2AF1F2 (armv7)
UUID: 91BE9A39-24BC-3F8D-B0EA-59E14C758617 (arm64)

$ dwarfdump --uuid Project.app.dSYM
UUID: 67AC6ECC-BFB2-3294-8728-CAF4CD2AF1F2 (armv7)
UUID: 91BE9A39-24BC-3F8D-B0EA-59E14C758617 (arm64)

#查询指定地址符号:--lookup参数的地址需要计算,推荐使用atos命令。
dwarfdump --arch=arm64 --lookup=[crash地址] [dSYM文件]

atos

macOS系统自带了一个分析Crash log的工具:atos。相比Xcode中的symbolicatecrash工具,atos更加灵活。atos分析符号信息的命令是:

atos -arch 架构 -o dSYM文件 -l 基地址 实际运行地址1 实际运行地址2

atos -arch arm64 -o Project.app.dSYM/Contents/Resources/DWARF/Project -l 0x10461c000 0x10722957c

IDA

IDA是强大的反汇编分析工具,使用IDA工具打开Mach-O二进制,会自动进行加载分析(二进制越大,耗时越长),完成后即可进行分析,常用的使用方式:

  • g 跳转到指定地址;
  • 空格 切换预览方式;
  • F5 生成伪代码,F5不可用时,也可以使用View->Open subViews->Generate pseudocode;
  • ESC 回退上一步;

汇编基础

要分析汇编代码,需要掌握必要的汇编基础知识。(以arm64为例)

寄存器

ARM64架构中共有34个寄存器,包括31个通用寄存器、SP、PC、CPSR。

  • 通用寄存器

r0 - r30是31个通用寄存器。每个寄存器可以存取一个64位大小的数。 当使用x0 - x30访问时,它就是一个64位的数。当使用w0 - w30访问时,访问的是这些寄存器的低32位。
r29又称FP寄存器(frame point),主要用来保存栈帧(栈底)指针。
r30又称LR寄存器(link register),主要用来保存函数返回地址。

  • SP:stack pointer,栈顶指针;
  • PC:用来记录当前执行的指令地址;
  • CPSR:状态寄存器。

指令

  • MOV

mov x0, x1:把寄存器r1的值赋给寄存器r0 mov w0, #0x11:把0x11写进r0的低16位

  • LDR 存储数据指寄存器
  • STR 读取寄存器的值
  • LDP 存储器连续读两个单元到两个寄存器
  • STP 把两个寄存器的值连续放在某个内存地址后
  • CMP 比较指令,需要用到CPSR存储结果
  • B 跳转指令

把要跳转的指令地址赋给PC寄存器(不能直接操作PC寄存器)

  • BL带返回地址的跳转指令

把要跳转的指令地址赋给PC寄存器,同时把下一个指令地址(返回地址)赋给LR寄存器

  • RET 子程序退出返回指令

函数调用

在ARM64中,函数的参数是保存在x0 - x7(w0 - w7)这8个寄存器里,如果函数超过8个参数,超过的参数则会保存在栈里。

分析

在上述示例中,funcA的偏移为:0x10722957c-0x10461c000=0x2C0D57C; 而在IDA中分析时,由于是ARM64架构,需要加上vmaddr的偏移,也就是实际上funcA在IDA中的地址应该是:0x2C0D57C+0x0000000100000000=0x102C0D57C;

在IDA中执行g 0x102C0D57C即可跳转到指定汇编地址,如下:

__text:0000000102C0D568 loc_102C0D568; CODE XREF: sub_102C0B940+73C↑j
__text:0000000102C0D568 MOV 	 W23, #0x7209
__text:0000000102C0D56C MOVK   W23, #0xABB2,LSL#16
__text:0000000102C0D570 MOV    W8, #0x18
__text:0000000102C0D574 LDR    X9, [SP,#0xD8]
__text:0000000102C0D578 UMADDL X8, W26, W8, X9
__text:0000000102C0D57C LDP    X0, X1, [X8]
__text:0000000102C0D580 MOV    X2, #0
__text:0000000102C0D584 MOV    X3, #0
__text:0000000102C0D588 BL     sub_102C15504
__text:0000000102C0D58C MOV    W17, #0x29E0
__text:0000000102C0D590 MOVK   W17, #0x11D9,LSL#16
__text:0000000102C0D594 MOV    W12, #0xA087
__text:0000000102C0D598 MOVK   W12, #0x60E3,LSL#16
__text:0000000102C0D59C MOV    W15, #0x3E3E
__text:0000000102C0D5A0 MOVK   W15, #0x1DD0,LSL#16
__text:0000000102C0D5A4 MOV    W14, #0xA2D3
__text:0000000102C0D5A8 MOVK   W14, #0xD95C,LSL#16
__text:0000000102C0D5AC ADRP   X11, #dword_1040AF698@PAGE
__text:0000000102C0D5B0 ADD    X11, X11, #dword_1040AF698@PAGEOFF
__text:0000000102C0D5B4 ADD    W8, W26, #1
__text:0000000102C0D5B8 STUR   W8, [X29,#-0x94]
__text:0000000102C0D5BC B      loc_102C107FC

由于上线的二进制是没有符号的,在IDA中的方法都会被以loc_前缀标记,比如上述的loc_102C0D568,sub_102C15504等。除此之外,还有其他形式:

  • sub_:指令和子函数起点;
  • loc_:指令;
  • byte_:字节数据;
  • word_:16位数据;
  • dword_:32位数据;
  • qword_:64位数据;
  • flt_:32位浮点数
  • dbl_:64位浮点数
  • stru_:结构体;

通过上述汇编代码,可以知道crash发生在:

__text:0000000102C0D57C LDP    X0, X1, [X8]
__text:0000000102C0D580 MOV    X2, #0
__text:0000000102C0D584 MOV    X3, #0
__text:0000000102C0D588 BL     sub_102C15504

上述片段的逻辑就是把参数填充至x0,x1,x2,x3 4个寄存器,然后调用sub_102C15504方法,在IDA中双击sub_102C15504即可跳转到其实现。

__text:0000000102C15504 sub_102C15504 ; CODE XREF: sub_102C0AB20+280↑p
__text:0000000102C15504 ; sub_102C0B940:loc_102C0D34C↑p ...
__text:0000000102C15504 STP X28, X27, [SP,#-0x60]!
__text:0000000102C15508 STP X26, X25, [SP,#0x10]

sub_102C15504方法的入口地址为0000000102C15504,计算:

#该方法的偏移为:
0x0000000102C15504-0x0000000100000000=0x2C15504;
#加上基地址:
0x2C15504+0x10461c000=0x107231504

执行查询:

atos -arch arm64 -o Project.app.dSYM/Contents/Resources/DWARF/Project -l 0x10461c000 0x107231504
_myfunc (in Project) + 0

IDA不能加载dSYM文件以便能看到查看符号化的汇编代码(直觉上应该是有这个功能,只是我还没发现怎么操作)。我的笨方法就是在IDA中计算好地址后,再通过atos工具符号化。

这个时候就很清晰了:在调用_myfunc之前,填充寄存器参数时发生了crash。

那么:

__text:0000000102C0D57C LDP    X0, X1, [X8]

为什么会发生crash呢?这个时候就需要再查看crash log:

Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Codes: 0x00000000 at 0x0000000108d6b900
...
Thread 0 crashed with ARM-64 Thread State:
  cpsr: 0x0000000060000000     fp: 0x000000016b7e0e70     lr: 0x000000010722958c     pc: 0x000000010722957c 
    sp: 0x000000016b7e0ab0     x0: 0x0000000000000001     x1: 0x0000000283f46c70    x10: 0x00000000ffffffff 
   x11: 0x00000001086cb698    x12: 0x0000000060e3a087    x13: 0x0000000000000000    x14: 0x00000000d95ca2d3 
   x15: 0x000000001dd03e3e    x16: 0x00000001d332016c    x17: 0x0000000011d929e0    x18: 0x0000000000000000 
   x19: 0x000000009bbaf60f     x2: 0x0000000000000008    x20: 0x000000008e29aca6    x21: 0x0000000000000000 
   x22: 0x00000000a1afcc16    x23: 0x00000000abb27209    x24: 0x00000000f6b78c6c    x25: 0x000000002eec7c6a 
   x26: 0x0000000000000260    x27: 0x0000000022f25493    x28: 0x0000000022f25494    x29: 0x000000016b7e0e70 
    x3: 0x00000001d332d0dc     x4: 0x00000000fcb2c797     x5: 0x00000000de2e92c3     x6: 0x0000000000000001 
    x7: 0x00000000b7115144     x8: 0x0000000108d6b900     x9: 0x0000000108d68000 

EXC_BAD_ACCESS类型的crash就是内存不可访问,也就是X8寄存器的地址无法访问。这个时候就需要对照源码,查看代码逻辑是否未做内存访问的校验,优化处理。

其它文章

iOS 13中dyld 3的改进和优化
Address Sanitizer的原理和使用
iOS Self-Sizing的一点优化
iOS Universal Links
iOS Asset Catalog and Bundle