需求分析
在开发 Android Native 程序时(仅 C/C++代码,无 APK 应用),之前在调试的过程中一直只是使用添加 LOG 的方式来定位程序的问题,而在 Linux 上开发平台程序时,可以方便使用 GDB 工具来调试,所以迫切的希望在调试 Android Native 程序也能一样方便。
探索过程
官方文档说明
在 Android 官方文档使用 GDB[1]中介绍了如何使用 GDB 调试。它提到的前置条件是使用 Android 源码中 gdbclient.py 脚本,此脚本会设置端口转发,在设备上启动相应的 gdbserver,在主机上启动相应的 gdb,配置 gdb 以查找符号,然后将 gdb 连接到远程 gdbserver。后续还提到可以使用 VS Code 调试程序前端(而非 GDB CLI 接口)来控制和调试在设备上运行的原生代码。它使用了 gdbclient.py 脚本生成 json 文件,然后在 VS Code 中的 launch.json 文件配置。若有调试机对应的 Android 源码,这当然是一种很好的方式,详细使用方法可以参考使用 VS Code 调试 Android C++代码[2]。但存在的问题是,我们没有对应的 Android 源码时,该怎么办呢?
前人使用记录
在Android gdb 调试[3]中详细阐述了使用 gdb 调试的过程。Android 对于 C/C++代码的调试方式一般选用 gdb+gdbserver 的方式,其中 gdbserver 运行在目标系统中(如手机),gdb 运行在宿主机上(如 linux)。一般 android 源码中已有编译好的 gdbserver 和 gdb 程序,如在高通 msm8976(64 位)平台上,使用的 gdbserver 位于:prebuilts/misc/android-arm64/gdbserver64,gdb 位于:prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android-gdb。在目标手机中,若系统是 userdebug 或 eng 版本,gdbserver 已经安装在 system/bin/下。在vscode debug Android 真机[4]中展示了如何使用 VS Code 调试 Android native 代码。里面简单的提到了如何配置 VS Code 中的 launch.json 文件。在Android Debugging with Visual Studio Code[5]中提到 gdb 工具可以使用 Android NDK 中内置的,而不是使用 Android 源码中的,显然这样比使用源码中的会更加方便。gdb 的具体路径:Windows \prebuilt\windows-x86_64\bin Linux \prebuilt\linux-x86_64\bin macOS \prebuilt\darwin-x86_64\bin 同时该文章详细记录了 VS Code 调试配置的过程,是一篇非常有用的参考文档。现在我们探索的过程已经差不多了,从中了解了如何使用 VS Code + NDK 在无需 Android 源码下调试 Native 程序。接下来我们实际操作一下,并观察是否会存在未知的问题。
来实践一下吧
先决条件
1、安装 Visual Studio Code 以及在其中安装C/C++扩展插件[6]。
2、下载好Android NDK[7],方便使用其中 GDB,它在/prebuilt/linux-x86_64/bin/目录下。
3、编译项目时,需要添加调试信息,即在 Android.mk 中添加 LOCAL_CFLAGS += -g,或者通过在 ndk-build 命令行上传递 NDK_DEBUG=1。同时如果项目的 Application.mk 文件指定 APP_OPTIM 设置,必须将其设置为 debug 以禁用编译器优化。
调试设置
1、VS Code 中打开需要调试的项目,点击运行>>启动调试(或直接按 F5 快捷键)。
在弹出的窗口中选择 C++(GDB/LLDB),若无此选项请检查C/C++扩展插件[8]是否安装。
选择默认配置,然后会在当前项目中.vscode 中生成 launch.json 文件。
2、launch.json 的设置属性如下所述。
program:要调试的程序。这应该指向带有调试符号的可执行文件的本地版本(非剥离版本),通常在项目的构建目录下的 obj/local/armeabi-v7a 中(对于 64 位构建,则位于 obj/local/arm64-v8a 中)。miDebuggerPath:gdb 可执行文件的路径。如上所示,它应指向 Android NDK 中的目录。miDebuggerServerAddress:要连接的目标地址。preLaunchTask:在启动调试器之前要执行的任务。
示例 launch.json 文件:
{
"version": "0.2.0",
"configurations": [
{
"name": "gdb android",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/obj/local/arm64-v8a/test",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
"setupCommands": [
{
"text": "-enable-pretty-printing",
"description": "为 gdb 启用整齐打印"
},
],
// 打印更多调试log
// "logging": {
// "engineLogging": true,
// },
"preLaunchTask": "build task",
"miDebuggerPath": "${env:ANDROID_NDK}/prebuilt/linux-x86_64/bin/gdb",
"miDebuggerServerAddress": ":9090"
}
]
}
有关其他信息,请参考 C/C ++调试文档[9],日志记录属性"engineLogging"可用于启用其他日志记录输出,这对于调试器未按预期运行时的故障排除很有用。
3、前面提到 preLaunchTask 任务,这些任务也可以用于支持调试。可以定义一个任务来编译代码,push 程序到手机端,转发调试器端口等操作。在.vscode 中添加一个 tasks.json 文件。如下:
{
"version": "2.0.0",
"tasks": [
{
"label": "build task",
"type": "shell",
"command": "source debug.sh"
}
]
}
注意 task 中"label"的值和 launch 中"preLaunchTask"的值一致。这里我们执行一个 shell 命令,将具体的任务放在 debug.sh 中处理。有关任务配置的更多信息,请参见VSCode 文档[10]。
4、在项目中新建 debug.sh 文件,内容如下:
#!/bin/bash
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=./Application.mk APP_BUILD_SCRIPT=Android.mk clean
ndk-build NDK_DEBUG=1 NDK_PROJECT_PATH=. NDK_APPLICATION_MK=./Application.mk APP_BUILD_SCRIPT=Android.mk -j16
adb push obj/local/arm64-v8a/test /system/bin/
adb shell chmod 777 /system/bin/test
adb forward tcp:9090 tcp:9090
# adb shell gdbserver64 :9090 /system/bin/test
gnome-terminal -- bash -c "adb shell gdbserver64 :9090 /system/bin/test"
a、首先使用 ndk-build 编译代码,里面增加了 NDK_DEBUG=1,使其生成的程序包含调试信息;再将生成的程序 push 到/system/bin/目录下,同时赋予可执行权限。
b、设置端口转发 在开发机上设置端口转发,命令如下:
adb forward tcp:9090 tcp:9090
命令说明:表示通过 adb 映射 tcp 端口 1234,命令中前面的是 local 的端口,后面的是 remote 的端口。命令中的端口号必须与 gdbserver 命令中的监听端口号相同,否则会导致 gdb 无法与 gdbserver 连接。
c、在目标设备上执行 gdbserver(32bit 的程序)或者 gdbserver64(64bit 的程序) 方式 1:调试运行中的应用或进程
test`
方式 2:如需在进程启动时对其进行调试
test
我们这里使用方式 2 测试,详细使用方法可以查看 Android 官方文档使用 GDB[11]。
启动调试
一切准备 ok,接下可以开始调试了。三种方式启动:1、通过单击 VS Code 窗口左侧的 Debug 图标,启用“开始调试”面板;2、从面板顶部的列表中选择“运行”,然后单击执行“启动调试”按钮;3、直接快捷键 F5。
一旦调试器开始连接,VSCode 调试控制台将显示来自调试器的消息,并在必要时允许执行手动调试器命令(必须停 止程序以执行调试命令)。调试面板将显示调试信息(变量监视,调用堆栈,断点等),调试器工具栏将提供对常见调试命令的访问。还支持基于鼠标光标的变量显示。
后记
在上述测试用例中,由于代码简单,调试过程比较流畅。但在实际项目使用中,发现开启调试时非常缓慢。下面尝试解决此问题。
在 VS Code 调试控制台中会打印一些 gdb 的信息,从中可以看到它从手机端的动态库中读取了符号表信息。这就是导致启动过程缓慢的原因。
设置 sysroot 指定动态库路径
在Very slow debugging[12]中提出了解决方案。通过 set sysroot 指定动态库的路径。我们需要先将所依赖的库 pull 到本机,如:
fingerprint=`adb shell getprop | grep ro.build.fingerprint`
old_fingerprint=$(cat debug_so_path/fingerprint.txt)
if [ "$old_fingerprint" != "$fingerprint" ]; then
echo "$fingerprint" > debug_so_path/fingerprint.txt
adb pull system/lib64/libm.so debug_so_path/system/lib64/
adb pull system/lib64/liblog.so debug_so_path/system/lib64/
adb pull system/lib64/libz.so debug_so_path/system/lib64/
adb pull system/lib64/libc.so debug_so_path/system/lib64/
adb pull system/lib64/libdl.so debug_so_path/system/lib64/
adb pull system/lib64/libc++.so debug_so_path/system/lib64/
adb pull system/lib64/libnetd_client.so debug_so_path/system/lib64/
adb pull system/bin/linker64 debug_so_path/system/bin/
fi
这里是判断了手机指纹信息,若调试手机变更,则需重新 pull。这时可以在上述的调试控制台中设置 solib_path。
exec
更加方便的设置方式是在前面提到的 launch.json 文件中直接添加。
"setupCommands": [
接下来测试中,发现依然比较慢,继续分析原因发现是因为依赖的库太多,需要加载很多符号表,导致缓慢。
减少库的依赖
使用 readelf 查看目标程序的依赖关系
test | grep NEED
我所在实际项目中由于依赖了 libandroid.so ,libandroid.so 又依赖了其它非常多的动态库,这就会导致加载符号表时非常慢,目前的解决方案是,在调试时修改代码,减少库的依赖,加快调试速度。此方法只是避开了问题,若有更好的方式,请告知。
参考资料
[1]使用 GDB: https://source.android.google.cn/devices/tech/debug/gdb?hl=zh-cn
[2]使用 VS Code 调试 Android C++代码: https://blog.csdn.net/zhaojia92/article/details/99774704
[3]Android gdb 调试: https://blog.csdn.net/l460133921/article/details/52931328/
[4]vscode debug Android 真机: https://blog.csdn.net/qq_40888343/article/details/103085967
[5]Android Debugging with Visual Studio Code: https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/android-debugging-with-visual-studio-code-r4820/
[6]C/C++扩展插件: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools
[7]Android NDK: https://developer.android.google.cn/ndk/downloads/
[8]C/C++扩展插件: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools
[9]C/C ++调试文档: https://code.visualstudio.com/docs/cpp/launch-json-reference
[10]VSCode 文档: https://code.visualstudio.com/docs/editor/tasks
[11]使用 GDB: https://source.android.google.cn/devices/tech/debug/gdb?hl=zh-cn
[12]Very slow debugging: https://stackoverflow.com/questions/38080460/very-slow-debugging