一 Native Crash 简介
Native Crash 是发生在 Android 系统中 C/C 层面的 Crash,具体参考: # Android 平台 Native Crash 详解捕获原理
二 Native C/C Libraries 简介
Android 开发通常是将军 Native 层代码打包为.so
格式动态库文件,然后提供 Java 层调用,.so
库文件通常有以下三源:
- Android 多媒体库、OpenGL ES 图形库等
- 引入的第三方库
- 开发者自行编译生成的动态库
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
的 中
结合代码分析,在 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 捕获解析
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 等项目使用。
在不同平台下使用平台特有的函数以及方式实现异常捕获:
Windows:通过 SetUnhandledExceptionFilter()设置崩溃回掉函数
Max OS:监听 Mach Exception Port 获取崩溃事件
Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件
图片右上角是一个完整的应用程序,它包含了三部分即程序代码、Breakpad Client
(即 brekapad 提供出来的静态库),调式信息
-
在 中 breakpad 的 symbol 生成工具借助应用层序中的 Debugging Information 这一部分生成一个 Google 自己的符号文件,最终在发布应用层序的时候使用 strip 将调式信息去除
-
在 中运行的应用程序是通过 strip 去除了调式信息的,若应用程序发生 Crash,Breakpad client 就会写 minidump 文件到指定目录,也可以将产生的 minidump 文件发送到远端服务器即 Crash Colletcor。
-
在 就可以利用 Build System 中产生的 symol 文件和 User’s System 中上报的 minidump 文件生成用户可读的 stack trace
3.2.3 使用示例
获取 breakpad 源码
github.com/google/brea…
执行安装 breakpad
1. cd breakpad 目录
2. 直接命令窗口输入:
./configure && make
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 工具进行解析即可得到问题方法与行号
作者:话唠扇贝 链接:https://juejin.cn/post/7124689738811834382 来源:稀土掘金