Android Native Crash 处理过程

我们经常可以看到log中输出这样的信息

 *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***                                                             
 Build fingerprint: 'Xiaomi/pisces/pisces:4.4.4/KTU84P/5.8.6:user/release-keys'
 Revision: '0'
 pid: 29088, tid: 29092, name: GC  >>> com.android.browser <<<
 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 00000000
     r0 00000000  r1 00000000  r2 0000bfc0  r3 00000000
     r4 415d30b0  r5 00000040  r6 00001000  r7 00000000
     r8 4008f028  r9 00001000  sl 54fc199f  fp 0000089b
     ip 00000000  sp 71838c8c  lr 41590c7b  pc 400b758c  cpsr 200a0010
     d0  0000000000004063  d1  0000000000000000
     d2  000000000000006e  d3  0000000000000001
    ... ...
    ... ...


 backtrace:
     #00  pc 0002258c  /system/lib/libc.so (memset+48)
     #01  pc 00078c77  /system/lib/libdvm.so (dvmJitResetTable+66)
     #02  pc 00072f9f  /system/lib/libdvm.so
     #03  pc 00073695  /system/lib/libdvm.so (dvmCompilerPerformSafePointChecks()+16)
     #04  pc 00028bb8  /system/lib/libdvm.so
     #05  pc 0007212d  /system/lib/libdvm.so
     #06  pc 00054175  /system/lib/libdvm.so
     #07  pc 0000d1d0  /system/lib/libc.so (__thread_entry+72)
     #08  pc 0000d368  /system/lib/libc.so (pthread_create+240)

    ... ...

这说明有程序发生了native crash,进程已经挂了,肯定是没办法自己打印出错误信息的,因此,就需要其他进程,在Android中就是debuggerd,来帮助崩溃进程来打印错误信息。

debugged作为系统的一个常驻service,会始终监听一个端口来处理请求,每次程序崩溃,都会用ptrace获取程序运行堆栈等信息,并打印出来。

下面,我们就来分析一下这个过程。

在进程一开始启动的时候,都会进入debuggerd_init函数。在这里,做的最重要的操作就是设置信号处理函数。

//* @bionic/linker/debugger.cpp
//* debuggerd_init()

    struct sigaction action;
    memset(&action, 0, sizeof(action));
    sigemptyset(&action.sa_mask);
    action.sa_sigaction = debuggerd_signal_handler;
    action.sa_flags = SA_RESTART | SA_SIGINFO;
    action.sa_flags |= SA_ONSTACK;

    sigaction(SIGABRT, &action, NULL);
    sigaction(SIGBUS, &action, NULL);
    sigaction(SIGFPE, &action, NULL);
    sigaction(SIGILL, &action, NULL);
    sigaction(SIGPIPE, &action, NULL);
    sigaction(SIGSEGV, &action, NULL);
#if defined(SIGSTKFLT)
    sigaction(SIGSTKFLT, &action, NULL);
#endif
    sigaction(SIGTRAP, &action, NULL);
}

可以看到,上述几个信号的信号处理函数会被设置为debuggerd_signal_handler。
现在假设我们的程序因为空指针异常崩溃了,则此时会产生SIGSEGV,内核就会帮我们回调debuggerd_signal_handler,同时将SIGSEGV的编号n以及信号的信息作为参数传入。要注意的是,在发生异常后,调用debuggerd_signal_handler时,仍是处于该崩溃的线程中。此时如果在信号处理函数debuggerd_signal_handler中gettid,得到的也是这个崩溃进程是tid。

在函数debuggerd_signal_handler中,做了这么一些事

//* @bionic/linker/debugger.cpp
//* debuggerd_signal_handler(int n, siginfo_t* info, void*)
    ... ...
    int s = socket_abstract_client(DEBUGGER_SOCKET_NAME, SOCK_STREAM);

先是调用socket_abstract_client发送端口请求。

//* @bionic/linker/debugger.cpp
//* socket_abstract_client(const char* name, int type)
    ... ...
    int err = TEMP_FAILURE_RETRY(connect(s, (sockaddr*) &addr, alen));
    ... ...
}

可以看到,在socket_abstract_client中用connect函数请求连接socket服务端。这里传入的参数addr,其初始化过程为

    sockaddr_un addr;
    ... ...
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_LOCAL;
    addr.sun_path[0] = 0;
    memcpy(addr.sun_path + 1, name, namelen);

这样就会请求连接名为DEBUGGER_SOCKET_NAME的端口。
而DEBUGGER_SOCKET_NAME端口在哪里接收呢?看名字就知道是debuggerd进程了。

那么,我们就来看一下degbuggerd的启动以及DEBUGGER_SOCKET_NAME 这个socket的建立过程。

debuggerd作为一个由init进程启动的service,是在init.rc中配置好了的。

service debuggerd /system/bin/debuggerd                                                                                      
    class main

init进程在启动时会直接执行/system/bin/debuggerd。由此便来到debuggerd的main函数部分。这里执行debuggerd是不带参数的,因此argc=1,即进入了do_server函数。

//* @system/core/debuggerd/debuggerd.c
int main(int argc, char** argv) {
    if (argc == 1) {
        return do_server();
    }
    ... ...
}

记得开头的时候所有进程的信号处理函数都被设置为了debuggerd_signal_handler,这样所有崩溃都会由debuggerd处理。然而debuggerd如果自己崩溃了,是没办法自己来处理自己的崩溃请求的。
因此do_server中做的事第一件事,是把信号处理重新设置为默认方式,这样debuggerd自己挂了后就直接由内核帮他收拾了。

//* @system/core/debuggerd/debuggerd.c
//  do_server()
    ... ...
    /*
     * debuggerd crashes can't be reported to debuggerd.
     * Reset all of the crash handlers.
     */
    signal(SIGILL, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
#ifdef SIGSTKFLT
    signal(SIGSTKFLT, SIG_DFL);
#endif

    // Ignore failed writes to closed sockets
    signal(SIGPIPE, SIG_IGN);
    ... ...
    act.sa_handler = SIG_DFL;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,SIGCHLD);
    act.sa_flags = SA_NOCLDWAIT;
    sigaction(SIGCHLD, &act, 0);

debuggerd创建一个服务端的名为DEBUGGER_SOCKET_NAME的端口,接着便进入了主循环中。在主循环中,调用accept函数不断等待来自客户端的连接。一旦收到连接请求,便调用handle_request处理请求。

//* @system/core/debuggerd/debuggerd.c
//  do_server()

    s = socket_local_server(DEBUGGER_SOCKET_NAME,
            ANDROID_SOCKET_NAMESPACE_ABSTRACT, SOCK_STREAM);
    if(s < 0) return 1;
    fcntl(s, F_SETFD, FD_CLOEXEC);

    LOG("debuggerd: " __DATE__ " " __TIME__ "\n");

    for(;;) {
        struct sockaddr addr;
        socklen_t alen;
        int fd;

        alen = sizeof(addr);
        XLOG("waiting for connection\n");
        fd = accept(s, &addr, &alen);
        if(fd < 0) {
            XLOG("accept failed: %s\n", strerror(errno));
            continue;
        }

        fcntl(fd, F_SETFD, FD_CLOEXEC);

        handle_request(fd);
    }
    return 0;
}

我们刚刚在崩溃进程的信号处理函数中的connect,就会在此被accept到。进而进入到handle_request进行对崩溃进程的处理。

//* @system/core/debuggerd/debuggerd.c
//  handle_request(int fd)

    XLOG("handle_request(%d)\n", fd);

    debugger_request_t request;
    memset(&request, 0, sizeof(request));
    int status = read_request(fd, &request);

通过read_request函数初始化request结构体变量,其中记录了崩溃进程发送请求的一些信息。

//* @system/core/debuggerd/debuggerd.c
//  read_request(int fd, debugger_request_t* out_request)
    ... ...
    int status = getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &len);
    ... ...
    status = TEMP_FAILURE_RETRY(poll(pollfds, 1, 3000));
    ... ...
    status = TEMP_FAILURE_RETRY(read(fd, &msg, sizeof(msg)));
    ... ...
    out_request->action = msg.action;
    out_request->tid = msg.tid;
    out_request->pid = cr.pid;
    out_request->uid = cr.uid;
    out_request->gid = cr.gid;
    out_request->abort_msg_address = msg.abort_msg_address;

    if (msg.action == DEBUGGER_ACTION_CRASH) {
        /* Ensure that the tid reported by the crashing process is valid. */
        char buf[64];
        struct stat s;
        enable_etb_trace(cr);
        snprintf(buf, sizeof buf, "/proc/%d/task/%d", out_request->pid, out_request->tid);
        if(stat(buf, &s)) {
            LOG("tid %d does not exist in pid %d. ignoring debug request\n",
                    out_request->tid, out_request->pid);
            return -1;
        }
    }
    ... ...

getsockopt获取进程信息,往requset中填入值,因为是进程崩溃了,之前提到过,崩溃进程发送的是DEBUGGER_ACTION_CRASH,因此还会校验tid是否属于相应的pid。这个tid是从socket请求中中读取的msg变量中取得,而pid是从连接信息中通过c标准调用函数getsockopt获得。

此处用poll等待值的传入,如果客户端有write,就会使得服务端监听的poll返回。并用read读取msg。

我们再回到崩溃进程,这里是客户端,也正是他write了msg。

//* @bionic/linker/debugger.cpp
//  debuggerd_signal_handler(int n, siginfo_t* info, void*)
    ... ...
    int s = socket_abstract_client(DEBUGGER_SOCKET_NAME, SOCK_STREAM);

    if (s >= 0) {
        debugger_msg_t msg;
        msg.action = DEBUGGER_ACTION_CRASH;
        msg.tid = tid;
        msg.abort_msg_address = reinterpret_cast<uintptr_t>(gAbortMessage);
        int ret = TEMP_FAILURE_RETRY(write(s, &msg, sizeof(msg)));

调用信号处理函数debuggerd_signal_handler时,仍是处于该崩溃的线程中,因此,gettid获取到的是崩溃线程自己的tid。然后写入action为DEBUGGER_ACTION_CRASH,再将msg写入socket端口。

另一头的debuggerd读取到了msg,并根据msg在read_request中初始化request,并依据request来做剩下的操作。

现在,正式开始debuggerd对崩溃进程的处理。

//* @system/core/debuggerd/debuggerd.c
//  handle_request(int fd)
    ... ...
    int status = read_request(fd, &request);
    if (!status) {
        ... ...
        if (ptrace(PTRACE_ATTACH, request.tid, 0, 0)) {
            LOG("ptrace attach failed: %s\n", strerror(errno));
        } else {
            bool detach_failed = false;
            bool attach_gdb = should_attach_gdb(&request);
            if (TEMP_FAILURE_RETRY(write(fd, "\0", 1)) != 1) {

首先,读取request成功后,debuggerd会对崩溃的线程进行PTRACE_ATTACH,PTRACE_ATTACH的同时,会向被attach的进程,也就是这里的崩溃进程,发出SIGSTOP信号。这时由于崩溃进程正在read,卡在内核态中,并不会收到信号停止运行。而只有在debuggerd执行了write(fd, “\0”, 1)之后。那边的read得以返回,而在此时接受到了SIGSTOP信号,这时才使得崩溃线程在read的时候真正停住了。

                ... ...
                for (;;) {
                    int signal = wait_for_signal(request.tid, &total_sleep_time_usec);

debuggerd接着往下走,进入到for循环中,PTRACE_ATTACH之后,崩溃线程就会变成debuggerd的子进程,此时wait_for_signal可以接收到指定子线程,也就是崩溃线程的信号,因此等到的就是刚才PTRACE_ATTACH的时候产生的SIGSTOP。在switch语句中对SIGSTOP做相应处理。

                    switch (signal) {
                    case SIGSTOP:
                        if (request.action == DEBUGGER_ACTION_DUMP_TOMBSTONE) {
                            ... ...
                        } else if (request.action == DEBUGGER_ACTION_DUMP_BACKTRACE) {
                            ... ...
                        } else {
                            // ... log ...
                            status = ptrace(PTRACE_CONT, request.tid, 0, 0);
                            // ... log ...                            
                            continue; /* loop again */
                        }

此时取得的对应action为DEBUGGER_ACTION_CRASH,对此做的相应处理便是PTRACE_CONT与continue。

PTRACE_CONT会使得crash进程继续走,之前崩溃进程停住的地方从read开始得以继续执行。
而执行continue会回到刚才for循环开始,用wait_for_signal等待崩溃线程的信号。

回到崩溃线程,debuggerd_signal_handler函数中

//* @bionic/linker/debugger.cpp
//  debuggerd_signal_handler(int n, siginfo_t* info, void*)

            ret = TEMP_FAILURE_RETRY(read(s, &tid, 1));
    ... ...
    ... ...

    signal(n, SIG_DFL);

    switch (n) {
        case SIGABRT:
        case SIGFPE:
        case SIGPIPE:
#ifdef SIGSTKFLT
        case SIGSTKFLT:
#endif
            (void) tgkill(getpid(), gettid(), n);
            break;
        default:    // SIGILL, SIGBUS, SIGSEGV
            break;
    }

首先,把信号处理方式设为默认,这样下次捕获到该信号时就不会再掉入处理函数debuggerd_signal_handler中。

然后,再次抛出异常该信号。
对于主动引发的信号,需要重新抛出,因为这类异常在恢复到原来的现场后是会继续执行的,如果不手动再次抛出,线程会直接继续执行下去,略过此次的错误,这不是我们想要的。
对于而其他由于异常后kernel产生的信号,在信号处理函数结束后,会恢复到之前的现场,再次引发本来的异常,产生原来的信号。
需要注意的是,虽然此时已经signal(n, SIG_DFL)将信号处理设置为默认方式了,但由于debuggerd还在ATTACH着,在没有DETACH前,再次引发的信号还是由debuggerd进行处理。

现在再回到debuggerd中,
记得我们回到了for循环的开头,此时wait_for_signal就可以接收到这个后来再次发出的信号了。

                    case SIGILL:
                    case SIGABRT:
                    case SIGBUS:
                    case SIGFPE:
                    case SIGSEGV:
                    case SIGPIPE:
#ifdef SIGSTKFLT
                    case SIGSTKFLT:
#endif
                        {
                        XLOG("stopped -- fatal signal\n");
                        // ... log ...
                        kill(request.pid, SIGSTOP);

                        tombstone_path = engrave_tombstone(request.pid, request.tid,
                                signal, request.abort_msg_address, !attach_gdb, false,
                                &detach_failed, &total_sleep_time_usec);
                        break;

首先会调用kill对整个崩溃进程发送SIGSTOP,使得进程中的所有线程,包含崩溃的线程和其他还在正常运行的线程都停住。可以回到开头看到linker默认定义信号处理函数的地方没有SIGSTOP,因为SIGSTOP的默认处理是不会被忽略和也无法重定义的,因此所有线程确实会被停下来而不会再次触发新的信号处理函数。
这里需要将其他线程都停下的原因是,原来已经有一个线程崩溃了,说明进程已经是异常的状态。而其他线程若继续运行,也可能会触发另外的异常信号,这样会干扰我们接下来打印堆栈和事后对异常根本原因的分析。因此停下来后我们可以尽可能的保留住一开始触发异常信号的现场。
在停止进程后,便可以开始调用engrave_tombstone打印tombstone崩溃信息了。

                if (ptrace(PTRACE_DETACH, request.tid, 0, 0)) {
                    LOG("ptrace detach from %d failed: %s\n", request.tid, strerror(errno));
                    detach_failed = true;
                }

到这里,debuggerd的使命就已经完成了,因此就可以PTRACE_DETACH异常线程,

            /* resume stopped process (so it can crash in peace). */
            kill(request.pid, SIGCONT);

由于之前发送了SIGSTOP,因此还需要再发送SIGCONT让线程再次运行下去,这样会重新触发新的异常,由kernel回收进程,如果有进行设置的话还会做coredump之类的操作。
这里你可能会有疑问,我们之前对于一些主动抛异常的信号,有进行重新抛出的操作,而这里并没有做这样的操作,那么,在继续执行后,原来某些主动Abort的异常会不会就这样忽略了呢?
答案在abort的实现中,。。。。。

至此,对于此次发生的崩溃debuggerd已经处理完毕,于是回到do_server的for循环中接着accept下一次socket请求。

我们来回顾一下,一共做了多少次信号处理。以空指针异常为例。
第一次,是在程序崩溃的时候。这时发出的是异常信号SIGSEGV,信号处理函数为程序一开始就注册的debuggerd_signal_handler,使得程序向debuggerd发送处理请求
第二次,是debuggerd在ptrace attach时发送的SIGSTOP信号,此时是由debuggerd进行处理,获取信号后使程序在read后继续运行,并将信号处理设置为默认方式,这样下次异常信号将不会由debuggerd_signal_handler处理。
第三次,是程序继续运行后再次发出的异常信号SIGSEGV,这时由于仍然被attach着,还是由debuggerd处理。此时debuggerd会进行tombstone的操作。接着detach进程,并让程序继续运行。
第四次,程序在被debuggerd detach后继续触发异常信号SIGSEGV,这时走的是默认处理方式,kernel会进行coredump以及回收进程资源等操作

因此,一共进行了四次信号处理过程。每一次的目的都加粗显示了,可以看到这四次都是必不可少的。
顺带提一下,其实进程还收到了一次SIGSTOP信号,是在debuggerd要触发tombstone之前做的kill(request.pid, SIGSTOP),然而这次没有wait它,而是在tombstone结束后kill(request.pid, SIGCONT)消除了SIGSTOP信号使程序继续往下走。

分析完整个过程,我们可以知道,如果不需要tombstone,信号可以直接走默认处理,这样kernel在崩溃后直接回收进程。debuggerd也不需要了,上述的所有环节都是多余的。
因此大费周章的目的,就是为了输出崩溃信息,获取tombstone。

### 回答1: Android原生崩溃(android native crash)是指在Android平台上,由于代码执行错误或者资源耗尽等原因,导致应用程序无法正常运行而崩溃或者闪退的现象。原生崩溃产生的原因可能是由于C或者C++代码编写错误、内存溢出、线程竞争等。针对原生崩溃问题,开发人员需要使用调试工具进行定位和修复。 ### 回答2: Android Native Crash发生在安卓应用程序运行时,由于C或C++库的错误或者其他原因导致应用程序崩溃。有时候Native Crash可能会影响整个设备,尤其是当Native Crash发生在系统级别的代码中时。 产生Native Crash的原因通常包括以下几个方面: 1. 内存管理问题:Native Crash通常与内存管理问题相关,这可能是由于访问未初始化的内存,使用错误的指针或释放已释放的内存等原因引起的。 2. 硬件问题:Native Crash也可能与设备相关的硬件问题有关,例如访问不可用的硬件资源或硬件设备故障。 3. 应用程序代码问题:Native Crash可能发生在应用程序代码的错误、资源泄漏、堆栈溢出等问题引起的。 4. 第三方库问题:Native Crash也可能由第三方库中的错误或bug引起。这些库可能没有经过充分的测试,或者与设备硬件不兼容。 为了更好地解决Native Crash问题,开发者可以通过日志或崩溃报告(Crash Report)来检测和分析崩溃日志,并查看堆栈跟踪信息来确定导致Native Crash的来源。在开发过程中,经常使用除了自己编写的代码之外的第三方库时,还可以考虑使用崩溃的回溯工具,如Firebase Crashlytics等。 总之,Native CrashAndroid应用程序开发过程中经常遇到的问题,它可能会对用户体验和开发进度产生重大影响,因此开发者需要强化对Native Crash的理解和分析能力,以更好地解决Native Crash的问题。 ### 回答3: Android Native Crash,指的是在 Android 系统中发生的本地崩溃。本地崩溃是指应用程序使用本地代码,而不是 Java 代码,导致应用程序崩溃的问题。本地代码可以是编写在 C/C++ 等语言中的库,或是应用程序本身所编写的 Native 代码。 本地代码崩溃后,会在应用程序崩溃的同时发生。本地崩溃可发生在 Android 应用程序中的任何部分,比如,应用程序本地库、Android 系统库等等。大多数情况下,本地崩溃是由于访问无效内存、访问不合法指针、数组越界等问题引起的。 为了解决本地崩溃问题,Android 提供了一些工具和技术。比如,使用 ndk-stack 工具可以解析本地崩溃日志。Android Studio 也提供了一些工具来分析应用程序崩溃的原因。同时,我们也可以在应用程序中添加自定义的日志跟踪信息,以便更好地了解应用程序的崩溃原因。 还有一些其他的技术可以使用,如使用 Google 的 Crashlytics 来跟踪应用程序的崩溃问题。这个平台可以帮助开发者收集和分析应用程序在用户设备上的崩溃信息,并彻底解决这些问题。此外,Android 还提供了一些实用工具和技术,如 ANR(Application Not Responding)错误处理器、Tracer for OpenGL ES 和 Traceview 示例等。 总之,Android Native CrashAndroid 系统中常见的崩溃问题之一。了解它的原因并采用适当的解决方案可以使得我们更好地保持我们的应用程序的稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值