嵌入式设备 gdbserver 可视化调试

技术背景

嵌入式设备资源相对较少,为了节省存储空间一般会对应用程序做 strip 操作。应用程序被 strip 后,符号表会被删除,导致 gdb 调试捕捉不到关键信息。选择 gdbserver 远程调试是一个不错的方案。

在目标板子上用 gdbserver 把应用程序运行起来,在 Linux 主机上用交叉编译工具链的 gdb 把没有 strip 的应用程序运行,并且连接到 gdbserver,就可以进行调试了,再配合 VSCode,还可以可视化调试。

gdbserver 编译

为了避免兼容的问题,gdb 的版本要跟交叉编译工具链的 gdb 版本保持一致。gdb server 是包含在 gdb 工程中的,因此,编译 gdbserver 要下载 gdb 源码压缩包,在 gdb 下载页面 下载交叉编译工具链对应的版本。

我使用的交叉编译工具链的 gdb 版本为 7.6.2,因此下载 gdb-7.6.2。

1
2
3
curl https://ftp.gnu.org/gnu/gdb/gdb-7.6.2.tar.bz2 -o gdb-7.6.2.tar.bz2
tar xvf gdb-7.6.2.tar.bz2
cd gdb-7.6.2/gdb/gdbserver

为了方便,把编译命令放到 shell 脚本中,执行脚本进行编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh

COMPILE_PREX=/home/kyson/toolchain/rsdk-4.6.4-4181-EB-3.10-u0.9.33-m32-150324/bin/mips-linux-
PREFIX=/tmp
HOST=mips-linux

AR=${COMPILE_PREX}ar
CC=${COMPILE_PREX}gcc
NM=${COMPILE_PREX}nm
CPP=${COMPILE_PREX}g++
RANLIB=${COMPILE_PREX}ranlib

CC=${CC} CPP=${CXX} NM=${NM} AR=${AR} RANLIB=${RANLIB} ./configure --prefix=${PREFIX} --host=${HOST}

make & make install

脚本的三个变量:

  • COMPILE_PREX: 指定交叉编译工具链前缀,要替换掉。

  • PREFIX: 指定执行 make install 的安装路径。

  • HOST: 指定目标系统类型。

这里指定的安装路径是 /tmp,编译成功后 gdbserver 位于 /tmp/local/bin/gdbserver,把它上传到目标设备就可以使用了。

gdbserver 使用

用交叉编译工具链的 gcc 编译应用程序,并上传到目标设备上。同样地,把前面编译生成的 gdbserver 上传到目标设备上。这里为了演示,写了一个简单的应用程序用来演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

static void copy_str(char *str)
{
/* 空指针赋值,段错误 */
strcpy(str, "Hello World");
}

int main(int argc, char **argv)
{
char *prt = NULL;

copy_str(prt);

return 0;
}

把 gcc 替换成自己本地交叉编译工具链的 gcc,编译应用程序。

1
/home/kyson/toolchain/rsdk-4.6.4-4181-EB-3.10-u0.9.33-m32-150324/bin/mips-linux-gcc -g my_prog.c -o my_prog

应用程序要同时在设备端和 Linux 主机端运行,设备端用 gdbserver 运行,Linux 主机端用交叉编译工具链的 gdb 运行。

嵌入式设备端

gdbserver 把将要调试的应用程序运行起来,设备端的程序可以是 strip 过的,这里演示使用的是上面编译的测试程序 my_prog

1
./gdbserver :12120 my_prog

12120 表示监听的端口号,随便指定一个可用的端口即可。这里没有指定 IP 地址,表示 “0.0.0.0”,监听所有网络接口,如需要指定某个网络接口,可以指定 IP 地址。

Linux 主机端

启动调试

用交叉编译工具链中的 gdb 把上面编译的应用程序运行起来。

1
/home/kyson/toolchain/rsdk-4.6.4-4181-EB-3.10-u0.9.33-m32-150324/bin/mips-linux-gdb --args my_prog

–args 表示要调试的可执行文件后的参数被传递,也就是调试的程序是接收命令行参数的,要加上此参数。

注意,主机端的程序不能是 strip 的,因为 gdb 调试要找到符号表,并且编译要加上 -g 调试选项,否则调试获取不到关键信息。

连接 gdbserver

gdb 启动后,连接到设备端的 gdbserver。

1
2
(gdb) target remote 192.168.0.210:12120
Remote debugging using 192.168.0.210:12120

192.168.0.210 是设备端的 IP 地址。

12120 是设备端的端口,必须跟 gdbserver 启动指定的端口一致。

Tips: 连接不上检查一下 Linux 主机和设备是否同处于一个局域网,设备端的防火墙是否关闭。

设置动态库路径(可选)

如果程序依赖外部的动态库,可以设置查找库的路径。

1
(gdb) set solib-search-path [Directories]

Directories 表示动态库所在的路径,多个路径用 : 分割。

开始调试

因为 gdbserver 已经把程序启动了,主机端进入调试模式不需要执行 r (run),直接执行 c (continue) 就可以进行调试了。 更多 gdb 用法,可以参考 A Sample GDB Session

1
2
3
4
5
6
7
8
9
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0040084c in copy_str (str=0x0) at main.c:8
8 strcpy(str, "Hello World");
(gdb) bt
#0 0x0040084c in copy_str (str=0x0) at main.c:8
#1 0x004009bc in main (argc=1, argv=0x7fff6ea4) at main.c:15

从上面的调试结果可以定位到程序运行到 copy_str 函数的 strcpy 出现了段错误,符合预期结果。

gdbservere 调试有时候会遇到诸如这样的警告。

1
2
warning: .dynamic section for "/home/kyson/toolchain/msdk-4.4.7-mips-EL-3.10-u0.9.33-m32t-140827/bin/../lib/libdl.so.0" is not at the expected address (wrong library or version mismatch?)
warning: .dynamic section for "/home/kyson/workspace/toolchain/msdk-4.4.7-mips-EL-3.10-u0.9.33-m32t-140827/bin/../lib/libm.so.0" is not at the expected address (wrong library or version mismatch?)

这是 Linux 主机端使用的动态库与设备端使用的动态库不一致导致的,通常不影响我们的调试,可以忽略。想要消除这个警告,可以把交叉编译工具链 lib 目录下的动态库打包上传到目标设备上的 /tmp 目录下,然后在设备上执行 export LD_LIBRARY_PATH=[动态库路径],让设备端使用的动态库跟 Linux 主机端的一致。

VSCode 可视化调试

gdbserver 调试跑通之后,就可以配置 VSCode 进行可视化调试了。效果图:

为了方便,这里使用默认的编译配置模板(在 VSCode 菜单栏选择 Terminal > Configure Default Build Task)以及调试配置模板(在 VSCode 菜单栏选择 Run > Add Configuration…),然后稍微修改调整。

编译配置

编译配置 tasks.json 只需要修改 command 参数为交叉编译工具链的 gcc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "C/C++: gcc build active file",
"command": "/home/kyson/toolchain/rsdk-4.6.4-4181-EB-3.10-u0.9.33-m32-150324/bin/mips-linux-gcc",
"args": [
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

对于复杂的工程,一般只需要修改编译配置 tasks.json,用 Makefile 规则来组织 gcc 编译,可以参考 VSCode 开发利器 - C/C++ 开发 - 编译配置

调试配置

调试配置 launch.json 要修改 miDebuggerPath 参数为交叉编译工具链的 gdb,并添加 miDebuggerServerAddress 参数指定 gdbserver 的 IP 地址以及端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "gcc - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: gcc build active file",
"miDebuggerServerAddress": "192.168.0.210:12120",
"miDebuggerPath": "/home/kyson/toolchain/rsdk-4.6.4-4181-EB-3.10-u0.9.33-m32-150324/bin/mips-linux-gdb"
}
]
}

参考文档

[1] Using the gdbserver Program

[2] Remote Debugging or Debugging with a Local Debugger Server

评论