简介:一份围绕GDB(GNU调试器)单步调试的PPT讲解资料,面向C/C++、Fortran及汇编语言开发者,尤其适合正在学习Linux命令行调试的初学者,也适合日常快速定位问题的开发人员。资料系统梳理了从编译加-g选项、启动调试到断点、观察点、捕捉点、信号处理、线程中断,再到查看数据、内存和调用栈的完整流程。内容分块介绍了初始化、各种暂停程序的手段,以及print、examine、backtrace、info等查看数据与栈信息的用法,并穿插条件断点、临时断点和输出格式化示例,同时解释step与next的区别。包内共1个PDF文件,约2.34MB,图文形式浓缩了基本命令与进阶用法,便于随身查阅与系统学习。该份资料已有117人学习下载,通过配套示例可以直观掌握单步调试关键操作,借助总结部分能构建自己的排错思路,更快定位代码异常位置,提升程序质量与开发效率。
1. GDB单步调试,为什么值得花一下午把这份资料啃透
C/C++程序崩溃时,多数人的第一反应是加printf,然后反复重跑、反复猜,运气好半小时定位,运气不好一下午搭进去。真正有效率的方式,是打开GDB,把程序按你自己想要的节奏一步一步推着走。GDB(GNU Debugger)是GNU工具链里最常用的C/C++调试工具,网上资料很零散,而这份GDB单步调试详解把完整链路讲得很清楚:从编译期加-g、启动GDB、设置断点,到step/next控制执行节奏、用watch盯变量、用backtrace看崩溃现场,最后还给了大量info命令和信号处理细节。适合正在学Linux C/C++的新手,也适合被线上问题逼着补课的工作者——照着敲,能一点点看到程序内部状态,比自己瞎猜靠谱得多。
2. 编译与装载:没有-g参数的二进制,GDB看到的全是地址
2.1 为什么必须加-g:调试信息才是GDB的“视力表”
GDB能读到的符号信息,完全取决于编译时往二进制里塞了什么。很多人第一次用GDB就翻车:打开一个系统自带的二进制,输入list,GDB回一句“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 *pArray@5 (gdb) whatis p (gdb) ptype pprint *pArray@5是所谓“人为数组”语法,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 -O0,C++程序对应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用成类IDE:alias、display、脚本化与core文件定位
6.1 简写、回车重复与TUI界面
GDB命令的长名字容易劝退新人,但几乎所有核心命令都有简写:r是run,c是continue,n是next,s是step,p是print,b是break,l是list,bt就是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单步调试笔记的梳理,能帮你在下次被段错误折磨时省下几个小时的冤枉时间。
本文还有配套的精品资源,点击获取