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 Self-Sizing的一点优化
iOS安全:Tweak开发环境及入门
iOS文件系统目录结构
iOS URLSession Authentication Challenge及SSL Pinning
iOS Flutter 开发环境部署
发表留言
您的电子邮箱地址不会被公开,必填项已用*标注。
留言板