[Android翻译]调试Android应用中的本地崩溃

1,574 阅读10分钟

一个简单的端到端例子

原文地址:proandroiddev.com/debugging-n…

原文作者:medium.com/@jacksonche…

发布时间:2019年2月25日 - 8分钟阅读

100个最受欢迎的Android应用已经安装了540亿次(截至2018年12月)。而其中85%的应用都包含有1000多个独立的原生库的原生代码。如果你在这100个应用中的任何一个应用上工作过,或者在类似的大型应用上工作过,那就有很多机会出问题!

安卓开发者应该可以自如地调试原生崩溃堆栈痕迹(安卓语中的 "墓碑")。但是,在本地的崩溃(即在低级的C/C++代码中)通常是复杂的,难以理解的。不仅如此,它们还可能在将控制权返回给Java/Kotlin代码之前,使JVM(Java虚拟机)崩溃。这意味着您无法在应用程序级别捕捉到异常,从而提供了糟糕的用户体验

在我们开始之前

安卓开发者文档为诊断原生崩溃提供了很多有用的信息,但缺乏有用的、全面的例子来学习。

注意:如果你对Android平台上的原生代码不熟悉,看看Android NDK指南也无妨。

原生库对许多应用都很有用,但一些常见的用例包括。

  • 从设备中挤出额外的性能,以实现低延迟或运行计算密集型应用,如游戏或物理模拟。
  • 重用你自己或其他开发者的C或C++库。

此外,原生库可以提供更高的应用安全性,并且可以在跨平台应用中重复使用。

一些现实世界的上下文

在过去的几个月里,我在Capital One工作的Android SDK团队开始整合一个第三方库,其中包括一些本地代码。该供应商的共享对象(.so文件)也被预先混淆了,这使得调试任何崩溃都很困难。

也许你的应用程序中包含的共享库已经被混淆了,你需要防止 "双重混淆"。如果你不防止额外的混淆,你很有可能会遇到问题。

在与我们的应用集成的过程中,我们注意到只有在我们发布的混淆构建中才会出现运行时崩溃。啊哦! 代码混淆对我们应用的安全性至关重要,所以我们需要在下一个版本发布前快速修复崩溃问题。

快速向@VarPete / Panayiotis "Pete" Varvarezis致敬,感谢他帮助我们调试崩溃,并给我指出了所有正确的方向。

所以,我们把调试这个问题的解决方案放在一起...... 好吧,让我们来看看一个简单的应用程序示例,并应用我们解决本地崩溃的分析和调试步骤。

示例应用程序 - NativeCrashApp

示例应用的序列图

注意:这个示例应用在制作应用中是没有逻辑意义的,只是作为教学辅助工具使用。所以,我就不多说了...

该应用程序的流程很简单(也没有必要),但包括一些有趣的行为。最主要和唯一的功能是以消费者友好的格式向用户显示他们设备的名称--而不是Build.MODEL返回的毫无生气的模型名称。我们正在使用Jared RummlerAndroidDeviceNames库来实现这一目的。

下面是流程...

1. 用户启动应用程序

在启动时,应用程序会用我们的自定义Android库查找设备名称。

2. 库调用到本机层面

原生级(我们的C++库)是通过JNI(Java Native Interface)调用的。

3. 通过反射调用回Android库

通过反射调用Android库来检查设备的人名。

4. 将设备名称返回链上

最后,我们将设备名称返回到应用程序,并将其显示在屏幕上。真是太爽了!

应用实例截图

:很显然,这一切可能只是发生在Activity中。Android库和C++库完全没有必要。但是,这样更有趣。

但是,Bugs怎么办?

为了这个练习的目的,我偷偷地添加了一些! 现在,在项目repo中查看破损的产品风味,你会发现一些bug,需要调试后才能如上所述的快乐路径工作。

代码收缩和混淆

作为负责任的Android开发者,我们希望通过利用代码缩减和混淆来提高应用的安全性。那么,让我们应用我们选择的代码混淆工具,比如ProGuard! 这个过程会检测并删除我们打包的应用中未使用的类、字段、方法和属性。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles 'custom-proguard-rules.pro'
        }
    }
}

Bug #1

不幸的是,当我们测试我们的应用程序的发行版时,我们发现了一个崩溃。

6495-6495/com.jacksoncheek.nativecrashapp.broken E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.jacksoncheek.nativecrashapp.broken, PID: 6495
    java.lang.UnsatisfiedLinkError: No implementation found for void com.jacksoncheek.a.a.a(boolean) (tried Java_com_jacksoncheek_a_a_a and Java_com_jacksoncheek_a_a_a__Z)
        at com.jacksoncheek.a.a.a(Native Method)
        at com.jacksoncheek.a.a.a(Unknown Source:17)
        at com.jacksoncheek.nativecrashapp.MainActivity.onCreate(Unknown Source:39)
        at android.app.Activity.performCreate(Activity.java:7136)
        at android.app.Activity.performCreate(Activity.java:7127)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

类com.jacksoncheek.a.a(boolean)没有实现,但我们不知道那是什么。让我们检查一下 ProGuard 输出的映射文件 mapping.txt。它包含了原始和混淆类、方法和字段名之间的翻译。

com.jacksoncheek.devicepropertieslib.DevicePropertiesNative -> com.jacksoncheek.a.a:
    java.lang.String TAG -> a
    boolean libraryLoaded -> b
    boolean nativeLibraryLoaded() -> b
    java.lang.String getDeviceNameNative() -> a
    java.lang.String checkDeviceProperties() -> c
    void setLogDebugMessages(boolean) -> a
    void <init>() -> <init>

好了,现在我们知道ProGuard不适当地混淆了我们的一些代码。这种类型的错误在混淆中很常见。 小贴士:ProGuard不会检查本地代码,所以它不会自动保留本地代码中通过反射调用的类或类成员。这时候也要--保留这些方法中的任何一个!

-keepclasseswithmembernames,includedescriptorclasses class * { 
    native <methods>; 
}
  • keepclasseswithmembernames - 保留类和本地方法名。
  • includedcriptorclasses - 保留返回和参数类型。

Bug #2

所以,让我们再次测试该应用程序。另一个(不同的)崩溃!

6615-6615/? A/crashapp.broke: JNI DETECTED ERROR IN APPLICATION: JNI CallObjectMethodV called with pending exception java.lang.NoSuchMethodError: no non-static method "Lcom/jacksoncheek/devicepropertieslib/DevicePropertiesNative;.getDeviceName()Ljava/lang/String;"
6615-6615/? A/crashapp.broke:   at java.lang.String com.jacksoncheek.devicepropertieslib.DevicePropertiesNative.checkDeviceProperties() ((null):-2)
6615-6615/? A/crashapp.broke:   at java.lang.String com.jacksoncheek.devicepropertieslib.DevicePropertiesNative.a() ((null):-1)
6615-6615/? A/crashapp.broke:   at void com.jacksoncheek.nativecrashapp.MainActivity.onCreate(android.os.Bundle) ((null):-1)
6615-6615/? A/crashapp.broke:   at void android.app.Activity.performCreate(android.os.Bundle, android.os.PersistableBundle) (Activity.java:7136)
6615-6615/? A/crashapp.broke:   at void android.app.Activity.performCreate(android.os.Bundle) (Activity.java:7127)
6615-6615/? A/crashapp.broke:   at void android.app.Instrumentation.callActivityOnCreate(android.app.Activity, android.os.Bundle) (Instrumentation.java:1271)
6615-6615/? A/crashapp.broke:   at android.app.Activity android.app.ActivityThread.performLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent) (ActivityThread.java:2893)
6615-6615/? A/crashapp.broke:   at android.app.Activity android.app.ActivityThread.handleLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.app.servertransaction.PendingTransactionActions, android.content.Intent) (ActivityThread.java:3048)
6615-6615/? A/crashapp.broke:   at void android.app.servertransaction.LaunchActivityItem.execute(android.app.ClientTransactionHandler, android.os.IBinder, android.app.servertransaction.PendingTransactionActions) (LaunchActivityItem.java:78)
6615-6615/? A/crashapp.broke:   at void android.app.servertransaction.TransactionExecutor.executeCallbacks(android.app.servertransaction.ClientTransaction) (TransactionExecutor.java:108)
6615-6615/? A/crashapp.broke:   at void android.app.servertransaction.TransactionExecutor.execute(android.app.servertransaction.ClientTransaction) (TransactionExecutor.java:68)
6615-6615/? A/crashapp.broke:   at void android.app.ActivityThread$H.handleMessage(android.os.Message) (ActivityThread.java:1808)
6615-6615/? A/crashapp.broke:   at void android.os.Handler.dispatchMessage(android.os.Message) (Handler.java:106)
6615-6615/? A/crashapp.broke:   at void android.os.Looper.loop() (Looper.java:193)
6615-6615/? A/crashapp.broke:   at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6669)
6615-6615/? A/crashapp.broke:   at java.lang.Object java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) (Method.java:-2)
6615-6615/? A/crashapp.broke:   at void com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run() (RuntimeInit.java:493)
6615-6615/? A/crashapp.broke:   at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:858)
6615-6615/? A/crashapp.broke: 
6615-6615/? A/crashapp.broke:     in call to CallObjectMethodV
6615-6615/? A/crashapp.broke:     from java.lang.String com.jacksoncheek.devicepropertieslib.DevicePropertiesNative.checkDeviceProperties()
6615-6615/? A/crashapp.broke: "main" prio=5 tid=1 Runnable
6615-6615/? A/crashapp.broke:   | group="main" sCount=0 dsCount=0 flags=0 obj=0x75233ee0 self=0xe525d000
6615-6615/? A/crashapp.broke:   | sysTid=6615 nice=-10 cgrp=default sched=0/0 handle=0xe9e84494
6615-6615/? A/crashapp.broke:   | state=R schedstat=( 179191705 116256360 118 ) utm=9 stm=8 core=1 HZ=100
6615-6615/? A/crashapp.broke:   | stack=0xff79a000-0xff79c000 stackSize=8MB
6615-6615/? A/crashapp.broke:   | held mutexes= "mutator lock"(shared held)

看起来像是另一个混淆错误。

java.lang.NoSuchMethodError: no non-static method "Lcom/jacksoncheek/devicepropertieslib/DevicePropertiesNative;.getDeviceName()Ljava/lang/String;"

这个就比较棘手了。我们的类名DevicePropertiesNative,方法名getDeviceName,参数类型()--在本例中是void,返回类型Ljava/lang/String都找不到。

所以,我们需要保留类和原生方法不被混淆,但也要保留返回和参数类型。这样可以保证整个方法签名与本地库保持兼容。

我们需要在ProGuard配置中添加一个特殊的-keep规则来防止混淆getDeviceName()方法。ProGuard手册中提供了很多关于不同配置选项的信息。

-keepclassmembers class com.jacksoncheek.devicepropertieslib.DevicePropertiesNative {
     java.lang.String getDeviceName();
}

Bug #3

所以,再次测试......原生的崩溃--终于!


6759-6759/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
6759-6759/? A/DEBUG: Build fingerprint: 'google/sdk_gphone_x86/generic_x86:9/PSR1.180720.061/5075414:userdebug/dev-keys'
6759-6759/? A/DEBUG: Revision: '0'
6759-6759/? A/DEBUG: ABI: 'x86'
6759-6759/? A/DEBUG: pid: 6740, tid: 6740, name: crashapp.broken  >>> com.jacksoncheek.nativecrashapp.broken <<<
6759-6759/? A/DEBUG: signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0xff799ffc
6759-6759/? A/DEBUG:     eax e51dc375  ebx e60f4754  ecx 00001000  edx e60f6254
6759-6759/? A/DEBUG:     edi 0000000a  esi ff79a568
6759-6759/? A/DEBUG:     ebp ff79a048  esp ff79a000  eip e607a7c4
6759-6759/? A/DEBUG: backtrace:
6759-6759/? A/DEBUG:     #00 pc 000727c4  /system/lib/libc.so (__sfvwrite+452)
6759-6759/? A/DEBUG:     #01 pc 00068b65  /system/lib/libc.so (__vfprintf+11221)
6759-6759/? A/DEBUG:     #02 pc 00087cac  /system/lib/libc.so (printf+92)
6759-6759/? A/DEBUG:     #03 pc 000008e8  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+56)
6759-6759/? A/DEBUG:     #04 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #05 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #06 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #07 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #08 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #09 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)
6759-6759/? A/DEBUG:     #10 pc 000008fe  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+78)

这是虚拟内存地址0xff799ffc处的一个分段故障(SIGSEGV),但它并没有提供多少有用的信息。SEGV_ACCERR发生在指针试图写入一个它有无效访问权限的对象时。

现在是时候挖掘日志并找到 "墓碑",即本地崩溃的崩溃转储。用*** ***在日志中搜索墓碑的开头。

这些信息包括

  • 构建指纹 - 匹配ro.build.fingerprint系统属性。
  • 硬件版本--与ro.revolution系统属性相匹配。
  • ABI(Application Binary Interface)--处理器指令集架构,armeabi-v7a是Android设备最常用的。
  • 崩溃的进程名称>>... <<<(和进程ID)和线程名称名称:...(和线程ID)
  • 终止信号类型SIGSEGV,该信号的接收方式SEGV_ACCER,以及内存中的故障地址。
  • CPU寄存器
  • 调用的堆栈内容(回溯)

原生崩溃调试

调查回溯

PC(程序计数器)值是相对于共享库位置的内存地址。这是我们了解最多的关于本机崩溃的信息和它在库中的位置。

6759-6759/? A/DEBUG: #03 pc 000008e8  /data/app/com.jacksoncheek.nativecrashapp.broken-MzKT-aIUbK4D4IxEzg3yTA==/lib/x86/libproperty-checker.so (accidentallyForceStackOverflow(int)+56)

我们的崩溃发生在libproperty-checker.so中调用栈顶部的内存地址000008e8。

Android NDK堆栈提供了两个有助于调试墓碑的工具--ndk-stack和addr2line。使用Android Studio包管理器安装NDK工具,并将NDK目录添加到你的.bash_profile路径中。

ndk-stack

ndk-stack工具将墓碑的堆栈痕迹符号化。它将内存地址转换为本地库源代码中相应的源文件和行号。

$NDK/ndk-stack -sym <path> [-dump <path>]

addr2line

你也可以使用addr2line工具,取原生代码导致崩溃的内存地址来获取源文件名和行。它是NDK工具链的一部分。确保将addr2line用于设备的ABI类型,例如x86(不常见)、armeabi或armeabi-v7a(最常见)。

在这种情况下,x86 ABI类型的addr2line路径是

~/Library/Android/sdk/ndk-bundle/toolchains/x86–4.9/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line

用法:

addr2line -C -f -e <libPath> <memoryAddress>

例子

i686-linux-android-addr2line -C -f -e libproperty-checker.so 000008e8
accidentallyForceStackOverflow(int)
~/NativeCrashApp/brokendevicepropertieslib/src/main/jni/propertyChecker.cpp:64

现在我们知道了原生方法不小心ForceStackOverflow(int),源文件propertyChecker.cpp,以及导致我们原生崩溃的第64行。

我们发现了我们的原生bug。该库通过无限调用一个非终止递归函数,意外地迫使堆栈溢出错误。这里的快速解决方案是删除这个方法的所有用法。

在现实世界中,你可能正在使用供应商库的发布版本,所以你不会有源代码可以使用。并不是所有的.so文件都对ndk-stack的调试有用,因为发布的库一般都使用剥离的二进制文件,这使得它们更难调试。

这就是addr2line工具真正有用的地方。如果你的崩溃所在的本地方法名没有被打印在墓碑中,而墓碑并不能保证所有的设备都是如此,你可以使用addr2line来获取本地方法名。

首先,解压.apk(解压即可),并从/lib目录下解压应用程序中打包的.so文件。然后解压设备ABI类型的共享库,例如armeabi-v7a。

注意:这些文件也位于/app/src/main/jniLibs目录中。

./i686-linux-android-addr2line -C -f -e libproperty-checker.so 000008e8
accidentallyForceStackOverflow(int)
??:?

我们没有得到崩溃发生的行号或源文件,因为APK只包含剥离的二进制文件,但我们找到了方法名--意外地ForceStackOverflow(int)。还是很有帮助的!

TL;DR - 原生崩溃调试步骤

  1. 检查多个设备架构类型的错误。
  2. 反编译.apk并确保每个架构的共享库(.so)文件存在。
  3. 通过检查共享库(.so)是否在运行时被加载,检查Android包管理器是否正确地将本地代码与应用程序一起安装。你可以使用Native Libs Monitor来轻松地检查设备上是否有本地库的应用程序(而且很容易),但我不能保证在具有专有调试构建的设备上使用这个应用程序的安全性。
  4. 在你的ProGuard配置中添加特殊的-keep规则,以保留类和原生方法不被混淆,但也保留返回和参数类型。
  5. 使用ndk-stack和addr2line工具分析本地崩溃 "墓碑"。

完整的代码在GitHub上。 在LinkedIn上与我联系,或在Twitter上关注我!


www.deepl.com 翻译