在调试和分析包含原生代码的应用时,通常需要使用在进程启动时需要启用的调试工具。这要求您在新的进程中运行应用,而不是从 zygote 克隆。示例包括:
- 使用 strace 追踪系统调用。
- 使用 malloc 调试 或 Address Sanitizer (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目录添加到您的项目目录中。对于您的应用支持的每个架构,您都必须在该原生库目录下提供一个wrap 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