NDK开发定位native异常
NDK 是使用 C/C++ 来进行开发,指针和内存管理是最重要也是最容易出问题的地方,就会遇到诸如内存地址访问错误、使用野指针、内存泄露、堆栈溢出、初始化错误、类型转换错误、数字除0等常见的问题,导致最后都是同一个结果:程序崩溃。当发生 NDK 错误后,logcat 打印的日志根据进行了偏移,无法直接通过日志信息定位错误发生的行数。
首先,当 NDK 程序在发生 Crash 时,它会在路径 /data/tombstones/ 下产生导致程序 Crash 的文件 tombstone_xx。并且 Google 还在 NDK 包中为我们提供了一系列的调试工具,例如 addr2line、objdump、ndk-stack。
一、 native 层的崩溃处理机制
在 Unix-like 系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。异常发生时,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。linux 把这些中断处理,统一为信号量,可以注册信号量向量进行处理。信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。
函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。
接收信号的任务是由内核代{过}{滤}理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。进程陷入内核态后,有两种场景会对信号进行检测:
- 进程从内核态返回到用户态前进行信号检测
- 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测
当发现有新信号时,便会进入信号的处理。信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。接下来进程返回到用户态中,执行相应的信号处理函数。信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,会不断的检测和处理信号。
二、Native崩溃日志格式及内容分析
2022-03-18 13:29:37.872 26666-26666/? A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 26666 (.logandroiddemo), pid 26666 (.logandroiddemo)
2022-03-18 13:29:37.915 26720-26720/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2022-03-18 13:29:37.915 26720-26720/? A/DEBUG: Build fingerprint: 'HONOR/TEL-AN00/HWTEL-H:10/HONORTEL-AN00/3.1.1.203C00:user/release-keys'
2022-03-18 13:29:37.915 26720-26720/? A/DEBUG: Revision: '0'
2022-03-18 13:29:37.915 26720-26720/? A/DEBUG: ABI: 'arm64'
---------------------------------------------------------机器信息----------------------------------------------------------------
2022-03-18 13:29:37.917 26720-26720/? A/DEBUG: SYSVMTYPE: Maple
APPVMTYPE: Art
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: Timestamp: 2022-03-18 13:29:37+0800
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: pid: 26666, tid: 26666, name: .logandroiddemo >>> com.example.logandroiddemo <<<
---------------------------------------------------------进程,线程,包名----------------------------------------------------------
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: uid: 10436
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
----------------------------------------------------引发进程崩溃的信号信息-----------------------------------------------------------
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: Cause: null pointer dereference
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x0 000000763bf086c0 x1 0000007fdd5d1784 x2 0000000000000006 x3 0000000010d810d8
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x4 0000000300000003 x5 00000000173c5650 x6 173c5620173c5638 x7 0000000000000000
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x8 0000000000000000 x9 0000000000000001 x10 0000000000430000 x11 000000763bdf7000
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x12 00000076c04783c0 x13 1803b8d645ffb1dd x14 0000000000000006 x15 ffffffffffffffff
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x16 00000076201b7e70 x17 00000076bfc62980 x18 00000076c2912000 x19 000000763be10800
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x20 0000000000000000 x21 000000763be10800 x22 0000007fdd5d19f0 x23 00000076c15c44d2
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x24 0000000000000004 x25 00000076c1b1e020 x26 000000763be108b0 x27 0000000000000001
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: x28 0000007fdd5d1780 x29 0000007fdd5d1750
2022-03-18 13:29:37.918 26720-26720/? A/DEBUG: sp 0000007fdd5d16e0 lr 000000763b953354 pc 00000076201b7ea0
----------------------------------------------进程崩溃时各寄存器值,随cpu不同而不同----------------------------------------------------
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: backtrace:
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #00 pc 0000000000006ea0 /data/app/com.example.logandroiddemo-VanHrh72D7snIXV6jJNkPg==/base.apk!libnative-lib.so (offset 0x2b5000) (Java_com_example_logandroiddemo_MainActivity_stringFromJNI+48) (BuildId: a5621f59e0623fa37bf37f2a2990f9cffc687334)
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #01 pc 000000000014c350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: c34524a56ff4fcd8ce5f2ffe56e5a86b)
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #02 pc 0000000000143334 /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub+548) (BuildId: c34524a56ff4fcd8ce5f2ffe56e5a86b)
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #03 pc 00000000001521a4 /apex/com.android.runtime/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+252) (BuildId: c34524a56ff4fcd8ce5f2ffe56e5a86b)
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #04 pc 00000000002edc9c /apex/com.android.runtime/lib64/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+384) (BuildId: c34524a56ff4fcd8ce5f2ffe56e5a86b)
2022-03-18 13:29:38.043 26720-26720/? A/DEBUG: #05 pc 00000000002e8f6c /apex/com.android.runtime/lib64/libart.so (bool art::interpreter::DoCall<false, false>(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+912) (BuildId: c34524a56ff4fcd8ce5f2ffe56e5a86b)..........截取部分内容
------------------------------------------------崩溃时函数调用堆栈信息,主要分析点-----------------------------------------------------
三、常见崩溃类型及原因
SIGSEGV 段错误 |
|
SEGV_MAPERR |
要访问的地址没有映射到内存空间。 比如上面对空指针的写操作, 当指针被意外复写为一个较小的数值时 |
SEGV_ACCERR |
访问的地址没有权限。比如试图对代码段进行写操作。 |
SIGFPE 浮点错误 |
一般发生在算术运行出错时 |
FPE_INTDIV |
除以0 |
FPE_INTOVE |
整数溢出 |
SIGBUS 总线错误 |
|
BUS_ADRALN |
地址对齐出错。arm cpu比x86 cpu 要求更严格的对齐机制,所以在 arm cpu 机器中比较常见。 |
SIGILL 错误 |
发生这种错误一般是由于某处内存被意外改写了 |
ILL_ILLOPC |
非法的指令操作码 |
ILL_ILLOPN |
非法的指令操作数 |
四、调试工具
一般的分析崩溃日志的工具都是利用含有调试信息的 so, 结合崩溃信息,分析崩溃点在源代码中的行数
优先使用带符号表的lib.so,无符号表的so库使用常规调试工具时无法定位到类名和错误行数需要使用IDA PRO分析汇编指令。
发布时要把带符号表的so进行备份或者上传,方便分析定位native崩溃
带符号表.so文件路径
不带符号表.so文件路径
1. addr2line
找到安装的NDK路径下 打开cmd输入下面命令(arm-linux-androideabi-addr2line.exe -help 查看所有使用方法)
arm-linux-androideabi-addr2line.exe -e $PROJECT_PATH\obj\local\arm64-v8a\xxx.so 偏移信息
例如:
C:\Users\user\AppData\Local\Android\Sdk\ndk\16.1.4479499\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin>
aarch64-linux-android-addr2line.exe -e
D:\fanyunlong_testDemo\LogAndroidDemo\app\build\intermediates\cmake\debug\obj\arm64-v8a\libnative-lib.so 0000000000006ea0
找到arm64-v8a下的libnative-lib.so
#00 pc 0000000000006ea0 /data/app/com.example.logandroiddemo-VanHrh72D7snIXV6jJNkPg==/base.apk!libnative-lib.so (offset 0x2b5000)
偏移信息为:0000000000006ea0
定位结果:
LogAndroidDemo/app/src/main/cpp/native-lib.cpp:15
10 extern "C" JNIEXPORT jstring JNICALL
11 Java_com_example_logandroiddemo_MainActivity_stringFromJNI(
12 JNIEnv* env,
13 jobject /* this */) {
14 int *p = 0;
15 *p = 1;
16 std::string hello = "Hello from C++";
17
18 return env->NewStringUTF(hello.c_str());
19 }
2. ndk-stack
选项 |
说明 |
-sym |
指定debug版本库的位置 |
-dump |
指定含有crash信息的文件 |
adb logcat | $NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a
您也可以使用 -dump
选项将 logcat 指定为输入文件。例如:
adb logcat > /tmp/foo.txt
$NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a -dump foo.txt
该工具会在开始解析 logcat 输出时查找第一行星号。例如:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***