从零搭建嵌入式远程调试环境,让ARM开发像本地调试一样丝滑
1. 前言
如果你是从STM32这类MCU开发转向嵌入式Linux的工程师,最怀念的恐怕不是某个具体的库函数,而是那份集成在Keil或IAR里、点一下就能设断点、看变量的调试体验。
借助 VS Code + gdb + gdbserver 这套方案,我们可以构建一套媲美桌面IDE的图形化调试工作流,直接对运行在ARM开发板上的程序进行源码级调试。


本文将手把手带你搭建这套环境,涵盖理论原理、环境配置、交叉编译、VSCode集成、调试技巧和常见坑点排查。无论你用的是i.MX6ULL、RK3399,还是树莓派、全志系列,这套思路都是相通的。
2. 为什么选择VSCode + gdb + gdbserver?
VSCode + gdb + gdbserver:图形化调试 + 远程调试,兼顾易用性和灵活性,且无需昂贵硬件,仅需标准网络连接。
在嵌入式Linux开发中,调试ARM程序通常有以下几种方式:

3. GDB远程调试原理
在动手配置之前,花几分钟理解底层原理,能帮助你在遇到问题时知道该从哪里入手。
GDB远程调试的本质是一种客户端-服务器(C/S)架构:

一个典型调试会话的数据流:

当你在Host上输入 break main,GDB先计算出main函数在目标内存中的地址(如0x10400),然后通过socket向gdbserver发送类似 Z0,10400,1 的报文(设置软件断点)。gdbserver收到后,在目标程序内存的0x10400处插入断点指令,并回复OK——整个过程对用户完全透明。
理解了这套机制,你就会明白为什么编译带调试信息的程序、确保网络连通性、以及Host与Target版本兼容性如此重要。
4. 环境准备
4.1 开发环境(PC端)

完整调试需要三个核心组件协同工作:
- 交叉编译器:编译目标程序
- 交叉GDB:PC端发起调试指令
- gdbserver:ARM端执行调试操作
三者必须源自同一工具链版本,确保指令集支持、浮点ABI、异常处理机制完全匹配。
4.2 目标设备(ARM开发板)
- 运行Linux系统(Raspbian、Buildroot、Yocto均可)
- 具备网络连接能力(有线或Wi‑Fi)
- 存储空间足够存放待调试程序和gdbserver
- 能够执行gdbserver
4.3 网络连接
PC和ARM板需在同一局域网(或通过USB网络共享连接),确保可以互相ping通。同时需检查防火墙设置。
5. 工具链获取与配置
准备arm格式的gdb及gdbserver工具。
一般交叉编译工具链里都包含有,如果没有则需要自己下载gdb源码进行交叉编译,gdb源码下载链接:
http://www.gnu.org/software/gdb/download/
例如,在工具链目录下可以看到 gdbserver 和 arm-linux-gnueabihf-gdb:


6. 实践
6.1 编译带调试信息的程序
要让GDB能够提供有效的调试信息,必须在编译时生成调试符号。在Makefile的CFLAGS中添加 -g 选项:
CFLAGS += -g -O0 # -O0禁用优化,便于调试
示例:假设有一个简单的ARM程序 demo.c(本文结尾附有完整代码),编译命令:
arm-linux-gnueabihf-gcc -g -O0 demo.c -o demo_arm
编译完成后,将生成的可执行文件拷贝到ARM开发板。
6.2 创建launch.json文件并修改
- 点击左侧边栏的 “运行和调试” 图标(或按
Ctrl+Shift+D)
- 点击 “创建一个launch.json文件”
- 选择 “C++(GDB/LLDB)” 作为调试环境
我们需要创建VSCode的launch.json文件并进行一些修改:




配置项说明:

关键点:program 指定的是宿主机本地的带调试信息的可执行文件,而不是开发板上的。GDB使用本地的符号信息与远程gdbserver交互。
最重要的两个键值对:
"miDebuggerPath": "/opt/rv1126/bin/arm-linux-gnueabihf-gdb",
"miDebuggerServerAddress": "192.168.3.12:9001"
其中,miDebuggerPath 表示的是arm格式gdb的路径;miDebuggerServerAddress 表示的是我们server端的地址,如192.168.3.12为开发板的IP,9001为端口号,可自行设置,其范围为 0~65536,0~1023 的端口一般由系统分配给特定的服务程序。
6.3 把gdbserver传到开发板上
我们需要将交叉编译器路径下的gdbserver传到开发板上,例如放到开发板的 /usr/bin 路径下:

6.4 启动gdbserver
首先需要启动开发板上的gdbserver,PC端才能连接进行调试,格式为:
gdbserver 开发板ip:端口号 要调试的程序
例如:

6.5 启动VSCode的gdb进行调试
启动调试会话:
- 确保开发板上gdbserver已启动并在等待连接
- 在VSCode中按
F5 启动调试
- VSCode连接到开发板后,即可使用图形化调试功能
调试功能一览:
- 设置断点:点击代码行号左侧
- 单步执行:Step Over / Step Into / Step Out
- 变量监视:鼠标悬停或添加至Watch窗口
- 调用栈查看:Call Stack面板
- 内存查看:Memory窗口
启动调试后,所有gdb标准功能都将在VSCode图形界面中呈现,与本地开发体验无异。


gdbserver支持多线程调试,对于复杂的多线程项目完全适用。
高级启动参数:
--once:调试会话结束后gdbserver自动退出
--no-startup-with-shell:不使用启动shell,提升稳定性
--debug:输出调试信息用于排错
7. 常见问题
7.1 问题1:网络连接失败
现象:Unable to connect to :2345
排查步骤:
- 确保宿主机与开发板在同一网段,互相能ping通
- 检查开发板防火墙是否开放端口:
iptables -L -n
- 确认gdbserver正在运行:
ps aux | grep gdbserver
- 验证端口监听状态:
netstat -an | grep 2345
7.2 问题2:断点不生效
现象:程序正常运行但断点从未触发
可能原因:
- 编译时未添加
-g 选项:GDB需要调试信息才能定位源码位置
- 优化级别过高:
-O2 或 -O3 会内联函数、重排指令,导致断点映射失败。调试时务必使用 -O0
- 版本不匹配:gdbserver版本与交叉GDB版本差异过大
7.3 问题3:程序在开发板上崩溃
现象:开发板上一运行程序就Segmentation Fault
排查方法:
- 在开发板上运行程序生成core文件:
ulimit -c unlimited
- 将core文件拷贝到宿主机
- 用交叉GDB分析:
arm-linux-gnueabihf-gdb ./hello_arm core
- 使用
backtrace 命令查看调用栈,快速定位崩溃位置
7.4 问题4:VSCode启动调试后报权限错误
现象:Permission denied 或类似错误
解决方法:
- 检查交叉GDB是否具有执行权限:
chmod +x /path/to/arm-linux-gnueabihf-gdb
- 确保开发板上待调试程序具有可执行权限
- 如果以root用户登录,确保SSH允许root登录
7.5 问题5:交叉编译工具链找不到头文件
现象:fatal error: stdio.h: No such file or directory
解决方法:
- 检查交叉编译工具链是否安装完整
- 验证路径配置:
arm-linux-gnueabihf-gcc -v 查看搜索路径
- 在VSCode的
c_cpp_properties.json 中正确设置 includePath
结语
VSCode + gdb + gdbserver这套调试方案,本质上是用gdb+gdbserver作为底层核心,借助VSCode将原本枯燥的命令行操作转化为直观的图形化界面。
当你的程序在开发板上跑飞,或者某个变量值莫名其妙地改变时,这种远程调试能力就像黑暗中的探照灯。从工具链配置到VSCode集成,从基本原理到常见坑点排查,本文呈现了一套完整、可复现的嵌入式调试工作流。
附录:demo.c源代码
附上文中调试所用的演示代码,方便读者复现整个过程:
#include <stdio.h>
void test0(void) {
int i = -1;
if (i = 0)
printf("i = %d\n", i);
else if (i = 1)
printf("i = %d\n", i);
else
printf("i = %d\n", i);
}
void test1(void) {
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int *p = &a[1];
int *p1 = (int*)(&a + 1);
printf("p[6] = %d\n", p[6]);
printf("*(p1 - 1) = %d\n", *(p1 - 1));
}
int main(void) {
test0();
test1();
printf("hello \n");
return 0;
}