使用GDB在VS Code调试Android C/C++代码(无需Android源码)

需求分析

在开发Android Native程序时(仅C/C++代码,无APK应用),之前在调试的过程中一直只是使用添加LOG的方式来定位程序的问题,而在Linux上开发平台程序时,可以方便使用GDB工具来调试,所以迫切的希望在调试Android Native程序也能一样方便。

探索过程

官方文档说明

在Android官方文档使用 GDB中介绍了如何使用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++代码。但存在的问题是,我们没有对应的Android源码时,该怎么办呢?

前人使用记录

Android gdb调试中详细阐述了使用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真机中展示了如何使用VS Code调试Android native代码。里面简单的提到了如何配置VS Code中的launch.json文件。在Android Debugging with Visual Studio Code中提到gdb工具可以使用Android NDK中内置的,而不是使用Android源码中的,显然这样比使用源码中的会更加方便。gdb的具体路径:
Windows <NDK_ROOT>\prebuilt\windows-x86_64\bin
Linux <NDK_ROOT>\prebuilt\linux-x86_64\bin
macOS <NDK_ROOT>\prebuilt\darwin-x86_64\bin
同时该文章详细记录了VS Code调试配置的过程,是一篇非常有用的参考文档。现在我们探索的过程已经差不多了,从中了解了如何使用VS Code + NDK在无需Android源码下调试Native程序。接下来我们实际操作一下,并观察是否会存在未知的问题。

来实践一下吧

先决条件

1、安装Visual Studio Code以及在其中安装C/C++扩展插件

2、下载好Android NDK,方便使用其中GDB,它在<NDK_ROOT>/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++扩展插件是否安装。
选择配置

选择默认配置,然后会在当前项目中.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 ++调试文档,日志记录属性"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文档

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:调试运行中的应用或进程

pid=`adb shell pidof test`
adb shell gdbserver64 :9090 --attach $pid

方式2:如需在进程启动时对其进行调试

adb shell gdbserver64 :9090 /system/bin/test

我们这里使用方式2测试,详细使用方法可以查看Android官方文档使用 GDB

启动调试

一切准备ok,接下可以开始调试了。
三种方式启动:
1、通过单击VS Code窗口左侧的Debug图标,启用“开始调试”面板;
2、从面板顶部的列表中选择“运行”,然后单击执行“启动调试”按钮;
3、直接快捷键F5。

一旦调试器开始连接,VSCode调试控制台将显示来自调试器的消息,并在必要时允许执行手动调试器命令(必须停​​止程序以执行调试命令)。调试面板将显示调试信息(变量监视,调用堆栈,断点等),调试器工具栏将提供对常见调试命令的访问。还支持基于鼠标光标的变量显示。

后记

在上述测试用例中,由于代码简单,调试过程比较流畅。但在实际项目使用中,发现开启调试时非常缓慢。下面尝试解决此问题。

在VS Code调试控制台中会打印一些gdb的信息,从中可以看到它从手机端的动态库中读取了符号表信息。这就是导致启动过程缓慢的原因。
调试控制台

设置sysroot指定动态库路径

Very slow debugging中提出了解决方案。通过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 set sysroot=./debug_so_path/

更加方便的设置方式是在前面提到的launch.json文件中直接添加。

            "setupCommands": [
                {
                    "text": "-enable-pretty-printing",
                    "description": "为 gdb 启用整齐打印"
                },
                // 新增
                {
                    "text": "set sysroot ${workspaceFolder}/debug_so_path/",
                    "description": "set sysroot path"
                },
            ],

接下来测试中,发现依然比较慢,继续分析原因发现是因为依赖的库太多,需要加载很多符号表,导致缓慢。

减少库的依赖

使用readelf查看目标程序的依赖关系

chenls@chenls-pc:arm64-v8a$ readelf -a test | grep NEED
  [ 9] .gnu.version_r    VERNEED          0000000000000510  00000510
 0x0000000000000001 (NEEDED)             共享库:[libm.so]
 0x0000000000000001 (NEEDED)             共享库:[liblog.so]
 0x0000000000000001 (NEEDED)             共享库:[libc.so]
 0x0000000000000001 (NEEDED)             共享库:[libdl.so]
 0x000000006ffffffe (VERNEED)            0x510
 0x000000006fffffff (VERNEEDNUM)         1
chenls@chenls-pc:arm64-v8a$ 

我所在实际项目中由于依赖了libandroid.so ,libandroid.so 又依赖了其它非常多的动态库,这就会导致加载符号表时非常慢,目前的解决方案是,在调试时修改代码,减少库的依赖,加快调试速度。此方法只是避开了问题,若有更好的方式,请告知。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值