【Android】NDK开发Crash分析

2022-12-05 09:26:14 浏览数 (1)


【Android】NDK开发Crash问题

手机user版本还是userdebug或是eng版本:adb shell getprop ro.build.type

因为使用的user版本的手机,所有没有权限读取到/data/tombstones日志,本次Crash case使用Logcat日志分析问题;可以看到,日志内容主要由下面几部分组成:(最主要的就是分析崩溃的过程和PID,终止的信号和故障地址和调用堆栈部分)

  • 构建指纹
  • 崩溃的过程和PID
  • 终止信号和故障地址
  • CPU寄存器
  • 调用堆栈
代码语言:javascript复制
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这个地址的相关两个汇编指令如下:

代码语言:javascript复制
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();

}

0 人点赞