26、远程调试:gdbserver与交叉编译环境
嵌入式开发里,调试一直是个让人头疼的事。你想想看,代码在PC上编译得好好的,一烧到板子上就出幺蛾子。没有屏幕、没有键盘,printf大法虽然管用,但效率实在太低了。这时候,远程调试就成了救命稻草。
说白了,远程调试就是让PC上的GDB通过网络,去控制板子上的gdbserver。PC负责解析符号、显示源码、接受你的命令;板子只管执行、设断点、传数据。两边各干各的活,配合起来就很舒服。
远程调试的核心架构
我画了一张图,帮你理清整个流程:
这个架构其实不复杂。PC上跑的是带图形界面的GDB,或者命令行GDB也行。板子上跑的是gdbserver,一个轻量级的代理程序。两边通过TCP连接,GDB远程协议在中间传数据。
交叉编译环境准备
做远程调试,第一步就是搭好交叉编译环境。我刚开始搞这个的时候,光配环境就折腾了两天。后来发现,其实核心就三样东西:
- 交叉编译工具链:比如 arm-linux-gnueabihf-gcc
- 目标板的gdbserver:需要交叉编译出来
- PC端的GDB:要支持目标架构,比如 arm-linux-gnueabihf-gdb
关键点:PC上的GDB和板子上的gdbserver必须是同一套源码编译出来的。版本不一致,协议可能不兼容,调试时会莫名其妙地断连。
我记得有一次,我用了Ubuntu自带的GDB去连板子上的gdbserver,结果一设断点就报错。查了半天,发现PC上的GDB是8.2版本,板子上的gdbserver是7.12版本。重新编译一遍,问题就解决了。
编译gdbserver
gdbserver的源码在GDB的源码包里。你下载GDB源码后,进入 gdb/gdbserver 目录,交叉编译即可:
# 下载GDB源码
wget https://ftp.gnu.org/gnu/gdb/gdb-13.2.tar.xz
tar -xf gdb-13.2.tar.xz
cd gdb-13.2
# 配置交叉编译环境
export CC=arm-linux-gnueabihf-gcc
# 进入gdbserver目录
cd gdb/gdbserver
# 配置
./configure --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
# 编译
make
# 编译完成后,当前目录下就有 gdbserver 可执行文件
file gdbserver
# 输出:gdbserver: ELF 32-bit LSB executable, ARM, version 1 (SYSV)
小技巧:编译时加上 --enable-static 选项,生成静态链接的gdbserver。这样板子上即使缺少动态库也能跑。我习惯在发布固件时,把静态编译的gdbserver直接烧进根文件系统里。
启动调试会话
环境准备好了,接下来就是实战。假设你的程序叫 hello_arm,已经交叉编译好,带调试符号(编译时加 -g 选项)。
第一步:在板子上启动gdbserver
# 在目标板上执行
gdbserver :1234 ./hello_arm
# 输出类似:
# Process ./hello_arm created; pid = 887
# Listening on port 1234
这里 :1234 表示监听所有网卡的1234端口。如果你只想让特定IP连接,可以写成 192.168.1.100:1234。
第二步:在PC上启动GDB并连接
# 在PC上执行
arm-linux-gnueabihf-gdb ./hello_arm
# 进入GDB后,连接远程目标
(gdb) target remote 192.168.1.100:1234
# 连接成功后,GDB会显示:
# Remote debugging using 192.168.1.100:1234
# Reading symbols from ./hello_arm...
连接上了,你就可以像调试本地程序一样操作了。设断点、单步、查看变量,全在PC上完成。
常用调试命令
远程调试和本地调试的命令基本一样。我列几个最常用的:
| 命令 | 说明 | 示例 |
|---|---|---|
| break | 设置断点 | b main |
| continue | 继续运行 | c |
| next | 单步跳过 | n |
| step | 单步进入 | s |
| 打印变量 | p var_name | |
| info registers | 查看寄存器 | info reg |
| backtrace | 查看调用栈 | bt |
| detach | 断开连接 | detach |
注意:远程调试时,不要用 Ctrl+C 中断程序。GDB会发送中断信号给gdbserver,但网络延迟可能导致信号丢失。正确做法是在GDB里按 Ctrl+C,或者用 interrupt 命令。
避坑指南
做远程调试这些年,我踩过不少坑。挑几个典型的说说:
- 符号表不匹配:板子上跑的程序和PC上的符号文件必须是同一个编译产物。我曾经把代码改了一行,重新编译后忘了同步到板子,结果调试时变量地址全乱了。
- 网络不稳定:无线网络调试时,偶尔会断连。我建议用有线连接,或者至少保证信号强度。如果必须用无线,可以试试降低GDB的包重传超时时间。
- 路径问题:GDB在PC上找源码时,默认用编译时的路径。如果源码移动了位置,需要用 directory 命令重新指定源码路径。
- 优化选项:调试时一定要用 -O0 编译。用 -O2 优化后,变量可能被优化掉,单步执行也会跳来跳去,根本没法调。
我曾经在调试一个网络协议栈时,发现某个变量值总是不对。单步跟踪了半小时,最后发现是编译器优化把变量放到了寄存器里,而GDB读到的寄存器值已经过时了。加上 -O0 重新编译,问题立刻暴露——原来是个指针越界。
进阶技巧:多进程调试
嵌入式系统里经常有fork子进程的场景。gdbserver默认只调试主进程,子进程一启动就脱离控制了。怎么办?
GDB 7.0以上版本支持 follow-fork-mode。你可以在GDB里设置:
(gdb) set follow-fork-mode child
(gdb) set detach-on-fork off
这样GDB会跟踪子进程。不过要注意,gdbserver也需要支持多进程调试。老版本的gdbserver可能不行,建议用GDB 10.0以上的版本。
调试共享库
如果你的程序用了动态库,调试时可能会遇到「找不到符号」的问题。这时候需要告诉GDB去哪里找库文件:
# 在GDB中设置库路径
(gdb) set solib-search-path /path/to/target/libs
# 或者用环境变量
export LD_LIBRARY_PATH=/path/to/target/libs
我习惯在启动GDB时,用一个脚本自动设置这些路径。省得每次手动敲:
# debug.gdb 文件内容
target remote 192.168.1.100:1234
set solib-search-path /home/project/arm_libs
symbol-file ./hello_arm
break main
continue
# 启动时加载脚本
arm-linux-gnueabihf-gdb -x debug.gdb
嗯,远程调试这东西,说白了就是一层窗户纸。配置好了,用起来比printf高效十倍。但配置过程确实有点繁琐,尤其是交叉编译环境。不过一旦配好,后面就一劳永逸了。
我个人建议,把gdbserver和调试脚本都集成到项目的构建系统里。每次编译完,自动把gdbserver和程序一起部署到板子上。这样调试时,只需要一条命令就能启动会话。
公众号:蓝海资料掘金营,微信deep3321