iOS Mach异常和signal信号

maoerya · · 406 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

摘要: 本着探究下iOS Crash捕获的目的,学习了下Crash捕获相关的Mach异常和signal信号处理,记录下相关内容,并提供对应的测试示例代码。Mach为XNU的微内核,Mach异常为最底层的内核级异常,在iOS系统中,底层Crash先触发Mach异常,然后再转换为对应的signal信号。 作者:阿里云-移动云-大前端团队 **原文链接:http://click.aliyun.com/m/43672/** 本着探究下iOS Crash捕获的目的,学习了下Crash捕获相关的Mach异常和signal信号处理,记录下相关内容,并提供对应的测试示例代码。Mach为XNU的微内核,Mach异常为最底层的内核级异常,在iOS系统中,底层Crash先触发Mach异常,然后再转换为对应的signal信号。 **1. iOS Mach异常** **1.1 XNU** Darwin是Mac OS和iOS的操作系统,而XNU是Darwin操作系统的内核部分。XNU是混合内核,兼具宏内核和微内核的特性,而Mach即为其微内核。 ![图片描述](http://img.blog.csdn.net/20180313150110241?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVucWlpbnNpZ2h0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) Darwin操作系统和MacOS、iOS系统版本号的对应如上图所示,Mac可执行下述命令查看Darwin版本号。 ``` system_profiler SPSoftwareDataType ``` **1.2 Mach** Mach:[mʌk],操作系统微内核,是许多新操作系统的设计基础。 Mach微内核中有几个基础概念: Tasks,拥有一组系统资源的对象,允许"thread"在其中执行。 Threads,执行的基本单位,拥有task的上下文,并共享其资源。 Ports,task之间通讯的一组受保护的消息队列;task可对任何port发送/接收数据。 Message,有类型的数据对象集合,只可以发送到port。 **1.3 模拟Mach Message发送** Mach提供少量API,苹果文档介绍较少。 ``` // 内核中创建一个消息队列,获取对应的port mach_port_allocate(); // 授予task对port的指定权限 mach_port_insert_right(); // 通过设定参数:MACH_RSV_MSG/MACH_SEND_MSG用于接收/发送mach message mach_msg(); ``` 下述代码模拟向`Mach Port发送Message`,接收Message后做处理: 首先调用`createPortAndAddListener`创建Mach Port; 调用sendMachPortMessage:向已创建的Mach Port发送消息; 执行结果示例: ``` 2018-02-27 09:33:37.797435+0800 xxx[54456:5198921] create a port: 41731 2018-02-27 09:33:37.797697+0800 xxx[54456:5198921] Send a mach message: [100]. 2018-02-27 09:33:37.797870+0800 xxx[54456:5199525] Receive a mach message:[100], remote_port: 0, local_port: 41731, exception code: 28672 ``` 示例代码: ``` // 创建Mach Port并监听消息 + (mach_port_t)createPortAndAddListener { mach_port_t server_port; kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port); assert(kr == KERN_SUCCESS); NSLog(@"create a port: %d", server_port); kr = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND); assert(kr == KERN_SUCCESS); [self setMachPortListener:server_port]; return server_port; } + (void)setMachPortListener:(mach_port_t)mach_port { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ mach_message mach_message; mach_message.Head.msgh_size = 1024; mach_message.Head.msgh_local_port = server_port; mach_msg_return_t mr; while (true) { mr = mach_msg(&mach_message.Head, MACH_RCV_MSG | MACH_RCV_LARGE, 0, mach_message.Head.msgh_size, mach_message.Head.msgh_local_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (mr != MACH_MSG_SUCCESS && mr != MACH_RCV_TOO_LARGE) { NSLog(@"error!"); } mach_msg_id_t msg_id = mach_message.Head.msgh_id; mach_port_t remote_port = mach_message.Head.msgh_remote_port; mach_port_t local_port = mach_message.Head.msgh_local_port; NSLog(@"Receive a mach message:[%d], remote_port: %d, local_port: %d, exception code: %d", msg_id, remote_port, local_port, mach_message.exception); abort(); } }); } // 向指定Mach Port发送消息 + (void)sendMachPortMessage:(mach_port_t)mach_port { kern_return_t kr; mach_msg_header_t header; header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); header.msgh_size = sizeof(mach_msg_header_t); header.msgh_remote_port = mach_port; header.msgh_local_port = MACH_PORT_NULL; header.msgh_id = 100; NSLog(@"Send a mach message: [%d].", header.msgh_id); kr = mach_msg(&header, MACH_SEND_MSG, header.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); } ``` **1.4 捕获Mach异常** `task_set_exception_ports()`,设置内核接收Mach异常消息的Port,替换为自定义的Port后,即可捕获程序执行过程中产生的异常消息。 执行结果示例: ``` 2018-02-27 09:52:11.483076+0800 xxx[55018:5253531] create a port: 23299 2018-02-27 09:52:14.484272+0800 xxx[55018:5253531] ********** Make a [BAD MEM ACCESS] now. ********** 2018-02-27 09:52:14.484477+0800 xxx[55018:5253611] Receive a mach message:[2405], remote_port: 23555, local_port: 23299, exception code: 1 ``` 示例代码: ``` + (void)createAndSetExceptionPort { mach_port_t server_port; kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &server_port); assert(kr == KERN_SUCCESS); NSLog(@"create a port: %d", server_port); kr = mach_port_insert_right(mach_task_self(), server_port, server_port, MACH_MSG_TYPE_MAKE_SEND); assert(kr == KERN_SUCCESS); kr = task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS | EXC_MASK_CRASH, server_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE); [self setMachPortListener:server_port]; } // 构造BAD MEM ACCESS Crash - (void)makeCrash { NSLog(@"********** Make a [BAD MEM ACCESS] now. **********"); *((int *)(0x1234)) = 122; } ``` **1.5 Runloop** Mach Port的应用不止于内核级别,在Cocoa Foundation和Core Foundation层同样有其应用,比如说:Runloop。 ![图片描述](http://img.blog.csdn.net/20180313150712349?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVucWlpbnNpZ2h0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) Runloop sources分两类: Input sources Port-Based sources Custom Input sources Timer sources 其中Port-Based sources即基于Mach Port,在Runloop中完成消息传递。 上述的Mach API为内核层透出接口,Cocoa Foundation和Core Foundation层分别封装了Mach Port的接口供调用,参考:[Apple - Runloop Programming Guard](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html?spm=a2c4e.11153959.blogcont499180.13.7f4034eaae8GID#//apple_ref/doc/uid/10000057i-CH16-131281),有详细的示例代码。 **2. signal信号** signal是一种软中断信号,提供异步事件处理机制。signal是进程间相互传递信息的一种粗糙方法,使用场景: 进程终止相关; 终端交互; 编程错误或硬件错误相关,系统遇到不可恢复的错误时触发崩溃机制让程序退出,比如:除0、内存写入错误等。 这里我们主要考虑系统遇到不可恢复的错误时即Crash时,信号相关的应用。signal信号处理是UNIX操作系统机制,所以Android平台理论上也是使用的,可以基于signal来捕获Android Native Crash。 **2.1 signal注册和处理** ``` signal() #import <sys/signal.h>; ``` 注册signal handler; 调用成功时,会移除signo信号当前的操作,以handler指定的新信号处理程序替代; 信号处理函数返回void,因为没有地方给该函数返回。 注册自定义信号处理函数,构造Crash后,发出信号并执行自定义信号处理逻辑。 【附】:Xcode Debug运行时,添加断点,在Crash触发前,执行pro hand -p true -s false SIGABRT命令。 ``` (lldb) pro hand -p true -s false SIGABRT NAME PASS STOP NOTIFY =========== ===== ===== ====== SIGABRT true false true 2018-02-27 12:57:25.284651+0800 xxx[58061:5651844] ********** Make a 'NSRangeException' now. ********** 2018-02-27 12:57:25.294945+0800 xxx[58061:5651844] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]' 2018-02-27 12:57:25.888332+0800 xxx[58061:5651844] [signal handler] - handle signal: 6 ``` 示例代码: ``` // 设置自定义信号处理函数 + (void)setSignalHandler { signal(SIGABRT, test_signal_handler); } static void test_signal_handler(int signo) { NSLog(@"[signal handler] - handle signal: %d", signo); } // 构造NSRangeException异常,触发SIGABRT信号发送 - (void)makeCrash { NSLog(@"********** Make a 'NSRangeException' now. **********"); NSArray *array = @[ @"aaa" ]; } ``` **2.2 LLDB Debugger** Xcode Debug模式运行App时,App进程signal被LLDB Debugger调试器捕获;需要使用LLDB调试命令,将指定signal处理抛到用户层处理,方便调试。 查看全部信号传递配置: ``` // process handle缩写 pro hand ``` 修改指定信号传递配置: ``` // option: // -P: PASS // -S: STOP // -N: NOTIFY pro hand -option false 信号名 // 例:SIGABRT信号处理在LLDB不停止,可继续抛到用户层 pro hand -s false SIGABRT ``` **2.3 可重入** 向内核发送信号时,进程可能执行到代码的任意位置,例:进程在执行重要操作,中断后可能产生不一致状态,或进程正在处理另一信号。因此要确保信号处理程序只执行可重入操作: 写中断处理程序时,假定中断进程可能处于不可重入函数中。 慎重修改全局数据。 **2.4 高级信号处理** signal()函数非常基础,只提供了最低限度的信号管理的标准。而sigaction()系统调用,提供更强大的信号管理能力。当信号处理程序运行时,可以用来阻塞特定信号的接收,也可以用来获取信号发送时各种操作系统和进程状态的信息。 示例代码: ``` // 设置自定义信号处理函数 + (void)setSignalHandlerInAdvance { struct sigaction act; // 当sa_flags设为SA_SIGINFO时,设定sa_sigaction来指定信号处理函数 act.sa_flags = SA_SIGINFO; act.sa_sigaction = test_signal_action_handler; sigaction(SIGABRT, &act, NULL); } static void test_signal_action_handler(int signo, siginfo_t *si, void *ucontext) { NSLog(@"[sigaction handler] - handle signal: %d", signo); // handle siginfo_t NSLog(@"siginfo: {\n si_signo: %d,\n si_errno: %d,\n si_code: %d,\n si_pid: %d,\n si_uid: %d,\n si_status: %d,\n si_value: %d\n }", si->si_signo, si->si_errno, si->si_code, si->si_pid, si->si_uid, si->si_status, si->si_value.sival_int); } ``` **3. 参考** [Apple - Understanding and Analyzing Application Crash Reports](https://developer.apple.com/library/content/technotes/tn2151/_index.html?spm=a2c4e.11153959.blogcont499180.14.7f4034eaPaEbHW) [Apple - Runloop Programming Guard](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html?spm=a2c4e.11153959.blogcont499180.15.7f4034ea2VxQ1J#//apple_ref/doc/uid/10000057i-CH16-131281) [Apple - Mach Overview](https://developer.apple.com/library/content/documentation/Darwin/Conceptual/KernelProgramming/Mach/Mach.html?spm=a2c4e.11153959.blogcont499180.17.7f4034eaKnjmxB#//apple_ref/doc/uid/TP30000905-CH209-TPXREF102) [漫谈iOS Crash收集框架](https://nianxi.net/ios/ios-crash-reporter.html?spm=a2c4e.11153959.blogcont499180.18.7f4034ears2oyi) [The LLDB Debugger](https://lldb.llvm.org/tutorial.html?spm=a2c4e.11153959.blogcont499180.19.7f4034eajSbC50) **识别以下二维码,阅读更多干货** ![图片描述](http://img.blog.csdn.net/20180313151950871?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVucWlpbnNpZ2h0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

406 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传