不少介绍操做系统的书在讲解操做系统的运行机制的时候都会提到“现代操做系统是靠中断驱动的软件”,这句话怎么理解?html
中断是指CPU对系统发生的某个事件作出的一种反应,CPU暂停正在执行的程序,保留现场后转去执行相应的处理程序,处理完该事件后再返回断点继续执行被“打断”的程序。git
而引入中断技术的初衷是提升多道程序运行环境中CPU的利用率,好比CPU能够在I/O的执行过程当中去执行其余指令,不用空闲地去等待(或简单轮询) I/O设备的执行完成,I/O设备执行完成再经过中断通知CPU,以提升CPU利用率。后来中断技术逐步发展,成为操做系统各项操做的基础,好比进程调度,现代操做系统的进程调度通常都是采用基于时间片的优先级调度算法,把CPU的时间划分为很细粒度的时间片,执行一个任务的时间片用完了,时钟经过时钟中断去通知CPU切换任务,再好比下面要讨论到的CPU异常处理,也是基于中断机制去完成的。github
中断(interrupt)和异常(exception)在不一样的CPU架构里有不一样的含义。算法
无论如何界定中断和异常,CPU发生异常时,都会将控制权从异常前的程序交给异常处理程序,并且CPU将得到 不会更低 的执行权利,好比执行用户态的应用程序发生异常,CPU将切换到内核态,并执行对应的异常处理程序。经典的CPU五级流水线中一条指令的生命周期为[取指、译码、执行、访存、写回],每一个阶段均可能出现CPU异常,好比在ARM架构下:数组
这两种异常对应的处理程序会直接或者间接调用 Mach 内核的 exception_triage()
函数,并将 EXC_BAD_ACCESS
做为入参传进去,exception_triage()
将会利用Mach消息传递机制投递异常。尽管Intel架构和ARM架构的CPU异常处理有些不一样,但异常处理程序都会直接或间接将异常类型(exception_type_t
)传给exception_triage()
函数来处理异常,以此来屏蔽不一样机器平台异常处理的差别。缓存
异常类型(exception_type_t
)在Mach层用int变量来存储,在osfmk/mach/exception_types.h
文件中能看到Mach层定义的十几种异常,如常见的bash
#define EXC_BAD_ACCESS 1 /* Could not access memory */
/* Code contains kern_return_t describing error. */
/* Subcode contains bad memory address. */
#define EXC_CRASH 10 /* Abnormal process exit */
#define EXC_CORPSE_NOTIFY 13 /* Abnormal process exited to corpse state */
复制代码
int main(int argc, const char * argv[]) {
int *pi = (int*)0x00001111;
*pi = 17;
return 0;
}
复制代码
上面这个程序中的非法内存访问将会用到上面列举三个异常类型,下面经过看源码、看书、代码调试来看下exception_triage()
函数都作了什么。架构
在《深刻解析Mac OS & iOS 操做系统》中有讲解xnu异常处理的过程,但不是特别详细,并且书的参考代码与最新代码也有出入,要把内核的异常处理流程弄清楚,须要看书、看源码,固然少不了断点调试。app
在MacOS上调试XNU要比在iOS上调试简单,使用到的工具是:LLDB + VMware Fusion + Kernel Debug Kit ,调试环境的搭建只需简单几个步骤便可,可参考 《MacOS内核调试环境搭建》 ,iOS上的调试能够参考lan beer 分享的 build your own iOS kernel debugger,连接里有分享的PPT和PoC ,惋惜目前的Poc仅支持iOS 11.1.2框架
这里记录个在MacOS上调试XNU的坑,若是虚拟机到达“wait for the debugger” 阶段,而且在主机经过“kdp-remote” 链接虚拟机成功,但虚拟机继续启动的过程当中一直卡在“Waiting for link to become available”,致使调试没法继续,就像这个帖子中描述的问题同样
虽然我也没找到问题的具体缘由,但摸出了个解决办法,就是在虚拟机启动时同时按下Option、Command、P 和 R,以reset NVRAM,将会进入到恢复模式,使用终端工具关闭虚拟机的SIP ,即输入命令csrutil disable,而后重启,启动后再走一遍 “内核替换”-> "设置boot-args" -> "清除kext缓存" -> "重启虚拟机" -> "主机链接虚拟机" 的流程,这时将会有百分之七十的几率能让虚拟机正常启动并可调试,若是不行就再试一次。
注:我使用的MacOs 版本是10.13.5,对应的XNU是4570.61.1,对应版本的源码没有放出,对比了前几个版本,我须要参考的源码都没有变更,因此参考源码是github上的xnu-4570.1.46
int main(int argc, const char * argv[]) {
char c = getchar();
int *pi = (int*)0x00001111;
*pi = 17;
return 0;
}
复制代码
首先使用gcc来把上面这个程序编译成二进制可执行程序,而后运行。在程序等待键盘输入的时候,能够用ps命令查看进程PID是352。
在运行程序以前我在osfmk/kern/exception.c
的 exception_triage_thread()
函数实现处打了三个断点
breakpoint set --file exception.c --line 447
breakpoint set --file exception.c --line 459
breakpoint set --file exception.c --line 472
复制代码
44七、45九、472 分别是往 thread 层、task 层、host 层的异常端口数组投递异常,对应如下三行代码
(447)kr = exception_deliver(thread, exception, code, codeCnt, thread->exc_actions, mutex);
(459)kr = exception_deliver(thread, exception, code, codeCnt, task->exc_actions, mutex);
(472)kr = exception_deliver(thread, exception, code, codeCnt, host_priv->exc_actions, mutex);
复制代码
这三个断点只有一个断住了,那就是第472 行代码,到这里能够验证如下结论
首先经过lldb在终端输出函数调用栈、线程状态、进程PID
(lldb) bt
* thread #1, stop reason = breakpoint 4.1
* frame #0: 0xffffff800f97f0c9 kernel.development`exception_triage_thread(exception=1, code=0xffffff8014debf50, codeCnt=2, thread=0xffffff801c7c2a10) at exception.c:472 [opt]
frame #1: 0xffffff800fad71fb kernel.development`user_trap [inlined] exception_triage(code=0x0000000000000001) at exception.c:504 [opt]
frame #2: 0xffffff800fad71df kernel.development`user_trap [inlined] i386_exception(exc=1, code=<unavailable>) at trap.c:1152 [opt]
frame #3: 0xffffff800fad71d7 kernel.development`user_trap [inlined] user_page_fault_continue(kr=<unavailable>) at trap.c:232 [opt]
frame #4: 0xffffff800fad71d1 kernel.development`user_trap(saved_state=0xffffff8017246b20) at trap.c:1093 [opt]
frame #5: 0xffffff800f921102 kernel.development`hndl_alltraps + 226
(lldb) e struct proc *$p_proc = (struct proc *)thread->task->bsd_info
(lldb) po $p_proc->p_pid
352
(lldb) po thread->state
4
复制代码
(注:线程状态用int变量存储,int state ,#define TH_SUSP 0x02 /*中止,或请求中止*/)
复制代码
以上log结合源码和《深刻解析Mac OS & iOS 操做系统》能够得出结论:
在Intel架构上,CPU执行用户态程序发生异常时会将对应进程挂起,并将CPU工做状态设置为内核态,还将执行XNU内核的异常处理程序。大多数操做系统都不会为每个陷阱(异常)设置独立的处理程序,而是为全部的陷阱设置一个处理程序,而后这个处理程序经过switch()
进行不一样的处理,或者根据预约义的表跳转到不一样的函数。XNU的作法也是如此,hndl_alltraps
是公共陷阱处理程序,user_trap
负责处理实际的陷阱,hndl_alltraps
是用汇编语言写的,而user_trap
是用C语言写的,在user_trap
的实现里会调用i386_exception
函数 ,i386_exception
函数会调用exception_triage
将陷阱转换为Mach 异常,在上面的程序中Mach 异常是 EXC_BAD_ACCESS
。
exception_triage()
函数的实现只有两行代码
kern_return_t
exception_triage(
exception_type_t exception,
mach_exception_data_t code,
mach_msg_type_number_t codeCnt)
{
thread_t thread = current_thread();
return exception_triage_thread(exception, code, codeCnt, thread);
}
复制代码
第一行获取当前线程,这是由于第二行调用 exception_triage_thread
把异常投递到异常端口时须要用到current thread,thread、task的异常端口数组都须要经过 thread 获取到:
thread->exc_actions;
task = thread->task;
task->exc_actions;
host_priv = host_priv_self();
host_priv->exc_actions;
复制代码
而thread、task的异常端口默认是NULL,host的异常端口是第一个用户态进程 launchd(PID 1)初始化的时候就设置好的了,并且内核初始化成功后全部的用户态进程都是launchd 的子进程,子进程经过父进程fork继承了父进程的异常端口,所以全部的用户态进程出现异常时,异常都能在host层获得统一处理。
launchd 进程是如何设置host的异常端口的?接受到异常消息如何处理?
内核初始化的过程当中,第一个用户态进程launchd 是在bsdinit_task()
函数里启动的,在启动launchd 进程前经过调用host_set_exception_ports()
函数,把全部的Mach 异常消息都定向到端口ux_exception_port
,这个端口由一个内核线程持有,这个内核线程里执行的ux_handle()
函数,这个函数里会在一个死循环里调用mach_msg_receive()
来接受ux_exception_port
端口上的消息,并且mach_msg_receive()
会阻塞线程。
ux_handle()
函数里接受到Mach消息后,会调用mach_exc_server()
,而mach_exc_server
会调用下面的handlers ,具体调用哪一个由参数 exception_behavior_t behavior
决定,该参数是设置异常端口时调用host_set_exception_ports()
传入的
catch_mach_exception_raise() 对应 EXCEPTION_DEFAULT 1 ,表示 xx
catch_mach_exception_raise_state() 对应 define EXCEPTION_STATE 2 ,表示
catch_mach_exception_raise_state_identity() 对应 define EXCEPTION_STATE_IDENTITY 3,表示
复制代码
catch_mach_exception_raise()
这些handle 会调用 ux_exception()
将Mach异常转换成Unix信号,好比 EXC_BAD_ACCESS
将会转换成 SIGSEGV
或 SIGBUS
,如代码所示
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)
{
switch(exception) {
case EXC_BAD_ACCESS:
if (code == KERN_INVALID_ADDRESS)
*ux_signal = SIGSEGV;
else
*ux_signal = SIGBUS;
break;
....
}
....
}
复制代码
在 catch_mach_exception_raise()
里拿到Mach异常对应的Unix信号后会再调用 threadsignal()
投递Unix信号,在threadsignal
的实现里经过几层函数调用,最后会调用到act_set_astbsd()
,在该函数里设置了AST(异步软件中断)信号
void
act_set_astbsd(
thread_t thread)
{
act_set_ast( thread, AST_BSD );
}
复制代码
AST 是人工引起的非硬件触发的陷阱,AST 是内核操做的关键部分,并且是调度事件的底层机制,也是BSD信号(Unix信号)投递的实现基础。当系统从一个陷阱返回时(return_from_trap
),系统不会当即返回用户态,而是要检查线程的ast字段以判断是否存在AST 须要处理。如代码所示,此时AST的标志位是 AST_BSD
,此标志位对应的handler 是bsd_ast()
函数。这时若是在exception_triage()
下了断点,断点将会被断住,此时能够经过lldb在终端输出 函数调用栈、进程PID、线程状态
(lldb) bt
* thread #1, stop reason = breakpoint 1.17
* frame #0: 0xffffff800fe75fc9 kernel.development`proc_prepareexit [inlined] exception_triage(exception=10, code=0x000000000b100001, codeCnt=2) at exception.c:504 [opt]
frame #1: 0xffffff800fe75fbc kernel.development`proc_prepareexit [inlined] task_exception_notify(exception=10, exccode=185597953, excsubcode=4369) at exception.c:547 [opt]
frame #2: 0xffffff800fe75f96 kernel.development`proc_prepareexit(p=0xffffff8018d90b60, rv=<unavailable>, perf_notify=1) at kern_exit.c:889 [opt]
frame #3: 0xffffff800fe75d86 kernel.development`exit_with_reason(p=0xffffff8018d90b60, rv=11, retval=<unavailable>, thread_can_terminate=1, perf_notify=1, jetsam_flags=<unavailable>, exit_reason=<unavailable>) at kern_exit.c:830 [opt]
frame #4: 0xffffff800fe90675 kernel.development`postsig_locked(signum=11) at kern_sig.c:3140 [opt]
frame #5: 0xffffff800fe90b07 kernel.development`bsd_ast(thread=<unavailable>) at kern_sig.c:3420 [opt]
frame #6: 0xffffff800f973e44 kernel.development`ast_taken_user at ast.c:207 [opt]
frame #7: 0xffffff800f9211bc kernel.development`return_from_trap + 172
(lldb) e struct proc *$proc_1 = (struct proc *)thread->task->bsd_info
(lldb) po $proc_1->p_pid
478
(lldb) po thread->state
4
复制代码
能够看到bsd_ast()
将会调用postsig_locked()
数,从/bsd/kern/kern_sig.c
postsig_locked()
的实现可知,若是当前进程没有设置 sigaction 捕获Unix信号的话,默认处理是调用 exit_with_reason()
,exit_with_reason()
间接调用task_exception_notify()
,task_exception_notify()
的做用是通知launchd 去启动ReportCrash 生成CrashLog,通知的方式也是经过Mach消息传递机制,因此断点会在exception_triage()
断住。
launchd 在初始化的过程当中设置了异常端口,而且将 MachExceptionHandler 设置为/System/Library/CoreServices/ReportCrash (iOS中的路径)
,ReportCrash将会生成Crash Log。前面说了 exception_triage
调用 exception_triage_thread()
投递异常,而exception_triage_thread()
函数里执行异常投递的函数是exception_deliver()
,查看上面log中的frame #0
能够看到函数入参exception=10 (EXC_CRASH)
,这是断点第二次在这断住,第一次断住是CPU异常转成Mach异常的时候,当时的exception=1 (EXC_BAD_ACCESS)
,exception_deliver()
函数将会利用入参 exception 从异常数组中取出具体的异常端口,因此第一次投递异常(CPU异常转Mach异常)和第二次投递异常给ReportCrash不会冲突。
此时再断点放掉,在 exception_triage_thread()
处将会再出现一次断点
(lldb) bt
* thread #1, stop reason = breakpoint 2.1
* frame #0: 0xffffff800f97ef47 kernel.development`exception_triage_thread(exception=13, code=0xffffff806fce3e40, codeCnt=2, thread=0xffffff801cded250) at exception.c:445 [opt]
frame #1: 0xffffff800f9acffe kernel.development`task_deliver_crash_notification(task=0xffffff801d9af000, thread=0xffffff801cded250, etype=<unavailable>, subcode=<unavailable>) at task.c:1798 [opt]
frame #2: 0xffffff800f9b6537 kernel.development`thread_terminate_self at thread.c:594 [opt]
frame #3: 0xffffff800f9bab30 kernel.development`thread_apc_ast(thread=0xffffff801cded250) at thread_act.c:934 [opt]
frame #4: 0xffffff800f973e6b kernel.development`ast_taken_user at ast.c:220 [opt]
frame #5: 0xffffff800f9211bc kernel.development`return_from_trap + 172
复制代码
能够从函数调用栈看出,这也是设置AST 致使的,此时的exception=13(EXC_CORPSE_NOTIFY)
,表示进程状态是僵尸状态,也就至关于死了。
经过打断点能够看出一个用户态应用程序非法访问内存致使的CPU异常,将会依次用到 EXC_BAD_ACCESS、EXC_CRASH、EXC_CORPSE_NOTIFY
这三个Mach异常类型。
CPU异常 -> Mach异常 -> BSD层的Unix信号 -> 用户态App Handler / 系统生成Crash Log 的流程能够简单粗略地画一个图
虽然iOS \ macOS 都提供了 ReportCrash用来收集Crash 信息,Debug模式下也提供了 lldb 的debugserver 捕获程序异常,但App 发版上架后出现Crash 不方便开发者收集,好比在iOS上须要用户容许与开发者共享分析数据,开发者才能够从 iTunes Connect 查看到Crash 上报信息,否则则要拿到发生Crash的设备才能查看到Crash信息。
为了方便快速定位、解决Crash,能够借鉴 ReportCrash 或 debugserver 捕获异常的思路来作一个三方的Crash 收集的框架,收集思路主要有三种:
Mach 虽然很是底层,但也提供了API给用户态应用程序使用,捕获Mach异常可使用如下几个API
// 这里有两个须要注意的点:
由于异常收集已经有成熟的三方框架了,KSCrash、PLCrashReport 等,后面参考开源框架,再结合个人RDA来搞点事情,有足够多的实践经验了再来这继续分享