【Android】NDK开发Crash问题
手机user版本还是userdebug或是eng版本:adb shell getprop ro.build.type
因为使用的user版本的手机,所有没有权限读取到/data/tombstones日志,本次Crash case使用Logcat日志分析问题;可以看到,日志内容主要由下面几部分组成:(最主要的就是分析崩溃的过程和PID,终止的信号和故障地址和调用堆栈部分)
- 构建指纹
- 崩溃的过程和PID
- 终止信号和故障地址
- CPU寄存器
- 调用堆栈
2022-11-21 16:24:58.226 7985-7985/? A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 7985 (gce.ndkpractice), pid 7985 (gce.ndkpractice)
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Softversion: PD2031I_A_5.12.1
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Time: 2022-11-21 16:24:58
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Build fingerprint: 'vivo/PD2031E/PD2031:10/QP1A.190711.020/compiler02151207:user/release-keys'
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Revision: '0'
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: ABI: 'arm64'
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Timestamp: 2022-11-21 16:24:58 0800
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: pid: 7985, tid: 7985, name: gce.ndkpractice >>> cn.com.codingce.ndkpractice <<<
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: uid: 10683
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Cause: null pointer dereference
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x0 0000007fe10e10ff x1 0000007fe10e1054 x2 000000000000000e x3 6a002b2b43206d6f
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x4 0000007b146f2636 x5 0000007fe10e10ff x6 7266206f6c6c6548 x7 2b2b43206d6f7266
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x8 0000000000000000 x9 000000000000007b x10 0000000000000000 x11 000000000000000e
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x12 0000000000000001 x13 409dd45575cb3d01 x14 0000000000000006 x15 ffffffffffffffff
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x16 0000007b146feec0 x17 0000007b146d91dc x18 0000007babb60000 x19 0000007b25010800
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x20 0000000000000000 x21 0000007b25010800 x22 0000007fe10e13b0 x23 0000007baa6bb4c7
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x24 0000000000000004 x25 0000007baad6a020 x26 0000007b250108b0 x27 0000000000000001
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: x28 0000007fe10e1140 x29 0000007fe10e1110
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: sp 0000007fe10e10a0 lr 0000007b146d9230 pc 0000007b146d91f0
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: backtrace:
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #00 pc 000000000000f1f0 /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (crashTest() 20) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #01 pc 000000000000f22c /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI 48) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #02 pc 0000000000140350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline 144) (BuildId: 402a81ae33e07fe7479455c29fd19662)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #03 pc 0000000000137334 /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub 548) (BuildId: 402a81ae33e07fe7479455c29fd19662)
---------------------------------省略部分-----------------------------------
崩溃过程和PID信息
从上面日志中的第9行中我们可以看到崩溃进程的基本信息,如下所示:
代码语言:javascript复制2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: pid: 7985, tid: 7985, name: gce.ndkpractice >>> cn.com.codingce.ndkpractice <<<
如果pid等于tid,那么就说明这个程序是在主线程中Crash掉的,名称的属性则表示Crash进程的名称以及在文件系统中位置。
终止信号和故障地址信息
从上面日志中的第11、12行中可以看到程序是因为什么信号导致了Crash以及出现错误的地址,如下所示:
代码语言:javascript复制2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
2022-11-21 16:24:58.265 8033-8033/? A/DEBUG: Cause: null pointer dereference
第10行的信息说明出现进程Crash的原因是因为程序产生了段错误的信号,访问了非法的内存空间,而访问的非法地址是0x0。另外这个例子中直接给出来问题原因是因为空指针,其它问题并不一定会给出此信息。
调用堆栈信息
调用栈信息是分析程序崩溃的非常重要的一个信息,它主要记录了程序在Crash前的函数调用关系以及当前正在执行函数的信息,上面例中的backtrace的信息如下所示:
代码语言:javascript复制2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: backtrace:
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #00 pc 000000000000f1f0 /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (crashTest() 20) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #01 pc 000000000000f22c /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI 48) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #02 pc 0000000000140350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline 144) (BuildId: 402a81ae33e07fe7479455c29fd19662)
2022-11-21 16:24:58.414 8033-8033/? A/DEBUG: #03 pc 0000000000137334 /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub 548) (BuildId: 402a81ae33e07fe7479455c29fd19662)
在上面的输出信息中,## 00,#01,#02 ......等表示的都是函数调用栈中栈帧的编号,其中编号越小的栈帧表示着当前最近调用的函数信息,所以栈帧标号#00表示的就是当前正在执行并导致程序崩溃函数的信息。在栈帧的每一行中,pc后面的16进制数值表示的是当前函数正在执行语句的在共享链接库或者可执行文件中的位置,然后/lib/arm64/libndkpractice.so
则表示的是当前执行指令是在哪个文件当中,后面的小括号则是注明对应的是哪个函数。
addr2line
addr2line是NDK中用来获得指定动态链接库文件或者可执行文件中指定地址对应的源代码信息,它们位于NDK包中的如下位置中,以arm64架构为例:
代码语言:javascript复制$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
其中NDK_HOME表示NDK的安装路径,另外具体架构和目录的对应关系如下:
工具链 | 位置 |
---|---|
arm | $TOOLCHAIN/arm-linux-androideabi/lib/ |
arm64 | $TOOLCHAIN/aarch64-linux-android/lib/ |
x86 | $TOOLCHAIN/i686-linux-android/lib/ |
x86_64 | $TOOLCHAIN/x86_64-linux-android/lib/ |
addr2line的使用说明如下所示:
代码语言:javascript复制Usage: ./aarch64-linux-android-addr2line [option(s)] [addr(s)]
Convert addresses into line number/file name pairs.
If no addresses are specified on the command line, they will be read from stdin
The options are:
@<file> Read options from <file>
-a --addresses Show addresses
-b --target=<bfdname> Set the binary file format
-e --exe=<executable> Set the input file name (default is a.out)
-i --inlines Unwind inlined functions
-j --section=<name> Read section-relative offsets instead of addresses
-p --pretty-print Make the output easier to read for humans
-s --basenames Strip directory names
-f --functions Show function names
-C --demangle[=style] Demangle function names
-h --help Display this information
-v --version Display the program's version
addr2line的基本用法如下所示:
代码语言:javascript复制➜./aarch64-linux-android-addr2line -f -e /Users/inke/AndroidStudioProjects/NDKPractice/app/build/intermediates/cmake/debug/obj/arm64-v8a/libndkpractice.so 000000000000f1f0
_Z9crashTestv
/Users/inke/AndroidStudioProjects/NDKPractice/app/src/main/cpp/native-lib.cpp:7
如上所示,通过addr2line工具,可以看到libndkpractice.so文件中地址000000000000f1f0对应的源码是什么了,它对应的是源码中app/src/main/cpp/native-lib.cpp:7处代码,查看上下文后,确定为空指针问题。
ndk-stack
Android NDK自从版本r6开始,提供了一个工具ndk-stack。这个工具能自动分析tombstone文件,能将崩溃时的调用内存地址和C 代码一行一行对应起来。
ndk-stack工具同样也位于NDK包中,它的路径如下所示:
代码语言:javascript复制$NDK_HOME/ndk-stack
ndk-stack的使用说明如下所示:
代码语言:javascript复制Usage: ndk-stack -sym PATH [-dump PATH]
Symbolizes the stack trace from an Android native crash.
-sym PATH sets the root directory for symbols
-dump PATH sets the file containing the crash dump (default stdin)
See <https://developer.android.com/ndk/guides/ndk-stack.html>.
其中,dump参数很容易理解,即dump下来的log文本文件,可以是Logcat日志或者tombstones日志;sym参数就是你的android项目下,编译成功之后,obj目录下的文件。
ndk-stack的基本用法如下所示:
代码语言:javascript复制adb logcat | $NDK_HOME/ndk-stack -sym /Users/inke/AndroidStudioProjects/NDKPractice/app/build/intermediates/cmake/debug/obj/arm64-v8a/
执行后得到的结果如下:
代码语言:javascript复制********** Crash dump: **********
Build fingerprint: 'vivo/PD2031E/PD2031:10/QP1A.190711.020/compiler02151207:user/release-keys'
#00 0x000000000000f1f0 /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (crashTest() 20) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
crashTest()
/Users/inke/AndroidStudioProjects/NDKPractice/app/src/main/cpp/native-lib.cpp:7:8
#01 0x000000000000f22c /data/app/cn.com.codingce.ndkpractice-6baWiatY2L09dp0lwyzETw==/lib/arm64/libndkpractice.so (Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI 48) (BuildId: fd24be4dce579e53d04e9f0623c45f4f67f02ef8)
Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI
/Users/inke/AndroidStudioProjects/NDKPractice/app/src/main/cpp/native-lib.cpp:15:5
#02 0x0000000000140350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline 144) (BuildId: 402a81ae33e07fe7479455c29fd19662)
objdump
上面两种工具都是将崩溃点对应到源码再进行分析,objdump 则是可以在汇编层对崩溃原因进行分析。所以这要求我们必须了解一些 arm/x86 汇编知识。
objdump也是ndk自带的一个工具,通常与addr2line在同一目录:
代码语言:javascript复制$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump
objdump的使用说明如下所示:
代码语言:javascript复制Usage: ./aarch64-linux-android-objdump <option(s)> <file(s)>
Display information from object <file(s)>.
At least one of the following switches must be given:
-a, --archive-headers Display archive header information
-f, --file-headers Display the contents of the overall file header
-p, --private-headers Display object format specific file header contents
-P, --private=OPT,OPT... Display object format specific contents
-h, --[section-]headers Display the contents of the section headers
-x, --all-headers Display the contents of all headers
-d, --disassemble Display assembler contents of executable sections
-D, --disassemble-all Display assembler contents of all sections
-S, --source Intermix source code with disassembly
-s, --full-contents Display the full contents of all sections requested
-g, --debugging Display debug information in object file
-e, --debugging-tags Display debug information using ctags style
-G, --stabs Display (in raw form) any STABS info in the file
-W[lLiaprmfFsoRt] or
--dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames,
=frames-interp,=str,=loc,=Ranges,=pubtypes,
=gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
=addr,=cu_index]
Display DWARF info in the file
-t, --syms Display the contents of the symbol table(s)
-T, --dynamic-syms Display the contents of the dynamic symbol table
-r, --reloc Display the relocation entries in the file
-R, --dynamic-reloc Display the dynamic relocation entries in the file
@<file> Read options from <file>
-v, --version Display this program's version number
-i, --info List object formats and architectures supported
-H, --help Display this information
objdump的基本用法如下所示:
代码语言:javascript复制./aarch64-linux-android-objdump -D /Users/inke/AndroidStudioProjects/NDKPractice/app/build/intermediates/cmake/debug/obj/arm64-v8a/libndkpractice.so > ~/Desktop/libndkpractice.so.txt
最后产生的结果文件如下:
代码语言:javascript复制000000000000f1dc <_Z9crashTestv>:
f1dc: d10043ff sub sp, sp, #0x10
f1e0: d2800008 mov x8, #0x0 // #0
f1e4: 52800f69 mov w9, #0x7b // #123
f1e8: f90007e8 str x8, [sp,#8]
f1ec: f94007e8 ldr x8, [sp,#8]
f1f0: b9000109 str w9, [x8]
f1f4: 910043ff add sp, sp, #0x10
f1f8: d65f03c0 ret
000000000000f1fc <Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI>:
f1fc: d101c3ff sub sp, sp, #0x70
f200: a9067bfd stp x29, x30, [sp,#96]
f204: 910183fd add x29, sp, #0x60
f208: d53bd048 mrs x8, tpidr_el0
f20c: f9401508 ldr x8, [x8,#40]
f210: f81f83a8 stur x8, [x29,#-8]
f214: f81d83a0 stur x0, [x29,#-40]
f218: f9001be1 str x1, [sp,#48]
f21c: b00000c1 adrp x1, 28000 <search_object 0x3e8>
f220: 9118a021 add x1, x1, #0x628
f224: d10083a0 sub x0, x29, #0x20
f228: 97ffff82 bl f030 <_ZNSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEC2IDnEEPKc@plt>
f22c: 97ffffb1 bl f0f0 <_Z9crashTestv@plt>
f230: 14000001 b f234 <Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI 0x38>
f234: f85d83a0 ldur x0, [x29,#-40]
f238: d10083a8 sub x8, x29, #0x20
f23c: f9000fe0 str x0, [sp,#24]
f240: aa0803e0 mov x0, x8
f244: 94000040 bl f344 <_ZNKSt6__ndk112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEE5c_strEv>
f248: f9400fe8 ldr x8, [sp,#24]
f24c: f9000be0 str x0, [sp,#16]
f250: aa0803e0 mov x0, x8
可以看到,0x000000000000f1f0
这个地址的相关两个汇编指令如下:
f1ec: f94007e8 ldr x8, [sp,#8]
f1f0: b9000109 str w9, [x8]
LDR R0, [R1]LDR是把R1中的值取出放到寄存器R0中LDR:load R0 from register R1
STR R0, [R1]STR是把R0中的值存入寄存器R1中,STR:store R0 to register R1
结合Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 7985
信息,配合崩溃信号列表:
信号 | 描述 |
---|---|
SIGSEGV | 内存引用无效。 |
SIGBUS | 访问内存对象的未定义部分。 |
SIGFPE | 算术运算错误,除以零。 |
SIGILL | 非法指令,如执行垃圾或特权指令 |
SIGSYS | 糟糕的系统调用 |
SIGXCPU | 超过CPU时间限制。 |
SIGXFSZ | 文件大小限制。 |
大体可以猜出来这一个空指针的问题。
项目代码
C
代码语言:javascript复制void crashTest() {
int *p = NULL;
*p = 666;
}
extern "C" JNIEXPORT jstring JNICALL
Java_cn_com_codingce_ndkpractice_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C ";
//crashTest(); // 空指针Crash案例
// return env->NewStringUTF(hello.c_str()); // 无返回值 Crash案例
}
Java
代码语言:javascript复制package cn.com.codingce.ndkpractice;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import cn.com.codingce.ndkpractice.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
// Used to load the 'ndkpractice' library on application startup.
static {
System.loadLibrary("ndkpractice");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
try {
nativeThrowException();
} catch (IllegalArgumentException e) {
Log.e("NativeMethod", e.getMessage());
}
}
public native void nativeThrowException();
/**
* A native method that is implemented by the 'ndkpractice' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}