debug能力是程序员重要能力,也能真实反映程序员的水平,也是晋升重要的能力!尤为崩溃排查分析解决能力!这节课将透过现象看透崩溃的本质!git
好比断点调试崩溃:程序员
App Store上应用崩溃,可经过Xcode中的Oragnizer
来获取,以下图:github
或者直接链接崩溃的设备经过Xcode->Device->View Device Logs来获取,以下图:objective-c
对于Mac App可经过控制台中的”崩溃报告“来获取,以下图:xcode
还能够经过第三方崩溃平台来获取,好比bugly
、友盟、KSCrash
等,以下图: markdown
那崩溃发生有哪些具体缘由:网络
cpu没法执行的代码架构
好比无效指令或操做、访问无效地址及不具备权限的内存地址、除以0等;app
有多是苹果bug,会产生非法指令错误,以下图所示:框架
僵尸对象,以下图:
好比64位系统,访问0~4GB地址空间会报无效地址,就会报段错误Segmentation fault
,以下图:
64位系统对应的__PAGEZERO
段地址空间为0~4GB
,在这个范围内全部访问权限-读、写和执行-都被撤销,所以若访问该地址就会引起MMU
的硬件页错误,进而产生一个异常。
代码以下:
小技巧:那如何调试过程当中直接让应用崩溃呢?是由于自己工程默认开启了
Debug excutable
选项,具体选项如图:
关闭该选项就能够直接让应用崩溃产生崩溃日志报告。
除以0,就会致使算术异常,致使EXC_ARITHMETIC
错误,以下图:
被系统强杀
应用内存消耗太高OOM
主线程长时间没法响应ANR
为了防止一个应用占用过多的系统资源,开发iOS的苹果工程师门设计了一个“看门狗”Watchdog
的机制。在不一样的场景下,“看门狗”会监测应用的性能。若是超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者们在crashlog
里面,会看到诸如0x8badf00d
这样的错误代码。
Watchdog
机制是iOS为了保持用户界面的响应引入的一种机制。若是咱们的应用未能及时的响应一些用户界面事件,如启动、暂停、恢复和终止,Watchdog
就会杀死程序并生成一个Watchdog
超时崩溃报告。Watchdog
超时时间并无明文规定,但一般会少于网络超时。
好比App应用启动时同步请求启动配置数据,如在网络差的状况下就会致使被
Watchdog
强杀,须要在真机上运行应用且不能xcode调试模式;
资源异常
线程频繁唤醒
Wakeups
是“资源异常”下的一个子类,指的是频繁唤醒线程,消耗cpu资源并增长功耗,在超过阈值并处于FATAL CONDITION
的条件下触发崩溃;若是300秒内的总wakeup数超过45000(300 * 150)就会被断定为超出阈值。
进程中的线程过多的占用了cpu,限制为 50%
,时间不超过 180秒
;
线程短期过多的磁盘写入
死锁
好比互斥锁同一线程屡次上锁就会致使死锁
NSLock *_lock = [[NSLock alloc]init];
[_lock lock];
[_lock lock];//屡次上锁
复制代码
非法的应用签名
后台执行超时
App退至后台后若执行时间过长就会致使被系统被杀,好比Backgroud Task
方式能够在后台执行3min,若超过3min还未运行完成就会被系统强杀,实例代码:
- (void)applicationDidEnterBackground:(UIApplication *)application {
//经过backgroud task方法延长后台时间 这个方法必须与endBackgroundTask一一对象
UIBackgroundTaskIdentifier _backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
}];
//此处为执行任务代码 一般用来保存应用程序关键数据数据
//执行关键任务
//当任务执行完成时 调用endBackgroundTask方法 调用后就会将app挂起
//若是在最后时间到了以前仍然没有调用endBackgroundTask方法 就会执行此回调
//一般时间为3分钟,若是时间到期以前调用endBackgroundTask方法 就会强制杀掉进程,就会形成崩溃
[application endBackgroundTask:_backgroundTaskIdentifier];
}
复制代码
设备总内存紧张
由于Mac平台存在内存交换机制,而iOS平台没有,就致使整个设备内存吃紧的时候,系统就会杀掉优先级不高且占用内存多大的应用;
设备过热
通常见于低端设备
语言触发异常
OC语言抛出异常
具体的异常以下:
常见的以下:
NSArray
越界访问产生的异常NSRangeException
,调试下会看到以下消息:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 0]'
复制代码
未找到的方法或者NSDictionary
添加nil
对象等致使的非法参数异常NSInvalidArgumentException
,如:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController hello]: unrecognized selector sent to instance 0x7fc65b604e30'
复制代码
kvc
未找到相对应的key
抛出NSUnknownKeyException
,结果以下:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7fd3f6806450> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key hello.'
复制代码
C++抛出异常
语言异常抛出后最后都会调用到
abort
来终止应用,调用栈以下图所示:
开发者触发
好比断言,NSAssert
或者assert
函数;
说了这么多场景,那崩溃时如何发生的呢?其底层机制是如何运做的呢?只有你理解了崩溃的机制才能更有效的防御及排查!接下来给你们一一剖析!首先,要明白“中断”是什么?
中断是重要的异步处理事件机制,不然对于外设须要经过轮询来确认外设的事件,太浪费cpu。中断的引入让外设可以“主动”通知操做系统,及打断操做系统和应用的正常执行,让操做系统完成外设的相关处理,而后在恢复操做系统和应用的正常执行,同时也是实现进程/线程抢占式调度的一个重要基石。
(不论是用户态仍是内核态)程序运行时,若中断发生就会打断如今的程序,进行上下文切换转入内核态并进入中断服务程序,中断服务程序执行完成后恢复被打断的程序继续执行,以下图所示:
中断的类型及中断服务程序由“中断向量表”来决定,在系统启动时由内核负责加载这个向量。中断类型又分为:
由CPU外部设备引发的外部事件如I/O中断、时钟中断等是异步产生的(即产生的时刻不肯定),与CPU的执行无关,咱们称之为异步中断(asynchronous interrupt)也称外部中断,简称中断。(interrupt)
异常
此异常非语言中的异常,虽然语言异常会转化称为中断异常;
把在CPU执行指令期间检测到不正常的或非法的条件(如除零错、地址访问越界)所引发的内部事件称做同步中断(synchronous interrupt),也称内部中断,简称异常(exception)。
Intel
架构中,中断向量表中前20个单元定义为异常
,具体以下:
从表中能够看出,异常有如下类型:
错误(fault)
指令遇到一个能够纠正的异常,而且处理器能够从新启动这条出现异常的指令,这种异常称为错误,好比“页错误”。
陷阱(trap)
相似于错误,可是错误处理完成后返回发生陷阱指令以后的那条指令。
停止(abort)
不可重启指令,好比上图中的#8同一条指令发生两次错误。
系统调用
把在程序中使用请求系统服务的系统调用而引起的事件,称做陷入中断(trap interrupt),也称软中断(soft interrupt),**系统调用(system call)**简称trap。
好比write函数;
XNU
的中断一般为“陷阱”,这不一样于异常中的“异常”;
自愿的内核转换,包括异常、系统调用,intel
架构能够经过INT
指令触发,在INT
指令的参数传入异常的编号便可(64位架构使用SYSCALL
指令),arm
架构经过SVC/SWI
指令来触发。好比INT 3
指令(是一条单独的指令),能够来触发断点
一张图就能够了解总体过程,以下图所示:
摘自清华大学《操做系统学习》课程
程序的崩溃都会转换为异常被cpu经过中断向量表指定的异常类型捕获,进而触发异常处理程序处理,好比cpu无效指令、无效的地址或者无权限的访问,这些都是硬件产生的异常;被系统强杀的崩溃最终会调用到kill
函数发送SIGKILL
信号进而引起应用被强杀;语言及开发者触发崩溃最终会经过abort
函数毅然会最终调用到kill
函数发送SIGABRT
信号引起应用被杀,这都是软件引起的异常。
那崩溃具体有哪些异常?下面带你们来一一探索,以下:
Mach
异常
这个后面跟你们详解,暂且不用管
BSD
Signal
信号
信号是什么?信号是一种异步处理的软中断,内核会发送给进程某些异步事件,这些异步事件可能来自硬件,好比除0或者访问了非法地址;也可能来自其余进程或用户输入,好比ctrl+c
,就会产生SIGINT
信号由内核发送至当前终端执行进程,若进程未处理该信号,就会致使进程退出;ctrl+\
就会产生SIGQUIT
退出信号来终止进程执行,而且会产生崩溃日志报告,以下图所示:
具体的demo以下:
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#define BUF_LEN 1024
//信号处理函数
static void sig_int(int signo) {
printf("handler signo:%d \n", signo);
}
int main(int argc, char **argv) {
char buf[BUF_LEN] = {0};
//处理信号,SIGINT默认终止进程
if (signal(SIGINT, sig_int) == SIG_ERR) {
printf("signal error:%s \n", strerror(errno));
}
//接收终端输入并打印输出
while (fgets(buf, sizeof(buf), stdin) != NULL) {
if (fputs(buf, stdout) == EOF) {
printf("fputs error:%s \n", strerror(errno));
return 1;
}
}
return 0;
}
复制代码
语言异常,OC/C++
前面已提到语言异常最终会转化为信号SIGABRT
进而引起应用终止;
用户抛出的异常
好比前面提到的断言也会调用abort
函数转化为SIGABRT
异常终止信号,该信号默认是终止进程并生成崩溃日志报告,以下图:
那最终不论是硬件异常仍是语言异常及用户抛出异常都会产生信号,那信号与Mach异常有何关系?首先来带你们了解下什么是
Mach
异常。
从名字来看就包含了Mach
和“异常”,那Mach
是什么?你们有没有这样的疑问,我这里给你们讲解一下iOS/Mac操做系统框架,以下图所示:
整个操做系统核心包括了Mach
微内核、BSD层(你们熟悉的POSIX
接口就在这一层)、I/O Kit设备驱动框架以及核心库,其中Mach
微内核负责进程和线程抽象、虚拟内存管理、任务管理以及进程间通讯和消息传递机制,因此Mach
微内核就是整个操做系统的核心。
鸿蒙系统也是基于微内核
与你们熟知的Runloop
中的Mach port
消息就是基于Mach
微内核的消息机制的体现,那Mach
异常是什么呢?以下图所示:
RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,若是没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,而后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
Mach
异常是在已有的消息传递架构上实现的一种独有的异常处理方法,是一种轻量级的架构
听起来很抽象,其实很简单,一句话归纳就是整个的异常机制是构建在
Mach
异常之上的,全部的硬件/软件异常都会首先转换为Mach
异常,进而转换为信号。
咱们看到的崩溃日志报告中的异常错误码是否是包含了EXC_xxx
,这里对应的就是Mach
异常,以下图:
那Mach
异常是如何转化为BSD
信号的呢?
一张图概述:
流程主要分三个步骤:
异常封装、转换、发送
exception_triage
来处理,该函数会将异常信息(好比所在任务、线程、异常类型等)转化为Mach
消息;exception_deliver
函数封装封装异常端口(包括线程、任务异常端口)、异常、异常错误码、线程寄存器状态等信息;mach_exception_raise
函数,该函数会经过mach_msg
发送mach
异常消息到异常端口;异常消息接收处理
内核启动建立第一个用户态进程launchd
的同时,会将进程的Mach
异常消息重定向到异常端口;
因用户态进程都是经过
launchd
进程fork
克隆出来,同时也随之继承了异常端口;
调用ux_handler_init
来建立一个内核线程开启ux_hanlder
异常处理函数;
ux_handler
函数会开启消息循环来接收异常线程的Mach
异常消息;
异常消息转换为BSD信号
mach_exc_server
函数;catch_mach_exception_raise
函数来捕获异常消息,同时会调用ux_exception
函数将异常消息转换为BSD
信号;threadsignal
函数来抛出信号;其中关键点就是Mach
异常消息经过消息发送的形式,发送到指定的异常端口,而该异常端口被内核线程持有,进而接收异常消息并转换为BSD
信号。
那最终信号如何处理呢?
上面咱们讲到用户态进程若指定了信号处理函数(好比SIGINT
)则能够本身来处理,若未指定呢?比较有意思的地方开始了,内核发现信号未存在异常处理函数,就会将其抛给崩溃报告守护进程ReportCrash
,这里能够查看Mac的进程就会发现该进程,以下: 该进程负责来获取异常消息及信号信息来生成崩溃日志报告。那问题来了,既然异常消息是经过
Mach
消息的形式发送出去的,那我是否是能够截获这个消息呢? 答案是确定的!具体就是来注册本身的异常端口来截获异常消息,具体见demo。