iOS Mach异常和signal信号

摘要:本着探究下iOSCrash捕获的目的,学习了下Crash捕获相关的Mach异常和signal信号处理,记录下相关内容,并提供对应的测试示例代码。Mach为XNU的微内核,Mach异常为最底层的内核级异常,在iOS系统中,底层Crash先触发Mach异常,然后再转换为对应的signal信号。

作者:阿里云-移动云-大前端团队

原文链接:http://click.aliyun.com/m/43672/

本着探究下iOSCrash捕获的目的,学习了下Crash捕获相关的Mach异常和signal信号处理,记录下相关内容,并提供对应的测试示例代码。Mach为XNU的微内核,Mach异常为最底层的内核级异常,在iOS系统中,底层Crash先触发Mach异常,然后再转换为对应的signal信号。

1.iOSMach异常

1.1XNU

Darwin是MacOS和iOS的操作系统,而XNU是Darwin操作系统的内核部分。XNU是混合内核,兼具宏内核和微内核的特性,而Mach即为其微内核。

Darwin操作系统和MacOS、iOS系统版本号的对应如上图所示,Mac可执行下述命令查看Darwin版本号。

system_profilerSPSoftwareDataType

1.2Mach

Mach:[mʌk],操作系统微内核,是许多新操作系统的设计基础。

Mach微内核中有几个基础概念:

Tasks,拥有一组系统资源的对象,允许"thread"在其中执行。

Threads,执行的基本单位,拥有task的上下文,并共享其资源。

Ports,task之间通讯的一组受保护的消息队列;task可对任何port发送/接收数据。

Message,有类型的数据对象集合,只可以发送到port。

1.3模拟MachMessage发送

●Mach提供少量API,苹果文档介绍较少。

//内核中创建一个消息队列,获取对应的port

mach_port_allocate();

//授予task对port的指定权限

mach_port_insert_right();

//通过设定参数:MACH_RSV_MSG/MACH_SEND_MSG用于接收/发送machmessage

mach_msg();

下述代码模拟向MachPort发送Message,接收Message后做处理:

●首先调用createPortAndAddListener创建MachPort;

●调用sendMachPortMessage:向已创建的MachPort发送消息;

●执行结果示例:

2018-02-2709:33:37.797435+0800xxx[54456:5198921]createaport:41731

2018-02-2709:33:37.797697+0800xxx[54456:5198921]Sendamachmessage:[100].

2018-02-2709:33:37.797870+0800xxx[54456:5199525]Receiveamachmessage:[100],remote_port:0,local_port:41731,exceptioncode:28672

●示例代码:

//创建MachPort并监听消息

+(mach_port_t)createPortAndAddListener{

mach_port_tserver_port;

kern_return_tkr=mach_port_allocate(mach_task_self(),MACH_PORT_RIGHT_RECEIVE,&server_port);

assert(kr==KERN_SUCCESS);

NSLog(@"createaport:%d",server_port);

kr=mach_port_insert_right(mach_task_self(),server_port,server_port,MACH_MSG_TYPE_MAKE_SEND);

assert(kr==KERN_SUCCESS);

[selfsetMachPortListener:server_port];

returnserver_port;

}

+(void)setMachPortListener:(mach_port_t)mach_port{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{

mach_messagemach_message;

mach_message.Head.msgh_size=1024;

mach_message.Head.msgh_local_port=server_port;

mach_msg_return_tmr;

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_tmsg_id=mach_message.Head.msgh_id;

mach_port_tremote_port=mach_message.Head.msgh_remote_port;

mach_port_tlocal_port=mach_message.Head.msgh_local_port;

NSLog(@"Receiveamachmessage:[%d],remote_port:%d,local_port:%d,exceptioncode:%d",

msg_id,

remote_port,

local_port,

mach_message.exception);

abort();

}

});

}

//向指定MachPort发送消息

+(void)sendMachPortMessage:(mach_port_t)mach_port{

kern_return_tkr;

mach_msg_header_theader;

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(@"Sendamachmessage:[%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-2709:52:11.483076+0800xxx[55018:5253531]createaport:23299

2018-02-2709:52:14.484272+0800xxx[55018:5253531]**********Makea[BADMEMACCESS]now.**********

2018-02-2709:52:14.484477+0800xxx[55018:5253611]Receiveamachmessage:[2405],remote_port:23555,local_port:23299,exceptioncode:1

●示例代码:

+(void)createAndSetExceptionPort{

mach_port_tserver_port;

kern_return_tkr=mach_port_allocate(mach_task_self(),MACH_PORT_RIGHT_RECEIVE,&server_port);

assert(kr==KERN_SUCCESS);

NSLog(@"createaport:%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);

[selfsetMachPortListener:server_port];

}

//构造BADMEMACCESSCrash

-(void)makeCrash{

NSLog(@"**********Makea[BADMEMACCESS]now.**********");

*((int*)(0x1234))=122;

}

1.5Runloop

MachPort的应用不止于内核级别,在CocoaFoundation和CoreFoundation层同样有其应用,比如说:Runloop。

Runloopsources分两类:

1.Inputsources

Port-Basedsources

CustomInputsources

2.Timersources

其中Port-Basedsources即基于MachPort,在Runloop中完成消息传递。

上述的MachAPI为内核层透出接口,CocoaFoundation和CoreFoundation层分别封装了MachPort的接口供调用,参考:Apple-RunloopProgrammingGuard,有详细的示例代码。

2.signal信号

signal是一种软中断信号,提供异步事件处理机制。signal是进程间相互传递信息的一种粗糙方法,使用场景:

进程终止相关;

终端交互;

编程错误或硬件错误相关,系统遇到不可恢复的错误时触发崩溃机制让程序退出,比如:除0、内存写入错误等。

这里我们主要考虑系统遇到不可恢复的错误时即Crash时,信号相关的应用。signal信号处理是UNIX操作系统机制,所以Android平台理论上也是使用的,可以基于signal来捕获AndroidNativeCrash。

2.1signal注册和处理

signal()

#import<sys/signal.h>;

注册signalhandler;

调用成功时,会移除signo信号当前的操作,以handler指定的新信号处理程序替代;

信号处理函数返回void,因为没有地方给该函数返回。注册自定义信号处理函数,构造Crash后,发出信号并执行自定义信号处理逻辑。

【附】:XcodeDebug运行时,添加断点,在Crash触发前,执行prohand-ptrue-sfalseSIGABRT命令。

(lldb)prohand-ptrue-sfalseSIGABRT

NAMEPASSSTOPNOTIFY

===========================

SIGABRTtruefalsetrue

2018-02-2712:57:25.284651+0800xxx[58061:5651844]**********Makea'NSRangeException'now.**********

2018-02-2712:57:25.294945+0800xxx[58061:5651844]***Terminatingappduetouncaughtexception'NSRangeException',reason:'***-[__NSSingleObjectArrayIobjectAtIndex:]:index1beyondbounds[0..0]'

2018-02-2712:57:25.888332+0800xxx[58061:5651844][signalhandler]-handlesignal:6

●示例代码:

//设置自定义信号处理函数

+(void)setSignalHandler{

signal(SIGABRT,test_signal_handler);

}

staticvoidtest_signal_handler(intsigno){

NSLog(@"[signalhandler]-handlesignal:%d",signo);

}

//构造NSRangeException异常,触发SIGABRT信号发送

-(void)makeCrash{

NSLog(@"**********Makea'NSRangeException'now.**********");

NSArray*array=@[@"aaa"];

}

2.2LLDBDebugger

XcodeDebug模式运行App时,App进程signal被LLDBDebugger调试器捕获;需要使用LLDB调试命令,将指定signal处理抛到用户层处理,方便调试。

●查看全部信号传递配置:

//processhandle缩写

prohand

●修改指定信号传递配置:

//option:

//-P:PASS

//-S:STOP

//-N:NOTIFY

prohand-optionfalse信号名

//例:SIGABRT信号处理在LLDB不停止,可继续抛到用户层

prohand-sfalseSIGABRT

2.3可重入

向内核发送信号时,进程可能执行到代码的任意位置,例:进程在执行重要操作,中断后可能产生不一致状态,或进程正在处理另一信号。因此要确保信号处理程序只执行可重入操作:

●写中断处理程序时,假定中断进程可能处于不可重入函数中。

●慎重修改全局数据。

2.4高级信号处理

signal()函数非常基础,只提供了最低限度的信号管理的标准。而sigaction()系统调用,提供更强大的信号管理能力。当信号处理程序运行时,可以用来阻塞特定信号的接收,也可以用来获取信号发送时各种操作系统和进程状态的信息。

●示例代码:

//设置自定义信号处理函数

+(void)setSignalHandlerInAdvance{

structsigactionact;

//当sa_flags设为SA_SIGINFO时,设定sa_sigaction来指定信号处理函数

act.sa_flags=SA_SIGINFO;

act.sa_sigaction=test_signal_action_handler;

sigaction(SIGABRT,&act,NULL);

}

staticvoidtest_signal_action_handler(intsigno,siginfo_t*si,void*ucontext){

NSLog(@"[sigactionhandler]-handlesignal:%d",signo);

//handlesiginfo_t

NSLog(@"siginfo:{\nsi_signo:%d,\nsi_errno:%d,\nsi_code:%d,\nsi_pid:%d,\nsi_uid:%d,\nsi_status:%d,\nsi_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-UnderstandingandAnalyzingApplicationCrashReports

●Apple-RunloopProgrammingGuard

●Wiki-Mach

●Apple-MachOverview

●漫谈iOSCrash收集框架

●TheLLDBDebugger

识别以下二维码,阅读更多干货

相关推荐