
简介一份围绕GDBGNU调试器单步调试的PPT讲解资料面向C/C、Fortran及汇编语言开发者尤其适合正在学习Linux命令行调试的初学者也适合日常快速定位问题的开发人员。资料系统梳理了从编译加-g选项、启动调试到断点、观察点、捕捉点、信号处理、线程中断再到查看数据、内存和调用栈的完整流程。内容分块介绍了初始化、各种暂停程序的手段以及print、examine、backtrace、info等查看数据与栈信息的用法并穿插条件断点、临时断点和输出格式化示例同时解释step与next的区别。包内共1个PDF文件约2.34MB图文形式浓缩了基本命令与进阶用法便于随身查阅与系统学习。该份资料已有117人学习下载通过配套示例可以直观掌握单步调试关键操作借助总结部分能构建自己的排错思路更快定位代码异常位置提升程序质量与开发效率。1. GDB单步调试为什么值得花一下午把这份资料啃透C/C程序崩溃时多数人的第一反应是加printf然后反复重跑、反复猜运气好半小时定位运气不好一下午搭进去。真正有效率的方式是打开GDB把程序按你自己想要的节奏一步一步推着走。GDBGNU Debugger是GNU工具链里最常用的C/C调试工具网上资料很零散而这份GDB单步调试详解把完整链路讲得很清楚从编译期加-g、启动GDB、设置断点到step/next控制执行节奏、用watch盯变量、用backtrace看崩溃现场最后还给了大量info命令和信号处理细节。适合正在学Linux C/C的新手也适合被线上问题逼着补课的工作者——照着敲能一点点看到程序内部状态比自己瞎猜靠谱得多。2. 编译与装载没有-g参数的二进制GDB看到的全是地址2.1 为什么必须加-g调试信息才是GDB的“视力表”GDB能读到的符号信息完全取决于编译时往二进制里塞了什么。很多人第一次用GDB就翻车打开一个系统自带的二进制输入listGDB回一句“No symbol table is loaded”然后只能面对一堆裸地址。原因很简单——发行版二进制的符号表基本都被剥离了。-g选项的作用是把调试信息写入目标文件包括源文件名、行号表、变量名、函数签名。没有它GDB相当于一个黑匣子只能看到指令地址有了它print i、break func、list这些命令才有意义。GDB默认调试的是C/C程序但Fortran和汇编也支持只是资料里最常用还是C/C场景。# 编译C程序加入调试信息并关闭优化 gcc -g -O0 -o tst tst.c # 编译C程序同样加入调试信息 g -g -O0 -o tst tst.cpp-g之后我一般会顺手加-O0这两者是配套的。-O0告诉编译器别做优化变量保持原样行号和源码严格对应。如果开了-O2变量可能被寄存器取代代码顺序可能被重排print出来的值会显得“莫名其妙”。想要连宏定义一起调试可以用-g3只想保留函数名和行号-g就够了。release版也可以保留-g便于事后分析core但调试体验会打折。2.2 三种启动方式加载文件、core文件和attach进程GDB启动方式不止一种文档里列了三类我用实际场景逐个拆# 方式一直接加载可执行文件最常用 gdb ./tst # 方式二同时加载程序和core文件用于崩溃事后分析 gdb ./tst core # 方式三attach到一个正在运行的进程适合服务类程序 gdb ./tst 3456第一种方式就是日常调试入口。第二种方式很重要程序崩溃后产生core dump时用gdb ./tst core加载再执行bt崩溃时调用栈一目了然。第三种方式适合守护进程这类不能直接run的程序把进程PID传给GDB它会用ptrace挂上去。注意权限问题attach别人家的进程需要同用户或root权限容器里还要确认ptrace_scope没把权限锁死。如果不小心在GDB里输入了gdb但没带文件也可以在启动后用file命令补加载符号表。配合set args可以预先设置程序运行参数show args确认参数(gdb) file ./tst (gdb) set args -b -x (gdb) show args2.3 第一轮完整会话list、break、run、print用一个最小程序把流程串起来文件名叫tst.c#include stdio.h int func(int n) { int sum 0, i; for (i 0; 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(250); printf(result[1-250] %ld\n, result); return 0; }按上面方式编译后进GDB做一轮基础操作(gdb) list (gdb) list 1, 24 (gdb) break main (gdb) break func (gdb) info break (gdb) run (gdb) print resultlist不带参数时从当前位置开始列源码带1, 24就是指定行号范围。break main在main函数入口下断点break func在func入口下断点。info break会列出当前所有断点编号、状态、地址和对应行。run启动程序遇到第一个命中断点停下这时再print result看变量值。这一轮走完基本就把GDB主流程摸通了。3. 暂停程序的三种姿势断点、观察点、捕捉点以及它们的适用场景3.1 断点行号、函数、地址与文件名定位断点是暂停程序最基础的手段。文档里给了四种断点写法我按优先级挨个说(gdb) break 16 # 在当前文件第16行停下 (gdb) break func # 进入func函数时停下 (gdb) break *0x08048456 # 在某条内存地址停下 (gdb) break tst.c:16 # 指定文件中的行前面两种日常用得最多。行号断点适合你知道大概位置时用函数断点适合“只要进入这个函数就要看”的场景。地址断点平时少用但在调试没有源码符号的第三方库时很管用。跨文件调试时记得用文件名:行号否则GDB只在当前文件里找。断点的执行时机是“在指定行执行之前停住”所以你可以放心地在那一行print看到的是该行代码执行前的状态。3.2 条件断点与ignore循环里的“精准刹车”循环里如果每次continue都停一次手会按抽筋。条件断点可以只在满足条件时才暂停(gdb) break 7 if i 100含义是程序执行到第7行时如果i 100才停。循环1000次前999次直接跑过第100次精准停住。如果断点已经设了可以单独用condition给已有断点补条件(gdb) condition 1 i 100还有一个容易被忽略的ignore命令跳过前N次断点命中(gdb) ignore 1 1000这个场景很典型循环前1000次都正常第1001次才崩。ignore 1 1000让断点前1000次不停第1001次命中。配合条件断点基本上循环类问题都能精准定位。3.3 观察点变量被读写的那一瞬间停下来断点只能停在一个固定位置但很多Bug是某个变量在某处被意外改写。观察点解决的就是这个问题它监视变量的读写事件(gdb) watch i # 变量i被写入时停下 (gdb) rwatch i # 变量i被读取时停下 (gdb) awatch i # 变量i被读或写时都停下watch最常用。它内部依赖硬件调试寄存器x86上通常只能同时下4个硬件观察点。观察点生效的前提是变量所在内存地址稳定局部变量在栈上函数退出后地址失效观察点也会跟着失效。后面避坑章节我会专门讲这个坑。3.4 捕捉点与信号处理异常也可以成为断点捕捉点监听某种事件发生文档里的catch系列命令很有价值(gdb) catch throw # C抛出异常时停下 (gdb) catch catch # C捕获异常时停下 (gdb) handle SIGPIPE stop print (gdb) handle SIGSEGV stop printC程序里异常被吞掉很难查catch throw在异常抛出那一刻就拉住程序这时候bt看调用栈能直接看到异常是从哪抛出来的。catch catch则是等到异常被捕获时才停适合排查异常处理逻辑。handle控制信号行为参数组合是nostop、stop、print、noprint、pass、nopass。比如某个网络程序老是被SIGPIPE干掉你希望它一收到SIGPIPE就停下而不是退出就设成stop print。想测试信号处理逻辑时还可以在断点处直接向程序发信号(gdb) signal SIGINT这会继续程序执行同时立刻把SIGINT信号传给程序测试自定义的信号处理函数是否正常。3.5 断点生命周期管理disable比delete更常用断点下多了之后管理也很关键(gdb) info breakpoints (gdb) disable 1 (gdb) enable 1 (gdb) delete 1 (gdb) clear tst.c:16info breakpoints输出里有Num、Type、Disp、Enb、Address、What几列Enb是n就表示断点被禁用。disable只是暂时关掉编号还在delete是彻底删除。clear 16是清理某一行上的所有断点。经验是调试中拿不准的断点先disable别急着delete因为程序多跑几轮你可能又需要它。4. 单步执行的节奏感next / step / finish 与数据检查4.1 next、step、finish三种执行粒度的选择单步执行是GDB调试的核心操作。新手最容易纠结next和step其实一句话就能区分清楚next执行当前行时不进入被调函数内部step会一头扎进去。(gdb) next # 别名 n执行一行不进入函数 (gdb) step # 别名 s执行一行进入函数 (gdb) finish # 跑完当前函数停在返回处并显示返回值 (gdb) until 12 # 继续执行到第12行n适合在main层面纵向扫逻辑s适合发现某个函数可疑时钻进去看细节。finish是“后悔药”用step误入了标准库或者某个人畜无害的函数finish直接跑完当前函数回到调用位置还顺手把返回值显示出来。until适合循环里的快速跳出比如你在循环体里设了断点想直接跑到循环外的某一行就用until 12。4.2 print格式化输出与人为数组print是查看变量值的主力命令文档里给了一长串格式参数我用表格整理一下格式含义/x按十六进制显示变量/d按十进制显示变量/u按十六进制显示无符号整型/o按八进制显示变量/t按二进制显示变量/c按字符格式显示变量/f按浮点数格式显示变量(gdb) print result (gdb) print/x i (gdb) print *pArray5 (gdb) whatis p (gdb) ptype pprint *pArray5是所谓“人为数组”语法pArray是一个指针但你知道它其实指向一个长度为5的数组5让GDB按5个元素来显示。whatis看变量类型ptype进一步输出结构体定义。此外print还能直接赋值set var i 100在调试循环时很好用可以直接改掉循环变量来跳过中间过程。4.3 x命令直接查看内存里的字节print看变量值x命令是看内存原始字节。文档里的示例是(gdb) x/10cw pFilePath这个命令的通用格式是x/[数量][格式][单位] 地址。10表示显示10个单元c表示按字符格式输出w表示每个单元占4字节。所以x/10cw pFilePath就是从pFilePath指向的位置开始以4字节为单位按字符格式显示10个单元。单位参数含义b单字节h双字节w四字节g八字节(gdb) x/8wx buf # 以4字节为单位按十六进制显示8个单元 (gdb) x/4bx buf # 以单字节为单位按十六进制显示4个字节 (gdb) x/s pFilePath # 把该地址当作字符串打印排查缓冲区溢出时x命令比print直接得多看一眼buf前后几个字节的内容是不是被改写成异常数据了。4.4 backtrace与帧切换崩溃时先问栈要答案程序崩溃后第一件事不是看代码而是看栈。backtrace或简写bt输出当前调用链(gdb) bt (gdb) bt 5 (gdb) frame 1 (gdb) info locals (gdb) up (gdb) downbt不带参数打印全部栈帧bt 5只打印栈顶5层frame 1切换到编号为1的栈帧然后info locals查看这一层的局部变量。up/down是在栈帧之间上下移动。段错误定位时这样一套组合拳下来崩溃现场基本就还原了。4.5 info命令全家桶与寄存器访问GDB的info子命令非常多文档里给了几十条我不全列挑几个高频的(gdb) info locals (gdb) info args (gdb) info registers (gdb) info threads (gdb) thread 2info locals看当前栈帧的局部变量info args看当前函数入参info registers看寄存器值。多线程程序用info threads列出所有线程和GDB分配的线程ID用thread 2切到2号线程继续排查。GDB还提供$pc、$fp、$sp、$ps这类标准寄存器名分别代表程序计数器、帧指针、栈指针和处理器状态。碰到栈被写坏的疑难杂症这几个寄存器值是最客观的线索。5. 常见问题与避坑记录单步调试踩过的五六条真实弯路5.1 断点行号越界或错位现象break 110报“Line number 110 out of range”或者断点明明下在main里程序却停在了诡异的位置。原因源码和正在调试的二进制不一致。最常见的是改了源码后只编译了部分文件GDB加载的还是旧的调试信息。解决先make clean再来一次完整编译确认file命令输出里源文件路径与当前目录匹配。GDB也可以dir命令手动添加源文件搜索路径但治标不治本重编才是正解。5.2 变量消失了print报No symbol现象print i提示“No symbol i in current context”或者明明有变量名GDB显示的全是地址。原因编译时忘了-g或者用了strip还有一种情况是开了-O2变量被编译器优化进寄存器甚至优化掉了。解决调试版本统一gcc -g -O0C程序对应g -g -O0。release版本保留-g但不要求-O0只是这时候变量“失真”要心里有数。info registers可以在变量被优化时退一步先看寄存器。5.3 断点下去了但程序跑过不停现象break func之后run程序正常运行结束断点一次都没命中。原因多线程程序里目标断点在另一个线程或者调用的共享库还没加载GDB把断点挂成了pending状态又或者目标函数被内联了符号名存在但执行时根本没走那条路径。解决先info breakpoints看断点状态如果是pending用set breakpoint pending on让它自动挂起等待库加载。多线程场景用info threads确认目标线程存在后break 文件:行号 thread 线程号精确下断点。内联函数可以尝试rbreak 正则给一系列相关函数都下断或者改用watch监控数据变化。5.4 step一步扎进printf或标准库源码现象明明只想单步step一步就跳进printf的源码满屏汇编想退退不出。原因step会进入任何被调用函数包括库函数。解决这种情况用next代替step或者finish一步退出。更彻底的做法是用skip命令屏蔽不需要进入的文件(gdb) skip file /usr/include/*/stdio.h (gdb) info skip把标准库文件加进skip列表之后step遇到这些位置的函数调用会自动跳过单步体验立刻干净很多。5.5 watch观察点突然失效现象watch i之后执行了一会儿GDB提示“Cannot insert watchpoint”或“Watchpoint is out of scope”。原因观察的对象是局部变量它活在栈上函数一旦返回栈帧销毁变量地址失效。硬件观察点数量有限x86一般4个同时下太多观察点也会报错。解决尽量在变量作用域存续期间完成观察把watch下在函数入口之后如果确实需要观察某个内存区域用地址表达式代替变量名。硬件观察点超限时可以用set can-use-hw-watchpoints 0强制切到软件观察点但程序会明显变慢。5.6 core文件不生成崩溃后无从查起现象程序段错误退出当前目录却找不到core或core.*文件。原因shell的ulimit -c被设成了0或者系统core_pattern指定了别的存储位置。解决先ulimit -c unlimited再跑程序。如果还是不生成检查/proc/sys/kernel/core_pattern有些发行版把core统一收进了systemd-coredump目录需要单独去那个目录找。6. 把GDB用成类IDEalias、display、脚本化与core文件定位6.1 简写、回车重复与TUI界面GDB命令的长名字容易劝退新人但几乎所有核心命令都有简写r是runc是continuen是nexts是stepp是printb是breakl是listbt就是backtrace。还有一条可以省很多事的规则直接敲回车会重复上一条命令。连续n单步时全程只需要先按一次n之后一直回车就行。(gdb) set pagination off (gdb) layout nextset pagination off关掉分页输出不会一次停一屏。layout next打开TUI分屏模式上半屏是源码下半屏是命令区观感接近IDE。配合layout prev可以在多个布局间切换。6.2 display每次停止都自动打印变量反复手动print同一个变量很烦display能把这个动作自动化(gdb) display i (gdb) display/x *p (gdb) info display (gdb) undisplay 1display i之后每次程序停止GDB都会自动打印i的值。display/x *p则按十六进制自动打印指针指向的内容。info display列出所有自动显示项undisplay 1取消编号为1的那一项。配合断点使用可以做到“每次停下关键数据自己冒出来”。6.3 一键复现段错误加载core文件跑三行命令日常开发里我处理段错误的固定套路是三步。第一步先确保core能生成ulimit -c unlimited ./tst第二步程序崩了之后用gdb加载可执行文件和core文件gdb ./tst core第三步别急着翻代码先bt看调用栈再frame 1切到出事那层配合info locals看现场变量。这套流程跑下来绝大多数段错误能在几分钟内定位比反复加printf重跑快得多。从那以后我每次遇到崩溃类问题都强制自己走一遍这个流程已经是肌肉记忆了。希望这篇GDB单步调试笔记的梳理能帮你在下次被段错误折磨时省下几个小时的冤枉时间。本文还有配套的精品资源点击获取