Android native crash sdk實現之crash捕獲&tombstone信息的生成

背景

本文主要是簡單介紹一下 native crash 的發生過程,如何捕獲,以及如何抓取并生成 Android tombstone 文件中的信息。

native crash 的發生

以下面代碼中的空指針問題為例來看下 crash 的發生:

static int getValue() {
    return *(int*)nullptr;
}

當我們調用上面的方法后,進程會異常退出:Segmentation fault。如果我們用lldb去運行他,可以看到更詳細的信息:stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x0)

我們先反編譯一下上面的代碼,看下他做了什么,以及為什么會導致進程異常退出:

getValue:
        mov     x0, 0
        ldr     w0, [x0]
        ret

這里可以看到他是從虛擬地址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));
    }
}

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
// ...

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. 子進程中我們通過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;
}

暫停所有線程的執行

上面提到通過 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;
        }
    }
}

獲取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);
}

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

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

然后我們可以實現一個簡版的棧回溯:

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. 某些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)

調用棧中通常會輸出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)

在輸出的調用棧信息中pc的值其實不是上面取到的虛擬地址,因為每次運行其虛擬地址都是變化的,輸出這個沒有意義,因此他輸出的是pc指向的代碼在其文件中的偏移。這個可以通過pc虛擬地址 - 對應elf的load bias來獲得。elf的load bias在Android PLT-GOT hook 實現提到過,感興趣的話可以看一下

符號名(函數名)獲取

函數的符號名存儲在 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;
}

elf中動態符號的查找之前在ELF 通過 Sysv Hash & Gnu Hash 查找符號的實現及對比中提過,感興趣的話可以看下。

(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;
        }
    }
}

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. 第一行中的 [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, &regs) == -1) {
        LOGE("PTRACE_GETREGS failed: %s", strerror(errno));
        return false;
    }
#else
    iovec iovec;
    iovec.iov_base = &regs;
    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;
}

獲取其他線程的調用棧信息跟上面提到的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);
}

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

幾個注意點

要實現好crash捕獲sdk還是比較復雜的,還有挺多地方要考慮,比如:預留一部分內存以應對oom類的crash,設置一個備用信號棧以應對stack overflow,預留一些fd以應對fd不足的crash等等。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容