Android ART虚拟机信号管理及崩溃捕获

FaultManager

FaultManager和与之相关的类共同组成了ART虚拟机用于处理来自Linux操作系统的某些信号管控模块。是处理异常信号的总管。
新版本的Android ART虚拟机中,FaultManager模块确实用SigchainAction替换了SignalAction

背景

FaultManager负责处理信号(如SIGSEGV),用于捕获和处理内存访问错误等异常。SignalAction是旧版中用于管理信号处理的结构体,而SigchainAction则是新引入的替代方案。

变化

  • SignalAction: 旧版中用于信号处理的结构体。
  • SigchainAction: 新版中替代SignalAction的结构体,提供更灵活的信号链管理。

原因

  • 灵活性: SigchainAction支持多个信号处理器的链式调用,增强了信号处理的灵活性。
  • 维护性: 新设计简化了代码结构,便于维护和扩展。
  • 性能: 优化了信号处理流程,提升了性能。

SigchainAction取代SignalAction是为了提升信号处理的灵活性、维护性和性能。

SigchainAction处理流程

SigchainAction 是 Android ART 虚拟机中用于管理信号处理链的模块,主要用于处理诸如 SIGSEGV(段错误)等信号。它的核心思想是通过信号链(Signal Chain)机制,允许多个信号处理器按顺序处理同一个信号,从而增强灵活性和可扩展性。
以下是 SigchainAction 的处理流程:


1. 信号链的初始化

  • 在 ART 虚拟机启动时,SigchainAction 会初始化信号链。
  • 信号链是一个链表结构,每个节点代表一个信号处理器(Signal Handler)。
  • 默认情况下,ART 会将自己的信号处理器(如用于处理 SIGSEGV 的处理器)注册到信号链中。

2. 信号处理器的注册

  • 当需要处理特定信号时,SigchainAction 会将新的信号处理器添加到信号链中。
  • 注册过程通过 AddSpecialSignalHandlerFn 函数完成,该函数会将新的处理器插入信号链的适当位置。
  • 信号链中的处理器按优先级顺序排列,ART 的处理器通常具有较高的优先级。

3. 信号的分发与处理

  • 当信号(如 SIGSEGV)发生时,操作系统会将控制权交给 SigchainAction 管理的信号链。
  • SigchainAction 会依次调用信号链中的每个处理器,直到某个处理器成功处理了信号。
  • 处理流程如下:
    1. 遍历信号链:从链表的头部开始,依次调用每个信号处理器的处理函数。
    2. 处理信号:每个处理器会检查信号是否与其相关。如果是,则尝试处理信号。
    3. 决定是否继续:如果某个处理器成功处理了信号,则终止信号链的遍历;否则,继续调用下一个处理器。

4. 默认处理

  • 如果信号链中的所有处理器都无法处理信号,SigchainAction 会将信号交给默认的信号处理器(通常是操作系统的默认行为,如终止进程)。

5. 信号链的清理

  • 当 ART 虚拟机关闭或某个信号处理器不再需要时,SigchainAction 会从信号链中移除相应的处理器。
  • 清理过程通过 RemoveSpecialSignalHandlerFn 函数完成。

关键特点

  • 链式处理:允许多个处理器按顺序处理同一个信号,增强了灵活性。
  • 优先级管理:处理器按优先级排列,确保关键信号(如 ART 的内存访问错误处理)优先被处理。
  • 动态注册与移除:支持运行时动态添加或移除信号处理器。

示例场景:处理 SIGSEGV

  1. 应用程序访问非法内存,触发 SIGSEGV 信号。
  2. 操作系统将信号传递给 SigchainAction 管理的信号链。
  3. ART 的信号处理器首先检查是否是由于 Java 堆内存访问错误引起的信号。
    • 如果是,ART 会尝试恢复执行(如抛出 NullPointerException)。
    • 如果不是,则将信号传递给下一个处理器。
  4. 如果信号链中的所有处理器都无法处理信号,进程会被终止。

SigchainAction 通过信号链机制实现了灵活、可扩展的信号处理流程,能够有效管理多个信号处理器,并确保关键信号(如内存访问错误)被优先处理。这种设计提升了 ART 虚拟机的稳定性和性能。

异常捕获

在 Android 应用中,可以利用 SigchainAction 机制捕获原生层(Native 层)的崩溃。Android 的 ART 虚拟机通过 SigchainAction 提供了对信号(如 SIGSEGVSIGABRT 等)的链式处理机制,开发者可以通过注册自定义信号处理器来捕获和处理原生层的崩溃。


实现步骤

1. 注册自定义信号处理器

  • 开发者可以通过 sigactionsigchain 相关 API 注册自定义信号处理器。
  • 在 Android 中,推荐使用 sigchain 提供的接口(如 AddSpecialSignalHandlerFn)来注册信号处理器,以确保与 ART 虚拟机的信号链机制兼容。
#include <signal.h>
#include <android/sigchain.h>

static bool HandleSignal(int signal, siginfo_t* info, void* context) {
    // 在这里处理信号,例如记录崩溃信息或恢复执行
    if (signal == SIGSEGV) {
        // 捕获段错误
        __android_log_print(ANDROID_LOG_ERROR, "CrashCapture", "SIGSEGV caught!");
        // 可以在这里记录堆栈信息或调用其他崩溃处理逻辑
    }
    // 返回 true 表示信号已处理,停止传递;返回 false 则继续传递给下一个处理器
    return true;
}

void RegisterSignalHandler() {
    sigchain_action sa = {
        .sc_sigaction = HandleSignal,
        .sc_mask = 0,
        .sc_flags = SA_SIGINFO,
    };
    sigchain_add_special_signal_handler(SIGSEGV, &sa);
}

2. 初始化信号处理器

  • 在 Native 代码的初始化阶段(如 JNI_OnLoadmain 函数中),调用注册信号处理器的函数。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    RegisterSignalHandler();
    return JNI_VERSION_1_6;
}

3. 捕获崩溃信息

  • 在自定义信号处理器中,可以通过以下方式捕获崩溃信息:
    • 使用 siginfo_t 结构体获取信号详细信息(如错误地址)。
    • 使用 ucontext_t 结构体获取 CPU 寄存器状态。
    • 使用 libunwindlibbacktrace 等库捕获堆栈信息。
#include <unwind.h>
#include <dlfcn.h>

static _Unwind_Reason_Code UnwindCallback(struct _Unwind_Context* context, void* arg) {
    Dl_info info;
    uintptr_t pc = _Unwind_GetIP(context);
    if (dladdr((void*)pc, &info) && info.dli_fname) {
        __android_log_print(ANDROID_LOG_ERROR, "CrashCapture", "pc: %p, %s", (void*)pc, info.dli_fname);
    }
    return _URC_NO_REASON;
}

static bool HandleSignal(int signal, siginfo_t* info, void* context) {
    if (signal == SIGSEGV) {
        __android_log_print(ANDROID_LOG_ERROR, "CrashCapture", "SIGSEGV caught at address: %p", info->si_addr);
        _Unwind_Backtrace(UnwindCallback, nullptr);
    }
    return true;
}

4. 处理崩溃

  • 在捕获到崩溃信号后,可以选择以下处理方式:
    • 记录崩溃日志并上传到服务器。
    • 尝试恢复执行(需谨慎,可能导致未定义行为)。
    • 终止进程以避免进一步损坏。

注意事项

  1. 信号处理器的限制

    • 信号处理器中只能调用异步信号安全函数(如 write_exit 等),避免调用非异步信号安全函数(如 mallocprintf 等)。
    • 如果需要记录复杂信息,建议在信号处理器中仅设置标志位,然后在主线程中处理。
  2. 与 ART 虚拟机的兼容性

    • 使用 sigchain 接口注册信号处理器可以确保与 ART 虚拟机的信号链机制兼容。
    • 避免直接使用 signalsigaction,可能会破坏 ART 的信号链。
  3. 多线程问题

    • 崩溃可能发生在任何线程中,信号处理器需要处理多线程并发问题。

通过 SigchainAction 机制,开发者可以在 Android 应用中捕获原生层的崩溃,并记录崩溃信息(如堆栈、寄存器状态等)。这种方法可以用于崩溃分析、调试和监控,但需要注意信号处理器的限制和多线程问题。

捕获到崩溃后,进程还能存活多久?

在 Android 应用中捕获到原生层崩溃(如 SIGSEGV)后,进程的存活时间取决于信号处理器的行为以及崩溃的具体原因:


1. 信号处理器的行为

  • 如果信号处理器返回 true,表示信号已被处理,进程可能会继续运行。
  • 如果信号处理器返回 false,信号会继续传递给下一个处理器,最终可能导致进程终止。
  • 如果信号处理器中调用了非异步信号安全函数(如 mallocprintf 等),可能会导致未定义行为,甚至立即崩溃。

2. 崩溃的原因

  • 可恢复的崩溃:某些崩溃(如访问未映射的内存)可以通过修复内存状态或跳过错误代码来恢复。此时,进程可以继续运行。
  • 不可恢复的崩溃:如果崩溃是由于堆栈损坏、内存泄漏或其他严重问题引起的,即使信号处理器尝试恢复,进程也可能很快再次崩溃。

3. 进程的存活时间

  • 短暂存活:如果崩溃是由于严重的内存损坏或堆栈溢出引起的,进程可能在信号处理器返回后立即崩溃。
  • 较长时间存活:如果崩溃是可恢复的,并且信号处理器成功修复了问题,进程可能会继续运行一段时间,直到再次遇到问题。
  • 不确定存活:如果信号处理器中调用了非异步信号安全函数,进程可能会进入未定义状态,随时崩溃。

4. 最佳实践

  • 记录崩溃信息:在信号处理器中,尽量只记录崩溃信息(如堆栈、寄存器状态),并避免复杂操作。
  • 尽快退出:在捕获到崩溃后,建议尽快调用 _exitabort 终止进程,以避免进一步损坏。
  • 异步处理:在信号处理器中设置标志位,然后在主线程中处理崩溃信息并退出。

示例:安全处理崩溃

以下是一个示例,演示如何在捕获崩溃后安全记录信息并终止进程:

#include <android/log.h>
#include <android/sigchain.h>
#include <signal.h>
#include <unwind.h>
#include <dlfcn.h>
#include <stdlib.h>

#define LOG_TAG "CrashCapture"

static _Unwind_Reason_Code UnwindCallback(struct _Unwind_Context* context, void* arg) {
    Dl_info info;
    uintptr_t pc = _Unwind_GetIP(context);
    if (dladdr((void*)pc, &info) && info.dli_fname) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "pc: %p, %s", (void*)pc, info.dli_fname);
    }
    return _URC_NO_REASON;
}

static bool HandleSignal(int signal, siginfo_t* info, void* context) {
    if (signal == SIGSEGV) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "SIGSEGV caught at address: %p", info->si_addr);
        _Unwind_Backtrace(UnwindCallback, nullptr);
    }
    // 记录完信息后,尽快终止进程
    _exit(1);
    return true; // 阻止信号继续传递
}

void RegisterSignalHandler() {
    sigchain_action sa = {
        .sc_sigaction = HandleSignal,
        .sc_mask = 0,
        .sc_flags = SA_SIGINFO,
    };
    sigchain_add_special_signal_handler(SIGSEGV, &sa);
}

extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    RegisterSignalHandler();
    return JNI_VERSION_1_6;
}

为了确保系统稳定性,建议在捕获崩溃后尽快记录信息并终止进程,避免进一步损坏。


debuggerd

1. debuggerd 是什么?

  • debuggerd 是 Android 系统中的一个守护进程(daemon),负责处理应用程序和系统进程的崩溃(crash)事件。
  • 当应用程序或系统进程发生崩溃(如段错误 SIGSEGV、非法指令 SIGILL 等)时,操作系统会将控制权交给 debuggerd,debuggerd 会被触发,负责收集崩溃时的详细信息。

应用程序的链接器(Linker)和运行时库(如 libc)可能会设置默认的信号处理器,但这些处理器通常会将信号传递给 debuggerd。如果应用程序注册了自定义信号处理器,可能会干扰 debuggerd 的正常工作,因此需要谨慎处理。


2. debuggerd 的功能

  • 捕获崩溃信息:当进程崩溃时,debuggerd 会捕获以下信息:
    • 崩溃的进程 ID(PID)和线程 ID(TID)。
    • 崩溃的信号(如 SIGSEGV)。
    • 崩溃时的寄存器状态。
    • 崩溃时的堆栈信息(包括 Java 和 Native 堆栈)。
    • 内存映射信息(/proc/<pid>/maps)。
  • 生成 tombstone 文件debuggerd 会将收集到的崩溃信息写入一个名为 tombstone 的文件中,供开发者分析。

3. tombstone 文件是什么?

  • tombstone 文件是 debuggerd 生成的崩溃日志文件,包含了崩溃时的详细信息。
  • 文件通常位于 /data/tombstones/ 目录下,命名为 tombstone_XXXX 是编号)。
  • 文件内容通常包括:
    • 崩溃的信号和原因。
    • 寄存器状态。
    • 堆栈回溯(backtrace)。
    • 内存映射信息。
  • 开发者可以通过分析 tombstone 文件来定位崩溃的原因。

4. debuggerd 的工作流程

  1. 崩溃发生:应用程序或系统进程发生崩溃(如段错误)。
  2. 触发 debuggerd:操作系统将崩溃信号传递给 debuggerd
  3. 收集信息debuggerd 连接到崩溃进程,收集寄存器、堆栈、内存映射等信息。
  4. 生成 tombstone 文件:将收集到的信息写入 tombstone 文件。
  5. 终止进程debuggerd 终止崩溃进程,防止进一步损坏系统。

5. debuggerd 与开发者

  • 对于开发者来说,debuggerd 是一个非常重要的工具,可以帮助定位和修复崩溃问题。
  • 通过分析 tombstone 文件,开发者可以了解崩溃的原因(如空指针解引用、堆栈溢出等)。
  • 开发者可以通过以下方式获取 tombstone 文件:
    • 使用 adb pull /data/tombstones/tombstone_XX 命令从设备中提取文件。
    • 使用 logcat 查看崩溃日志。

6. 示例:tombstone 文件内容

以下是一个 tombstone 文件的示例内容:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Android/aosp_flounder/flounder:7.1.1/NYC/enh12211018:userdebug/test-keys'
Revision: '0'
ABI: 'arm'
pid: 1234, tid: 1234, name: example_process  >>> com.example.app <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
    r0 00000000  r1 00000000  r2 00000000  r3 00000000
    r4 00000000  r5 00000000  r6 00000000  r7 00000000
    r8 00000000  r9 00000000  sl 00000000  fp 00000000
    ip 00000000  sp bee6f5d0  lr b6e8f5a7  pc b6e8f5a8  cpsr 400f0010

backtrace:
    #00 pc 000125a8  /system/lib/libc.so (strlen+72)
    #01 pc 0000a5a4  /system/lib/libutils.so (_ZN7android6String8C1EPKc+32)
    #02 pc 0000a5c0  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
    #03 pc 0000a5dc  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
    #04 pc 0000a5f8  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
    #05 pc 0000a614  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
    #06 pc 0000a630  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
    #07 pc 0000a64c  /system/lib/libutils.so (_ZN7android6String8C1ERKS0_+16)
...

Firebase Crashlytics 异常上报原理

Firebase Crashlytics 这样的异常上报工具,通常不会直接依赖 Android 系统的 tombstone 文件来上报崩溃日志。相反,它们会基于自己的机制捕获崩溃信息,并结合 FaultManager 或类似的信号处理机制来实现崩溃捕获和上报。以下是详细解释:


1. tombstone 文件的局限性

  • tombstone 文件是 Android 系统生成的崩溃日志,主要用于记录 Native 层(C/C++)的崩溃信息。
  • 它的生成依赖于 debuggerd,且通常需要 root 权限才能访问 /data/tombstones/ 目录。
  • tombstone 文件不包含 Java 层的崩溃信息(如 Java 异常堆栈),因此对于混合开发的应用(Java + Native)来说,tombstone 文件的信息并不完整。

2. Firebase Crashlytics 的崩溃捕获机制

Firebase Crashlytics 和其他类似的异常上报工具(如 Bugsnag、Sentry)通常会结合以下机制来捕获崩溃信息:

Java 层崩溃捕获

  • Thread.setDefaultUncaughtExceptionHandler
    • Crashlytics 会设置一个自定义的 UncaughtExceptionHandler,用于捕获 Java 层未捕获的异常(如 NullPointerExceptionRuntimeException 等)。
    • 当 Java 层发生崩溃时,Crashlytics 会捕获异常堆栈、线程信息、设备信息等,并将其上报到服务器。

Native 层崩溃捕获

  • 信号处理机制(Signal Handling)
    • Crashlytics 会注册自定义的信号处理器(Signal Handler),用于捕获 Native 层的崩溃信号(如 SIGSEGVSIGABRT 等)。
    • 当 Native 层发生崩溃时,信号处理器会捕获崩溃信息(如寄存器状态、堆栈回溯等),并将其保存到本地。
  • libunwindlibbacktrace
    • Crashlytics 使用这些库来捕获 Native 层的堆栈信息,类似于 debuggerd 的功能。
  • FaultManager 的关系
    • Crashlytics 的信号处理机制与 Android 的 FaultManager 类似,但它是独立实现的,不依赖系统提供的 debuggerdtombstone 文件。

混合崩溃捕获

  • 对于混合开发的应用(Java + Native),Crashlytics 会同时捕获 Java 层和 Native 层的崩溃信息,并将其合并为一份完整的崩溃日志。

3. 崩溃日志的上报

  • 本地缓存
    • 崩溃信息会先保存到本地(通常是应用的私有目录),等待合适的时机上报。
  • 异步上报
    • 当下次应用启动时,Crashlytics 会将缓存的崩溃日志异步上报到服务器。
  • 网络优化
    • 为了节省流量和电量,Crashlytics 会对崩溃日志进行压缩和批量上报。

4. tombstone 文件的区别

  • 独立性
    • Crashlytics 不依赖 tombstone 文件,而是自己实现了一套崩溃捕获和上报机制。
  • 跨平台支持
    • Crashlytics 支持 Android、iOS 等多个平台,而 tombstone 文件是 Android 特有的。
  • 信息丰富度
    • Crashlytics 的崩溃日志通常比 tombstone 文件更丰富,包含 Java 层和 Native 层的信息,以及设备信息、用户信息等。

5. 示例:Firebase Crashlytics 的崩溃日志

以下是一个 Firebase Crashlytics 崩溃日志的示例:

Fatal Exception: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(java.lang.CharSequence)' on a null object reference
   at com.example.app.MainActivity.onCreate(MainActivity.java:25)
   at android.app.Activity.performCreate(Activity.java:8000)
   at android.app.Activity.performCreate(Activity.java:7984)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3404)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3595)
   at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
   at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
   at android.os.Handler.dispatchMessage(Handler.java:106)
   at android.os.Looper.loop(Looper.java:223)
   at android.app.ActivityThread.main(ActivityThread.java:7660)
   at java.lang.reflect.Method.invoke(Method.java)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

  • Firebase Crashlytics 和其他异常上报工具通常不依赖 tombstone 文件,而是基于自己的机制捕获崩溃信息。
  • 它们通过 UncaughtExceptionHandler 捕获 Java 层崩溃,通过信号处理器捕获 Native 层崩溃,并将两者结合生成完整的崩溃日志。
  • 这种机制比 tombstone 文件更灵活、更全面,且不依赖系统权限,适合在发布的应用中使用。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容