【Android】NDK开发Crash分析

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行中我们可以看到崩溃进程的基本信息,如下所示:

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以及出现错误的地址,如下所示:

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的信息如下所示:

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架构为例:

$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的使用说明如下所示:

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的基本用法如下所示:

➜./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包中,它的路径如下所示:

$NDK_HOME/ndk-stack

ndk-stack的使用说明如下所示:

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的基本用法如下所示:

adb logcat | $NDK_HOME/ndk-stack -sym /Users/inke/AndroidStudioProjects/NDKPractice/app/build/intermediates/cmake/debug/obj/arm64-v8a/

执行后得到的结果如下:

********** 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在同一目录:

$NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-objdump

objdump的使用说明如下所示:

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的基本用法如下所示:

./aarch64-linux-android-objdump -D /Users/inke/AndroidStudioProjects/NDKPractice/app/build/intermediates/cmake/debug/obj/arm64-v8a/libndkpractice.so > ~/Desktop/libndkpractice.so.txt

最后产生的结果文件如下:

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++

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

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();

}

猜你喜欢

转载自blog.csdn.net/weixin_43874301/article/details/127969779