异常简介
处理器和系统内核中有设计标识不同事件的状态码,这些状态被编码为不同的位和信号。每次处理器和内核检测到状态的变化时,便会触发一个事件,该事件称为异常。
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。这些异常号由处理器和操作系统的内核设计者分配。
当处理器检测到事件时,会通过一张被称为异常表的跳转表,进行间接过程调用到一个专门设计用来处理这类事件的操作系统的子程序,称为异常处理程序。
在系统启动时(计算机启动),操作系统会分配和初始化一张异常表,该表的起始地址存放在一个称为异常表基寄存器(exception table base register
)的特殊CPU寄存器中。异常号是异常表中的索引。通过起始地址与异常号,找到异常程序的调用地址,最终执行异常程序。
异常的类别
异常一般分四类:中断(interrupt
),陷阱(trap
),故障(fault
),终止(abort
)。
中断:是来自处理器外部的I/O
异常信号导致的,不是由任何一条专门的指令造成的,从这个层面上来讲,它是异步的。硬件中断的处理程序通常被称为中断处理程序。比如:拔插U
盘。
陷阱:是有意的异常,是执行一条指令的结果。和中断一样,陷阱处理程序将控制返回到下一条指令。陷阱最终的用途,是在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。比如用户程序经常需要向内核请求服务,比如读一个文件(read
)、创建一个进程(fork
)、加载一个新的程序(execve
)或者终止当前进程(exit
),这些操作都需要通过触发陷阱异常,来执行系统内核的程序来实现。
故障:是由错误情况引起的,它可能被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果故障处理程序可以修正这个错误情况,便会将控制放回到故障指令,从而重新执行它,如果不能修正,故障处理程序便会将控制转移到系统内核的abort()
函数,abort()
最终会终止引起故障的应用程序。
一般保护性故障,
Unix
不会尝试恢复,而是将这种保护性故障报告为段违规(segmentation violation
)对应信号为SIGSEGV
,然后终止程序。比如:除零、程序尝试写入只读的文本段等。
故障的经典示例便是缺页异常。
终止:是由不可恢复的致命错误造成的结果。终止从不将控制返回给应用程序。如:奇偶校验错误(机器硬件错误检测)
Unix
信号(signal
)
更高层的软件形式的异常,一个信号就是一条消息,它通知进程某种类型的消息已经在系统中发生了。信号提供了一种向用户进程通知这些异常发生的机制,也允许进程中断其他进程。
Linux
与macOS
都是类Unix
系统,下表列举了Linux
系统支持的30
多种不同类型的信号,有许多Linux
信号在macOS
同样适用:
号码 | 名字 | 默认行为 | 相应事件 | 号码 | 名字 | 默认行为 | 相应事件 |
---|---|---|---|---|---|---|---|
1 | SIGHUP | 终止 | 终端线挂起 | 16 | SIGSTKFLT | 终止 | 协处理器上的栈故障 |
2 | SIGINT | 终止 | 来自键盘的中断 | 17 | SIGCHLD | 忽略 | 一个子进程暂停或者终止 |
3 | SIGQUIT | 终止 | 来自键盘的退出 | 18 | SIGCONT | 忽略 | 若进程暂停则继续进程 |
4 | SIGILL | 终止 | 非法指令 | 19 | SIGSTOP | 停止直到下一个SIGCONT | 不来自终端的暂停信号 |
5 | SIGTRAP | 终止并转储存储器 | 跟踪陷阱 | 20 | SIGTSTP | 停止直到下一个SIGCONT | 来自终端的暂停信号 |
6 | SIGABRT | 终止并转储存储器 | 来自abort 函数的终止信号 |
21 | SIGTTIN | 停止直到下一个SIGCONT | 后台进程从终端读 |
7 | SIGBUS | 终止 | 总线错误 | 22 | SIGTTOU | 停止直到下一个SIGCONT | 后台进程向终端写 |
8 | SIGFPE | 终止并转储存储器 | 浮点异常 | 23 | SIGURG | 忽略 | 套接字上的紧急情况 |
9 | SIGKILL | 终止 | 杀死程序 | 24 | SIGXCPU | 终止 | CPU时间超出限制 |
10 | SIGUSER1 | 终止 | 用户定义的信号1 | 25 | SIGXFSZ | 终止 | 文件大小超出限制 |
11 | SIGSEGV | 终止 | 无效的存储器引用(段故障) | 26 | SIGVTALRM | 终止 | 虚拟定时器期满 |
12 | SIGUSER2 | 终止 | 用户定义的信号2 | 27 | SIGPROF | 终止 | 剖析定时器期满 |
13 | SIGPIPE | 终止 | 向一个没有用户读的管道做写操作 | 28 | SIGWINCH | 忽略 | 窗口大小变化 |
14 | SIGALRM | 终止 | 来自alarm 函数的定时器信号 |
29 | SIGIO | 终止 | 在某个描述符上执行I/O操作 |
15 | SIGTERM | 终止 | 软件终止信号 | 30 | SIGPWR | 终止 | 电源故障 |
系统内核
Mac OS X
&iOS
&iPad OS
系统内核都是Darwin。Darwin
包含了开放源代码的XNU
混合内核,它包含了Mach
/BSD
,BSD
是建立在Mach
之上提供标准化(POSIX
)的API
,XNU
的核心是Mach
。下图为OS X
内核架构,查看来源。
Mach
:是一个微内核的操作系统。微内核
仅处理最核心的任务,其他任务交给用户态的程序,包括文件管理,设备驱动等服务,这些服务被分解到不同的地址空间,彼此消息传递需要IPC
。主要负责:线程与进程管理、虚拟内存管理、进程通信与消息传递、任务调度。与之对应的单内核
则是把所有的服务放在相同的地址空间下,服务之间可相互调用。
BSD
:是Unix
的衍生系统。主要负责:Unix
进程模型、POSIX
线程模型以及相关原语、文件系统访问、设备访问、网络协议栈、Unix
用户与群组。
异常来源
iOS中异常主要来源于硬件异常、软件异常、Mach
异常、Signal
异常,它们之间的关系如下图:
Mach异常
Mach异常是系统内核级异常,是由CPU
触发一个陷阱引发,调用到Mach
的异常处理程序,将来自硬件的异常转换为Mach
异常,然后将Mach
异常传递到相应的thread
、task
、host
,若无结果返回,任务便会被终止。
Mach异常传递涉及到的内核函数如下图:
依据上图提供信息,查阅苹果开源资料,找到对应的函数信息,简单列举如下(详细查阅请前往此处):
struct ppc_saved_state *trap(int trapno,
struct ppc_saved_state *ssp,
unsigned int dsisr,
unsigned int dar) {
//...
doexception(exception, code, subcode);
//...
}
void doexception(
int exc,
int code,
int sub) {
exception_data_type_t codes[EXCEPTION_CODE_MAX];
codes[0] = code;
codes[1] = sub;
exception(exc, codes, 2);
}
// Des:The current thread caught an exception.
// We make an up-call to the thread's exception server.
void exception(
exception_type_t exception,
exception_data_t code,
mach_msg_type_number_t codeCnt)
{
thread_act_t thr_act;
task_t task;
host_priv_t host_priv;
struct exception_action *excp;
mutex_t *mutex;
assert(exception != EXC_RPC_ALERT);
if (exception == KERN_SUCCESS)
panic("exception");
/*
* Try to raise the exception at the activation level.线程级别
*/
thr_act = current_act();
mutex = mutex_addr(thr_act->lock);
excp = &thr_act->exc_actions[exception];
exception_deliver(exception, code, codeCnt, excp, mutex);
/*
* Maybe the task level will handle it. 任务级别
*/
task = current_task();
mutex = mutex_addr(task->lock);
excp = &task->exc_actions[exception];
exception_deliver(exception, code, codeCnt, excp, mutex);
/*
* How about at the host level? 主机级别
*/
host_priv = host_priv_self();
mutex = mutex_addr(host_priv->lock);
excp = &host_priv->exc_actions[exception];
exception_deliver(exception, code, codeCnt, excp, mutex);
/*
* Nobody handled it, terminate the task. 没有处理终止
*/
// ...
(void) task_terminate(task);
thread_exception_return();
/*NOTREACHED*/
}
// Make an upcall to the exception server provided.
void exception_deliver(
exception_type_t exception,
exception_data_t code,
mach_msg_type_number_t codeCnt,
struct exception_action *excp,
mutex_t *mutex)
{
///...
int behavior = excp->behavior;
switch (behavior) {
case EXCEPTION_STATE: {
///EXCEPTION_STATE:Send a `catch_exception_raise_state` message
///including the thread state.
//..
kr = exception_raise_state(exc_port, exception,
code, codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
//..
return;
}
case EXCEPTION_DEFAULT:
///EXCEPTION_DEFAULT表示:Send a `catch_exception_raise` message
///including the thread identity.
//..
kr = exception_raise(exc_port,
retrieve_act_self_fast(a_self),
retrieve_task_self_fast(a_self->task),
exception,
code, codeCnt);
//..
return;
case EXCEPTION_STATE_IDENTITY: {
/// EXCEPTION_STATE_IDENTITY:表示Send a `catch_exception_raise_state_identity` message
///including the thread identity and state.
//..
kr = exception_raise_state_identity(exc_port,
retrieve_act_self_fast(a_self),
retrieve_task_self_fast(a_self->task),
exception,
code, codeCnt,
&flavor,
state, state_cnt,
state, &state_cnt);
//..
return;
}
default:
panic ("bad exception behavior!");
}
}
关于如何捕获Mach异常,苹果文档描述很少,也没有提供可用的API,具体的Mach内核的API介绍,可从此处查阅。
Sigal信号
BSD
派生自Unix
操作系统,属于类Unix
系统,基于Mach
内核进程任务,提供POSIX
应用程序接口。详见维基百科-XNU。基于此,Unix Signal
机制同样适用苹果操作系统。苹果系统对于Unix Signal
的定义,可通过#import
跳转查看。
Mach异常-> Signal信号
苹果操作系统Mach
异常与Signal
信号共存。Mach
将操作系统的核心部分当做独立进程运行,与BSD
服务进程之间通过IPC
机制实现消息传递。同理Mach
内核态的异常也是基于IPC
将异常消息发送到BSD
,BSD
将消息转换为用户态的Signal
信号。具体流程如下:
- 苹果内核启动时会执行
bsdinit_task()
并最终调用ux_handler_init()
方法。
void bsdinit_task(void)
{
proc_t p = current_proc();
struct uthread *ut;
thread_t thread;
process_name("init", p);
ux_handler_init();
thread = current_thread();
(void) host_set_exception_ports(host_priv_self(),
EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),//pilotfish (shark) needs this port
(mach_port_t) ux_exception_port,
EXCEPTION_DEFAULT| MACH_EXCEPTION_CODES,
0);
ut = (uthread_t)get_bsdthread_info(thread);
bsd_init_task = get_threadtask(thread);
init_task_failure_data[0] = 0;
#if CONFIG_MACF
mac_cred_label_associate_user(p->p_ucred);
mac_task_label_update_cred (p->p_ucred, (struct task *) p->task);
#endif
load_init_program(p);
lock_trace = 1;
}
-
ux_handler_init()
初始化一个ux_handler()
方法,并创建线程执行它。
void ux_handler_init(void)
{
thread_t thread = THREAD_NULL;
ux_exception_port = MACH_PORT_NULL;
(void) kernel_thread_start((thread_continue_t)ux_handler, NULL, &thread);
thread_deallocate(thread);
proc_list_lock();
if (ux_exception_port == MACH_PORT_NULL) {
(void)msleep(&ux_exception_port, proc_list_mlock, 0, "ux_handler_wait", 0);
}
proc_list_unlock();
}
-
ux_handler()
申请用于接收Mach
内核消息的端口(port
)集合,接收来自Mach
的异常消息
static void ux_handler(void)
{
task_t self = current_task();
mach_port_name_t exc_port_name;
mach_port_name_t exc_set_name;
/*
* Allocate a port set that we will receive on.
*/
if (mach_port_allocate(get_task_ipcspace(ux_handler_self), MACH_PORT_RIGHT_PORT_SET, &exc_set_name) != MACH_MSG_SUCCESS)
panic("ux_handler: port_set_allocate failed");
/*
* Allocate an exception port and use object_copyin to
* translate it to the global name. Put it into the set.
*/
if (mach_port_allocate(get_task_ipcspace(ux_handler_self), MACH_PORT_RIGHT_RECEIVE, &exc_port_name) != MACH_MSG_SUCCESS)
panic("ux_handler: port_allocate failed");
if (mach_port_move_member(get_task_ipcspace(ux_handler_self),
exc_port_name, exc_set_name) != MACH_MSG_SUCCESS)
panic("ux_handler: port_set_add failed");
if (ipc_object_copyin(get_task_ipcspace(self), exc_port_name,
MACH_MSG_TYPE_MAKE_SEND,
(void *) &ux_exception_port) != MACH_MSG_SUCCESS)
panic("ux_handler: object_copyin(ux_exception_port) failed");
proc_list_lock();
thread_wakeup(&ux_exception_port);
proc_list_unlock();
/* Message handling loop. */
for (;;) {
struct rep_msg {
mach_msg_header_t Head;
NDR_record_t NDR;
kern_return_t RetCode;
} rep_msg;
struct exc_msg {
mach_msg_header_t Head;
/* start of the kernel processed data */
mach_msg_body_t msgh_body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
/* end of the kernel processed data */
NDR_record_t NDR;
exception_type_t exception;
mach_msg_type_number_t codeCnt;
mach_exception_data_t code;
/* some times RCV_TO_LARGE probs */
char pad[512];
} exc_msg;
mach_port_name_t reply_port;
kern_return_t result;
exc_msg.Head.msgh_local_port = CAST_MACH_NAME_TO_PORT(exc_set_name);
exc_msg.Head.msgh_size = sizeof (exc_msg);
#if 0
result = mach_msg_receive(&exc_msg.Head);
#else
result = mach_msg_receive(&exc_msg.Head, MACH_RCV_MSG,
sizeof (exc_msg), exc_set_name,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL,
0);
#endif
if (result == MACH_MSG_SUCCESS) {
reply_port = CAST_MACH_PORT_TO_NAME(exc_msg.Head.msgh_remote_port);
///收到消息后调用 mach_exc_server()
if (mach_exc_server(&exc_msg.Head, &rep_msg.Head)) {
///收到消息,回复消息
result = mach_msg_send(&rep_msg.Head, MACH_SEND_MSG,
sizeof (rep_msg),MACH_MSG_TIMEOUT_NONE,MACH_PORT_NULL);
if (reply_port != 0 && result != MACH_MSG_SUCCESS)
mach_port_deallocate(get_task_ipcspace(ux_handler_self), reply_port);
}
}
else if (result == MACH_RCV_TOO_LARGE)
/* ignore oversized messages */;
else
panic("exception_handler");
}
}
ux_handler(void)
收到Mach
内核消息后,便会调用mach_exc_server
函数,这个函数会根据异常的行为调用对应的catch_mach_exception_raise()
,catch_mach_exception_raise_state()
, 和catch_mach_exception_raise_state_identity()
,catch_mach_exception_raise()
会触发Mach异常消息到Unix
信号的转换。而关于mach_exc_server()
的实现,并未像其他函数直接给出,具体请见此处。调用
catch_mach_exception_raise()
将Mach
异常转换为Unix
信号,最终发送到对应线程。
kern_return_t catch_mach_exception_raise(
__unused mach_port_t exception_port,
mach_port_t thread,
mach_port_t task,
exception_type_t exception,
mach_exception_data_t code,
__unused mach_msg_type_number_t codeCnt
)
{
///...
/*
* Convert exception to unix signal and code.
*/
ux_exception(exception, code[0], code[1], &ux_signal, &ucode);
///struct uthread *ut
///struct proc *p;
ut = get_bsdthread_info(th_act);
p = proc_findthread(th_act);
///...
/*
* Send signal.
*/
if (ux_signal != 0) {
ut->uu_exception = exception;
//ut->uu_code = code[0]; // filled in by threadsignal
ut->uu_subcode = code[1];
threadsignal(th_act, ux_signal, code[0]);
}
if (p != NULL)
proc_rele(p);
thread_deallocate(th_act);
///...
}
static void ux_exception(
int exception,
mach_exception_code_t code,
mach_exception_subcode_t subcode,
int *ux_signal,
mach_exception_code_t *ux_code)
{
/*
* Try machine-dependent translation first.
*/
if (machine_exception(exception, code, subcode, ux_signal, ux_code))
return;
switch(exception) {
case EXC_BAD_ACCESS:
if (code == KERN_INVALID_ADDRESS)
*ux_signal = SIGSEGV;
else
*ux_signal = SIGBUS;
break;
case EXC_BAD_INSTRUCTION:
*ux_signal = SIGILL;
break;
case EXC_ARITHMETIC:
*ux_signal = SIGFPE;
break;
case EXC_EMULATION:
*ux_signal = SIGEMT;
break;
case EXC_SOFTWARE:
switch (code) {
case EXC_UNIX_BAD_SYSCALL:
*ux_signal = SIGSYS;
break;
case EXC_UNIX_BAD_PIPE:
*ux_signal = SIGPIPE;
break;
case EXC_UNIX_ABORT:
*ux_signal = SIGABRT;
break;
case EXC_SOFT_SIGNAL:
*ux_signal = SIGKILL;
break;
}
break;
case EXC_BREAKPOINT:
*ux_signal = SIGTRAP;
break;
}
}
ux_exception()
函数,展示了Mach
异常与Signal
信号转换关系。关于iOS
中Mach
异常信号的定义,可通过#include
跳转查看。
硬件异常
硬件异常依据前文所述,主要为:中断、缺陷、故障、终止。硬件异常的触发流程如下图:
软件异常
应用级别的异常,在iOS中就是NSException
。如果NSException
异常没有捕获处理(try-catch),系统最终会调用abort()
函数,向应用程序发送SIGABRT
的信号。
void abort() {
///...
/* abort() should call pthread_kill to deliver a signal to the aborting thread
* This helps gdb focus on the thread calling abort()
*/
if (__is_threaded) {
//...
(void)pthread_kill(pthread_self(), SIGABRT);
} else {
//...
(void)kill(getpid(), SIGABRT);
}
//...
}
异常捕获
上文分析可知道硬件异常与软件异常最终都会转换为Unix Signal
,因此对于Signal
信号的处理,可以覆盖大部分的崩溃信息。除此之外系统给我们提供的NSException
,可以用来获取更详细的奔溃信息。基于此,下文我们将只对Signal
和NSException
的捕获进行简单示例。
Signal捕获
在进行Signal
捕获时,需要注意覆盖问题。因为每个Signal
对应一个Handler
的处理函数,当我们通过绑定我们自己的Hanlder
来收集奔溃信息时,可能会覆盖其他三方库已经绑定的Handler
导致他们无法收集奔溃信息。
核心代码如下:
//头文件
#import
#import "execinfo.h"
///1.用以保存旧的handler
static struct sigaction *previous_signalHandlers = NULL;
///2.定义我们要处理的信号
static int signals[] = {SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGPIPE,SIGSEGV,SIGSYS,SIGTRAP};
///3.注册`Handler`
+ (BOOL)registerSignalHandler; {
///初始化我们的Sigaction
struct sigaction action = { 0 };
///初始化存放旧的Sigaction数组
int count = sizeof(signals) / sizeof(int);
if (previous_signalHandlers == NULL) {
previous_signalHandlers = malloc(sizeof(struct sigaction) * count);
}
action.sa_flags = SA_SIGINFO;
sigemptyset(&action.sa_mask);
/// 绑定我们的处理函数
action.sa_sigaction = &_handleSignal;
for (int i = 0; i < count; i ++) {
///遍历信号
int signal = signals[i];///or *(signals + i)
///绑定新的`Sigaction`,存储旧的`Sigaction`
int result = sigaction(signal, &action, &previous_signalHandlers[i]);
/// 绑定失败
if (result != 0) {
NSLog(@"signal:%d,error:%s",signal,strerror(errno));
for (int j =i--; j >= 0;j--) {
/// 恢复旧的Sigaction,此次函数返回NO
sigaction(signals[j], &previous_signalHandlers[j], NULL);
}
return NO;
}
}
return YES;
}
/// 4. 信号处理函数
void _handleSignal(int sigNum,siginfo_t *info,void *ucontext_t) {
/// todo our operation
NSLog(@"❌拦截到崩溃信号:%d,打印堆栈信息:%@",[CrashSignals callStackSymbols]);
/// 获取`sigNum`在信号数组中对应的`index`
int index = -1,count = sizeof(signals) / sizeof(int);
for (int i = 0; i < count; i++) {
if (*(signals + i) == sigNum) {
index = i;
break;
}
}
if (index == -1) return;
/// 取出旧的`Sigaction`
struct sigaction previous_action = previous_signalHandlers[index];
if (previous_action.sa_handler == SIG_IGN) {
//`SIG_IGN`忽略信号,`SIG_DFL`默认方式处理信号
return;
}
/// 恢复旧的`Sigaction`与Signal的绑定关系
sigaction(sigNum, &previous_action, NULL);
/// 重新抛出这个`Signal`,此时便会被`previous_action`的处理程序拦截到。
raise(sigNum);
}
//5. 函数的调用栈
+ (NSArray*)callStackSymbols {
/// ` int backtrace(void ** buffer , int size )`
/// void ** buffer:在`buffer`指向的数组返回程序栈桢的回溯,
/// void ** buffer: Each item in the array pointed to by buffer is of type void *
void* backtrace_buffer[128];
/// 返回值可能比 128大,大便截断,小则全部显示
int numberOfReturnAdderss = backtrace(backtrace_buffer, 128);
///char **backtrace_symbols(void *const *buffer, int size);
/// `backtrace_symbols()` translates the addresses into an array of strings that describe the addresses symbolically
/// The size argument specifies the number of addresses in buffer
char **symbols = backtrace_symbols(backtrace_buffer, numberOfReturnAdderss);
/// 提取每个返回地址对应的符号信息,栈桢是嵌套的
NSMutableArray *tempArray = [[NSMutableArray alloc]initWithCapacity:numberOfReturnAdderss];
for (int i = 0 ; i < numberOfReturnAdderss; i++) {
char *cstr_item = symbols[i];
NSString *objc_str = [NSString stringWithUTF8String:cstr_item];
[tempArray addObject:objc_str];
}
return [tempArray copy];
}
NSException捕获
系统提供了对应的处理iOS系统中未被捕获的NSException
的API
,我们只需要按照API
进行操作即可,但与Signal
一样,需要注意多处注册的覆盖问题,避免影响项目中其他收集程序。
核心代码如下:
///1.声明用以保存旧的`Hanlder`的静态变量
static NSUncaughtExceptionHandler *previous_uncaughtExceptionHandler;
///注册处理应用级异常的`handler`
+ (void)registerExceptionHandler; {
previous_uncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&_handleException);
}
///我们的异常处理程序
void _handleException(NSException *exception) {
/// Todo our operation
NSLog(@"✅拦截到异常的堆栈信息:%@",exception.callStackSymbols);
/// 传递异常
if (previous_uncaughtExceptionHandler != NULL) {
previous_uncaughtExceptionHandler(exception);
}
// 杀掉程序,这样可以防止同时抛出的SIGABRT被Signal异常捕获
/// kill (cannot be caught or ignored)
kill(getpid(), SIGKILL);
}
调试验证
Xcode
的Debug
环境下,Signal
异常与NSException
异常都会被Xcode
调试器拦截,不会走到我们的处理程序。因此代码的调试验证,笔者采用模拟器运行程序后,停止运行,脱离Xcode
的调试环境,重新在模拟器打开程序,开启Mac
的控制台程序,点击按钮触发奔溃,查看控制台对应模拟器的log
记录,来验证是否正确捕获。另:Signal
奔溃采用kill(getpid(), SIGBUS);
来触发。
参考资料
https://flylib.com/books/en/3.126.1.109/1/
http://shevakuilin.com/ios-crashprotection/
https://minosjy.com/2021/04/10/00/377/
https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sigaction.2.html