崩溃时,你还可以做什么

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

知识背景

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

  • 异常触发:EXC_BAD_ACCESS,通过task_exception_notify 触发异常处理程序。

  • 接着是Exception 一系列调用。

    • Mach异常处理程序exception_triage(),负责将异常转换成Mach 消息
    • exception_triage()通过调用exception_deliver()尝试把异常投递到thread、task最后是host。
    • 首先尝试将异常抛给thread端口,然后尝试抛给task端口,最后再抛给host端口(默认端口)。
    • exception_deliver 通过调用mach_exception_raise ,触发异常。
  • ux_exception 捕抓到异常信号,把异常转为Unix signal ,并投递到出错线程。

那么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 :

  • catch_mach_exception_raise()
  • catch_mach_exception_raise_state()
  • catch_mach_exception_raise_state_identity()

具体调用哪一个由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步:

  • 设置处理崩溃信号的handler
  • 触发崩溃
  • 修改崩溃时的线程上下文,即:ucontext

让程序可以继续跑下去,可以设置当前线程的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容