本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一 Native Crash 简介
Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解
二 Native C/C++ Libraries 简介
Android 开发中通常是将 Native 层代码打包为
.so
格式的动态库文件,然后供 Java 层调用,
.so
库文件通常有以下三种来源:
2.1
.so
文件组成
一个完整的
.so
文件由 C/C++代码和一些 debug 信息组成,这些 debug 信息会记录
.so
中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表 symbolic 信息,这种
.so
被称为未 strip 的,通常体积会比较大。
通常 release 的
.so
都是需要经过 strip 操作,strip 之后的
.so
中的 debug 信息会被剥离,整个 so 的体积也会缩小许多。
可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析。如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。
所以,这些 debug 信息尤为重要,是我们分析 Native Crash 问题的关键信息,那么我们在编译
.so
时 候务必保留一份未被 strip 的
.so
或者剥离后的符号表信息,以供后面问题分析。
2.2 查看 so 状态
也可以通过命令行来查看
.so
的状态,Linux 下使用 file 命令即可,在命令返回值里面可以查看到
.so
的一 些基本信息。
如下代码所示,stripped 代表是没有 debug 信息的
.so
,with debug_info, not stripped 代表携带 debug 信息的
.so
。
file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped
2.3 获取 strip 和未被 strip 的 so
目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。
strip 之前的 so 路径:{project}/app/build/intermediates/merged_native_libs
strip 之后的 so 路径:{project}/app/build/intermediates/stripped_native_libs
三 Native Crash 捕获与解析
3.1 通过 DropBox 日志解析
Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制。主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。
相关文件记录存储目录:/data/system/dropbox
只需要将 DropBox 的日志获取到即可进行分析解决,下面贴上一份 Log 示例。
DropBox 中的 Tombstone 文件显示,Native Crash 发生在动态库 libnativedemo.so 中,具体的方法和行数可以用 Android/SDK/NDK 提供的工具 linux-android-addr2line 来进一步定位。
addr2line 工具通常在 ndk 目录下,例如:
${SDK Path}/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
然后使用命令行,既可将偏移地址转换为 crash 方法和行数
arm-linux-androideabi-addr2line [option(s)] [addr(s)]
简单来说就是 arm-linux-androideabi-addr2line + 可选项 + 异常地址
[option(s)] 介绍 @ 从文件中读取 options -a 在结果中显示地址 addr -b 设置二进制文件的格式 -e 设置输入文件(常用:选项后面需要跟报错的共享库,用于 addr2line 程序分析) -i unwind inline function -j Read section-relative offsets instead of addresses -p 让输出更易读 -s 在输出中,剥离文件夹名称 -f 显示函数名称 -C (大写的) 将输出的函数名 demangle -h 输出帮助 -v 输出版本信息
使用 addr2line 进行解析,结果可以看到,Native Crash 发生在文件 native-lib.cpp
的 17 行的 Crash() 方法中
结合代码分析,在 Crash() 中,对空指针 *a 进行了赋值操作,所以造成了 crash。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
* 引起 crash
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
extern "C"
JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
通过读取 DropBox 获得 crash log -> addr2line 解析偏移地址的方法确实可以定位到 native crash 发生的现场,但是 DropBox 只有系统应用能访问,非系统应用拿不到日志。对于非系统应用,可以使用 google 提供的开源工具 BreakPad 进行监测分析。
3.2 通过 BreakPad 捕获解析
3.2.1 breakpad 简介
BreakPad 是 Google 开发的一个跨平台 C/C++ dump
捕获开源库,崩溃文件使用微软的 minidump
格式存储,也支持发送这个 dump 文件到你的服务器,breakpad 可以在程序崩溃时触发 dump 写入操作,也可以在没有触发 dump 时主动写 dump 文件。breakpad 支持 windows、linux、macos、android、ios 等。目前已有 Google Chrome, Firefox, Google Picasa, Camino, Google Earth 等项目使用。
3.2.2 实现原理
在不同平台下使用平台特有的函数以及方式实现异常捕获:
Windows:通过 SetUnhandledExceptionFilter()设置崩溃回掉函数
Max OS:监听 Mach Exception Port 获取崩溃事件
Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件
工作原理示意图
图片右上角是一个完整的应用程序,它包含了三部分即程序代码、Breakpad Client
(即 brekapad 提供出来的静态库),调式信息
在 Build System中 breakpad 的 symbol 生成工具借助应用层序中的 Debugging Information 这一部分生成一个 Google 自己的符号文件,最终在发布应用层序的时候使用 strip 将调式信息去除
在 User's System中运行的应用程序是通过 strip 去除了调式信息的,若应用程序发生 Crash,Breakpad client 就会写 minidump 文件到指定目录,也可以将产生的 minidump 文件发送到远端服务器即 Crash Colletcor。
在 Crash Collector就可以利用 Build System 中产生的 symol 文件和 User's System 中上报的 minidump 文件生成用户可读的 stack trace
3.2.3 使用示例
获取 breakpad 源码
执行安装 breakpad
1. cd breakpad 目录
2. 直接命令窗口输入:
./configure && make
移植 Breakpad 到客户端程序
breakpad 源码导入应用程序 cpp 目录下
然后在 breakpad 中创建 CMakeLists.txt
cmake_minimum_required(VERSION 3.18.1)
#导入头文件
include_directories(src src/common/android/include)
#支持汇编文件的编译
enable_language(ASM)
#源文件编译为静态库
add_library(breakpad STATIC
src/client/linux/crash_generation/crash_generation_client.cc
src/client/linux/dump_writer_common/thread_info.cc
src/client/linux/dump_writer_common/ucontext_reader.cc
src/client/linux/handler/exception_handler.cc
src/client/linux/handler/minidump_descriptor.cc
src/client/linux/log/log.cc
src/client/linux/microdump_writer/microdump_writer.cc
src/client/linux/minidump_writer/linux_dumper.cc
src/client/linux/minidump_writer/linux_ptrace_dumper.cc
src/client/linux/minidump_writer/minidump_writer.cc
src/client/linux/minidump_writer/pe_file.cc
src/client/minidump_file_writer.cc
src/common/convert_UTF.cc
src/common/md5.cc
src/common/string_conversion.cc
src/common/linux/breakpad_getcontext.S
src/common/linux/elfutils.cc
src/common/linux/file_id.cc
src/common/linux/guid_creator.cc
src/common/linux/linux_libc_support.cc
src/common/linux/memory_mapped_file.cc
src/common/linux/safe_readlink.cc)
#导入相关的库
target_link_libraries(breakpad log)
breakpad 中的 CMakeLists.txt 创建完成后,还需要在 cpp 目录下的 CMakeLists.txt 中进行配置,将刚刚创建的 CMakeLists.txt 引入进去
cmake_minimum_required(VERSION 3.18.1)
#引入头文件
include_directories(breakpad/src breakpad/src/common/android/include)
add_library(nativecrash SHARED nativecrashlib.cpp)
#添加子目录,会自动查找这个目录下的 CMakeList
add_subdirectory(breakpad)
target_link_libraries(nativecrash log breakpad)
breakpad 初始化
然后在自己项目的 native 文件中对 breakpad 进行初始化,如下
#include <jni.h>
#include <string>
#include "breakpad/src/client/linux/handler/exception_handler.h"
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"
* 引起 crash
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
//回调函数
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context,
bool succeeded) {
printf("Dump path: %s\n", descriptor.path());
return false;
//breakpad 初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_initNative(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
Java 层代码
Java 层传入 Crash dump 文件的保存路径,用于崩溃时文件的生成
package com.elijah.nativedemo;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.File;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("nativedemo");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init(this);
findViewById(R.id.crash)
.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
nativeCrash();
public static void init(Context context){
Context applicationContext = context.getApplicationContext();
File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
if(!file.exists()){
file.mkdirs();
initNative(file.getAbsolutePath());
* 模拟崩溃
public static native void nativeCrash();
* 初始化 breakpad
* @param path
private static native void initNative(String path);
捕获 Crash,解析 dump
Native Crash 产生后,breakpad 会捕获 crash 信息,生成后缀为.dmp
的 dump 文件到指定目录下。
.dmp 格式的文件通常无法查看,需要解析工具对这个文件进行解析。解析工具在步骤“执行安装 breakpad”中就已经生成在 breakpad/src/processor
目录下,名为 minidump_stackwalk
。
输入如下指令即可解析 dump 文件
./minidump_stackwalk my.dump > crash.txt
生成的 crash.txt 如下图所示,关键代码是红框的部分,Thread 0 后面有一个 crashed 标识,说明这里是发生崩溃的线程,而下面就是崩溃的文件以及内存地址,使用 3.1 中介绍的 addr2line 工具进行解析即可得到问题方法与行号