iOS崩溃捕获原理
定义自己的信号处理函数,代替内核的默认处理,需要监听从当mach微内核发生mach异常(EXC_BAD_ACCESS,EXC_CRASH等,在这一瞬间先暂停所有子线程(除捕获线程外),捕获到各个线程的backtrace,backtrace只能以16进制地址信息存储、产生异常的线程、捕获发生的时间、可以获取其他硬件信息、获取当前进程加载的二进制镜像文件包括镜像开始地址,结束地址,镜像名字)、app info,崩溃的uuid,Last Exception Backtrace等;与之对应mach异常转换为unix信号量(SIGSEGV,SIGBUS、SIGILL、SIGTRAP等),退出进程。需要用到相关API介绍绍:http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/。
如何拿到线程调用栈:
1、Objective-c层API:[NSThread callStackSymbols]
2、C语言层API:backtrace();
3、通过寄存器数据获取
SP:指向当前线程栈的栈顶地址;
FP:指向当前栈帧的地址;
LR:保存当前栈帧结束后要返回的地址;
PC:保存下一条存取指令的地址;
4、其它应用层的语言有相应的API获取崩溃的堆栈,在捕获异常时,可以发送SIGABRT信号给整个异常捕获系统;
如何将地址还原成可读的符号
1、苹果官方使用的基本方式:从手机中导出奔溃的.ips文件,使用symbolicatecrash工具,XXX.app的可执行文件,XXX.app.dSY文件进行还原栈信息,需要3者文件中的UUID完全一样;
2、可以直接将整个崩溃日志放到在Xcode -> Devices and Simulators中 View Device Logs中查看,可以自动帮你符号化;
3、使用atos命令可以符号化一个地址,也可以批量符号化多个地址;
XCode可以查看用户崩溃最多的记录
Xcode -> Organizer的Crashes种查看用户上传的crash log 这里出现的用户crash频率最高,时间最近;
Bugly与github开源的PLCrashReporter, KSCrash
1、bugly目前拿到的堆栈和PLCrashReporter拿到的堆栈几乎完全一样(bugly目前本地符号化比PLCrashReporter本地符号化更详细,后来推测,放到服务器端符号化缺少的系统符号表);
2、bugly目前拿到crash thread获取到的文件行号不太正确(包括PLCrashReporter);
3、bugly使用buglySymboliOS.jar工具提取XXX.app.dSY,并生成符号表,根据.ips文件中的奔溃的16进制地址对照符号表,最后还原成对应的文件名,类名,函数名,行号;
4、KSCrash能获取到mach异常,c++异常,oc异常,主线程死锁,fatal signals,自定义crashs(本地符号化没有比bugly更详细);
5、这里会出现bugly拿到其他线程堆栈和苹果生成的crash report其他线程堆栈会不一致的现象,这种现象是由于获取奔溃处理异常,有先后顺序,通常bugly,PlCrashReporter优先于系统捕获(通常时间间隔1秒)。
6、bugly根据不同的日志级别进行上报,这一点在客户端写死了日志级别,我们可以做到在云端控制日志级别上报。
疑惑:bugly符号化的时候,没有采用苹果官方使用的方式如XXX.app/XXX,XXX.app.dSY.ips,symbolicatecrash,但在网站上需要上传符号表文件,在没上传有的crahsh已经做到完全符号化,所以此时不需要上传符号表文件,对于这种疑惑bugly使用本地符号化策略,在运行时采用不精准的启用方式和符号数据,一些符号化依赖于runtime内部运行知识(使用Objective-C元数据查找方法和类名,这依赖于Objective-C运行时数据的详细解析,包括未定义的标志和其他运行时内部。 因此,如果运行时更改不兼容,它可能会返回不正确的数据),这些知识在未来ios版本中可能会发生变化,DWARF符号化数据更加准确(也就是苹果官方推荐使用XXX.app.dSY文件符号化),强烈建议仅对非发布版本启用本地符号。
本地符号化的原理
根据崩溃地址先找到崩溃的镜像如:(.dylib,app可执行文件,这些为mach-o文件,可以使用MachOView打开,也可以使用某种规则解析二进制流文件),然后找到基础段位置+每个镜像都会有一定的偏移,但都不会相同slide(ASLR地址空间布局随机),从struct symtab_command结构体中,根据相应规则读取符号表,字符串表(每一个mach-o文件都会存在),再根据给定的地址找到最接近的64位的struct nlist_64或者32的位struct nlist实体变量,最后在符号表中找到nlist实体变量的索引,在字符串表中的找到相应的字符信息。
本地符号在崩溃时会执行更多代码,存在风险,并且本地符号化有一部分不会被符号化,一般我遇到的是系统框架无法被符号化,未符号化的那部分16进制地址放到后台去符号化,鉴于此需要找到崩溃的设备的CPU架构,设备版本,然后找到对应的
系统符号表/Users/xxx/Library/Developer/Xcode/iOS DeviceSupport/11.0 (15A372)/Symbols/usr/lib/system/libdyld.dylib
11.0 (15A372)对应的OS Version,鉴于此,iOS系统版本需要几十种,相关介绍:https://github.com/Zuikyo/iOS-System-Symbols;
libdyld.dylib 对应系统版本,在奔溃一瞬间,会有很多系统库,需要找到对应的版本,系统库,否则,符号化的数据不正确;
atos命令使用与Bugly jar 包工具
使用命令基本用法:atos -arch arm64 -o libdyld.dylib -l 0x183cd7000(libdyld.dylib二进制镜像的开始地址) 0x0000000183cd856c(需要符号化的地址),还原系统16进制地址,可以有两种方式实现:
第一种:在服务端使用MAC OS平台,完全可以写一个shell脚本或者使用python,将需要的参数传入,结合atos命令,将被符号化的字符串写入到关系数据库,最后达到和bugly完全一样的效果。
第二种:bugly使用方式,使用符号表工具iOS版本buglySymboliOS.jar工具针对指定系统版本的系统符号表如:libdyld.dylib 生成关联映射的符号表包含三个字段(开始地址,结束地址,函数名称)系统符号表目前未提供行号,最后存入到关系数据库中,最后通过16进制地址查询进行还原;当然buglySymboliOS.jar工具也可以对我们XXX.app.dSY生成相应的符号表,包含(开始地址,结束地址,函数名称,文件名:行号)然后存入到数据库。附上:(另外利用runtime的特色,在崩溃之前,根据16进制地址,去相应元数据也能找到对应的类名,函数名,为本地符号化提供基础)。
维护针对最新iOS设备需要加入新的系统符号框架;如果要达到精准的(app如果开启bitcode,每次需要从apple下载可执行文件,XXX.app.dSY文件,app如果关闭bitcode,在编译时保存好XXX.app.dSY文件)。