简介GDBGNU调试器是最常用的命令行调试工具之一这份PPT面向C/C、Fortran及汇编程序开发者重点解决程序调试中“如何逐行跟踪、定位崩溃点、检查变量与调用栈”等实际问题。内容从基础流程讲起包括编译时添加-g参数、用gdb加载可执行文件、设置断点、使用run/step/next/continue控制执行以及用print/display查看变量并进一步覆盖观察点、捕捉点、信号处理、线程中断、条件断点、内存查看和backtrace调用栈分析等高级技巧配有命令示例与参数说明适合刚接触GDB的初学者快速上手也适合日常查询常用调试命令的开发者参考。资源包共1个文件为PDF格式大小2.34MB便携易读发布至今已有117人学习下载是理解GDB单步调试全流程的一份精炼资料。1. GDB 单步调试从命令清单到能跑通的最小流程排查段错误还在用 printf 大法稍微复杂一点的 C/C 工程printf 定位问题的成本会随代码量指数级上升。GDB 单步调试的价值在于它让你直接看到程序在哪一行停住、变量在哪个赋值语句之后变了、调用栈是从哪里一路跌到崩溃点的。这份文档把 GDB 常用命令串成了一条完整调试流程——从 gcc -g 编译、gdb 启动、断点设置到 next/step 单步、print 查变量、bt 看栈全部覆盖还附了可运行的 tst.c 演示程序。适合刚接触命令行调试的 C/C 开发者、准备复试的考研学生以及从 IDE 调试器转向命令行工具的嵌入式工程师。2. 编译与启动-g 参数是入门的第一道坎2.1 编译阶段没有调试信息GDB 看到的全是内存地址GDB 调试的第一前提是目标文件里包含符号表和源码行号映射。这一步由编译器的 -g 选项搞定# 编译 C 代码 gcc -g tst.c -o tst # 编译 C 代码 g -g main.cpp -o main不加 -g 也能启动 gdb但进去之后 list 看不到源码break 函数名会提示找不到符号print 变量会直接报错。你面对的是一个黑匣子所有函数名、变量名都变成了一串十六进制地址调试体验约等于反汇编。这里有个容易被忽略的细节-g 只是把调试信息写进目标文件不会改变程序的实际行为也不影响代码优化级别。我见过不少人在 Makefile 里只写了 -g 没写 -O0结果断点设在第 10 行程序却停在第 12 行——这是因为编译器把几行代码合并优化掉了。所以本地调试的标准配置是 -g -O0gcc -g -O0 tst.c -o tst-O0 告诉编译器不要做任何优化源码行号和机器指令严格一一对应单步调试才谈得上精准。如果想看宏定义用 -g3如果用的 GCC 版本较新可以指定 -gdwarf-4 或 -g3 来获得更丰富的调试信息格式。提示调试完记得用 -O2 重新编译一份做性能验证别拿 -O0 的二进制去压测那结果不能代表线上表现。2.2 启动方式与源代码定位三种姿势各有用途启动 gdb 有几种常见姿势适用场景完全不同# 方式一直接启动并加载可执行文件 gdb tst # 方式二先进入 gdb再用 file 命令加载 gdb (gdb) file tst # 方式三调试 core 文件程序崩溃后留下的现场 gdb ./tst core # 方式四附加到正在运行的进程服务程序排障常用 gdb -p 12345方式三和方式四是很多新手不知道的。程序崩了之后第一时间用ulimit -c unlimited打开 core 生成开关再用gdb ./tst core加载能直接定位崩溃点。方式四适合调试守护进程这类不能前台运行的程序gdb 会自动 attach 上去。加载完文件后先用 list 确认源码行号避免断点设置错位置(gdb) list # 从当前行开始列出源码 (gdb) list 1 # 从第 1 行开始列出 (gdb) list main # 从 main 函数开头列出 (gdb) list 10, 20 # 列出 10 到 20 行程序运行参数也要在启动阶段设好。比如测试程序需要读一个配置文件直接在 gdb 里指定(gdb) set args -c config.ini (gdb) show args # 确认参数已生效set args 的优先级和命令行传参一致gdb --args tst -c config.ini也能达到同样效果。参数设置不检查文件是否存在拼错了路径程序运行时才会报错这点注意一下。list 和 set args 是启动阶段最常用的两个命令前者解决断点设在哪后者解决程序跑起来需要什么环境。3. 断点与暂停机制break、watch、catch 怎么选3.1 断点设置行号、函数、条件断点与断点管理断点是 GDB 最核心的暂停手段break 命令支持四种形式(gdb) break tst.c:16 # 在源码第 16 行停住 (gdb) break func # 进入 func 函数入口时停住 (gdb) break 46 if testsize100 # 条件断点满足条件才停 (gdb) break filename:function # 指定文件的函数处设置实际调试中行号断点是主力函数断点次之。条件断点在循环里查特定值非常好用——比如要查 i100 时程序的状态不用按 100 次 next直接设置break 8 if i100。但条件断点有个性能坑每次执行到该行GDB 都要计算一次条件表达式。如果这个表达式本身调用了程序里的函数而那个函数又有副作用程序状态就会被悄悄改变。我一般只在简单比较表达式上用条件断点复杂判断用前面提到的 watch 方案。断点设置完不是就结束了管理断点同样重要(gdb) info breakpoints # 查看所有断点信息 (gdb) delete breakpoint 1 # 删除编号为 1 的断点 (gdb) disable breakpoint 2 # 禁用 2 号断点保留位置 (gdb) enable breakpoint 2 # 重新启用 (gdb) clear 16 # 清除当前文件第 16 行的所有断点delete 和 disable 的区别是delete 是删除disable 是暂时关闭。调试循环里断点命中几百次后你会发现每次都停会很烦这时 disable 比 delete 明智——后面可能还要用。info breakpoints 输出的 Enb 列是 y/n一眼能看出哪些断点处于激活状态。3.2 观察点、捕捉点与信号不打断也能盯住变量断点要求你事先知道会出问题的那一行在哪但很多 Bug 的特征是某个变量在某些时刻被改成了非法值。这时观察点更合适(gdb) watch myvar # 变量被写入时停住 (gdb) rwatch myvar # 变量被读取时停住 (gdb) awatch myvar # 变量被读或被写都停住watch 的原理是硬件断点或软件单步监视对变量地址做访问监控。它不需要你猜测赋值语句的位置——变量一被改动就触发。代价是性能如果变量在一段循环里被频繁读写程序运行会慢到一个量级。能用断点定位的先断点定位实在查不到再上 watch。watch 对局部变量有作用域限制出了函数作用域 GDB 会自动删除观察点这是不少人踩过的坑。捕捉点 catch 针对的是事件而不是位置(gdb) catch throw # C 抛出异常时停住 (gdb) catch catch # C 捕捉到异常时停住 (gdb) catch exec # 调用 exec 系统调用时停住 (gdb) catch fork # 调用 fork 时停住文档里写得清楚exec、fork、vfork、load、unload 这些事件只在 HP-UX 平台下有效。也就是说Linux 下 catch 实际可用的主要是 throw 和 catch。调试 C 异常导致的神秘崩溃catch throw 比在可能抛异常的所有函数入口打断点高效得多——你直接停在了抛出点调用栈完整保留。信号处理用 handle控制 GDB 收到信号时的行为(gdb) handle SIGPIPE stop print参数表如下参数行为nostop信号到达时不停程序只打消息stop信号到达时停住程序print信号到达时显示一条消息noprint不显示信号消息pass把信号交给被调试程序处理nopassGDB 拦截信号不交给程序调试网络服务时 SIGPIPE 非常烦人——客户端断开连接程序收到 SIGPIPE 直接终止。用handle SIGPIPE nostop nopass让信号不要中断调试会话能省不少事。线程断点是服务端调试的刚需(gdb) info threads # 查看当前线程列表 (gdb) break tst.c:12 thread 3 # 只在 3 号线程命中 (gdb) break tst.c:12 thread 3 if count 10 # 加条件多线程程序里同一个断点可能被多个线程命中如果只在某个特定线程上复现问题不加 thread 限定就得反复 continue 跳过无关命中。thread 后面的编号来自 info threads 的第一列不是操作系统的 TID先查再用。4. 单步执行与数据观察next/step 的区别不止一个4.1 单步执行三兄弟next、step、continue 与 finish 的配合单步调试的核心命令有三条外加一个收尾命令命令行为适用场景next (n)执行一行不进入函数内部主流程排查step (s)执行一行进入函数内部跟踪被调函数逻辑continue (c)继续执行到下一个断点或结束跳过确定无误的代码段finish跑完当前函数并返回已经进了函数想退出来next 和 step 的选择直接影响调试效率。next 在遇到函数调用时把它当成一整行执行完step 则会钻进数内部一行行跑。在 main 函数里做整体流程梳理时用 next想确认某个函数的内部实现细节时再用 step 进去。如果 step 进去了想退出来finish 是后悔药——直接执行完当前函数停在返回处。下面用一个完整的调试会话演示这组命令的输出效果。源文件 tst.c 内容如下#include stdio.h int func(int n) { int sum 0, i; for (i 1; i n; i) { sum i; } return sum; } int main() { int i; long result 0; for (i 1; i 100; i) { result i; } printf(result[1-100] %ld\n, result); result func(100); printf(result[1-100] func(100) %ld\n, result); return 0; }编译启动gcc -g -O0 tst.c -o tst gdb tst(gdb) break 17 Breakpoint 1 at 0x4004d6: file tst.c, line 17. (gdb) run Starting program: /home/dev/tst Breakpoint 1, main () at tst.c:17 17 long result 0; (gdb) next 18 for (i 1; i 100; i) { (gdb) next 20 result i; (gdb) print i $1 1 (gdb) continue Continuing. result[1-100] 5050 Breakpoint 2, func (n100) at tst.c:5 5 int sum 0, i;这个会话里做了三件事断点设在 main 的第 17 行run 运行后停住next 逐行执行并配合 print 看 i 的值continue 直接跑到下一个断点func 函数入口。注意 func 的断点是在会话前设置的continue 才会停在函数入口——如果没设断点程序会一路跑到底。进入 func 之后的单步策略要变(gdb) next 6 for (i 1; i n; i) { (gdb) next 8 sum i; (gdb) print i $2 1 (gdb) print sum $3 1 (gdb) finish Run till exit from #0 func (n100) at tst.c:5 0x000000000040052e in main () at tst.c:24 Value returned is $4 5050在 func 内部继续用 next 逐行走用 print 盯住 i 和 sum 的变化确认求和逻辑无误后finish 直接执行完函数回到 mainGDB 还会打印返回值。4.2 数据与内存查看print 的格式控制、examine 内存和调用栈回溯print 是检查变量值的主力命令但很多人只用默认格式遇到指针和数组就抓瞎。print 支持格式修饰符(gdb) print /x i # 按十六进制显示 (gdb) print /d i # 按十进制显示 (gdb) print /t i # 按二进制显示 (gdb) print /c i # 按字符显示 (gdb) print /f i # 按浮点数显示排查二进制位相关的 Bug 时 /t 非常有用——比如检查某个标志位寄存器的第 3 位是否为 1二进制格式一眼看清。查指针指向的一片内存用数组语法(gdb) print *array10 # 打印 array 指向的前 10 个元素 (gdb) print h10 # 打印变量 h 后面的 10 个整数arraylen 语法本质上是人为数组不检查越界。你要是写print *ptr1000而 ptr 实际只指向 10 个元素GDB 会打印出一堆不可读的乱值不会报错。所以用之前先确认分配长度。这里补充一个实用命令 whatis 和 ptype(gdb) whatis array type int * (gdb) ptype struct_node type struct { int value; struct node *next; }whatis 只看类型名ptype 会把 struct 的完整定义展开。调试复杂链表结构时ptype 比翻头文件快得多。examine简写 x直接查看指定地址的内存内容(gdb) x/10cw pFilePath # 以 word(4字节)为单位打印 10 个按字符格式 (gdb) x/20bx buffer # 以单字节为单位打印 20 个十六进制格式x 命令的参数格式是x/[数量][格式][单位大小] 地址单位大小 b 是单字节、h 双字节、w 四字节、g 八字节默认四字节。检查缓冲区数据时x/64bx buf比 print 更直观——看到的就是内存里的原始字节序列。函数调用栈用 backtrace(gdb) bt # 打印完整调用栈 (gdb) bt 5 # 只打印栈顶 5 层 (gdb) bt -5 # 只打印栈底 5 层bt 输出从当前执行点一路追溯到 main 甚至 libc 的入口。段错误时 bt 是救命命令——程序崩溃后先 bt看最底层的用户函数是哪一个崩在什么位置十有八九问题就出在那附近。info 系列命令按需查信息(gdb) info locals # 当前栈帧的局部变量 (gdb) info registers # 所有整数寄存器 (gdb) info breakpoints # 断点状态 (gdb) info threads # 线程列表调试时随时 info locals可以一次看到当前函数里所有变量的值免去一个个 print。程序被优化过导致局部变量不可见时info locals 会提示 optimized out这时就得回编译环节加 -O0。5. 常见问题排查五个高频翻车现场5.1 变量显示optimized out无法打印现象next 走到某一行print 一个明明存在的局部变量GDB 返回optimized out或者显示的值和预期完全不符。原因可执行文件编译时开了优化-O2 或 -O3。编译器将变量直接优化进寄存器或者把多行代码合并成一条指令调试信息里的源码行号和变量的存储位置就不再与源码一一对应。解决本地调试统一使用gcc -g -O0编译。如果必须在 -O2 下调试比如要复现线上特定表现用print $eax这类寄存器查看方式绕过变量名但定位成本会高很多。血泪经验线上压测程序产生的 core 文件加载后看到的变量状态与实际的差异非常大优先用 -O0 -g 复现问题再分析。5.2 断点在循环里不命中或命中时机不对现象设置break 8 if i100程序跑完循环也没停或者停在 i101 的位置。原因条件写错是最常见的原因——break if i100是赋值不是比较恒为真第一次进入断点就停住。另一个原因是条件断点表达式里有函数调用函数返回值被改变导致判断失真。解决条件断点只用、!、、这类纯比较表达式严禁在条件里写赋值语句和函数调用。先普通断点看住循环用 print 确认变量实际值再上条件断点。我自己就犯过if i100这种错误断点直接变成每次循环都停还以为是 GDB 的 bug。5.3 断点命中了但停在错误的位置现象断点设在第 10 行程序停在第 12 行或者 list 显示的源码和编辑器里看到的不一致。原因编译时开了优化导致指令重排源码行号和机器指令的映射发生偏移。另一个常见原因是源码文件被修改过但可执行文件没有重新编译——GDB 加载的是旧调试信息行号对不上新源码。解决确认 Makefile 里是 -g -O0 组合确认每次改完代码都重新编译了目标文件。在 GDB 里用info source查看当前加载的源码路径和编辑器里的文件做对比防止因 vscode 工作区目录不同而加载了陈旧源码。5.4 多线程程序断点总在奇怪的地方停住现象给线程 A 设了断点结果在另一线程里也停了用 continue 跳过时其他线程又可能先命中同一个断点调试次序错乱。原因GDB 默认的 all-stop 模式是任何一个线程触发断点所有线程全部暂停。如果不加 thread 限定断点对任意线程都生效。解决断点时带上线程号——break tst.c:12 thread 3。配合info threads确认线程编号再用thread 3切换当前线程上下文观察该线程的局部变量。多线程调试的核心是先指定线程再指定断点。5.5 搜 gdb 找到了不相关的东西现象搜索 gdb 相关资料时搜到了 qgis 打开 gdb 的教程、地理数据库文件格式之类的结果跟 GNU 调试器完全无关。原因GDB 同时是 ESRI 地理数据库Geodatabase的文件格式后缀GIS 领域提到 gdb 指的是矢量数据文件和 GNU Debugger 撞了缩写。解决找调试器资料时把关键词加全比如 gdb 单步调试 GNU gdb 命令避免搜到 GIS 内容。反过来GIS 从业者搜 gdb 时加 GIS 和 地理数据库 缩小范围。两个领域互不相关纯属缩写撞车。6. 进阶技巧从命令行到脚本化的调试捷径命令行逐条敲命令是基本功但真正提效的是把重复操作固化成脚本。GDB 支持从文件批量执行命令日常调试可以直接复用一套固定流程# debug.cmd 内容 break main run next print result continuegdb -batch -x debug.cmd ./tst-batch 模式跑完脚本自动退出适合在自动化测试里输出崩溃点信息。更实用的做法是把 GDB 配置文件写到项目仓库里统一管理# .gdbinit set pagination off set print pretty on set print array-indexes on以后每次启动 gdb 自动加载这些设置不用重复敲。我喜欢在临时调试会话里配合 vscode 使用——vscode 的 launch.json 里配置 miDebuggerPath: /usr/bin/gdb再用 stopAtEntry: true 让程序启动后先停住效果等价于命令行 break 第一行{ name: Debug C, type: cppdbg, request: launch, program: ${workspaceFolder}/tst, args: [], stopAtEntry: true, cwd: ${workspaceFolder}, miDebuggerPath: /usr/bin/gdb }命令行调试的优势在脚本化IDE 的优势在可视化观察变量和寄存器。各取所长我一般用 vscode 写代码和设按钮真正涉及条件断点和复杂 watch 时切回命令行。嵌入式场景里vscode 配 qemu 的 gdb 远程调试是标准打法——qemu 启动带-s参数监听端口本地 gdb 用 target remote 连上去对内核或裸机程序做交叉调试qemu-system-arm -M vexpress-a9 -kernel kernel.bin -S -s(gdb) target remote :1234 (gdb) file kernel.elf (gdb) break start_kernel (gdb) continueGDB 13.2 版本对 C20 协程的调试支持已经很完善print 协程帧里的局部变量和标准库容器都不再是乱码新版还改进了 pretty printer 的性能打印大型 STL 容器时不再卡顿。如果你用的是老发行版自带的旧版本 gdb建议自己编译一份新版——低版本 gdb 对 C 新特性的支持缺失会让有些变量显示成不可读的原始内存。最后分享一个习惯从那以后我每次调试前都强制走一遍固定流程——确认编译参数是 -g -O0、确认断点设到了正确的文件名和行号、确认 set args 带了完整参数、跑之前想清楚是 next 还是 step。这套检查只需要几十秒但能避免大部分GDB 怎么不听使唤的玄学时刻。希望帮到你。本文还有配套的精品资源点击获取