漫谈iOS Crash收集框架
漫谈iOS Crash收集框架
为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrash,plcrashreporter,CrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 Crashlytics,Hockeyapp ,友盟,Bugly 等等。
是否集成越多的Crash日志收集服务就越保险?
自己收集的Crash日志和系统生成的Crash日志有分歧,应该相信谁?
为什么有大量Crash日志显示崩在main函数里,但函数栈中却没有一行自己的代码?
野指针类的Crash难定位,有何妙招来应对?
想解释清这些问题,必须从Mach异常说起
Mach异常与Unix信号
iOS系统自带的 Apple’s Crash Reporter 记录在设备中的Crash日志,Exception Type项通常会包含两个元素: Mach异常 和 Unix信号。
1
2
|
Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3 |
Mach异常是什么?它又是如何与Unix信号建立联系的? Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。
所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。
因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。既然最终以信号的方式投递到出错的线程,那么就可以通过注册signalHandler来捕获信号:
1
|
signal (SIGSEGV,signalHandler); |
捕获Mach异常或者Unix信号都可以抓到crash事件,这两种方式哪个更好呢? 优选Mach异常,因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过Unix信号的方式来兼容开发。
因为硬件产生的信号(通过CPU陷阱)被Mach层捕获,然后才转换为对应的Unix信号;苹果为了统一机制,于是操作系统和用户产生的信号(通过调用kill和pthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。
Crash收集的实现思路
正如上述所说,可以通过捕获Mach异常、或Unix信号两种方式来抓取crash事件,于是总结起来实现方案就一共有3种。
1)Mach异常方式
2)Unix信号方式
1
|
signal (SIGSEGV,signalHandler); |
3)Mach异常+Unix信号方式
Github上多数开源项目都采用的这种方式,即使在优选捕获Mach异常的情况下,也放弃捕获EXC_CRASH异常,而选择捕获与之对应的SIGABRT信号。著名开源项目plcrashreporter在代码注释中给出了详细的解释:
We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for EXC_CRASH.
另外,需要重点说明的是:对于应用级异常NSException,还需要特殊处理。 你是否见过崩溃在main函数的crash日志,但是函数栈里面没有你的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
Thread 0 Crashed: 0 libsystem_kernel.dylib 0x3a61757c __semwait_signal_nocancel + 0x18 1 libsystem_c.dylib 0x3a592a7c nanosleep$NOCANCEL + 0xa0 2 libsystem_c.dylib 0x3a5adede usleep$NOCANCEL + 0x2e 3 libsystem_c.dylib 0x3a5c7fe0 abort + 0x50 4 libc++abi.dylib 0x398f6cd2 abort_message + 0x46 5 libc++abi.dylib 0x3990f6e0 default_terminate_handler() + 0xf8 6 libobjc.A.dylib 0x3a054f62 _objc_terminate() + 0xbe 7 libc++abi.dylib 0x3990d1c4 std::__terminate( void (*)()) + 0x4c 8 libc++abi.dylib 0x3990cd28 __cxa_rethrow + 0x60 9 libobjc.A.dylib 0x3a054e12 objc_exception_rethrow + 0x26 10 CoreFoundation 0x2f7d7f30 CFRunLoopRunSpecific + 0x27c 11 CoreFoundation 0x2f7d7c9e CFRunLoopRunInMode + 0x66 12 GraphicsServices 0x346dd65e GSEventRunModal + 0x86 13 UIKit 0x32124148 UIApplicationMain + 0x46c 14 XXXXXX 0x0003b1f2 main + 0x1f2 15 libdyld.dylib 0x3a561ab4 start + 0x0 可以看出是因为某个NSException导致程序Crash的,只有拿到这个NSException,获取它的reason,name,callStackSymbols信息才能确定出问题的程序位置。 /* NSException Class Reference */ @property(readonly, copy) NSString *name; @property(readonly, copy) NSString *reason; @property(readonly, copy) NSArray *callStackSymbols; @property(readonly, copy) NSArray *callStackReturnAddresses; 方法很简单,可通过注册NSUncaughtExceptionHandler捕获异常信息: static void my_uncaught_exception_handler (NSException *exception) { //这里可以取到 NSException 信息 } NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler); |
将拿到的NSException细节写入Crash日志,精准的定位出错程序位置:
1
2
3
4
5
6
7
8
9
10
|
Application Specific Information: *** Terminating app due to uncaught exception 'NSUnknownKeyException' , reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key key.' Last Exception Backtrace: 0 CoreFoundation 0x2f8a3f7e __exceptionPreprocess + 0x7e 1 libobjc.A.dylib 0x3a054cc objc_exception_throw + 0x22 2 CoreFoundation 0x2f8a3c94 -[NSException raise ] + 0x4 3 Foundation 0x301e8f1e -[NSObject(NSKeyValueCoding) setValue:forKey:] + 0xc6 4 DemoCrash 0x00085306 -[ViewController crashMethod] + 0x6e 5 DemoCrash 0x00084ecc main + 0x1cc 6 DemoCrash 0x00084cf8 start + 0x24 |
那么,是不是收到了大量crash在main函数却没有NSException信息的日志,就代表自己集成的Crash日志收集服务没有注册NSUncaughtExceptionHandler呢?不一定,还有另外一种可能,就是被同时存在的其他Crash日志收集服务给坑了。
多个Crash日志收集服务共存的坑
是的,在自己的程序里集成多个Crash日志收集服务实在不是明智之举。通常情况下,第三方功能性SDK都会集成一个Crash收集服务,以及时发现自己SDK的问题。当各家的服务都以保证自己的Crash统计正确完整为目的时,难免出现时序手脚,强行覆盖等等的恶意竞争,总会有人默默被坑。
1)拒绝传递 UncaughtExceptionHandler
如果同时有多方通过NSSetUncaughtExceptionHandler注册异常处理程序,和平的作法是:后注册者通过NSGetUncaughtExceptionHandler将先前别人注册的handler取出并备份,在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递。不传递强行覆盖的后果是,在其之前注册过的日志收集服务写出的Crash日志就会因为取不到NSException而丢失Last Exception Backtrace等信息。(P.S. iOS系统自带的Crash Reporter不受影响)
在开发测试阶段,可以利用 fishhook 框架去hookNSSetUncaughtExceptionHandler方法,这样就可以清晰的看到handler的传递流程断在哪里,快速定位污染环境者。不推荐利用调试器添加符号断点来检查,原因是一些Crash收集框架在调试状态下是不工作的。
检测代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
static NSUncaughtExceptionHandler *g_vaildUncaughtExceptionHandler; static void (*ori_NSSetUncaughtExceptionHandler)( NSUncaughtExceptionHandler * ); void my_NSSetUncaughtExceptionHandler( NSUncaughtExceptionHandler * handler) { g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); if (g_vaildUncaughtExceptionHandler != NULL) { NSLog(@ "UncaughtExceptionHandler=%p" ,g_vaildUncaughtExceptionHandler); } ori_NSSetUncaughtExceptionHandler(handler); NSLog(@ "%@" ,[NSThread callStackSymbols]); g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler(); NSLog(@ "UncaughtExceptionHandler=%p" ,g_vaildUncaughtExceptionHandler); } |
对于越狱插件注入应用进程内部,恶意覆盖NSSetUncaughtExceptionHandler的情况,应用程序本身处理起来比较弱势,因为越狱环境下操作时序的玩法比较多权利比较大。
2)Mach异常端口换出+信号处理Handler覆盖
和NSSetUncaughtExceptionHandler的情况类似,设置过的Mach异常端口和信号处理程序也有可能被干掉,导致无法捕获Crash事件。
3)影响系统崩溃日志准确性
应用层参与收集Crash日志的服务方越多,越有可能影响iOS系统自带的Crash Reporter。由于进程内线程数组的变动,可能会导致系统日志中线程的Crashed 标签标记错位,可以搜索abort()等关键字来复查系统日志的准确性。 若程序因NSException而Crash,系统日志中的Last Exception Backtrace信息是完整准确的,不会受应用层的胡来而影响,可作为排查问题的参考线索。
ObjC野指针类的Crash
收集Crash日志这个步骤没有问题的情况下,还是有很多全系统栈的日志的情况,没有自己一行代码,分析起来十分棘手,ObjC野指针类的Crash正是如此,这里推荐几篇好文章:
除此之外,在Crash日志中补充记录一些额外信息可以辅助定位,如切面标记线程出处、队列出处,记录用户操作轨迹等等……
来源:程序媛念茜的博客
漫谈iOS Crash收集框架的更多相关文章
- iOS Crash日志
Understanding Crash Reports on iPhone OS https://developer.apple.com/videos/wwdc/2010/?id=317 http:/ ...
- iOS: Crash文件解析(一)
iOS Crash文件的解析(一) 开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退.脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断. ...
- iOS Crash文件的解析
iOS Crash文件的解析 开发程序的过程中不管我们已经如何小心,总是会在不经意间遇到程序闪退.脑补一下当你在一群人面前自信的拿着你的App做功能预演的时候,流畅的操作被无情地Crash打断.联想起 ...
- 漫谈PHP组件、框架、Composer那些事
什么是组件 组件是一组打包的代码,是一系列相关的类.接口和Trait,用于帮助我们解决PHP应用中某个具体问题.例如,你的PHP应用需要收发HTTP请求,可以使用现成的组件如guzzle/guzzle ...
- 了解和分析iOS Crash
WeTest 导读 北京时间凌晨一点,苹果一年一度的发布会如期而至.新机型的发布又会让适配相关的同学忙上一阵子啦,并且iOS Crash的问题始终伴随着移动开发者.本文将从三个阶段,由浅入深的介绍如何 ...
- 李洪强iOS开发之iOS社区收集
李洪强iOS开发之iOS社区收集 项目 简述 github 全球最大的代码仓库,无论是iOS开发还是Android开发没有人不知道这个网站,它也是一个社区,你可以去follow(关注)某些人或公司. ...
- 李洪强IOS开发之iOS好项目收集
李洪强IOS开发之iOS好项目收集 在这里收集一些最近出现的比较实用好玩的框架或者项目,会不断更新 项目 简述 日期 SCTableViewCell 类似与QQ侧滑删除Cell的Demo 201501 ...
- 【iOS】系统框架学习
iOS的系统架构分为四个层次:核心操作系统层(Core OS layer).核心服务层(Core Services layer).媒体层(Media layer)和可触摸层(Cocoa Touch l ...
- iOS crash log 解析 symbol address = stack address - slide 运行时获取slide的api 利用dwarfdump从dsym文件中得到symbol
概述: 为什么 crash log 内 Exception Backtrace 部分的地址(stack address)不能从 dsym 文件中查出对应的代码? 因为 ASLR(Address spa ...
随机推荐
- VBA中自定义类和事件的(伪)注册
想了解一下VBA中自定义类和事件,以及注册事件处理程序的方法. 折腾了大半天,觉得这样的方式实在称不上“注册”,所以加一个“伪”字.纯粹是瞎试,原理也还没有摸透.先留着,有时间再接着摸. 做以下尝试: ...
- phpcms标签大全V9
转自:http://blog.csdn.net/cloudday/article/details/7343448调用头部 尾部 {template "content"," ...
- VS中的路径宏 vc++中OutDir、ProjectDir、SolutionDir各种路径 转
说明 $(RemoteMachine) 设置为“调试”属性页上“远程计算机”属性的值.有关更多信息,请参见更改用于 C/C++ 调试配置的项目设置. $(References) 以分号分隔的引用列表被 ...
- 利用Java的读写锁实现缓存的设计
Java中的读写锁: 多个读锁不互斥, 读锁与写锁互斥, 写锁与写锁互斥, 这是由JVM自行控制的,我们只要上好相应的锁即可. 缓存的设计: package com.cn.gbx; import ja ...
- li下沉 margin-top越界 浮动带来的影响
使用li嵌套实现的二级导航菜单,在IE浏览器下显示正常,而在谷歌及360极速模式下最后的几个li标签下沉了,其实在webkit内核的浏览器中都会有这种情况,如下图: 出现此种状况,有两种可能: 1.导 ...
- img_jquerydim
- AJAX的简介
AJAX 指异步JavaScript及XML(Asynchronous JavaScript And XML(异步JavaScript和XML)),是指一种创建交互式网页应用的网页开发技术. 国内通常 ...
- iOS开发之Xcode 6更新默认不支持armv7s架构
最近一次的Xcode 6更新默认不再支持arm7s架构,究竟是要废除不用呢还是仅仅只是一个疏忽? 目前的Xcode 6配置里定义${ARCHS_STANDARD}为armv7, arm64,当然这个定 ...
- unsigned 整型实现无溢出运算
普通的 int 整型能表示的范围很有限,所以刷题时很多时候不得不用 long long 来存更大的数据.或者找出数列中某个只出现一次(或奇数次)的数(其余的数均出现两次 / 偶数次),用异或运算的经典 ...
- 转: 浅谈C/C++中的指针和数组(二)
转自:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242419.html 浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组 ...