rokevin
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
移动
前端
语言
  • 基础

    • Linux
    • 实施
    • 版本构建
  • 应用

    • WEB服务器
    • 数据库
  • 资讯

    • 工具
    • 部署
开放平台
产品设计
  • 人工智能
  • 云计算
计算机
其它
GitHub
  • Native Crash

  • Tombstone
    • 1. 什么是 Tombstone?
    • 2. 和 Java Crash 的区别
    • 3. 在哪里?
    • 4. 里面有什么?(你看日志会遇到)
    • 5. 什么时候会出现?
    • 6. 超简短总结(记住这个)
  • Tombstone分析
    • 1. 先准备:获取tombstone文件
    • 2. tombstone文件核心结构(重点看这5部分)
    • 3. 核心步骤:从tombstone定位到具体C/C++代码行
      • 步骤1:提取关键信息(从tombstone里找3个值)
      • 步骤2:用addr2line工具解析偏移地址(找到具体代码行)
    • 4. 常见tombstone崩溃原因解读(新手必记)
    • 5. 避坑点(新手常犯)
    • 6. 总结
  • 根据tombstone文件里的信息解决问题
    • 核心思路
    • 1. 第一步:从tombstone中明确“崩溃根因”(先定因)
    • 2. 第二步:针对性修复(按崩溃类型给出解决方案)
      • 场景1:最常见——空指针/野指针(SIGSEGV MAPERR)
      • 场景2:内存访问越界(SIGSEGV ACCERR)
      • 场景3:内存重复释放/使用已释放内存(double free)
      • 场景4:断言失败/内存分配失败(SIGABRT)
      • 场景5:内存对齐错误(SIGBUS)
    • 3. 第三步:通用避坑&预防措施(减少Native崩溃)
    • 4. 第四步:验证修复效果
    • 5. 总结

Native Crash

native crash 是指在 native 代码中发生的崩溃,通常是由于访问了空指针、数组越界、空引用等导致的。 缩写为 NE。

Tombstone

tombstone [ˈtuːmstəʊn] : 是指在 native 代码中发生崩溃时,系统会生成一个 tombstone 文件,用于记录崩溃信息,在 Android 里,tombstone 就是 “崩溃现场的尸体报告”。

1. 什么是 Tombstone?

  • 当 APP 或 系统进程 发生 Native 崩溃(C/C++ 层崩溃,比如空指针、越界、堆破坏)
  • 系统会把崩溃时的:
    • 寄存器
    • 堆栈
    • 内存信息
    • 线程信息
  • 全部保存成一个文件,这个文件就叫: ✅ tombstone

它的本质: Native 崩溃的完整现场快照 = tombstone

2. 和 Java Crash 的区别

  • Java 崩溃:有 CrashLog、有 Exception,能直接看到哪行错了
  • Native 崩溃:系统不会直接给你 Java 栈,而是生成 tombstone 文件

一句话: Java 崩溃看日志,Native 崩溃看 tombstone。

3. 在哪里?

路径固定:

/data/tombstones/tombstone_00
/data/tombstones/tombstone_01
…

最多保存 10 个,满了覆盖旧的。

4. 里面有什么?(你看日志会遇到)

  • 崩溃进程号、线程号
  • 崩溃原因(SEGV 空指针、堆越界、栈溢出…)
  • 完整 Native 调用栈
  • 寄存器值
  • 内存镜像
  • 所有线程的堆栈

用来定位:C/C++ 到底在哪崩的。

5. 什么时候会出现?

  • 你用了 JNI
  • 用了第三方 SO 库
  • 系统底层(Surface、OpenGL、Audio、Camera 等)崩了
  • 内存越界、野指针、double free、stack overflow

只要是 Native 层崩溃 → 必出 tombstone。

6. 超简短总结(记住这个)

tombstone = Android 系统为 Native 崩溃生成的崩溃现场报告。

Tombstone分析

如何看懂Android的tombstone文件、定位Native崩溃的具体代码行,这是解决APP Native层闪退的核心技能,我会用“新手能看懂”的方式,从“看什么”→“怎么看”→“怎么定位”一步步讲透。

1. 先准备:获取tombstone文件

首先得拿到tombstone文件(以tombstone_00为例),有2种常用方式:

  1. 真机/模拟器(有root权限):

    # 1. 进入设备shell
    adb shell
    # 2. 切换到tombstones目录
    cd /data/tombstones
    # 3. 复制文件到电脑
    cp tombstone_00 /sdcard/
    adb pull /sdcard/tombstone_00 ~/Desktop/
    
  2. 无root权限(仅能获取自己APP的崩溃): 部分手机厂商(如小米、华为)会把APP的tombstone文件放到/data/data/你的包名/files/或通过开发者模式导出。

2. tombstone文件核心结构(重点看这5部分)

我用一个真实的tombstone片段拆解,你只要抓住核心字段,就能快速定位问题:

// ===================== 1. 崩溃基本信息(先看这个!)=====================
---------- tombstone_00: 2026-02-28 15:30:00 ----------
pid: 12345, tid: 67890, name: Thread-1  >>> com.example.myapp <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
// 关键解读:
// pid/tid:崩溃的进程/线程号
// name:崩溃的线程名(比如JNI线程、渲染线程)
// >>> 包名 <<<:哪个APP崩了
// signal 11 (SIGSEGV):最常见的崩溃类型(空指针/内存访问无效)
// code 1 (SEGV_MAPERR):访问了不存在的内存地址(比如ptr=NULL时解引用)
// fault addr 0x0:崩溃的内存地址(0x0就是空指针!)

// ===================== 2. 寄存器信息(辅助定位)=====================
r0 00000000  r1 00000001  r2 00000000  r3 00000000
r4 00000000  r5 7f8a1234  r6 00000000  r7 00000000
// 不用深钻,重点看r0/r1/r2是否有0x0(空指针)

// ===================== 3. 核心:Native调用栈(定位崩溃行的关键)=====================
backtrace:
    #00 pc 00008abc  /data/app/com.example.myapp/lib/arm64/libmyso.so (native_crash_func+12)
    #01 pc 00012def  /data/app/com.example.myapp/lib/arm64/libmyso.so (Java_com_example_myapp_MainActivity_nativeDoSomething+28)
    #02 pc 0000000f  /data/app/com.example.myapp/oat/arm64/base.odex (offset 0x1234)
// 关键解读:
// #00:崩溃的最顶层(直接触发崩溃的函数)
// pc 00008abc:崩溃在so库中的偏移地址
// libmyso.so:崩溃的SO库名
// native_crash_func+12:崩溃在native_crash_func函数的第12行(或偏移12字节)
// #01:调用这个崩溃函数的上层函数(JNI层函数)

// ===================== 4. 线程信息 ======================
Threads:
  pid: 12345, tid: 67890, name: Thread-1  >>> com.example.myapp <<<
  pid: 12345, tid: 67891, name: RenderThread
// 看崩溃发生在哪个线程(比如主线程/子线程/渲染线程)

// ===================== 5. 崩溃原因总结 ======================
cause: null pointer dereference (SIGSEGV)
// 系统总结的崩溃原因(空指针解引用)

3. 核心步骤:从tombstone定位到具体C/C++代码行

这是最关键的环节,分2步就能搞定:

步骤1:提取关键信息(从tombstone里找3个值)

  • 崩溃的SO库名:libmyso.so
  • 崩溃的偏移地址:00008abc(pc后的数值)
  • 设备架构:arm64(从路径里的arm64能看出来)

步骤2:用addr2line工具解析偏移地址(找到具体代码行)

addr2line是NDK自带的工具,能把SO库的偏移地址转换成“文件名+行号”,操作如下:

  1. 找到NDK里的addr2line: 路径一般在:NDK路径/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line (架构对应:arm64→aarch64-linux-android-addr2line;arm→arm-linux-androideabi-addr2line)

  2. 执行解析命令:

    # 核心命令模板:addr2line -f -e SO库路径 偏移地址
    aarch64-linux-android-addr2line -f -e ./libmyso.so 00008abc
    
  3. 输出结果(示例):

    native_crash_func
    /Users/xxx/project/jni/native_crash.cpp:25
    

    ✅ 直接定位到:native_crash.cpp文件的第25行!

4. 常见tombstone崩溃原因解读(新手必记)

崩溃原因含义常见场景
SIGSEGV (SEGV_MAPERR)访问不存在的内存地址C/C++空指针解引用、野指针
SIGSEGV (SEGV_ACCERR)无权限访问内存读写只读内存、访问内核空间内存
SIGABRT主动调用abort()assert断言失败、内存分配失败
SIGBUS内存对齐错误非对齐的内存访问(比如ARM架构)
SIGFPE浮点运算错误除0、浮点溢出

5. 避坑点(新手常犯)

  1. 必须用带符号表的SO库: 解析时要用编译时生成的libmyso.so(带debug信息),不能用打包时剥离符号表的SO库,否则解析不出行号。
  2. 架构要匹配: arm64的崩溃要用arm64的addr2line,arm的崩溃要用arm的addr2line,否则解析结果错误。
  3. 偏移地址要去掉0x: tombstone里的pc地址是00008abc(十六进制),命令里直接用这个值,不用加0x前缀。

6. 总结

  1. tombstone核心价值:记录Native崩溃的完整现场,关键看backtrace里的SO库和偏移地址;
  2. 定位核心步骤:提取SO库+偏移地址 → 用addr2line解析 → 找到具体代码行;
  3. 新手重点:先看崩溃原因(如SIGSEGV)和backtrace的#00行,再用addr2line解析偏移地址,就能快速定位问题。

根据tombstone文件里的信息解决问题

根据tombstone文件里的信息,一步步解决Native层崩溃问题,这是从“定位问题”到“修复问题”的核心环节——按“先定因→再修复→最后验证”的流程,结合实际场景落地。

核心思路

解决tombstone问题的核心逻辑是: 从tombstone中提取崩溃类型+调用栈 → 定位到具体代码行 → 分析崩溃根因 → 针对性修复 → 验证修复效果

1. 第一步:从tombstone中明确“崩溃根因”(先定因)

先根据tombstone里的关键信息,判断崩溃的核心类型(新手先记最常见的5种):

崩溃类型(tombstone里的关键词)核心原因典型特征
SIGSEGV (SEGV_MAPERR)空指针/野指针解引用fault addr=0x0(偏移地址为0)
SIGSEGV (SEGV_ACCERR)无权限访问内存读写只读内存、访问已释放的内存
SIGABRT主动终止(断言/内存分配失败)日志中有“abort()”“assert failed”
SIGBUS内存对齐错误ARM架构下非对齐的内存访问(如char转int)
SIGFPE浮点运算错误除0、浮点溢出

示例:如果tombstone里写着 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0,直接判定是「空指针解引用」。

2. 第二步:针对性修复(按崩溃类型给出解决方案)

结合最常见的崩溃类型,给出可落地的修复方法,每个类型都配“代码问题示例+修复代码”。

场景1:最常见——空指针/野指针(SIGSEGV MAPERR)

问题代码(C/C++):
// 问题:ptr未初始化,直接解引用
void native_crash_func() {
    char* ptr = NULL;
    // 崩溃点:解引用空指针,触发SIGSEGV
    *ptr = 'a'; 
}
修复方案:

核心是「访问指针前先判空」+「确保指针指向有效内存」:

void native_crash_func_fixed() {
    char* ptr = NULL;
    // 修复1:判空保护
    if (ptr != NULL) {
        *ptr = 'a';
    } else {
        // 兜底逻辑:打印日志/返回错误
        __android_log_print(ANDROID_LOG_ERROR, "NativeCrash", "ptr is null!");
        return;
    }

    // 或者:确保指针指向有效内存
    char buf[10] = {0};
    ptr = buf;
    *ptr = 'a'; // 安全访问
}

场景2:内存访问越界(SIGSEGV ACCERR)

问题代码:
// 问题:数组长度为5,访问第6个元素(越界)
void array_out_of_bound() {
    int arr[5] = {1,2,3,4,5};
    // 崩溃点:arr[5]超出数组范围,访问非法内存
    int val = arr[5]; 
}
修复方案:

核心是「边界检查」+「使用安全的容器/函数」:

void array_out_of_bound_fixed() {
    int arr[5] = {1,2,3,4,5};
    int index = 5;
    // 修复1:边界检查
    if (index >=0 && index < sizeof(arr)/sizeof(arr[0])) {
        int val = arr[index];
    } else {
        __android_log_print(ANDROID_LOG_ERROR, "NativeCrash", "index out of bound!");
        return;
    }

    // 或者:使用C++的std::vector(自带边界检查)
    std::vector<int> vec = {1,2,3,4,5};
    if (index < vec.size()) {
        int val = vec[index];
    }
}

场景3:内存重复释放/使用已释放内存(double free)

问题代码:
// 问题:ptr被free后再次访问,或重复free
void double_free_crash() {
    char* ptr = (char*)malloc(10);
    free(ptr); // 第一次释放
    // 崩溃点1:访问已释放的内存
    *ptr = 'b'; 
    // 崩溃点2:重复释放
    free(ptr); 
}
修复方案:

核心是「释放后置空+避免重复释放」+「使用智能指针」:

void double_free_fixed() {
    char* ptr = (char*)malloc(10);
    if (ptr != NULL) {
        free(ptr);
        // 修复1:释放后立即置空,避免野指针
        ptr = NULL; 
    }

    // 修复2:访问前判空,避免使用已释放内存
    if (ptr != NULL) {
        *ptr = 'b';
    }

    // 修复3(C++):使用智能指针(自动管理内存,避免手动free)
    std::unique_ptr<char[]> smart_ptr(new char[10]);
    smart_ptr[0] = 'b'; // 无需手动释放,超出作用域自动销毁
}

场景4:断言失败/内存分配失败(SIGABRT)

问题代码:
// 问题1:断言失败触发abort()
void assert_crash() {
    int ret = -1;
    // 断言失败,触发abort()
    assert(ret == 0); 
}

// 问题2:内存分配失败后直接使用
void malloc_fail_crash() {
    // 申请超大内存,malloc返回NULL
    char* ptr = (char*)malloc(1024*1024*1024*10); 
    // 未判空直接使用,触发崩溃
    *ptr = 'c'; 
}
修复方案:

核心是「替换断言为主动检查」+「内存分配后判空」:

void assert_fixed() {
    int ret = -1;
    // 修复1:替换assert为条件检查(断言只在debug生效,release会被移除)
    if (ret != 0) {
        __android_log_print(ANDROID_LOG_ERROR, "NativeCrash", "ret is not 0!");
        return;
    }
}

void malloc_fail_fixed() {
    char* ptr = (char*)malloc(1024*1024*1024*10);
    // 修复2:内存分配后必须判空
    if (ptr == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "NativeCrash", "malloc failed!");
        return;
    }
    *ptr = 'c';
    free(ptr);
    ptr = NULL;
}

场景5:内存对齐错误(SIGBUS)

问题代码(ARM架构):
// 问题:char*是1字节对齐,转int*(4字节对齐)后访问,触发SIGBUS
void bus_error_crash() {
    char buf[5] = {1,2,3,4,5};
    // 崩溃点:p的地址不是4字节对齐
    int* p = (int*)(buf + 1); 
    int val = *p;
}
修复方案:

核心是「保证内存对齐」+「使用memcpy安全拷贝」:

void bus_error_fixed() {
    char buf[5] = {1,2,3,4,5};
    int val = 0;
    // 修复:用memcpy拷贝,避免直接强制类型转换
    memcpy(&val, buf + 1, sizeof(int));
}

3. 第三步:通用避坑&预防措施(减少Native崩溃)

除了针对性修复,这些通用措施能从根源减少tombstone问题:

  1. 开启编译警告+静态检查: 在CMakeLists.txt中添加编译选项,让编译器提前发现问题:

    # 开启所有警告+将警告视为错误
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
    # 开启地址sanitizer(ASAN),调试阶段检测内存问题
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
    
  2. JNI层做严格校验: 从Java传过来的参数(如jstring、jobject),先校验是否为NULL,再转换使用:

    JNIEXPORT void JNICALL
    Java_com_example_myapp_MainActivity_nativeDoSomething(JNIEnv *env, jobject thiz, jstring str) {
        // JNI层判空
        if (str == NULL) {
            return;
        }
        const char* c_str = env->GetStringUTFChars(str, NULL);
        // 使用完释放资源
        env->ReleaseStringUTFChars(str, c_str);
    }
    
  3. 避免在子线程操作Java对象: Native子线程中访问Java对象(如jobject),容易因Java对象被GC回收导致野指针,需用NewGlobalRef创建全局引用:

    // 正确做法:创建全局引用
    jobject global_obj = env->NewGlobalRef(local_obj);
    // 子线程中使用global_obj
    // 使用完释放全局引用
    env->DeleteGlobalRef(global_obj);
    
  4. 测试覆盖边界场景: 重点测试「空参数」「超大数据」「频繁创建/释放内存」等场景,提前暴露问题。

4. 第四步:验证修复效果

修复后,需要确认问题是否解决:

  1. 复现验证:执行触发崩溃的操作,确认APP不再闪退;
  2. 检查tombstone:查看/data/tombstones/目录,不再生成新的tombstone文件;
  3. 日志验证:通过adb logcat查看Native日志,确认没有崩溃相关的错误信息;
  4. 长期验证:集成到测试环境,通过Monkey测试/压测,确认无复现。

5. 总结

  1. 定因核心:从tombstone中提取signal/code/fault addr,快速判断崩溃类型;
  2. 修复核心:空指针判空、内存越界检查、避免重复释放、保证内存对齐;
  3. 预防核心:开启编译检查、JNI层严格校验、使用智能指针、覆盖边界测试。
最近更新:: 2026/3/3 19:05
Contributors: luokaiwen