面试的时候被问过一个场景:"你的机器人程序跑着跑着突然segfault了,你怎么排查?"
我当时说了句"加printf看看"。面试官没说话,但我能感觉到他不太满意。
后来才知道,GDB是C/C++开发者的标配调试工具。在机器人开发里,很多问题是运行时的——内存越界、空指针、段错误,光靠printf根本定位不了。GDB能让你在程序运行时暂停、检查变量、查看调用栈,甚至回溯崩溃前的现场。
今天就把GDB最核心的用法讲一遍。不用全记住,先把断点、单步、查看变量这几个学会,面试的时候就有东西可聊了。
编译时加-g:调试的前提
很多人忘了这一步。GDB要工作,需要可执行文件里包含调试信息。编译的时候得加-g参数:
g++ -g -o robot_node robot_node.cpp如果用CMake,在CMakeLists.txt里加上:
set(CMAKE_BUILD_TYPE Debug)Debug模式会自动加上-g参数。Release模式会做优化,变量可能被编译器优化掉,GDB里就看不到了。
还有个细节:优化等级别开太高。-O2和-O3会导致代码重排,调试的时候执行顺序和你写的不一样,看着会很困惑。调试阶段用-O0或者干脆不加优化。
启动GDB和基础命令
gdb ./robot_node进去之后就是一个(gdb)提示符。几个最基本的命令:
run 运行程序(可以带参数:run --ros-args ...) break main 在main函数设断点 break 42 在第42行设断点 continue 继续运行到下一个断点 next 单步执行(不进入函数内部) step 单步执行(进入函数内部) print 变量名 查看变量的值 quit 退出GDBnext和step的区别很重要。next把函数调用当成一步执行完,step会进入函数内部逐行执行。调试的时候大部分情况用next就够了,只有你想看某个函数内部怎么执行的时候才用step。
断点:调试的核心
断点就是告诉程序"跑到这里停一下"。
break main 在main函数入口设断点 break robot_node.cpp:58 在第58行设断点 break MyClass::initSensor 在类的成员函数设断点 break robot_node.cpp:58 if i > 100 条件断点,只在i>100时暂停条件断点特别有用。比如你有个循环跑1000次,第500次出了问题。你不想单步按500次,就设个条件断点,让程序在前499次正常跑,到第500次才暂停。
info breakpoints 查看所有断点 delete 2 删除第2号断点 disable 1 禁用第1号断点(不删除,只是不生效) enable 1 重新启用还有个高级用法:watchpoint。监控某个变量的值变化,一旦变了就暂停。
watch sensor_data 当sensor_data的值改变时暂停
在机器人项目里,如果你怀疑某个传感器数据在某个时刻突然跳变,用watchpoint就能抓住那个瞬间。
程序暂停后做什么
程序在断点处暂停后,你可以检查当前的状态。
print x 打印变量x的值 print arr[0] 打印数组第一个元素 print *ptr 打印指针指向的值 print this->sensor_config 打印当前对象的成员p是print的简写,老手都直接用p。
p /x value 以十六进制显示 p /t value 以二进制显示 p /c value 以字符显示
查看调用栈:
backtrace 显示完整的函数调用链(简写bt) frame 2 切换到第2层调用栈 info locals 查看当前栈帧的所有局部变量
backtrace在排查崩溃问题时特别关键。程序segfault之后,用bt就能看到崩溃前的函数调用链,从最底层到最顶层,一目了然。你就知道是从哪个函数调到哪个函数,在哪一行出的问题。
处理segfault:机器人开发最常见的崩溃
segfault(段错误)是C++开发中最常见的崩溃类型。原因通常是访问了空指针、数组越界、或者使用了已释放的内存。
在GDB里调试segfault的标准流程:
gdb ./robot_node (gdb) run # 程序崩溃 (gdb) bt # 查看调用栈,找到崩溃位置 (gdb) frame N # 切换到崩溃的那个栈帧 (gdb) info locals # 查看所有局部变量 (gdb) p ptr # 检查可疑的指针有一次我写激光雷达数据处理,程序跑了几分钟就segfault。用GDB一跑,bt显示崩溃在一个数组访问的地方。p index一看,index值是65536,但数组大小只有1024。原来是点云数据量突然增大,index越界了。加了个边界检查就解决了。
如果程序不是每次都崩溃,而是跑一段时间才崩,可以用core dump。让程序崩溃时生成一个core文件,然后用GDB加载分析:
ulimit -c unlimited # 允许生成core文件 ./robot_node # 运行,等它崩溃 # 崩溃后会生成core文件 gdb ./robot_node core # 加载core文件分析 (gdb) bt # 查看崩溃时的调用栈在机器人项目中使用GDB
ROS节点可以直接用GDB调试:
gdb --args ros2 run my_package my_node如果节点是launch启动的,可以在launch文件里设置参数让节点在GDB下运行。或者先用GDB启动节点,再让其他节点连接它。
调试多线程程序时,GDB也能处理:
info threads 查看所有线程 thread 2 切换到第2号线程 thread apply all bt 查看所有线程的调用栈
机器人项目通常有多个线程(传感器读取、控制循环、通信等),出问题时thread apply all bt能帮你看清每个线程当时在干什么,哪个线程卡住了,哪个线程崩了。
面试中怎么聊GDB
面试官问调试经验,不要只说"我会用GDB"。要讲具体场景:
"有一次机器人运行时导航模块segfault了,我用GDB加载core文件,bt看到崩溃在路径规划的一个数组访问处,检查发现是栅格地图的索引越界。修复后加了边界检查和单元测试,再也没出过这个问题。"
这种回答比说十句"我会用break和print"都有说服力。
GDB脚本:自动化调试
如果你经常需要重复同一个调试步骤(比如每次都在某个函数上设断点、打印变量、继续运行),可以把这些命令写成GDB脚本:
# debug_script.gdb break processCloud commands 1 print cloud->size() continue end run然后用gdb -x debug_script.gdb ./my_node加载脚本。这在排查需要反复触发的问题时特别有用,省去每次手动输入命令的麻烦。
GDB调试的实战技巧
实际项目中最常用的GDB调试流程:先用gdb启动程序,用break设断点,用run运行程序。程序崩溃后用bt查看调用栈,用info locals查看局部变量。一个实用技巧是用catch throw捕获C++异常,快速定位异常抛出的位置。另外gdb -core corefile可以直接分析core dump文件,在生产环境排查问题时特别有用。
补充一点:在GDB中可以用display命令设置自动显示的变量,每次程序暂停时都会打印这些变量的值,调试循环特别方便。
给你的建议
先学会用GDB处理segfault。这是最实用的场景,也是面试最常问的。能看懂backtrace,能检查变量,基本就够了。
不要怕GDB的命令行界面。它确实不如图形化调试器直观,但在服务器上、在远程机器人上,你往往只有命令行可用。习惯了之后,GDB的效率其实比IDE的调试器更高。
还有一个建议:平时写代码就养成加断言的习惯。assert(ptr != nullptr)、assert(index < size),这些断言能在问题发生的第一时间暴露出来,比事后用GDB查core dump容易得多。
上一篇:第100篇 Git团队协作——分支策略、冲突解决和Code Review流程
下一篇预告:第102篇 GDB进阶——多线程调试、core dump分析和机器人崩溃排查