在开发的日常中,经常会遇到一些极端偶现的Bug,有些Bug很难以复现,所以一般的解决计划是接入PLCrashReporter这些第三方的溃散核算东西,从保存的溃散文件中读取相应的溃散信息。那么这些溃散核算东西又是依据什么原理运作的呢?我对此产生了很大的兴趣,所以对此做了一些调研,以下是我的成果:

Task & Thread & Process

在谈到运用溃散之前,首要需求知道的是,iOS操作体系的内核是XNU,它是一个混合内核,而这个混合内核的中心便是Mach这个微内核。

Process

操作体系被规划作为一个渠道,而运用运转在这个渠道之上。**每一个运转中的运用的实例都是一个进程(process)。**当然,一般情况下咱们描述的是用户角度的进程。和许多使命的体系一样,一个可履行程序的一个实例便是一个进程,UNIX也是依据这个概念创立的。而每一个实例都经过一个独有的Process ID来标识(PID),即使是同一个可履行程序的不同实例,也是有不同的PID的。而许多进程进一步或许成为进程组,一般经过向一个Group发送信息,用户能够控制多个进程。一个进程能够经过调用setpgrp(2) 来参加进程组。

而在BSD这一层,BSD Process则更为详细一些,包括了内部的多个线程,以及对应的Mach Task等等。

Task

首要要说到的便是Mach中的Task这个概念,Mach Task是体系资源的集合,每一个Task都包括了一个虚拟的地址空间(分配内存),一个端口权限名称空间,还有一个或许几个线程。在Mach内核中,Task是体系分配资源的基本单位。它和咱们熟悉的进程的概念是十分相识的,可是Mach TaskProcess是有差异的,比较而言Mach Task要供给更少的功用。在Process中,有信号、组、文件描述符等等。而Mach Task用于资源的分配和同享,它是资源的容器。

因为Mach是XNU这个混合内核中的微内核,所以Mach中的Mach Task是无法供给其他操作体系中的“进程”中的逻辑的,Mach Task仅仅供给了最重要的一些基础的完成,作为资源的容器。

而在BSD层中,BSD的process(其实也便是iOS的进程)和Mach Task是一一对应的。

Thread

理论上,Thread是CPU调度的基本单位。iOS中的进程和POSIX 线程(pthread)是别离依据Mach task和Mach thread的顶层完成。一个线程是相当轻量级的实体,创立一个新线程和操作一个线程的开销是十分低的。

Mach threads是在内核中被完成的,Mach thread是最基本的核算实体,它归于且仅归于一个Mach task,这个Mach task界说了线程的虚拟地址内存空间。值得一提的是POSIX线程模型是除Windows之外,一切的操作体系都支撑的一套标准的线程API,而iOS和OS X比其他体系都要愈加支撑pthread

Mach Task是没有自己的生命周期的,因为它并不会去履行使命,只要线程才会履行指令。当它说“task Y does X”的时分,这其实意味着“包括在task Y中的一个线程履行了X操作”。

你真的懂iOS的异常捕获吗?

疑问

因为Task是XNU的微内核Mach独有的,这个就和咱们熟知的进程,线程等等会有一些差异,所以这儿就提出了几个问题

1、Task和进程到底是什么联系?

首要要清晰的是task和进程是一一对应的联系,从springborad翻开的每一个进程,其实在内核里都有一个task与之对应。Task仅仅进程资源的容器,并不具有一般进程应该拥有的功用。

2、进程和线程到底是什么差异?

线程是资源调度的最小单位。

进程是资源分配的最小单位,而在OS X以及iOS体系中,每一个进程对应的仅有资源容器便是Task。

反常的简述

运用一般运转在用户态的,可是当运用需求去自动运用体系调用,或许说在被迫遇到一些反常或许中断的时分,运用都会有用户态进入到内核态,这个时分相当于体系收回了运用的运转权限,它要在内核态中去做一些特殊的处理。(system calls, exceptions, and interrupts)

而**接下来咱们要说的反常(Exception),它就会运用由用户态进入到内核态。**这儿就学习了腾讯Bugly的一张图来表明这种联系:

你真的懂iOS的异常捕获吗?

可是在iOS中一切的反常都会使得运用从用户态进入到内核态吗?

反常的分类

在所遇到的场景中,反常基本只要一种产生的原因,那便是工程师写的代码呈现了问题,然后导致了反常的产生,引起了程序的溃散。而产生的反常成果能够分类为两类:一种是硬件反常,一种是软件反常。

比方咱们做了一个除0操作,这在CPU履行指令的时分呈现指令反常,这便是一个hardware-generated 反常,再比方咱们写Objective-C业务的进程中,给一个不存在的目标发送了音讯,在Runtime时会抛出反常,这便是software-generated 反常。当然了假如不做处理他们都会导致程序的溃散,而假如要做处理,那就需求知道怎么去捕获这些反常。

这儿再重复一下:**尽管都是咱们写的软件过错,可是形成的反常成果却或许是硬件反常,亦或是软件反常,**而只要硬件反常才会产生上述的用户态到内核态的转化。

Mach Exception

Mach Exception的传递

在上面咱们说到了硬件反常,硬件反常会产生用户态→内核态的转化,那么有哪些反常归于硬件反常呢?

  • 企图拜访不存在的内存
  • 企图拜访违反地址空间保护的内存
  • 因为不合法或未界说的操作代码或操作数而无法履行指令
  • 产生算术过错,例如被零除、上溢、或许下溢
  • ……

以上这些都归于硬件反常,可是这些硬件反常和咱们说到的Mach Exception有什么联系呢?

Mach内核供给了一个依据IPC的反常处理东西,其间反常被转化为message。当反常产生的时分,一条包括反常的mach message,例如反常类型、产生反常的线程等等,都会被发送到一个反常端口。而线程(thread),使命(task),主机(host)都会维护一组反常端口,当Mach Exception机制传递反常音讯的时分,它会按照thread → task → host 的次序传递反常音讯(这三者便是线程,进程,和体系的递进联系),假如这三个等级都没有处理反常成功,也便是收到KERN_SUCCESS 成果,那么内核就会停止该进程。在/osfmk/kern/exception.c 的源码中会经过exception_trige() 办法来进行上述音讯传递的流程,此办法内部调用exception_deliver() 往对应等级的反常端口发送信息:

// 源码地址:https://opensource.apple.com/source/xnu/xnu-2050.24.15/osfmk/kern/exception.c
void exception_trige(
				exception_type_t        exception, 
				mach_excpetion_data_t   code, 
				mach_msg_type_number_t  codeCnt) {
	...
	kern_return_t kr;
	...
	// 1、Try to raise the exception at the activation level.
	// 线程等级
	thread = current_thread()
	mutex = &thread->mutex;
	excp = &thread->exc_actions[exception];
	kr = exception_deliver(thread, esception, code, codeCnt, excp, mutex);
	if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
			goto out;
	}
	....
	// 2、Maybe the task level will handle it.
  	// 进程等级
	task = current_task();
	mutex = &task->lock;
	excp = &task->exc_actions[exception];
	kr = exception_deliver(thread, exception, code, codeCnt, excp, mutex);
	if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
			goto out;
	}
	...
	// 3、How about at the host level?
	// 主机等级
	host_priv = host_priv_self();
	mutex = &host_priv->lock;
	excp = &host_priv->exc_actions[exception];
	kr = exception_deliver(thread, exception, code, codeCnt, excp, mutex);
	if (kr == KERN_SUCCESS || kr == MACH_RCV_PORT_DIED) {
			goto out;
	}
	// 在MAC中还有一步,那便是假如这儿启动了KDB,那么就运用KDB调试反常。
	/*
	 * 4、Nobody handled it, terminate the task.
	 */
	(void) task_terminate(task);
	.....
out:
	if ((exception != EXC_CRASH) && (exception != EXC_RESOURCE))
		thread_exception_return();
	return;
}

怎么处理Mach Exception?

已然反常产生了,那么反常就需求得到处理。反常处理程序是反常音讯的承受者,它运转在自己的线程,尽管说它能够和产生反常的线程在同一个task中(也便是同一个进程中),可是它一般运转在其他的task中,比方说一个debugger。假如一个线程想处理这个task的反常音讯,那么就需求调用task_set_exception_ports() 来注册这个task的反常端口。这样的话,只要这个进程呈现了硬件反常终究都会转化为Mach Exception Mesaage并传递给注册的端口,然后被反常处理程序承受到,处理接纳到的反常音讯。以下是反常code对应详细的原因:

Exception Notes
EXC_BAD_ACCESS 无法拜访内存
EXC_BAD_INSTRUCTION 不合法或许未界说的指令或许操作数
EXC_ARITHMETIC 算术反常(例如被零除)
EXC_EMULATION 遇到仿真支撑指令
EXC_SOFTWARE 软件生成的反常(比方浮点数核算的反常)
EXC_BREAKPOINT 盯梢或许断点(比方Xcode的断点,就会产生反常)
EXC_SYSCALL Unix体系调用
EXC_MACH_SYSCALL Mach体系调用
EXC_RPC_ALERT RPC警告

当然,并不是一切的反常引发的Exception都是咱们所说的反常,这其间有的是体系调用,或许断点如EXC_SYSCALL,所以设置反常端口的时分,就需求去考虑到这一点,如下方的myExceptionMask 局部变量存储了需求捕获的几种反常类型:

exception_mask_t myExceptionMask;
myExceptionMask = EXC_MASK_BAD_ACCESS |       /* Memory access fail */
                                EXC_MASK_BAD_INSTRUCTION |  /* Illegal instruction */
                                EXC_MASK_ARITHMETIC |       /* Arithmetic exception (eg, divide by zero) */
                                EXC_MASK_SOFTWARE |         /* Software exception (eg, as triggered by x86's bound instruction) */
                                EXC_MASK_BREAKPOINT |        /* Trace or breakpoint */
                                EXC_MASK_CRASH;
// 注意:这儿必须要运用THREAD_STATE_NONE和plcrash结构中运用的坚持一致
// 
rc = task_set_exception_ports(mach_task_self(),
                                  myExceptionMask,
                                  myexceptionPort,
                                  (EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES),
                                  THREAD_STATE_NONE);

这儿得着重强调一下端口设置办法的参数:

kern_return_t task_set_exception_ports
(
	task_t task,
	exception_mask_t exception_mask,
	mach_port_t new_port,
	exception_behavior_t behavior,
	thread_state_flavor_t new_flavor
);

在这之中xx_set_exception_ports()behavior 参数指定来产生反常时发送的反常音讯的类型。

behavior Notes
EXCEPTION_DEFAULT catch_exception_raise音讯:包括线程标识
EXCEPTION_STATE catch_exception_raise_state: 包括线程状况
EXCEPTION_STATE_IDENTITY catch_exception_raise_state_identity: 包括线程标识和状况

flavour 参数指定要与反常音讯一起发送的线程状况的类型,假如不需求,能够运用THREAD_STATE_NONE 。可是要注意的是,无论线程状况是否在反常音讯中被发送,反常处理程序都能够运用thread_get_state()thread_set_state() 别离查询和设置犯错线程的状况。

而默许情况下,线程等级的反常端口都被设置为null端口,而task等级的反常端口,会在fork() 期间被继承,一般也是null 端口(fock其实指的是从内核fock出一个进程)。所以这个时分,压力就来到了Host的反常端口(也便是机器级的反常端口),这儿产生了什么呢?

接下来,咱们详细看一看假如一款Mac运用当线程中产生反常时,假如咱们不做任何处理,会产生什么?(Apple自己的exception handler的处理流程)

1、内核会将过错线程挂起,而且发送一条音讯给适合的反常端口。

2、过错线程坚持挂起状况,等待音讯回复。

3、exception_deliver() 办法向线程的反常端口发送音讯,未得到成功回复。

4、exception_deliver() 办法向task的反常端口发送音讯,未得到成功回复。

5、exception_deliver() 办法向host的反常端口发送音讯。

3、具有接纳反常端口权限的任意task中的反常处理线程将取出该音讯(在Mac上一般是KDB调试程序)

4、反常处理程序调用exc_server 办法来处理该音讯。

5、exc_server 依据端口设置的 behavior 参数来挑选调用什么办法来获取相应的线程信息:catch_exception_raise()、catch_exception_raise_state()、catch_exception_raise_state_identity() ,便是三个函数之一

6、假如上述函数处理后回来KERN_SUCCESS ,那么exc_server() 准备回来音讯发送到内核,使得线程从反常点持续履行。假如反常不是致命的,而且经过该函数修复了问题,那么修复线程的状况能够使得线程持续。

7、假如上述函数处理后回来的不是KERN_SUCCESS ,那么内核将停止该task。

这也便是为什么在Mac上假如Xcode溃散之后,Mac上会呈现Xcode溃散的陈述界面,一起体系会将Xcode封闭。

假如咱们自己捕获处理之后,能否直接将调用办法exc_server 将音讯持续往后转发呢?答案是否定的,因为在iOS中exc_server 并不是一个public的API,所以根本无法运用。那么咱们捕获反常之后怎么转发给其他的端口呢?这个后面进行描述。

上述进程的详细处理流程如下图:

你真的懂iOS的异常捕获吗?

实际上在体系启动的时分,Host反常端口对应的反常处理程序就现已初始化好了,一起,Unix的反常处理也是在这儿初始化,它会将Mach反常转化为Unix signals。在体系启动时,内核的BSD层经过bsdinit_task()办法[源码在:bsd/kern/bsd_ init.c中]来进行初始化的:

//源码地址:https://opensource.apple.com/source/xnu/xnu-7195.81.3/bsd/kern/bsd_init.c.auto.html
void
bsdinit_task(void)
{
	proc_t p = current_proc();
	process_name("init", p);
	/* Set up exception-to-signal reflection */
	ux_handler_setup();
	
}

然后bsdinit_task()它会调用ux_handler_init (在最新的xnu-7195.81.3中为ux_handler_setup)办法来进行设置反常监听端口:

/// 源码地址:https://opensource.apple.com/source/xnu/xnu-7195.81.3/osfmk/kern/ux_handler.c.auto.html
/*
 * setup is called late in BSD initialization from initproc's context
 * so the MAC hook goo inside host_set_exception_ports will be able to
 * set up labels without falling over.
 */
void
ux_handler_setup(void)
{
	ipc_port_t ux_handler_send_right = ipc_port_make_send(ux_handler_port);
	if (!IP_VALID(ux_handler_send_right)) {
		panic("Couldn't allocate send right for ux_handler_port!\n");
	}
	kern_return_t kr = KERN_SUCCESS;
	/*
	 * Consumes 1 send right.
	 *
	 * Instruments uses the RPC_ALERT port, so don't register for that.
	 */
	kr = host_set_exception_ports(host_priv_self(),
	    EXC_MASK_ALL & ~(EXC_MASK_RPC_ALERT),
	    ux_handler_send_right,
	    EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES,
	    0);
	if (kr != KERN_SUCCESS) {
		panic("host_set_exception_ports failed to set ux_handler! %d", kr);
	}
}

这儿host_set_exception_ports 办法注册host等级的ux_exception_port反常端口,当这个端口承受到反常信息之后,反常处理线程会调用**handle_ux_exception** 办法,这个办法会调用ux_exception 将mach信息转化为signal信号,随后会将转化的unix signal投递到过错线程:threadsignal(thread, ux_signal, code, TRUE); 详细的转化办法如下:

/*
 * Translate Mach exceptions to UNIX signals.
 *
 * ux_exception translates a mach exception, code and subcode to
 * a signal.  Calls machine_exception (machine dependent)
 * to attempt translation first.
 */
static int
ux_exception(int exception,
    mach_exception_code_t      code,
    mach_exception_subcode_t   subcode)
{
	int machine_signal = 0;
	/* Try machine-dependent translation first. */
	if ((machine_signal = machine_exception(exception, code, subcode)) != 0) {
		return machine_signal;
	}
	switch (exception) {
	case EXC_BAD_ACCESS:
		if (code == KERN_INVALID_ADDRESS) {
			return SIGSEGV;
		} else {
			return SIGBUS;
		}
	case EXC_BAD_INSTRUCTION:
		return SIGILL;
	case EXC_ARITHMETIC:
		return SIGFPE;
	case EXC_EMULATION:
		return SIGEMT;
	case EXC_SOFTWARE:
		switch (code) {
		case EXC_UNIX_BAD_SYSCALL:
			return SIGSYS;
		case EXC_UNIX_BAD_PIPE:
			return SIGPIPE;
		case EXC_UNIX_ABORT:
			return SIGABRT;
		case EXC_SOFT_SIGNAL:
			return SIGKILL;
		}
		break;
	case EXC_BREAKPOINT:
		return SIGTRAP;
	}
	return 0;
}

Unix Signal

Mach现已供给了底层的反常机制,可是依据Mach exception,Apple在内核的BSD层上也建立了一套信号处理体系。这是为什么呢?原因很简单,其实便是为了兼容Unix体系。而依据Linux的安卓也是兼容Unix的,所以安卓的反常也是抛出的Signal。当然这儿得说明,在现代的Unix体系中,Mach反常仅仅导致信号生成的一类事情,还有许多其他的事情或许也会导致信号的生成,比方:显式的调用kill(2)或许killpg(2)、子线程的状况变化等等。

信号机制的完成只要是两个重要的阶段:信号生成和信号传递。信号生成是确保信号被生成的事情,而信号传递是对信号处理的调用,即相关信号动作的履行。而每一个信号都有一个默许动作,在Mac OS X上能够是以下事情:

1、停止反常进程

2、Dump core停止反常进程

3、暂停进程

4、假如进程停止,持续进程;否则疏忽

5、疏忽信号

当然这些都是信号的默许处理办法,咱们能够运用自界说的处理程序来重写信号的默许处理办法,详细来说能够运用sigaction 来自界说,详细的代码实例咱们在后续的捕获信号的demo中有描述。

Mach Exception转化为Signal

Mach反常假如没有在其他当地(thread,task)得到处理,那么它会在ux_exception() 中将其转化为对应的Unix Signal信号,以下是两者之间的转化:

Mach Exception Unix Signal 原因
EXC_BAD_INSTRUCTION SIGILL 不合法指令,比方除0操作,数组越界,强制解包可选形等等
EXC_BAD_ACCESS SIGSEVG、SIGBUS SIGSEVG、SIGBUS两者都是过错内存拜访,可是两者之间是有差异的:SIGBUS(总线过错)是内存映射有效,可是不允许被拜访; SIGSEVG(段地址过错)是内存地址映射都失效
EXC_ARIHMETIC SIGFPE 运算过错,比方浮点数运算反常
EXC_EMULATION SIGEMT hardware emulation 硬件仿真指令
EXC_BREAKPOINT SIGTRAP trace、breakpoint等等,比方说运用Xcode的断点
EXC_SOFTWARE SIGABRT、SIGPIPE、SIGSYS、SIGKILL 软件过错,其间SIGABRT最为常见。

Mach反常转化为了Signal信号并不代表Mach反常没有被处理过。有或许存在线程级或许task级的反常处理程序,它将承受反常音讯并处理,处理完毕之后将反常音讯转发给ux_exception() 这也将导致终究反常转化为Signal。

软件反常转化为Signal

除了上述引发CPU Trap的反常之外,还有一类反常是软件反常,这一类反常并不会让进程进入内核态,所以它也并不会转化为Mach Exception,而是会直接转化为Unix Signal。而由Objective-C产生的反常便是软件反常这一类,它将直接转换为Signal信号,比方给目标发送未完成的音讯,数组索引越界直接引发SIGABRT信号,作为对比Swift的数组反常会导致CPU Trap,转化为EXC_BAD_INSTRUCTION反常音讯。

那为什么Objective-C反常仅仅软件反常,而不会触发CPU Trap?

因为Objective-C写的代码都是依据Runtime运转的,所以反常产生之后,直接会被Runtime处理转化为Unix Signal,一起,关于这类反常,咱们能够直接运用**NSSetUncaughtExceptionHandler** 设置处理办法,即使咱们设置了处理办法,OC反常依旧会被转发为信号,一起值得说明的是注册Signal的处理程序运转于的线程,以及**NSSetUncaughtExceptionHandler** 的处理程序运转于的线程,便是反常产生的线程,也便是哪个线程犯错了,由哪个线程来处理。

Mach Exception和Unix Signal的差异

Mach Exception的处理机制中反常处理程序能够在自己创立的处理线程中运转,而该线程和犯错的线程甚至能够不在一个task中,即能够不在一个进程中,因而反常处理不需求过错线程的资源来运转,这样能够在需求的时分直接获得过错线程的反常上下文,而Unix Signal的处理无法运转在其他的线程,只能在过错线程上处理,所以Mach反常处理机制的优势是很明显的,比方说debugging场景,咱们平常打断点的时分,其实程序运转到这儿的时分会给Xcode这个task中的注册反常端口发EXC_BREAKPOINT音讯,而Xcode收到之后,就会暂停在断点处,在处理完之后(比方点击跳过断点),将发送音讯回来到Xcode,Xcode也将持续跑下去。

这也是Mach Exception处理机制的优势,它能够在多线程的环境中很好的运转,而信号机制只能在犯错线程中运转。而其实Mach反常处理程序能够以更细粒度的办法来运转,因为每一种Mach反常音讯都能够有自己的处理程序,甚至是每一个线程,每一个Task单独处理,可是要说明的是,线程级的反常处理程序一般适用于过错处理,而Task级的反常处理程序一般适用于调试。

那么Unix Signal的优势是什么呢?便是全!无论是硬件反常还是软件反常都会被转化为Signal。

在《Mac OS X and iOS Internals To the Apple Core》这本书中说到:为了统一反常处理机制,一切的用户自身产生的反常并不会直接转化为Unix信号,而是会先下沉到内核中转化为Mach Exception,然后再走Mach反常的处理流程,终究在host层转化为UnixSignal信号。

可是我是不同意这个观念的,因为在我注册的Task等级的反常处理程序中并不会捕获Objective-C产生的反常(如数组越界),它是直接转化为SIGABRT的。而软件反常产生的Signal,实际上都是由以下两个API:kill(2)或许pthread_kill(2)之一生成的反常信号,而我这两个办法的源码中并没有看到下沉到内核中的代码,而是直接转化为Signal并投递反常信号。流程如下图所示,其间psignal() 办法以及psignal_internal() 办法的源码都在[/bsd/kern/kern_sig.c]文件中。

你真的懂iOS的异常捕获吗?

反常的捕获

捕获反常的办法

说了这么多反常是什么,反常怎样分类,那么接下来咱们详细来说说咱们怎么捕获反常,可是再聊怎么捕获之前,且考虑一下,咱们应该选用哪种办法来捕获呢?从上述可知Mach Exception反常处理机制只能捕获硬件反常,而Unix反常处理机制都能捕获,所以大略有两种办法能够挑选:

1、Unix Signal

2、Mach Exception and Unix Signal

微软有一个十分著名的溃散核算结构**PLCrashReport ,**这个结构也是供给了两种核算溃散的计划:

typedef NS_ENUM(NSUInteger, PLCrashReporterSignalHandlerType) {
		PLCrashReporterSignalHandlerTypeBSD = 0,    /// 一种是BSD层,也便是Unix Signal办法
		PLCrashReporterSignalHandlerTypeMach = 1    /// 一种是Mach层,也便是Mach Exception办法
}

关于第二种计划,假如看网上许多文章,都说说到到PLCrashReport这个库中说:

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.

意思便是说,假如不捕获SIGABRT 信号,那么Mach Exception接到EXC_CRASH音讯会产生进程的死锁,可是我不认可这个观念,原因如下:

1、在我自己测验Demo的进程中,发现需求捕获SIGABRT 信号的原因是软件反常并不会下沉到Mach内核转化为Signal,而是会直接发出SIGABRT 信号,所以需求捕获。

2、即使我在task的task_set_exception_ports 办法中设置了需求捕获EXC_CRASH反常,当反常产生时也不会呈现死锁的情况。

3、假如看BSD层中将Mach反常转化为Signal的源码中ux_exception办法的详细完成,会发现根本就不会处理EXC_CRASH的情况,正如上述列表中的Mach Exception和Unix Signal的对应联系

所以我的结论是捕获SIGABRT信号,仅仅因为软件反常并不会形成Mach Exception,而是直接会被转化SIGABRT信号,并向过错线程投递。也便是说:只选用Mach Exception无法捕获软件反常,所以需求额外捕获SIGABRT信号。 那么详细来说怎么捕获呢?

捕获反常的实践——Unix Signal

// 1、首要是确认注册哪些信号
+ (void)signalRegister {
    ryRegisterSignal(SIGABRT);
    ryRegisterSignal(SIGBUS);
    ryRegisterSignal(SIGFPE);
    ryRegisterSignal(SIGILL);
    ryRegisterSignal(SIGPIPE);
    ryRegisterSignal(SIGSEGV);
    ryRegisterSignal(SIGSYS);
    ryRegisterSignal(SIGTRAP);
}
// 2、实际的注册办法:将信号和action关联,此处我的处理办法为rySignalHandler
static void ryRegisterSignal(int signal) {
    struct sigaction action;
    action.sa_sigaction = rySignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}
// 3、完成详细的反常处理程序
static void rySignalHandler(int signal, siginfo_t* info, void* context) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Signal Exception:\n"];
    [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised. \n", signalName(signal)]];
    // 因为注册了信号溃散回调办法,体系回来调用
    for (NSUInteger index = 0; index < NSThread.callStackSymbols.count; index ++) {
        NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
        [mstr appendString:[str stringByAppendingString:@"\n"]];
    }
    [mstr appendString:@"threadInfo: \n"];
    [mstr appendString:[[NSThread currentThread] description]];
    NSString *path = [NSString stringWithFormat:@"%@/Library/signal.txt",NSHomeDirectory()];
    [mstr writeToFile:path atomically:true encoding:NSUTF8StringEncoding error:nil];
    exit(-1);
}

上面的流程很简单,我会在收到Signal信号之后,由过错线程来履行反常处理程序,履行完毕之后,运用exit(-1) 强制退出。

问题一:假如仅仅履行一个写入文件的操作之后不退出即不履行exit(-1)会产生什么?

它将会导致该犯错线程履行完写入文件的操作之后,持续履行的时分仍然呈现反常,仍然会抛出信号,然后又会抛给该线程处理反常,所以变成了一个死循环,导致一直在将过错信息写入文件。

问题二:假如不想运用exit(-1) 又想正常作业,应该怎么做呢?

// 1、首要取消掉一切绑定的action
// 2、然后处理完之后运用raise(signal) 将信号发给进程做默许处理
static void rySignalHandler(int signal, siginfo_t* info, void* context) {
    [Signal unRegisterSignal];
	...
	raise(signal);
}
static int monitored_signals[] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGPIPE, SIGSEGV, SIGSYS, SIGTRAP};
static int monitored_signals_count = (sizeof(monitored_signals) / sizeof(monitored_signals[0]));
+ (void)unRegisterSignal {
    for (int i = 0; i < monitored_signals_count; i++) {
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = SIG_DFL;
        sigemptyset(&sa.sa_mask);
        sigaction(monitored_signals[i], &sa, NULL);
    }
}

上述计划其实是仿照的PLCrashReport 结构中的写法,主张阅读相关源码。

问题三:假如过错线程是子线程,然后Signal投递到子线程处理,这个时分影响主线程吗?

不影响,因为Signal反常处理程序在过错线程运转,这个和主线程无关,当然,假如过错线程是主线程,那就另当别论了。

捕获反常的实践——Mach Exception + Unix Signal

相对而言运用Mach Exception的反常处理机制要稍微杂乱一些,Unix Signal的捕获上述现已说到了,接下来便是Mach Exception反常的捕获了。

+ (void)setupMachHandler {
    kern_return_t rc;
		// 1、分配端口
    rc = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &myexceptionPort);
    if (rc != KERN_SUCCESS) {
        NSLog(@"声明反常端口没有成功");
    }
    // 2、增加mach_send的权限
    rc = mach_port_insert_right(mach_task_self(), myexceptionPort, myexceptionPort, MACH_MSG_TYPE_MAKE_SEND);
    if (rc != KERN_SUCCESS) {
        NSLog(@"增加权限失利");
    }
    exception_mask_t myExceptionMask;
		// 3、设置需求承受哪些反常信息
    myExceptionMask = EXC_MASK_BAD_ACCESS |       /* Memory access fail */
                                EXC_MASK_BAD_INSTRUCTION |  /* Illegal instruction */
                                EXC_MASK_ARITHMETIC |       /* Arithmetic exception (eg, divide by zero) */
                                EXC_MASK_SOFTWARE |         /* Software exception (eg, as triggered by x86's bound instruction) */
                                EXC_MASK_BREAKPOINT |        /* Trace or breakpoint */
                                EXC_MASK_CRASH;
		// 4、task_set_exception_ports设置task等级的反常端口
    rc = task_set_exception_ports(mach_task_self(),
                                  myExceptionMask,
                                  myexceptionPort,
                                  (EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES),
                                  THREAD_STATE_NONE);
		// 5、初始化反常处理线程,并设置反常处理办法。
    pthread_t thread;
    pthread_create(&thread, NULL, exc_handler, NULL);
}
// 6、反常处理程序
// 相似RunLoop的思路,运用一个while-true循环来确保线程不会退出,一起运用mach_msg来一直接纳音讯
static void* exc_handler(void *ignored) {
    mach_msg_return_t rc;
    // 自界说一个音讯体
    typedef struct {
        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;
        integer_t code[2];
        int flavor;
        mach_msg_type_number_t old_stateCnt;
        natural_t old_state[144];
        kern_return_t retcode;
    } Request;
    Request exc;
    exc.Head.msgh_size = 1024;
    exc.Head.msgh_local_port = myexceptionPort;
    while (true) {
        rc = mach_msg(&exc.Head,
                      MACH_RCV_MSG | MACH_RCV_LARGE,
                      0,
                      exc.Head.msgh_size,
                      exc.Head.msgh_local_port, // 这是一个全局的变量
                      MACH_MSG_TIMEOUT_NONE,
                      MACH_PORT_NULL);
        if (rc != MACH_MSG_SUCCESS) {
            NSLog(@"没有成功承受到溃散信息");
            break;
        }
        // 将反常写入文件(当然, 你也能够做自己的自界说操作)			
        break;
    }
		exit(-1);
}

代码很简单理解,收到反常之后就会履行相应的处理代码,处理完反常之后履行exit(-1) 退出运用。仍然是问自己几个问题:

问题一:不做exit(-1)操作会产生什么,反常会不停写入吗?

不然,因为这儿接纳到反常音讯之后,就没有对外转发了,只会停留在task这一级,可是因为反常线程没有得到康复,所以表现出来的状况便是反常线程堵塞。

问题二:不做exit(-1),反常线程是子线程,会对主线程有影响吗?

不会,它只会堵塞反常线程,对主线程没有影响。换言之,UI事情正常响应。

问题三:Mach Exception收到音讯处理之后就不会向外转发了,那假如想转发呢?

能够向原端口回复你的处理成果,这就会由体系默许向上转发,终究转化为Unix信号。

static void* exc_handler(void *ignored) {
    mach_msg_return_t rc;
    // 自界说一个音讯体
    typedef struct {
        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;
        integer_t code[2];
        int flavor;
        mach_msg_type_number_t old_stateCnt;
        natural_t old_state[144];
        kern_return_t retcode;
    } Request;
		....
		// 处理完音讯之后,咱们回复处理成果
    Request reply;
    memset(&reply, 0, sizeof(reply));
    reply.Head.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(exc.Head.msgh_bits), 0);
    reply.Head.msgh_local_port = MACH_PORT_NULL;
    reply.Head.msgh_remote_port = exc.Head.msgh_remote_port;
    reply.Head.msgh_size = sizeof(reply);
    reply.NDR = NDR_record;
    reply.retcode = KERN_SUCCESS;
    /*
     * Mach uses reply id offsets of 100. This is rather arbitrary, and in theory could be changed
     * in a future iOS release (although, it has stayed constant for nearly 24 years, so it seems unlikely
     * to change now). See the top-level file warning regarding use on iOS.
     *
     * On Mac OS X, the reply_id offset may be considered implicitly defined due to mach_exc.defs and
     * exc.defs being public.
     */
    reply.Head.msgh_id = exc.Head.msgh_id + 100;
    mach_msg(&reply.Head,
             MACH_SEND_MSG,
             reply.Head.msgh_size,
             0,
             MACH_PORT_NULL,
             MACH_MSG_TIMEOUT_NONE,
             MACH_PORT_NULL);
    return NULL;
}

参阅

  1. 《Mac OS X and iOS Internals To the Apple Core》
  2. Mac OS X Internals: A Systems Approach 第九章
  3. kernel源码
  4. Android 渠道 Native 代码的溃散捕获机制及完成
  5. PLCrashReporter