iOS Crash 流程化3:Crash 产生和符号化的原理
- iOS Crash 流程化3:Crash 产生和符号化的原理
- 异常类型
- Mach异常
- Unix信号
- 异常的产生
- 线程回溯
- 符号化回溯线程
- 符号在二进制中的偏移量
- atos
- 符号化回溯线程
- 符号化内幕
- 小小结
- 线程的状态寄存器
- Binary Images
- 小结
- 异常类型
iOS 的异常类型(Exception Type)由两部分构成:Mach异常、Unix信号异常。
异常类型
Mach异常
苹果系统有一个微内核,叫做XNU,它的源码可以在opensource上下载到。Mach是XNU的核心,因而,Mach异常就指Mach内核异常。Mach包含三部分内容:thread,task,host。后续的章节中很多地方都会用到Mach。不妨移步到Mach IPC Interface,了解下Mach暴露给用户的API。
Mach暴露给了用户部分API,允许用户和内核交互。用户态的开发者可以通过Mach API设置thread、task、host的异常端口,来捕获Mach异常,抓取Crash事件。
Mach异常包括:
#define EXC_BAD_ACCESS 1 /* Could not access memory */
/* Code contains kern_return_t describing error. */
/* Subcode contains bad memory address. */
#define EXC_BAD_INSTRUCTION 2 /* Instruction failed */
/* Illegal or undefined instruction or operand */
#define EXC_ARITHMETIC 3 /* Arithmetic exception */
/* Exact nature of exception is in code field */
#define EXC_EMULATION 4 /* Emulation instruction */
/* Emulation support instruction encountered */
/* Details in code and subcode fields */
#define EXC_SOFTWARE 5 /* Software generated exception */
/* Exact exception is in code field. */
/* Codes 0 - 0xFFFF reserved to hardware */
/* Codes 0x10000 - 0x1FFFF reserved for OS emulation (Unix) */
#define EXC_BREAKPOINT 6 /* Trace, breakpoint, etc. */
/* Details in code field. */
#define EXC_SYSCALL 7 /* System calls. */
#define EXC_MACH_SYSCALL 8 /* Mach system calls. */
#define EXC_RPC_ALERT 9 /* RPC alert */
#define EXC_CRASH 10 /* Abnormal process exit */
#define EXC_RESOURCE 11 /* Hit resource consumption limit */
Unix信号
信号是通知进程已发生某种情况的软中断技术。例如:某个进程执行了除法操作,其除数为0,则将名为SIGFPE(浮点异常)的信号发送给该进程。
异常的产生
那么,怎么会有两种异常信息呢?
念茜的漫谈iOS Crash收集框架阐述了两者的关系,这里再重复下。
苹果系统是基于Unix系统的,苹果的大牛们为了兼容Unix信号,将Mach异常转化为Unix信号,并投射到异常的线程,这样做的目的是:对于不懂Mach异常的人,也可以使用Unix信号捕获异常。所以,Crash日志有两种异常信息。
Mach和Unix关系图:
image
所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。
捕获Mach异常或者Unix信号都可以抓到crash事件,这两种方式哪个更好呢?
优选Mach异常,因为Mach异常的处理会先于Unix信号处理,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。
所以,Crash日志中的EXC_BAD_ACCESS 是Mach异常信息,SIGSEGV是Unix信号异常信息。
小贴士:
因为硬件产生的信号(通过CPU陷阱)被Mach层捕获,然后才转换为对应的Unix信号;苹果为了统一机制,于是操作系统和用户产生的信号(通过调用kill和pthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。
线程回溯
符号化回溯线程
线程的回溯是APP Crash瞬间,程序中所有线程的逆向调用堆栈。线程回溯对我们修复Crash非常有用,根据线程回溯,可以分析、定位程序崩溃的原因。
下面将崩溃的代码、未符号化崩溃日志、符号化崩溃日志贴出来,做个对比性的理解。
@implementation ViewController
- (IBAction)onCrash:(__unused id)sender {
char* ptr = (char*)-1;
*ptr = 10; ///这里程序崩溃了
}
@end
未符号化的崩溃日志
符号化的崩溃日志
符号在二进制中的偏移量
未符号化的崩溃日志中红色文字展示了几个名词:镜像文件、加载地址、堆栈地址。以及没有展示出来的一个名词:符号在二进制中的偏移量。他们的含义分别为:
- 镜像文件:是可执行二进制文件和二进制文件依赖的动态库的总称。
- 堆栈地址:是代码在内存中执行的内存地址。
- 镜像的加载地址:程序执行时,内核会将包含程序代码的镜像加载到内存中,镜像在内存中的基地址就是加载地址。程序每次启动时,镜像的加载地址是随机的。所以,同一代码在不同的设备中执行时,堆栈地址是不一样的。
- 符号在二进制中的偏移量:按照字面意思理解吧。它以通过下面的公式得到:
符号在二进制中的偏移量 = 堆栈地址 - 镜像的加载地址
符号在二进制中的偏移量非常有用,我们就是根据它,从符号文件中查找出地址对应的代码符号。这里的符号文件指的是:带有符号表的可执行二进制文件、dSYM文件,这两种文件在后续章节中都统称为符号文件。
那么怎么将未符号化的崩溃日志符号化呢?
atos
苹果自带的atos命令行工具可以查找地址对应的符号,在终端中输入:
/usr/bin/atos -o [符号文件] -arch arm64 -l 0x100030000 0x000000010003522c
输出结果如下:
-[ViewController onCrash:] (in Simple-Example) (ViewController.m:10)
是不是很简单的就将地址转换为符号?是的,只需将符号文件(-o指定)、代码构架(-arch指定)、加载地址(-l指定)、堆栈地址传入atos命令,就能解析出符号。
atos命令解析出了堆栈地址为0x000000010003522c、加载地址为0x100030000对应的符号。符号为[ViewController onCrash:],也验证了崩溃发生在onCrash函数中,也验证了崩溃日志中的地址是可以符号化的。
符号化原理是什么?怎么就通过地址找到了Crash代码的符号,这就涉及符号化内幕。
符号化内幕
符号化的内幕就是:==在符号文件中,通过偏移量查找符号==。下面,一步步的来分析,首先计算Crash地址在符号文件中的偏移量,为000000010000522c。
符号在二进制中的偏移量 = 堆栈地址 - 镜像的加载地址 = 0x000000010003522c - 0x100030000 = 000000010000522c
在符号文件中直接找地址000000010000522c,应该是找不到,在后续你可以理解。我们使用逆向方法,根据符号-[ViewController onCrash:],找对应的地址,比较是不是000000010000522c,如果是,就充分说明了,通过偏移量是可以查找到内存地址对应的符号的。在终端中输入下面的命令:
nm [符号文件] | grep "ViewController onCrash:"
输出如下
00008320 t -[ViewController onCrash:]
0000000100005224 t -[ViewController onCrash:]
输出的第一行是armv7s构架的符号,第二行是arm64构架的符号,Crash日志显示的代码构架是arm64,使用第二行,符号-[ViewController onCrash:]对应的偏移量是0000000100005224,而不是 000000010000522c,这个公式是在stack overflow上找到的,就相差8!后来写日志组织测试用例的时候,忽然明白了为什么差那一点点。
原来,我们通过nm命令查找出的符号地址对,是函数入口地址和对应的函数调用的符号对,仅仅是函数调用的符号,没有函数内部代码的符号,而程序是崩溃到函数内部,崩溃到*ptr = 10
这句话,内部代码的地址怎么可能和入口地址一样呢!相差一点点!
下面根据偏移量000000010000522c和代码推算函数的入口地址吧,看看是什么。崩溃代码*ptr = 10
前面只有一个语句—定义初始化指针char* ptr = (char*)-1
,在64位系统上指针的地址占8个字节,000000010000522c - 8= 0000000100005224,果然是0000000100005224。
这个不就是函数的入口地址嘛。原来那一点点的原因在这里。那么偏移量0000000100005224 对应的符号正是-[ViewController onCrash:]
。
上面通过nm 命令查找符号可能不直观,可以通过可视化工具MachOView查看。验证下吧,选择 Debug Symbols(ARM64_ALL)->Symbol Table->Symbols
,然后在右上角的搜索框中输入符号:-[ViewController onCrash:]
,结果如下:
通过这个工具可以直观的查看到符号和地址的对应关系。
小小结
终于把符号化和符号化原理阐述完了简单回顾下:
- 可以通过系统的atos符号化崩溃日志的单个符号
- 符号化内部原理就是:根据符号在二进制中的偏移量,在符号文件中查找对应的符号。其中:==符号在二进制中的偏移量 = 堆栈地址 - 镜像的加载地址==。
线程的状态寄存器
Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x000000010050b460 x1: 0x0000000100102cea x2: 0x00000001004339d0 x3: 0x00000001740f8f00
x4: 0x00000001740f8f00 x5: 0x00000001740f8f00 x6: 0x0000000000000001 x7: 0x0000000000000000
x8: 0xffffffffffffffff x9: 0x000000000000000a x10: 0x00000001b3ad0018 x11: 0x00c1580100c15880
x12: 0x0000000000c15800 x13: 0x0000000000c15900 x14: 0x0000000000c158c0 x15: 0x0000000000c15801
x16: 0x0000000000000000 x17: 0x00000001000c1224 x18: 0x0000000000000000 x19: 0x00000001740f8f00
x20: 0x00000001004339d0 x21: 0x0000000100102cea x22: 0x000000010050b460 x23: 0x0000000170240bd0
x24: 0x000000017400db90 x25: 0x0000000000000001 x26: 0x0000000000000000 x27: 0x00000001b2822000
x28: 0x0000000000000040 fp: 0x000000016fd41ab0 lr: 0x0000000194aea7b0
sp: 0x000000016fd41a90 pc: 0x00000001000c122c cpsr: 0x60000000
这是APP crash的时候,ARM64 构架CPU的32个寄存器的值, 其中 fp 帧指针、sp 堆栈指针,lr 是返回地址指针,这三个都比较有用,用来逐级回溯线程调用栈。
Binary Images
image镜像文件就是上面讲的可执行程序 和 依赖的所有动态库。
镜像文件中包括镜像的加载地址,和线程回溯中的镜像加载地址指的是一个地址。加载地址后面有个UUID,符号文件中也有个UUID,只有这两个地址一致,才能解析出地址对应的符号。符号文件中的UUID可以通过终端中输入下面的命令得到:
dwarfdump —u [符号文件]
输出如下:
UUID: C8E0E6E4-F761-3A19-B231-A31C1BB9037A (armv7)
UUID: 39BBB8F4-CCB0-3193-8491-C007931CA05E (arm64)
第二行的arm64构架的UUID居然和图8中的红色矩形框中UUID必须一致,这才表示代码对应的符号能在这个符号文件中找到,如果不一致,就没法解析出地址对应的符号。不论是Xcode,还是symbolicatecrash,都解析不了。
也可以通过MachOView查看符号文件的UUID,结果如下:
image小结
这里阐述了日志的产生原因和符号化崩溃日志的原理。同时提及了几个有用的工具:
- file 文件类型显示工具(The file-type displaying tool,位于/usr/bin/file);
- atos (将数字地址转换为镜像或可执行程序中的符号工具,convert numeric addresses to symbols of binary images or processes,位于/usr/bin/atos);
- nm(符号表展示工具,The symbol table display tool,位于 /usr/bin/nm);
- 可视化查看Mach-O工具,MachOView