漫谈 iOS Crash 收集框架

转载(漫谈 iOS Crash 收集框架

前言

很早以前就和念茜认识,念茜不但技术功底扎实,而且长得很漂亮,说她是 iOS 界的女神都一点不为过。虽然网上和她常常交流,但是线下和念茜只见过两次,可惜都没能合影,实在遗憾。

念茜也是一个爱分享的人,她很早就开始写博客,所以她在 iOS 圈的知名度很高,从她的微博粉丝数上就能看出来。她喜欢研究安全,博客上有很多安全方面的文章。

有一次我们在交流技术中涉及到了一些 Crash 收集的知识,当时感觉她研究得很深入。前几天了解到她做打算写一篇相关的文章,我赶紧就拿一个 Sublime Text 的 License 的条件换得了文章在微信公众平台的独家发表权。

念茜是一个善良的女孩,她一直在资助一位贫困学生上学。在本文中,念茜提出的一些问题其实在某些著名的 Crash 收集服务中都没有处理好。我问念茜为啥不指明呢?念茜说:“不广告不诋毁”。我想,这可能就是一个专心做技术人的处事之道吧。

以下为文章正文,如果觉得有用,欢迎给她打赏。

为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如KSCrashplcrashreporterCrashKit等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙 Crash 统计产品,如CrashlyticsHockeyapp友盟Bugly等等。

是否集成越多的 Crash 日志收集服务就越保险?

自己收集的 Crash 日志和系统生成的 Crash 日志有分歧,应该相信谁?

为什么有大量 Crash 日志显示崩在 main 函数里,但函数栈中却没有一行自己的代码?

野指针类的 Crash 难定位,有何妙招来应对?

想解释清这些问题,必须从 Mach 异常说起。

Mach 异常与 Unix 信号

iOS 系统自带的 Apple’s Crash Reporter 记录在设备中的 Crash 日志,Exception Type 项通常会包含两个元素: Mach 异常 和 Unix 信号。

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 来捕获信号:

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 信号方式

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 anEXC_CRASHmach 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 forEXC_CRASH.

另外,需要重点说明的是:对于应用级异常 NSException,还需要特殊处理。

你是否见过崩溃在 main 函数的 crash 日志,但是函数栈里面没有你的代码:

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 日志,精准的定位出错程序位置:

Application Specific Information:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryI 0x14554d00> 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 收集框架在调试状态下是不工作的。

检测代码示例:

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 正是如此,这里推荐几篇好文章:

如何定位 Obj-C 野指针随机 Crash(一):先提高野指针 Crash 率

http://bugly.qq.com/blog/?p=200

如何定位 Obj-C 野指针随机 Crash(二):让非必现 Crash 变成必现

http://bugly.qq.com/blog/?p=308

如何定位 Obj-C 野指针随机 Crash(三):加点黑科技让 Crash 自报家门

http://bugly.qq.com/blog/?p=335

分析 objc_msgSend() 处崩溃的小技巧

http://www.sealiesoftware.com/blog/archive/2008/09/22/objc_explain_So_you_crashed_in_objc_msgSend.html

除此之外,在 Crash 日志中补充记录一些额外信息可以辅助定位,如切面标记线程出处、队列出处,记录用户操作轨迹等等……

感谢阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 以下为文章正文,如果觉得有用,欢迎给她打赏。 为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务...
    赤色追风阅读 2,536评论 1 11
  • 比较好的转载:http://www.cocoachina.com/ios/20151218/14748.html转...
    liudhkk阅读 929评论 0 2
  • 来源:程序媛念茜的博客 Crash日志收集 为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,...
    幸福的鱼阅读 1,151评论 0 2
  • 本文就捕获iOS Crash、Crash日志组成、Crash日志符号化、异常信息解读、常见的Crash五部分介绍。...
    xukuangbo_阅读 1,569评论 0 0
  • 第九周里感觉还是没把力气用尽。但这个问题在这周里被充分认识到了,这些认识上的长进都是长足的。很多方面结束徘徊,下周...
    5779cc3e3627阅读 225评论 0 1