iOS App程序崩溃的抓取与分析
备用: xun开源代码地址
前言
- cpu无法执行,除以0,无权限内存地址(pagezero-4g)无效
- pagezero,拦截空指针的访问。 隔绝32位操作系统
- 被系统强杀
- oom
- ANR
- 资源异常
- 死锁
- 非法应用签名
- 后台超时
- 内存紧张
- 设备过热
- oc异常
- 数组越界
- C++异常
- 断言
- 中断
- 外部中断(IO)
- 异常中断
- 系统调用
具体处理
- Mach异常捕获方式
- mach_port_allocate 创建一场端口
- mach_port_insert_right 申请 set_exception_ports 的权限
- xxx_set_exception_ports 设置异常端口
- 循环等待异常消息
- Unix 信号方式 signal(SIGSEGV, signalHandler)
- 除了OC层面的异常捕捉之外,很多内存错误、访问错误的地址产生的crash则需要利用unix标准的signal机制,注册SIGABRT, SIGBUS, SIGSEGV等信号发生时的处理函数。该函数中我们可以输出栈信息,版本信息等其他一切我们所想要的。
- Mach异常 + Unix信号方式
- 某个NSException导致程序Crash的,只有拿到这个NSException,获取它的reason,name,callStackSymbols信息才能确定出问题的程序位置。
- 方法很简单,可通过注册NSUncaughtExceptionHandler捕获异常信息
1
2
3
4
5static void my_uncaught_exception_handler (NSException *exception) {
//这里可以取到 NSException 信息
}
NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);
多个 Crash 日志收集服务共存
- 拒绝传递 UncaughtExceptionHandler
- 开发测试阶段,可以利用 fishhook 框架去hookNSSetUncaughtExceptionHandler方法,这样就可以清晰的看到handler的传递流程断在哪里,快速定位污染环境者。
- Mach异常端口换出+信号处理Handler覆盖
- 影响系统崩溃日志准确性
- 若程序因NSException而Crash,系统日志中的Last Exception Backtrace信息是完整准确的,不会受应用层的胡来而影响,可作为排查问题的参考线索。
概述
Mach: 微内核,负责操作系统中基本职责:进程和线程抽象、虚拟内存管理、任务调度、进程间通信和消息传递机制。
大家熟知的NSSetUncaughtExceptionHandler() + signal() / sigaction()的方式收集Crash,但是stack overflow并不能被此方法扑捉到;
通常我们所说的异常,一般是有处理器陷阱引发的,通用的Mach异常处理程序exception_triage(),负责将异常转换成Mach 消息。exception_triage()通过调用exception_deliver()尝试把异常投递到thread、task最后是host。首先尝试将异常抛给thread端口,然后尝试抛给task端口,最后再抛给host端口(默认端口),如果没有一个端口返回KERN_SUCCESS,那么任务就会被终止。
代码太多,不再贴了,自行下载源码,找到 osfmk/kern/exception.c 文件。 里面有
exception_triage()和exception_deliver()函数
。关于Exception
的相关定义,可以在osfmk/mach/exception_types.h
里面查看。下面只贴伪代码
1 |
|
1 |
|
当第一个BSD进程调用bsdinit_task()函数(源码位于bsd/kern/bsd_init.c
)启动时,这函数还调用了ux_handler_init()函数(位于bsd/uxkern/ux_exception.c
)设置了一个Mach内核线程跑ux_handler()的。
1 |
|
每一个thread、task及host自身都有一个异常端口数组,通过调用xxx_set_exception_ports()(xxx为thread、task或host)可以设置这些异常端口。 xxx_set_exception_ports()第四个参数为exception_behavior_t behavior,这将会使用到与行为相匹配的实现(exc.defs 或 mach_exc.defs)。
各种行为都在host层被catch_[mach]_exception_xxx处理,64位的对应的是有mach函数(可在/bsd/uxkern/ux_exception.c查看)。
这些函数通过调用ux_exception()将异常转换为对应的UNIX信号,并通过threadsignal()将信号投递到出错线程。
所以,如果异常是栈溢出,那么signal是SIGSEGV而不是SIGBUS;如果进程退出了或者线程/进程未准备好处理signal,所注册的signal()是无法接收信号的。
把Mach exception 和 UNIX signal 的转换制表后,如下
exception type | signal |
---|---|
EXC_BAD_ACCESS | 1、SIGSEGV (KERN_INVALID_ADDRESS) 2、SIGBUS(其它) |
EXC_BAD_INSTRUCTION | SIGILL |
EXC_ARITHMETIC | SIGFPE |
EXC_EMULATION | SIGEMT |
EXC_SOFTWARE | 1、SIGSYS(EXC_UNIX_BAD_SYSCALL) 2、SIGPIPE(EXC_UNIX_BAD_PIPE) 3、SIGABRT(EXC_UNIX_ABORT) 4、SIGKILL(EXC_SOFT_SIGNAL) |
EXC_BREAKPOINT | SIGTRAP () |
实战
在Mach中,异常是通过内核中的主要设施-消息传递机制-进行处理的。一个异常与一条消息并无差别,由出错的线程或任务(通过 msg_send())发送,并通过一个处理程(通过 msg_recv())接收。
由于Mach的异常以消息机制处理而不是通过函数调用,exception messages可以被转发到先前注册的Mach exception处理程序。这意味着你可以插入一个exception处理程序,而不干扰现有的无论是调试器或Apple’s crash reporter。可以使用mach_msg() // flag MACH_SEND_MSG发送原始消息到以前注册的处理程序的Mach端口,将消息转发到一个现有的处理程序。
1 | void catchMACHExceptions() { |
接下来,我们测试一下。
1 | - (void)test |
结果如下:
1 | Exc handler listening |
我们可以查看mach/exception_types.h
对exception type的定义
Exception: 1, 即为EXC_BAD_ACCESS
code: 2, 即KERN_PROTECTION_FAILURE
而ux_exception() 函数告诉我们Code与signal是怎么转换的。
也就是 Exception = EXC_BAD_ACCESS, code = 2 对应的是SIGBUS信号,又因为为是stack overflow,信号应该是SIGSEGV。
那么结这个exception就是:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_PROTECTION_FAILURE
再试试其他的:
1 | int *pi = (int*)0x00001111; |
Exc handler listening
Got message 2401. Exception : 1 Flavor: 0. Code 1/4369. State count is 8(lldb)
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS
最后
除了上面硬件产生的信号,另外还有软件产生的信号。软件产生的信号来自kill()、pthread_kill()两个函数的调用,大概过程是这样的:kill()/pthread_kill() –> … –> psignal_internal() –> act_set_astbsd()。最终也会调用act_set_astbsd()发送信号到目标线程。这意味着Mach exception流程是不会走的。
另外,在abort()源码注释着:<rdar://problem/7397932> abort() should call pthread_kill to deliver a signal to the aborting thread
, 它是这样调用的(void)pthread_kill(pthread_self(), SIGABRT);。Apple’s Crash Reporter 把SIGABRT
的Mach exception type记为EXC_CRASH
,不同与上面转变表。
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
所以尽管Mach exception handle 比 UNIX signal handle 更有优势,但我们还是须要注册signal handle用于处理EXC_SOFTWARE/EXC_CRASH。
附
崩溃日志的字段认识
- Incident Identifier: 事件标识符,每个crash文件对应一个唯一的标识符。
- CrashReporter Key: 匿名设备标识符。
- Hardware Model:设备型号;
- Process: 进程名;
- Identifier:app Identifier;
- Exception Type: 异常类型;
- Exception Codes: 异常代码;
- Termination Reason:进程被结束的原因
Crash Reports (https://developer.apple.com/library/content/technotes/tn2151/_index.html)