背景

本文主要是简单介绍一下 native crash 的发生过程,如何捕获,以及如何抓取并生成 Android tombstone 文件中的信息。

native crash 的发生

以下面代码中的空指针问题为例来看下 crash 的发生:

static int getValue() {
    return *(int*)nullptr;
}
  • 1.
  • 2.
  • 3.

当我们调用上面的方法后,进程会异常退出:Segmentation fault。如果我们用lldb去运行他,可以看到更详细的信息:stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x0)

我们先反编译一下上面的代码,看下他做了什么,以及为什么会导致进程异常退出:

getValue:
        mov     x0, 0
        ldr     w0, [x0]
        ret
  • 1.
  • 2.
  • 3.
  • 4.

这里可以看到他是从虚拟地址0处读取32bits数据写入w0寄存器(x0的低32位,高32位自动清0),然而操作系统不会为虚拟地址0建立映射,因此MMU在将虚拟地址0转换到物理地址时找不到相应的页表项,于是MMU会产生一个 Data Abort,cpu exception level会切换到 EL1(操作系统内核所在的特权级),并执行内核启动时所预设的exception handler。

以上面的case为例,内核可以根据 ESR_EL1 寄存器中的信息判断异常原因,从 FAR_EL1 寄存器中获得导致异常的虚拟地址(比如上面的 0x0)。然后内核会向对应进程发送信号(比如上面的 SIGSEGV),然后在从内核态返回用户态之前会处理下信号,以SIGSEGV为例,默认行为就是杀死进程,所以我们看到进程crash了。

native crash 的捕获

上面提过native代码发生异常时,系统会向进程发送信号,而大多数信号是可以设置信号处理器的,如果设置了自定义的 signal handler,那么系统会调用我们设置的signal handler,在这个handler中我们可以收集一下进程的状态信息,这就完成了crash的捕获以及信息的收集了。(不是所有的信号都能捕获,比如 SIGKILL,SIGSTOP)

下面是一个简单的设置 signal handler 的代码:

struct sigaction action;
action.sa_flags = SA_ONSTACK | SA_SIGINFO;
sigfillset(&action.sa_mask);
action.sa_sigaction = crashHandler;

for (auto& sig : targetSignals) {
    if (sigaction(sig.signum, &action, &sig.oldAction) != 0) {
        LOGE("failed to set sig action for signal: %d, with error: %s", sig.signum, strerror(errno));
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
Android tombstone 文件信息

在发生native crash的时候,系统会为我们抓取非常详细的信息写入 tombstone 文件中供我们分析,我们先来看看 tombstone 文件中有哪些信息,长什么样子(不同Android 版本生成的 tombstone 信息有些许差异,但主要信息是一致的):

// Build fingerprint, ABI, Timestamp 等信息

// crash进程id,线程id,线程名,进程名
pid: 5577, tid: 5577, name: rashkitdemo.app  >>> com.crashkitdemo.app <<<

// 具体的 signal,不同的 signal 信息会有些许不同
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000000

// crash 线程的寄存器信息
x0  0000000000000000  x1  00000075bd01f048  x2  000000730bd4dc88  x3  b400007444a23b20
x4  000000730bd4dc88  x5  0000007ff5d5d0f4  x6  0000000000000004  x7  0000000000000004
x8  0000000000000002  x9  0000000000000000  x10 0000000000000007  x11 0000000000000007
x12 0000007310415000  x13 0000007ff5d5cdf0  x14 00000075bd01f049  x15 00000000ebad6a89
x16 000000729a3569e4  x17 0000007ff5d5e1a0  x18 00000075e4b10000  x19 b400007444a23b20
x20 0000000000000000  x21 b400007444a23be8  x22 0000000000000000  x23 0000000000000000
x24 0000007ff5d5e2e0  x25 00000075bd01f048  x26 0000007ff5d5e320  x27 00000075bd01f068
x28 0000007ff5d5e1b0  x29 0000007ff5d5e190
lr  000000729a356a00  sp  0000007ff5d5e170  pc  000000729a356a60  pst 0000000060001000

// 调用栈信息
backtrace:
      #00 pc 0000000000000a60  /data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/lib/arm64/libcrashkitdemo.so (BuildId: 4b4514f52a0aaa0f2c3a61c53b2ebce22daa6f90)
      #01 pc 00000000000009fc  /data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/lib/arm64/libcrashkitdemo.so (BuildId: 4b4514f52a0aaa0f2c3a61c53b2ebce22daa6f90)
      #02 pc 0000000000377030  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3)

// 以寄存器值为地址取出附近的内存数据
memory near x1 ([anon:dalvik-LinearAlloc]):
    00000075bd01f020 000000730fb77110 1039000112c78888  .q..s.........9.
    00000075bd01f030 ffee000100000003 00000075dc728160  ........`.r.u...
    00000075bd01f040 000000730fb77110 1020010a12c78888  .q..s......... .
    00000075bd01f050 fffe000200000004 000000729a3569e4  .........i5.r...
    00000075bd01f060 000000730fb76e60 1838000412c78888  `n..s.........8.
    00000075bd01f070 ffdf016900000005 00000075dc728178  ....i...x.r.u...
    00000075bd01f080 000000730fb77110 0000000000000000  .q..s...........
    00000075bd01f090 70a3524870979e50 0000000012c78888  P..pHR.p........
    00000075bd01f0a0 704c0a007047f100 0000000000000000  ..Gp..Lp........
    00000075bd01f0b0 0000000071250e68 00000000712524a8  h.%q.....$%q....
    00000075bd01f0c0 00000075bd01f008 00000075bd01f028  ....u...(...u...
    00000075bd01f0d0 00000075bd01f048 00000075bd01f068  H...u...h...u...
    00000075bd01f0e0 000000007068f190 0000000000000000  ..hp............
    00000075bd01f0f0 0000000000000000 0000000000000000  ................
    00000075bd01f100 0000000000000000 0000000000000000  ................
    00000075bd01f110 0000000000000000 0000000000000000  ................

// 进程的内存映射信息
memory map (2775 entries):
--->Fault address falls at 00000000'00000000 before any mapped regions
    00000000'12c00000-00000000'2abfffff rw-         0  18000000  [anon:dalvik-main space (region space)]
    00000000'7047f000-00000000'70737fff rw-         0    2b9000  [anon:dalvik-/system/framework/boot.art]
    00000000'70738000-00000000'70780fff rw-         0     49000  [anon:dalvik-/system/framework/boot-core-libart.art]
    00000000'70781000-00000000'707aafff rw-         0     2a000  [anon:dalvik-/system/framework/boot-okhttp.art]
    00000000'707ab000-00000000'707e8fff rw-         0     3e000  [anon:dalvik-/system/framework/boot-bouncycastle.art]
    00000000'707e9000-00000000'707e9fff rw-         0      1000  [anon:dalvik-/system/framework/boot-apache-xml.art]

// 其他线程的寄存器、调用栈信息
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
Cmdline: com.crashkitdemo.app
pid: 5577, tid: 5600, name: Runtime worker  >>> com.crashkitdemo.app <<<
uid: 10190
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
    x0  b4000074a4a270a8  x1  0000000000000080  x2  0000000000000000  x3  0000000000000000
    x4  0000000000000000  x5  0000000000000000  x6  0000000000000000  x7  7f7f7f7f7f7f7f7f
    x8  0000000000000062  x9  b8fd705938aff065  x10 0000000000000002  x11 0000000000000020
    x12 0000000100000000  x13 0000000000000000  x14 0000000000000000  x15 00000075e41c8000
    x16 000000731020fad0  x17 00000075cca95e00  x18 00000072f4f20000  x19 b4000074a4a27098
    x20 b400007444a272c0  x21 b4000074a4a270a8  x22 0000000000000000  x23 b4000074a4a27098
    x24 00000075e41c7cb0  x25 00000075e41c8000  x26 0000000000000001  x27 0000000000014000
    x28 0000000000016000  x29 00000075e41c7b40
    lr  000000730fa2cfb0  sp  00000075e41c7b30  pc  00000075cca95e1c  pst 0000000060001000

7 total frames
backtrace:
      #00 pc 0000000000062e1c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c)
      #01 pc 000000000022cfac  /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
Cmdline: com.crashkitdemo.app
pid: 5577, tid: 5601, name: Runtime worker  >>> com.crashkitdemo.app <<<
uid: 10190
tagged_addr_ctrl: 0000000000000001 (PR_TAGGED_ADDR_ENABLE)
pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
    x0  b4000074a4a270a8  x1  0000000000000080  x2  0000000000000000  x3  0000000000000000
    x4  0000000000000000  x5  0000000000000000  x6  0000000000000000  x7  7f7f7f7f7f7f7f7f
    x8  0000000000000062  x9  b8fd705938aff065  x10 0000000000000001  x11 0000000000000020
    x12 0000000100000000  x13 0000000000000000  x14 0000000000000000  x15 00000075e4128000
    x16 000000731020fad0  x17 00000075cca95e00  x18 00000072f6c34000  x19 b4000074a4a27098
    x20 b400007444a256f0  x21 b4000074a4a270a8  x22 0000000000000000  x23 b4000074a4a27098
    x24 00000075e4127cb0  x25 00000075e4128000  x26 0000000000000001  x27 0000000000014000
    x28 0000000000016000  x29 00000075e4127b40
    lr  000000730fa2cfb0  sp  00000075e4127b30  pc  00000075cca95e1c  pst 0000000060001000

7 total frames
backtrace:
      #00 pc 0000000000062e1c  /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28) (BuildId: a87908b48b368e6282bcc9f34bcfc28c)
      #01 pc 000000000022cfac  /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks(art::Thread*)+140) (BuildId: b10f5696fea1b32039b162aef3850ed3)
//...

// 打开的 fd 信息
open files:
    fd 0: /dev/null
    fd 1: /dev/null
    fd 2: /dev/null
    fd 3: socket:[55224]
    fd 4: /sys/kernel/tracing/trace_marker
    fd 5: /dev/null
    fd 6: /apex/com.android.art/javalib/core-oj.jar
    fd 7: /apex/com.android.art/javalib/core-libart.jar
    fd 8: /apex/com.android.art/javalib/okhttp.jar
    fd 9: /apex/com.android.art/javalib/bouncycastle.jar
    fd 10: /apex/com.android.art/javalib/apache-xml.jar
    fd 11: /system/framework/framework.jar
    fd 12: /system/framework/framework-graphics.jar
    fd 13: /system/framework/ext.jar
    fd 14: /system/framework/telephony-common.jar
    fd 15: /system/framework/voip-common.jar
    fd 16: /system/framework/ims-common.jar

// logcat 信息
--------- log main
03-12 13:52:34.149  5577  5577 I rashkitdemo.app: Late-enabling -Xcheck:jni
03-12 13:52:34.167  5577  5577 I rashkitdemo.app: Using CollectorTypeCC GC.
03-12 13:52:34.228  5577  5577 D CompatibilityChangeReporter: Compat change id reported: 171979766; UID 10190; state: ENABLED
03-12 13:52:34.228  5577  5577 D CompatibilityChangeReporter: Compat change id reported: 242716250; UID 10190; state: ENABLED
03-12 13:52:34.232  5577  5577 W ziparchive: Unable to open '/data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/base.dm': No such file or directory
03-12 13:52:34.232  5577  5577 W ziparchive: Unable to open '/data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/base.dm': No such file or directory
03-12 13:52:34.233  5577  5577 D nativeloader: Configuring clns-6 for other apk /data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/base.apk. target_sdk_version=34, uses_libraries=, library_path=/data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/lib/arm64:/data/app/~~DhNE9mAtTabYo-_2ImmDcw==/com.crashkitdemo.app-qJya3FZwp4w4-QjqVnVlOA==/base.apk!/lib/arm64-v8a, permitted_path=/data:/mnt/expand:/data/user/0/com.crashkitdemo.app
// ...
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
tombstone 信息生成

从上面的例子可以看到,Android 系统生成的 tombstone 中主要包含的信息有:

  1. crash进程id,进程名,线程id,线程名
  2. signal 信息
  3. crash线程寄存器信息
  4. crash线程调用栈
  5. crash线程寄存器值为地址附近的内存数据
  6. crash进程内存映射信息
  7. 其他线程寄存器&调用栈信息
  8. 打开的fd信息
  9. logcat中的日志信息

这些信息已经相当丰富,但是我们没有权限读取系统生成的tombstone信息。(有root权限是可以的,通过 adb bugreport 也可以拿到,但是对于获取线上crash信息而言都是不行的)所以下面我们自己来实现这些信息的获取。

暂停crash进程的执行

上面提到我们需要获取内存数据,各个线程的寄存器、调用栈等信息,因此我们在收到signal的时候应该尽可能快的“冻结”crash进程中所有线程的执行,以避免破坏现场。于是我们在收到signal的时候会fork一个子进程,子进程会先“冻结”crash进程的执行,然后在子进程中完成上面列出来的一系列信息的收集。

fork子进程&传递crash信息

我们需要将crash的一些关键信息传递给子进程,比如crash的线程id,siginfo,ucontext_t,以及抓取的信息要写入的位置等。有多种方式可以用来传递这些信息,下面的示例代码我们以管道来实现:

if (int pipeFds[2]; pipe(pipeFds) == 0) {
    switch (pid_t pid; pid = fork()) {
        case -1: {
            LOGE("failed to fork process: %s", strerror(errno));
            break;
        }
        case 0: {
            // child process, close pipe's write end
            close(pipeFds[1]);
            if (pipeFds[0] != STDIN_FILENO) {
                if (dup2(pipeFds[0], STDIN_FILENO) == -1) {
                    LOGE("failed to redirect stdin to pipe's read end: %s", strerror(errno));
                    _exit(1);
                }
                close(pipeFds[0]);
            }

            setenv("LD_LIBRARY_PATH", gNativeLibPath, 1);

            std::string dumpperPath(gNativeLibPath);
            if (dumpperPath.back() != '/') {
                dumpperPath += '/';
            }
            dumpperPath += "libcrashdumpper.so";
            execl(dumpperPath.c_str(), "crashdumpper", (char*)nullptr);
            LOGE("execl failed: %s", strerror(errno));
            _exit(1);
        }
        default: {
            // parent process, close pipe's read end
            close(pipeFds[0]);
            // write crash info into pipe
            if (write(pipeFds[1], &crashInfo, sizeof(CrashInfo)) != sizeof(CrashInfo)) {
                LOGE("failed to write crash info into pipe");
            }
            // wait dumpper process to finish
            waitDumpperProcess(pid);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

几个小点稍微解释一下:

  1. 子进程中我们通过dup2将管道的读端重定向到stdin,方便后续可执行文件读取
  2. 设置LD_LIBRARY_PATH环境变量是因为我们的dumper程序依赖apk内的libc++_shared.so,这样方便动态链接器查找到
  3. libcrashdumpper.so其实是一个可执行程序,这样命名并放到lib中,安装时系统会自动解压到nativeLibraryDir中,并有可执行权限
  4. waitDumpperProcess是通过waitpid等待dumper进程执行完成
暂停crash进程的执行

在本文开头曾提到“不是所有的信号都能捕获,比如 SIGKILL,SIGSTOP”,此处SIGSTOP就派上用场了,当一个进程(此处其实应该说是线程,只是信号机制是出现在线程机制之前的,所以这块的api以及部分描述有时候会用process)收到SIGSTOP后内核会停止对其的调度直到收到SIGCONT

因此如果我们要暂停某个线程的执行就可以向他发送SIGSTOP,如果要暂停整个进程的执行就可以向这个进程下的所有线程发送SIGSTOP。不过后续我们需要获取crash进程的内存、寄存器等信息,使用ptrace api 比较方便,我们也不用自己发送SIGSTOPPTRACE_ATTACH到目标线程即可(PTRACE_ATTACH 请求也会发送SIGSTOP)。

获取crash进程的所有线程id

/proc/${pid}/task 目录为每个子线程包含一个子目录,目录名就是线程的id,因此我们可以获取到crash进程的所有子线程的id:

std::vector<pid_t> loadThreads(pid_t pid) {
    std::string path("/proc/");
    path += std::to_string(pid);
    path += "/task";

    std::vector<pid_t> tids;
    DIR* dir = opendir(path.c_str());
    if (!dir) {
        return tids;
    }

    dirent* ent;
    while((ent = readdir(dir))) {
        int tid = parseTid(ent->d_name);
        if (tid != -1) {
            tids.push_back(tid);
        }
    }
    closedir(dir);

    return tids;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
暂停所有线程的执行

上面提到通过 ptrace attach 到指定线程可以暂停其执行,不过 ptrace 方法返回后对应线程可能还没有停止执行,可以通过 waitpid 确保其停止执行,因此我们可以先向crash进程的所有线程发送PTRACE_ATTACH请求,然后再wait。

void suspendThreads(const std::vector<pid_t>& tids) {
    for (auto tid : tids) {
        if (ptrace(PTRACE_ATTACH, tid, nullptr, nullptr) == -1) {
            LOGE("failed ptrace attach to thread: %d, with error: %s", tid, strerror(errno));
        }
    }

    // wait for stop
    for (auto tid : tids) {
        errno = 0;
        while (waitpid(tid, nullptr, __WALL) < 0) {
            if (errno != EINTR) {
                LOGE("waitpid: %d failed, %s", tid, strerror(errno));
                break;
            }
            errno = 0;
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
获取crash进程id,进程名,线程id,线程名
  1. crash进程是我们dumper进程的父进程,因此通过getppid可获取其进程id
  2. 通过/proc/${pid}/cmdline可获取进程名
  3. crash线程id已经通过管道传递过来了
  4. 通过/proc/${tid}/comm可获取线程名(Linux中线程是全局唯一的,因此 /proc/pid/task/{pid}/task/pid/task/{tid} 和 /proc/${tid} 指向同一个目录)
signal 信息

可以通过siginfo拿到signal number,fault addr等信息,如果要拿到siginfo以及ucontext_t,在注册signal action的时候需要添加SA_SIGINFOflag。

static void printSignalInfo(const siginfo_t& info) {
    char faultAddr[17];
    if (signalHasFaultAddr(info.si_signo)) {
        snprintf(faultAddr, sizeof(faultAddr), "%p", info.si_addr);// context->uc_mcontext.fault_address
    } else {
        snprintf(faultAddr, sizeof(faultAddr), "--------");
    }
    LOGI("signal: %d, code: %d, %s, fault addr: %s", info.si_signo, info.si_code, strsignal(info.si_signo), faultAddr);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • signalHasFaultAddr: 并非所有的异常都有 fault addr,所以此处有个简单的判断,如果没有的话,就输出: fault addr: --------
crash线程寄存器信息

crash 线程寄存器信息是在ucontext_t数据结构中,已经通过管道传递给dumper进程了,我们只需要按照tombstone格式输出就行,示例代码如下:

static void printRegisters(FILE* out, const ucontext_t& context) {
    for (int i = 0; i < 28; i += 4) {
        fprintf(out, "x%-2d %016llx  x%-2d %016llx  x%-2d %016llx  x%-2d %016llx"
        , i, context.uc_mcontext.regs[i]
        , i + 1, context.uc_mcontext.regs[i + 1]
        , i + 2, context.uc_mcontext.regs[i + 2]
        , i + 3, context.uc_mcontext.regs[i + 3]
        );
    }
    fprintf(out, "x28 %016llx  x29 %016llx", context.uc_mcontext.regs[28], context.uc_mcontext.regs[29]);

    fprintf(out, "sp  %016llx  lr  %016llx  pc  %016llx  pst %016llx"
    , context.uc_mcontext.sp
    , context.uc_mcontext.regs[30]
    , context.uc_mcontext.pc
    , context.uc_mcontext.pstate
    );
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
crash线程调用栈
栈回溯

这个就是要实现栈回溯功能,但是在Android上实现栈回溯还是比较麻烦的,有很多文章介绍这方面内容,此处就以最简单最高效的基于fp的栈回溯方案来实现一下~

这个方案的原理是:如果编译的时候启用了-fno-omit-frame-pointer选项(target是aarch64时,通常是启用的),那么编译器会用x29寄存器(也就是fp)保存当前栈帧的起始地址,而fp指向的栈元素中保存上一个栈帧的起始地址(也就是 pre fp),紧靠着的下一个栈元素存放函数的返回地址(lr)。因此根据fp我们能找到一个个栈帧的起始位置,也就能找到一个个栈帧的返回地址(lr),而函数调用地址就是对应返回地址的上一条指令,因此就完成了回溯。

我们现在是在dumper进程中,没法直接读取crash进程的内存数据,不过上面提到过可以借助ptrace系统调用来实现,示例代码如下:

std::optional<long> readData(pid_t pid, void* addr) {
    errno = 0;
    long data = ptrace(PTRACE_PEEKDATA, pid, addr, nullptr);
    if (errno != 0) {
        return {};
    }
    return data;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

然后我们可以实现一个简版的栈回溯:

for (int i = 0; i < max; ++i) {
    auto preFp = readData(pid, (void*)fp);
    if (!preFp) {
        return;
    }
    auto preLr = readData(pid, (uint64_t*)fp + 1);
    if (!preLr) {
        return;
    }
    uint64_t prePc = preLr.value() - 4;
    // todo process pc
    fp = preFp.value();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

几点补充:

  1. 某些so可能没有启用-fno-omit-frame-pointer,另外穿过jni、jit、oat等代码时可能也会存在问题, 所以回溯过程中可能会出现SIGSEGV等问题,一般可以通过 sigsetjmp,siglongjmp做一下保护 ,不过上面是通过ptrace系统调用读取的,如果地址访问存在问题会设置errno,不会出现异常(signal), 在我们的readData中已经做过判断,这种情况下会返回std::nullopt
  2. 也可以加个优化:通过pthread_attr_getstack获取线程栈的地址范围,如果fp超出范围就提前终止回溯
  3. 我们上面通过lr - 4来获取上一条指令的地址,这对于aarch64来讲没问题,因为指令长度固定4字节, 因此我们可以精确计算。但如果是aarch32,或者是x86这种变长指令集的话怎么处理呢?一种简单的方法是使用lr - 1, 这个地址一定落在上一条指令中,通过他获取对应的行号信息也是准确的。
pc处代码所在文件的路径
#02 pc 0000000000377030  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3)
  • 1.

调用栈中通常会输出pc处代码所在文件的路径,这个比较好实现:/proc/${pid}/maps中存储了所有内存映射信息,包括映射起始虚拟地址,权限,路径名等信息,因此根据上一步拿到的pc虚拟地址就可以从maps中找到对应的路径信息

pc处代码在文件中的偏移
#02 pc 0000000000377030  /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: b10f5696fea1b32039b162aef3850ed3)
  • 1.

在输出的调用栈信息中pc的值其实不是上面取到的虚拟地址,因为每次运行其虚拟地址都是变化的,输出这个没有意义,因此他输出的是pc指向的代码在其文件中的偏移。这个可以通过pc虚拟地址 - 对应elf的load bias来获得。

符号名(函数名)获取

函数的符号名存储在 elf 文件的符号表(.symtab SHT_SYMTAB)中,配合字符串表(.strtab)可以加载出所有的符号信息,离上一步获取的相对pc(文件内偏移量)最近的(symbol.st_value <= relative_pc)的类型为STT_FUNC的符号即是我们要找的符号名(函数名)。

因为.symtab & .strtab不是运行时需要的section,所以有可能会被strip掉,即使没有strip掉,他们大概率也不会被映射进内存。我们可以先check一下,如果这2个section已经被映射进内存,那么我们直接读内存数据解析,否则我们直接解析elf文件。下面给个解析elf文件中所有符号的偏移&符号名的示例代码:

static std::vector<std::pair<uint64_t, std::string>> loadSymbolsFromPath(const std::string& path) {
    auto mapedFile = MapedFile::mapFile(path.c_str());
    if (!mapedFile) {
        LOGE("failed to mmap file: %s", path.c_str());
        return {};
    }

    auto ptr = (uint64_t)mapedFile->ptr();
    auto ehdr = *(ElfW(Ehdr)*)ptr;

    auto shdrStart = ptr + ehdr.e_shoff;
    int symtabIdx = -1;
    for (int i = 0; i < ehdr.e_shnum; ++i) {
        auto shdr = *(ElfW(Shdr)*)(shdrStart + ehdr.e_shentsize * i);
        if (shdr.sh_type == SHT_SYMTAB) {
            symtabIdx = i;
            break;
        }
    }
    if (symtabIdx == -1) {
        LOGE("symtab not found in file");
        return {};
    }

    auto symtab = *(ElfW(Shdr)*)(shdrStart + ehdr.e_shentsize * symtabIdx);
    auto strtab = *(ElfW(Shdr)*)(shdrStart + ehdr.e_shentsize * symtab.sh_link);
    assert(strtab.sh_type == SHT_STRTAB);

    auto symtabStart = ptr + symtab.sh_offset;
    auto symtabEnd = symtabStart + symtab.sh_size;
    auto strtabStart = ptr + strtab.sh_offset;
    // parse symbol
    symtabStart += symtab.sh_entsize;// skip first: STN_UNDEF
    auto symbolCount = symtab.sh_size / symtab.sh_entsize - 1;
    std::vector<std::pair<uint64_t, std::string>> syms;
    syms.reserve(symbolCount);

    do {
        auto sym = *(ElfW(Sym)*)symtabStart;
        if (ELF_ST_TYPE(sym.st_info) == STT_FUNC) {
            syms.emplace_back(sym.st_value, (const char*) (strtabStart + sym.st_name));
        }
        symtabStart += symtab.sh_entsize;
    } while (symtabStart < symtabEnd);
    return syms;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.

(art_quick_generic_jni_trampoline+144) 符号名后面的 +xxx 指的是pc距离符号基地址的偏移:relative_pc - symble.st_value

获取elf的BuildId

build-id 是linker根据输入使用md5、sha1等算法计算的一个checksum,用于标记一次编译的。比如上面我们提到要获取elf文件的符号信息需要符号表,字符串表,但通常我们发布的elf都是strip过的,同时保留一个未strip版本的elf,然后我们的crash sdk在获取到elf的 build-id 以及 relative-pc 信息后上报到服务端,在服务端就可以根据build-id来找到对应的未strip的elf,然后来解析相应的符号信息。

build-id信息是存储在PT_NOTE类型的segment中的,其name的值是“GNU”,因此我们可以像如下代码那样读取 build-id 的信息:

for (int i = 0; i < ehdr.e_phnum; ++i) {
    auto phdr = *(ElfW(Phdr)*)((uint64_t)phdrData + i * ehdr.e_phentsize);
    switch (phdr.p_type) {
        // ...
        case PT_NOTE: {
            auto noteStart = (const char*)(loadBias_ + phdr.p_vaddr);
            auto noteEnd = noteStart + phdr.p_memsz;
            do {
                auto note = *(ElfW(Nhdr)*) noteStart;
                if (strncmp(noteStart + sizeof(ElfW(Nhdr)), "GNU", sizeof("GNU")) == 0) {
                    auto descStart = noteStart + sizeof(ElfW(Nhdr)) + note.n_namesz;
                    auto descEnd = descStart + note.n_descsz;

                    std::stringstream buf;
                    buf.fill('0');
                    buf.setf(std::ios_base::hex, std::ios_base::basefield);
                    for (; descStart < descEnd; ++descStart) {
                        buf.width(2);
                        buf << (uint32_t) *descStart;
                    }
                    buildId_ = buf.str();
                    break;
                }

                noteStart += sizeof(ElfW(Nhdr)) + note.n_namesz + note.n_descsz;
            } while (noteStart < noteEnd);
            break;
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
crash线程寄存器值为地址附近的内存数据
memory near x1 ([anon:dalvik-LinearAlloc]):
    00000075bd01f020 000000730fb77110 1039000112c78888  .q..s.........9.
    00000075bd01f030 ffee000100000003 00000075dc728160  ........`.r.u...
    00000075bd01f040 000000730fb77110 1020010a12c78888  .q..s......... .
    00000075bd01f050 fffe000200000004 000000729a3569e4  .........i5.r...
    00000075bd01f060 000000730fb76e60 1838000412c78888  `n..s.........8.
    00000075bd01f070 ffdf016900000005 00000075dc728178  ....i...x.r.u...
    00000075bd01f080 000000730fb77110 0000000000000000  .q..s...........
    00000075bd01f090 70a3524870979e50 0000000012c78888  P..pHR.p........
    00000075bd01f0a0 704c0a007047f100 0000000000000000  ..Gp..Lp........
    00000075bd01f0b0 0000000071250e68 00000000712524a8  h.%q.....$%q....
    00000075bd01f0c0 00000075bd01f008 00000075bd01f028  ....u...(...u...
    00000075bd01f0d0 00000075bd01f048 00000075bd01f068  H...u...h...u...
    00000075bd01f0e0 000000007068f190 0000000000000000  ..hp............
    00000075bd01f0f0 0000000000000000 0000000000000000  ................
    00000075bd01f100 0000000000000000 0000000000000000  ................
    00000075bd01f110 0000000000000000 0000000000000000  ................
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  1. 第一行中的 [anon:dalvik-LinearAlloc] 是以x1寄存器的值为虚拟地址,在/proc/${pid}/maps中找到对应的内存映射项的pathname
  2. 第一列是虚拟地址,中间两列是内存值,最后一列是内存值的ascii表示,不可打印的字符用’.'代替

这个信息有时候是有用的,比如数组越界的case,有可能会发现相同的字符串序列多次出现,就可以查看下相关代码是否有问题。

crash进程内存映射信息

这个上面已经提到过了,读取/proc/${pid}/maps就OK了

其他线程寄存器&调用栈信息

对于crash的线程,发生crash时的寄存器信息是由操作系统给我们的(context.uc_mcontext.regs)不用我们去获取。其他线程的寄存器信息可以通过ptrace PTRACE_GETREGS or PTRACE_GETREGSET来获取,示例代码如下:

bool getThreadRegs(pid_t tid, user_regs_struct& regs) {
#ifdef PTRACE_GETREGS
    if (ptrace(PTRACE_GETREGS, tid, nullptr, ®s) == -1) {
        LOGE("PTRACE_GETREGS failed: %s", strerror(errno));
        return false;
    }
#else
    iovec iovec;
    iovec.iov_base = ®s;
    iovec.iov_len = sizeof(regs);
    if (ptrace(PTRACE_GETREGSET, tid, NT_PRSTATUS, &iovec) == -1) {
        LOGE("PTRACE_GETREGSET failed: %s", strerror(errno));
        return false;
    }
#endif
    return true;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

获取其他线程的调用栈信息跟上面提到的crash线程栈回溯实现一致,此处就忽略了。

有一点需要注意的是:对于crash线程的信息(寄存器值&调用栈)都是发生crash时的准确信息,而其他线程的寄存器值、调用栈都是crash之后一段时间的状态,所以我们文章开头提到要尽可能快的“冻结”crash进程中的所有线程,尽量接近crash现场。

打开的fd信息

/proc/${pid}/fd中保存了进程打开的所有fd信息,都是符号连接,文件名是fd的数值,内容是fd对应的名称,读取fd信息的示例代码如下:

void dumpOpenFds(pid_t pid) {
    char pathBuf[PATH_MAX];
    snprintf(pathBuf, sizeof(pathBuf), "/proc/%d/fd", pid);

    DIR* dir = opendir(pathBuf);
    if (!dir) {
        LOGE("failed to open dir: /proc/%d/fd", pid);
        return;
    }

    char fdValue[PATH_MAX];

    dirent* ent;
    while((ent = readdir(dir))) {
        if (ent->d_type == DT_LNK) {
            snprintf(pathBuf, sizeof(pathBuf), "/proc/%d/fd/%s", pid, ent->d_name);
            auto count = readlink(pathBuf, fdValue, sizeof(fdValue) - 1);
            if (count < 0) {
                continue;
            }
            fdValue[count] = '\0';
            LOGI("fd: %s -> %s", ent->d_name, fdValue);
        }
    }
    closedir(dir);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
logcat中的日志信息

crash的时候收集最近的logcat信息通常是有帮助的,有些crash需要依赖系统日志来分析,如果我们编译时没有移除app内打印logcat的字节码的话,crash附近的业务log对分析、定位问题通常也有帮助,要收集logcat信息比较简单:fork一个子进程执行/system/bin/logcat即可,示例代码如下:

void dumpLogcat(const char* output) {
    switch (pid_t pid; pid = fork()) {
        case -1: {
            LOGE("fork failed: %s", strerror(errno));
            break;
        }
        case 0: {
            execl("/system/bin/logcat", "-t", "10000", "-d", "-v", "threadtime", "-f", output, (char*) nullptr);
            LOGE("execl failed: %s", strerror(errno));
            _exit(1);
        }
        default: {
            waitLogcatProcess(pid);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
几个注意点

要实现好crash捕获sdk还是比较复杂的,还有挺多地方要考虑,比如:预留一部分内存以应对oom类的crash,设置一个备用信号栈以应对stack overflow,预留一些fd以应对fd不足的crash等等。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

Android native crash sdk实现之crash捕获&tombstone信息的生成_5e