iOS进阶之路iOS技能iOS精品文章-性能优化(APM)

崩溃时,你还可以做什么

2017-08-09  本文已影响851人  vedon_fu

程序崩溃的时候还可以做哪些操作,以前都是看别人博客多。发现理解还是不够深入,因此记录学习过程。写得不对的,请不吝赐教😁

知识背景

源码版本:xnu-3789.51.2
下面是针对源码整理出来的一份时序图:

那么ux_exception 是如何捕抓到信号的呢?查看一下ux_exception 的源码。整理如下:

第一个BSD进程调用bsdinit_task()函数启动时,这函数还调用了ux_handler_init()函数设置了一个Mach内核线程跑ux_handler。而ux_handler 设置了一个Message loop用于监听异常。

__attribute__((noreturn))
static void
ux_handler(void)
{
    task_t      self = current_task();
    mach_port_name_t    exc_port_name;
    mach_port_name_t    exc_set_name;
    /* self->kernel_vm_space = TRUE; */
    ux_handler_self = self;
    /*
     *  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);

        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");
    }
}

重点来看这一段:

#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);
        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);
        }
    }

当从port 收到发过来的消息(mach_msg_receive),通过调用mach_exc_server,触发handlers :

具体调用哪一个由behavior决定。

# define EXCEPTION_DEFAULT  1  // Send a catch_exception_raise message including the identity.

# define EXCEPTION_STATE  2 // Send a catch_exception_raise_state message including the thread state.

# define EXCEPTION_STATE_IDENTITY    3 // Send a catch_exception_raise_state_identity message including the thread identity and state.

另外Mach层在BSD层上,当异常发生时,如果Mach没有相应的处理程序,那么就会转到BSD层处理,也就是上面的流程。那么Mach层的处理是怎样的?

Mach Port

使用Xcode进行debug,其中主要的模块是lldb的debugserver。那么它是怎样获取到程序的exceptions 的呢?进程间的通讯,在这里用的是:Mach Port。通过Mach Port,debugserver可以截获程序的异常。

以下是debugserver的开源代码,地址在这里

截图高亮的位置就是关键的地方。里面的注释写得很清楚,就不多说了。通过m_exception_port ,debugserver可以截获程序的异常消息。

为程序添加接收exception的port 步骤简化如下:

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);

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(task, EXC_MASK_BAD_ACCESS, server_port, EXCEPTION_DEFAULT|MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

函数说明,建议看一下,说得很清楚。

如果把task的负责处理EXC_MASK_BAD_ACCESS的port 关闭,是不是就接收不了异常了???

答案:是的

重点来了,崩溃来了,还可以做什么?

重要信息保存
崩溃前信息做最后的保存工作,这里说得很清楚。信息保存应该根据各自需要而定,重点看一下如何把程序强制救回。

强制救回

这里首先要了解ucontext_t 是什么?简单来说,它是线程运行的一个上下文。具体如下图所示:

通过它可以知道崩溃时线程的上下文。把程序救回来关键一步在:如何调整当前崩溃线程的上下文!包括修改当前寄存器的值等都是可以的。

下面的Demo很好的展示了这一过程(必须在真机上运行),其主要过程分3步:

让程序可以继续跑下去,可以设置当前线程的pc。处理了崩溃的信号后,程序可以继续执行,但是有一点要注意,崩溃处理函数不能做太多事情,因为系统认为这个signal还在处理状态,此时如果再触发崩溃,就再也进不来了,程序会被kill掉。

ucontext->uc_mcontext->__ss.__pc = 崩溃处理函数地址。

demo 代码是把lr寄存器的值,直接赋给pc。程序直接执行下一条指令,需要注意的是,这个时候程序的状态是不稳定的,因为部分寄存器的值已经被污染了,可能随时发生崩溃。

* pc 是当前运行的指令地址
* lr 保存的是函数返回后的下一条指令的地址。
ucontext->uc_mcontext->__ss.__pc = ucontext->uc_mcontext->__ss.__lr;
//
//  ViewController.m
//  UContext
//
//  Created by vedon on 04/08/2017.
//  Copyright © 2017 vedon. All rights reserved.
//

#import "ViewController.h"
#include <mach/task.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>

void sig_handler(int sig, siginfo_t *info, void *context)
{
    ucontext_t *ucontext = context;
    NSMutableString *str = [NSMutableString stringWithFormat:@"Signal caught: %d \n",sig];
    
    [str appendString:[NSString stringWithFormat:@"pc 0x%llx\n", ucontext->uc_mcontext->__ss.__pc]];
    [str appendString:[NSString stringWithFormat:@"lr 0x%llx\n", ucontext->uc_mcontext->__ss.__lr]];
    [str appendString:[NSString stringWithFormat:@"fp 0x%llx\n", ucontext->uc_mcontext->__ss.__fp]];
    [str appendString:[NSString stringWithFormat:@"sp 0x%llx\n", ucontext->uc_mcontext->__ss.__sp]];
    [str appendString:[NSString stringWithFormat:@"uc_stack size 0x%lx\n", sizeof(ucontext->uc_stack.ss_size)]];
    [str appendString:[NSString stringWithFormat:@"uc_stack ss_sp 0x%llx\n", (long long)ucontext->uc_stack.ss_sp]];
    
    
    if (ucontext->uc_link != NULL)
    {
        [str appendString:@"uc_link : \n"];
        [str appendString:[NSString stringWithFormat:@"pc 0x%llx\n", ucontext->uc_link->uc_mcontext->__ss.__pc]];
        [str appendString:[NSString stringWithFormat:@"lr 0x%llx\n", ucontext->uc_link->uc_mcontext->__ss.__lr]];
        [str appendString:[NSString stringWithFormat:@"fp 0x%llx\n", ucontext->uc_link->uc_mcontext->__ss.__fp]];
        [str appendString:[NSString stringWithFormat:@"sp 0x%llx\n", ucontext->uc_link->uc_mcontext->__ss.__sp]];
    }
    else
    {
        [str appendString:@"uc_link is null"];
    }
    NSLog(@"%@",str);
    ucontext->uc_mcontext->__ss.__pc = ucontext->uc_mcontext->__ss.__lr;
}


@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //把当前程序的task 接受EXC_BAD_ACCESS的exception port  设置为空
    //这样,debugserver就不会捕抓到异常。当然这只是为了可以在
    //debug环境下也可以单步调试sig_handler。如果没有设置,那就只能通
    //过device log 看sig_handler的输出了。

    int ret = task_set_exception_ports(
                                       mach_task_self(),
                                       EXC_MASK_BAD_ACCESS,
                                       MACH_PORT_NULL,//m_exception_port,
                                       EXCEPTION_DEFAULT,
                                       0);
    if (ret == 0)NSLog(@"Disable lldb to catch exceptions");
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = sig_handler;
    
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGKILL, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    
    [self invokeCrash];
    
    NSLog(@"Resume after crash");
}
- (void)invokeCrash
{
    void *a = calloc(1, sizeof(void *));
    NSLog(@"Crash Addr of a: 0x%llx", (long long)a);
    ((void(*)())a)();
}

@end

exception port 已经关掉了,但是只是在Mach 层不处理异常。例如
EXC_BAD_ACCESS 这种异常最后会转换为BSD signal, lldb 的
debugger 还需要屏蔽处理BSD signal 。

下面屏蔽处理SIGBUS,至此,sig_handler 就可以顺利的在debug的情况下调试了。

(lldb) pro handle SIGBUS -s false

结论

针对某些特殊的崩溃,例如:WebCore内的某些系统bug,可以使用这种极端的修复方法。

😄😄😄😄😄😄
另外,个人觉得通过记录或者是分享,能大大加深对相关知识的理解。

Ref:
Handling unhandled exceptions and signals
Kernel Architecture Overview
Apple Open Source
LLDB ---debugserver

以下与主题无关,仅做记录。

@startuml
actor CPU
CPU -> exception : task_exception_notify
note left
通知的类型可能是:
EXC_CRASH/EXC_GUARD/
EXC_BAD_ACCESS 等等。
其中如果vm_fault调用了
kern_cs的cs_invalid_page,
那么,最终会通过threadsignal 
发出EXC_BAD_ACCESS。
end note
exception -> exception : exception_triage
exception -> exception : exception_triage_thread
exception -> exception : exception_deliver
exception -> exception : mach_exception_raise
...Message Loop...
ux_exception -> ux_exception : catch_mach_exception_raise
note right
catch_mach_exception_raise 会在mach_exc_server调用后触发
end note
ux_exception -> ux_exception : ux_exception
note right
把exception转换为unix signal。
EXC_BAD_ACCESS : SIGSEGV/SIGBUS
EXC_BAD_INSTRUCTION : SIGILL
etc.
end note

ux_exception -> kern_sig : threadsignal
note right 
Send a signal caused by a trap to a specific thread.
end note
@enduml

@startuml
bsd_init -> bsd_init : bsdinit_task
bsd_init -> ux_exception : ux_handler_init
ux_exception -> ux_exception : ux_handler
activate ux_exception
note right
Message handling loop
end note
......
@enduml

上一篇下一篇

猜你喜欢

热点阅读