在调试和分析包含原生代码的应用时,使用在进程启动时需要启用的调试工具通常很有用。这要求您在新的进程中运行应用,而不是从 zygote 克隆。例如:
- 使用 strace 跟踪系统调用。
- 使用 malloc 调试 或 地址消毒剂 (ASan) 查找内存错误。
- 使用 Simpleperf 进行分析。
使用包装 shell 脚本
使用 wrap.sh
很容易
- 编译一个包含以下内容的自定义可调试 APK:
- 一个名为
wrap.sh
的 shell 脚本。有关更多详细信息,请参阅 创建包装 shell 脚本 和 打包 wrap.sh。 - 您的 shell 脚本需要的任何额外工具(例如您自己的
strace
二进制文件)。
- 一个名为
- 在设备上安装可调试 APK。
- 启动应用。
创建包装 shell 脚本
启动包含 wrap.sh
的可调试 APK 时,系统会执行该脚本并将启动应用的命令作为参数传递。该脚本负责启动应用,但可以进行任何环境或参数更改。该脚本应遵循 MirBSD Korn shell (mksh) 语法。
以下代码段展示了如何编写一个简单的 wrap.sh
文件,该文件仅启动应用
#!/system/bin/sh exec "$@"
Malloc 调试
要通过 wrap.sh
使用 malloc 调试,您需要包含以下行
#!/system/bin/sh LIBC_DEBUG_MALLOC_OPTIONS=backtrace logwrapper "$@"
ASan
在 ASan 文档 中,有一个关于如何为 ASan 执行此操作的示例。
打包 wrap.sh
要利用 wrap.sh
,您的 APK 必须可调试。确保在您的 Android 清单中的 <application>
元素中配置了 android:debuggable="true"
设置,或者如果您使用的是 Android Studio,则您已在 build.gradle
文件中配置了调试构建。
还需要在您的应用的 build.gradle
文件中将 useLegacyPackaging
设置为 true
。在大多数情况下,此选项默认设置为 false
,因此您可能需要将其显式设置为 true
,以避免出现任何意外情况。
您必须将 wrap.sh
脚本与应用的原生库一起打包。如果您的应用不包含原生库,请手动将 lib 目录添加到您的项目目录中。对于您的应用支持的每种架构,您都必须在该原生库目录下提供一个包装 shell 脚本副本。
以下示例展示了用于支持 ARMv8 和 x86-64 架构的文件布局
# App Directory |- AndroidManifest.xml |- … |- lib |- arm64-v8a |- ... |- wrap.sh |- x86_64 |- ... |- wrap.sh
Android Studio 仅打包来自 lib/
目录的 .so
文件,因此如果您是 Android Studio 用户,则需要将您的 wrap.sh
文件放在 src/main/resources/lib/*
目录中,以便它们被正确打包。
请注意,resources/lib/x86
将在 UI 中显示为 lib.x86
,但它实际上应该是一个子目录。
使用 wrap.sh 进行调试
如果您想在使用 wrap.sh
时附加调试器,则您的 shell 脚本需要手动启用调试。 如何执行此操作在不同版本之间有所不同,因此此示例显示了如何在所有支持 wrap.sh
的版本中添加相应的选项。
#!/system/bin/sh
cmd=$1
shift
os_version=$(getprop ro.build.version.sdk)
if [ "$os_version" -eq "27" ]; then
cmd="$cmd -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable $@"
elif [ "$os_version" -eq "28" ]; then
cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable $@"
else
cmd="$cmd -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y $@"
fi
exec $cmd