GDB单步调试详解:断点、变量查看与避坑指南
发布时间:2026/10/31 8:43:59 锦皓数字建站

简介这份GDB单步调试详解PDF面向C/C、GNU工具链学习者及需要排查程序运行问题的开发者定位于命令行调试技能的入门与进阶梳理。内容围绕编译时加入-g、启动GDB、设置断点、run运行、step/next/continue单步控制、print/display查看变量以及backtrace分析调用栈展开并延伸到观察点、捕捉点、信号处理、线程中断、条件断点、临时断点、输出格式控制、查看内存与info系列命令等高级用法。资源包仅含1个PDF文件大小约2.34MB页面篇幅紧凑便于在电脑或移动端直接翻阅适合作为课堂笔记、实验辅助、技术分享提纲或调试命令速查材料。已有118人学习读者可从中系统理解GDB暂停程序、查看数据与栈信息、定位异常调用路径的完整思路并通过大量命令示例积累可复用的排错方法对课程实验、项目自测和面试复习均有参考价值。1. 从一份 GDB 单步调试 PPT 说起命令行里到底谁在替你盯着变量带新人排查一个段错误时对方在图形界面里点了二十几次「下一步」最后停在崩溃前一行指着变量问我为什么显示optimized out。我让他把编译命令贴出来-O2开着-g没加。这类问题跟 IDE 好不好用没半点关系根子在调试信息和单步语义没搞明白。这份《GDB 单步调试详解》PPT 讲的就是这条链路先用-g编译、gdb加载再break下断next/step/finish控制节奏最后靠print、x、backtrace把程序停住那一刻的状态读出来。它适合刚在 Linux 下写 C/C 的学生也适合平时只靠图形调试器、一遇 core 文件就手足无措的从业者。GDB 是 GNU 工具链里最不像「插件」的那一环装完不等于能用得先让程序愿意被看。2. 编译期就得埋好伏笔-g 选项、优化等级与符号加载调试失败十次里有六次不是 GDB 命令敲错而是二进制文件里根本没带够信息。这一章先把「程序愿意被看」这件事办妥后面的断点和单步才有意义。2.1 -g 到底往可执行文件里塞了什么不加-gGDB 打开程序后看到的是地址、寄存器编号和一堆机器指令函数名和变量名全部消失。加上-g编译器会把 DWARF 调试信息写进目标文件核心是四类内容行号表哪条机器指令对应源文件哪一行、变量与参数的存储位置栈偏移还是某个寄存器、类型描述struct布局、typedef展开、函数原型。没有这四样break 38不知道 38 指哪print total不知道total在哪。# 带调试信息、关闭优化编译单步和变量可见性最接近源码 gcc -g -O0 -Wall -Wextra stat_demo.c -o stat_demo # 对照产物不带 -g gcc -O0 -Wall stat_demo.c -o stat_demo_nosym # 看调试段有没有真的写进去 readelf -S ./stat_demo | grep -E debug|symtab这段命令的逻辑是先产出两个二进制一个带符号一个不带再用 binutils 里的readelf检查 ELF 段表。带-g的产物会多出.debug_info、.debug_line、.debug_str、.symtab这几段.debug_line就是行号表的物理载体。参数上-g默认级别等价于-g2-g3会额外保留宏定义能在 GDB 里info macro-ggdb是 GCC 针对 GDB 的扩展格式跨编译器兼容性差一些只在确定全程用 GNU 工具链时考虑。-O0单独解释它告诉优化器不要重排、不要内联、不要把变量提到寄存器是单步调试可信度的地基。我一般还会补一句file和readelf的交叉验证file ./stat_demo看有没有with debug_info, not strippedreadelf看段两个都对上再进 GDB。很多「断点设不上」的求助最后都卡在这第一步。2.2 优化等级和调试信息打架的地方-g和-O2可以同时存在但代价是单步会「漂」。优化器把三行循环展开成一条指令把局部变量塞进寄存器又复用把短函数直接内联掉行号表只能记个大概。表现出来就是next跳过了你明明写了的赋值语句print报变量不存在backtrace里的栈帧少了一层。编译选项单步是否贴合源码变量可见性建议使用阶段-O0 -g基本逐行对应绝大多数可见首次定位问题-Og -g局部会跳行少量变量被优化复核逻辑分支-O2 -g行号明显漂移常见optimized out复现性能相关缺陷无-g只剩地址完全不可见不用于调试表格里的-Og是 GCC 提供的「调试友好优化」但不同编译器支持程度不一样我不把它当默认选项。实际做法是先用-O0 -g把逻辑跑通、把变量看清确认问题范围后再用-Og或-O2复现一遍确认不是优化引入的差异。反过来如果只在-O2下崩溃、-O0下正常那本身就是一条强线索——大概率是未定义行为、越界写或者竞态而不是编译器「玄学」。还有一点容易忽略发布包通常会strip掉调试段。线上拿到 core 之后bt全是地址不是 GDB 坏了是符号在打包时被删了。所以核心服务的构建脚本里我习惯单独留一份未 strip 的二进制和对应的源码版本作为事后调试的「后悔药」。2.3 三种加载目标的方式与 file、set argsGDB 启动时加载什么决定了你后面能查什么。常见有四条路径对应四个场景。# 方式一直接带可执行文件启动最常用 $ gdb -q ./stat_demo # 方式二先起 gdb再手动加载 $ gdb -q (gdb) file ./stat_demo # 方式三attach 到正在运行的进程 $ gdb -q -p $(pgrep -n stat_demo) # 方式四事后调试 core 文件 $ gdb -q ./stat_demo /path/to/core.stat_demo # 加载后核对与传参 (gdb) info files (gdb) set args 12,7,33,5,28 (gdb) show args (gdb) directory /path/to/src逻辑上方式一适合「从零跑一遍」方式二适合中途换了目标文件比如先看 core 再切到可执行文件方式三适合服务类程序进程已经卡住但不能重启方式四是最常见的线上排障入口可执行文件提供符号core 提供崩溃瞬间的内存镜像两者必须严格来自同一次构建版本对不上时栈会错位得离谱。参数说明-q去掉启动横幅省得每次滚动set args设置的是run时传给程序的参数show args回看当前设置不设置时run会复用上一次的参数这个特性在反复调试时很省事但也容易误用directory在源码目录和可执行文件不在同一路径时补搜索路径否则list只能看到「No source file」info files用来确认符号到底从哪个文件读进来的。attach 模式要留意权限目标进程属主和当前用户不一致时通常连不上。3. 断点体系行断点、条件断点、观察点与捕捉点怎么选让程序停下来有五种手段断点、观察点、捕捉点、信号、线程停止。选错手段的代价是调试节奏被拖垮——在十万次循环里用普通断点一次一次continue属于纯体力活。3.1 行断点、函数断点与条件断点(gdb) break stat_demo.c:38 # 指定文件第 38 行 (gdb) break sum_all # 函数入口 (gdb) break parse_token if n 3 # 条件断点 (gdb) tbreak stat_demo.c:45 # 只命中一次的临时断点 (gdb) info breakpoints # 查看全部断点 (gdb) disable 2 (gdb) enable 2 (gdb) delete 1 (gdb) clear stat_demo.c:38 # 按行号清除逻辑说明break后面跟行号时如果不带文件名指的是「当前源文件」多文件项目里这是最常见的设错来源我习惯一律写成文件名:行号。函数断点落在函数体第一条语句上比重视行号稳。条件断点是在命中点先求值再决定停不停表达式里能引用当前作用域的变量和参数适合「只想看第 100 次循环」的场景。参数与坑tbreak命中一次自动删除适合确认某行是否被执行过info breakpoints输出里的Enb列是启用状态、Disp列是命中后是否保留disable/enable比delete温和排查完还能恢复clear按位置删delete按编号删两者别混。条件断点如果条件本身很重比如调函数、遍历大结构会让程序慢到怀疑人生这时候改用「先跑到大致位置再手动判断」更划算。3.2 观察点watch、rwatch、awatch 的触发条件断点管「执行到哪」观察点管「数据什么时候变」。这三条命令的差别必须分清否则会出现「明明变量变了却没停」的翻车。(gdb) watch total # total 被写时停住 (gdb) rwatch s.value # s.value 被读时停住 (gdb) awatch count # 读或写都停住 (gdb) info watchpoints逻辑说明watch监视表达式或变量的值是否被修改是找「谁把这块内存改坏了」的主力rwatch在被读时触发用于定位「谁在读这个已经被释放的指针」awatch是两者的并集代价也最大。三者都要求在变量处于作用域内时设置否则 GDB 会报「No symbol」。参数与限制观察点底层依赖硬件调试寄存器数量通常只有几个超了之后 GDB 会退化成软件观察点——每执行一步就比对一次内存速度会断崖式下降。所以别 watch 整个大数组或者*ptr指向的大块区域正确做法是 watch 单个字段或者先用条件断点缩小范围。另一个限制是变量被优化进寄存器后观察点可能直接失效回到-O0是唯一稳妥解。3.3 捕捉点、信号与线程断点(gdb) catch throw # C 抛异常时停 (gdb) catch fork # 调用 fork 时停 (gdb) handle SIGPIPE stop print nopass (gdb) info signals SIGSEGV (gdb) break sum_all thread 2 if count 0 (gdb) info threads逻辑说明catch面向事件而不是代码位置throw/catch管 C 异常fork/vfork/exec/load/unload管进程与动态库事件其中exec、fork这几类在部分平台上支持程度有限用之前最好先help catch确认。handle是信号策略表stop表示收到信号就停、nostop表示不停只打印、print表示打印消息、noprint表示静默、pass表示把信号交给程序自己处理、nopass表示拦下来不给程序。参数说明handle SIGPIPE stop print nopass这组配置的意思是「收到 SIGPIPE 时停住、打印一行、不要把它交给程序」适合排查「程序莫名其妙被信号干掉」的问题。反过来如果你的程序自己注册了SIGINT处理函数做清理调试时就必须handle SIGINT nostop pass否则 CtrlC 全被 GDB 吃掉程序永远等不到自己那套逻辑。线程断点的写法是break 位置 thread 线程号 if 条件线程号是 GDB 分配的不是系统 TID要用info threads现查。4. 单步执行的语义边界next、step、finish 与停止后的信息提取断点让程序停住单步决定下一步走多远查看命令决定你能从停住的那一刻榨出多少信息。这三件事的边界不清楚调一天也调不出结论。4.1 next、step、finish、until 四条命令的语义差别(gdb) break main (gdb) run (gdb) next # 执行一行遇到函数调用当整体跳过 (gdb) step # 执行一行遇到函数调用进入函数内部 (gdb) finish # 跑完当前函数打印返回值后停住 (gdb) until 45 # 一直执行到第 45 行 (gdb) continue # 跑到下一个断点或程序结束逻辑说明next和step都按「源码行」推进区别只在函数调用处——前者把整个调用当一行后者钻进去。finish是「我不想再看这个函数了」它会把当前栈帧执行完并打印Value returned is ...非常适合确认一个工具函数到底返回了什么。until是跳出循环的快捷键比连点next快一个数量级。参数与边界until不带参数时会一直跑到当前行号更大的位置带行号时跑到指定行且只在当前栈帧内有效跨函数不会生效。step遇到没有调试信息的函数比如系统库会退化成next这不是命令失效是那边没有行号表。汇编级还有nexti/stepi按机器指令走只有在确认「源码行和指令对不上」时才用得上。4.2 循环里单步别用 next 硬点用条件断点和 ignore一个跑 100 次的循环从第 1 次点next点到第 100 次属于调试里最低效的动作。四种替代手段按场景选(gdb) break stat_demo.c:38 if count 99 # 只在地下第 99 次停 (gdb) ignore 1 50 # 前 50 次命中直接放过 (gdb) until 41 # 从当前位置跑到循环结束行 (gdb) finish # 直接结束整个函数逻辑说明条件断点把「判断」交给 GDB程序只在满足条件时停ignore是给已存在的断点加一个计数前 N 次命中不打断适合「我知道前几十次没问题只想看后面的」until适合「循环体里已经确认无误想快速跳到循环后面」finish是彻底放弃这个函数的细粒度观察。参数说明ignore的第一个参数是断点编号不是行号要先info breakpoints查条件断点里的变量必须在命中位置可见循环变量被优化掉时会失效until的目标行必须大于当前行往回跑用jump慎用会跳过部分初始化逻辑。4.3 print 的格式符与历史变量(gdb) print total (gdb) print/x total # 十六进制 (gdb) print/d count # 有符号十进制 (gdb) print/u count # 无符号十进制 (gdb) print/c buf[0] # 按字符显示 (gdb) print/f avg # 按浮点显示 (gdb) print $1 # 引用上一条 print 的结果 (gdb) print $1 $2 (gdb) whatis total (gdb) ptype struct sample逻辑说明print不只是「看一眼」它接受完整表达式能做算术、能调函数、能解引用结构体。格式符是斜杠后的单字母直接决定同一块内存按什么类型解释。$1、$2是 GDB 自动维护的值历史变量后续表达式可以直接引用省去重复输入长表达式。参数与一处文档笔误/x十六进制、/d有符号十进制、/u无符号十进制、/o八进制、/t二进制、/c字符、/f浮点、/a地址。这份 PPT 在「查看数据」一节里把/u写成了「按十六进制格式显示无符号整型」这是笔误——/u是十进制无符号十六进制只有/x。照着文档敲/u却看到十进制数时别怀疑自己是文档错了。whatis只看类型名ptype会把结构体成员完整展开排查结构体布局时用后者。4.4 examine 按字节读内存print解决「这个变量是什么」x解决「这块内存里到底是什么」。当类型系统骗了你——比如指针被强转、缓冲区越界、结构体对齐和预期不符——只有按字节看才靠得住。(gdb) x/5dw total # 从 total 起读 5 个 4 字节按十进制 (gdb) x/8cb buf # 读 8 个字节每个按字符显示 (gdb) x/s line # 按字符串读 (gdb) x/4i $pc # 从当前指令起反汇编 4 条 (gdb) x/2gx s # 读 2 个 8 字节十六进制逻辑说明x的语法是x/数量 格式 单位三段可以只写一部分缺省值分别是「1 个」「十六进制」「4 字节」。total取地址buf本身是指针时直接写变量名$pc是程序计数器配合i格式就能看到当前停在哪条指令上。参数说明单位字母b单字节、h双字节、w四字节、g八字节默认w。读字符串用x/s读宽字符或 UTF-8 时可能会截断需要自己按字节拼。x/4i $pc在「源码行对不上指令」时特别有用——你能直接看到编译器到底生成了什么比盯着源码猜快得多。4.5 backtrace、frame 与寄存器(gdb) backtrace # 当前调用栈 (gdb) bt full # 带局部变量的完整栈 (gdb) frame 1 # 切到第 1 层栈帧 (gdb) info locals # 当前帧的局部变量 (gdb) info args # 当前帧的参数 (gdb) info registers rip rsp rbp (gdb) print $pc (gdb) print $sp逻辑说明backtrace回答「我是怎么走到这里的」输出从#0当前帧往上编号越往上越靠近main。frame N切帧之后print、info locals的作用域都跟着变这是排查「参数传错层」的关键动作。寄存器里$pc是下一条要执行的指令地址$sp是栈顶$fp/$rbp是当前帧基址崩溃现场对栈回溯全靠这几个值。参数说明bt full会把每层的局部变量一起打出来栈深的时候输出很长通常配合bt -3只看顶部三层。info registers不带参数会打印全部寄存器带上名字只显示关心的几个。寄存器命名跟架构有关x86-64 是rip/rsp/rbp别的架构名字不同用info registers先看一遍再决定。这部分内容 PPT 里以$pc、$fp、$sp、$ps四个标准名做了归纳够用来入门真到看崩溃现场还是要以实际架构为准。5. 避坑与排查五条踩出来的记录GDB 的命令本身不难难在出错时它给的信息往往指向错误方向。下面五条按「现象 → 原因 → 解决」写都是我在真实项目里反复遇到的。5.1 符号与断点相关的三个坑现象一bt出来全是地址和??一个函数名都没有。原因可执行文件没带-g或者带-g之后又被strip掉了.symtab和.debug_info段缺失。core 文件本身没问题是符号提供方没到位。 解决用readelf -S确认调试段是否存在用file看是否not stripped。如果是线上 core必须找到同一次构建、未 strip 的二进制再加载版本不一致时不要硬看栈帧会错得毫无规律。现象二断点明明设上了run之后程序直接跑完一次都没停。原因-O2下代码被内联或重排行号表里的位置和实际执行流对不上另一种情况是多文件项目里break 38指的是「当前源文件」而你以为指的是另一个文件。 解决先降到-O0复核断点一律写成文件名:行号或直接下在函数入口用info line 文件名:行号查这一行对应的实际地址范围地址是空的就说明这行被优化没了。现象三print变量报No symbol xxx in current context或者optimized out。原因前者是作用域问题——你停在了别的栈帧或别的函数里后者是优化问题——变量被塞进寄存器、被复用或者干脆被消除。另外这份 PPT 里/u的格式说明写错了照着敲会得到不符合预期的输出也常被误当成「变量值不对」。 解决先bt看自己在第几层frame N切回正确栈帧再打印确认是优化问题就回-O0重编格式符以/x十六进制、/u十进制无符号为准文档里的笔误要自己纠正。5.2 观察点、信号与线程的两个坑现象四watch一个变量之后程序慢到几乎跑不动甚至像卡死。原因硬件观察点数量有限超出后 GDB 退化成软件观察点每执行一步就比对一次内存如果 watch 的是大块内存或指针指向的整个结构比对成本直接爆炸。 解决把 watch 范围缩到单个字段别 watch*ptr能用条件断点替代的就替代确认变量没被优化进寄存器否则 watch 根本不触发。现象五程序收到 SIGSEGV 之后 GDB 没停或者 CtrlC 完全没反应。原因handle策略被改过信号被pass给程序自己处理了GDB 只打印一行就继续跑CtrlC 发的SIGINT同样可能被程序注册的处理函数接管。 解决进 GDB 先info signals SIGSEGV和info signals SIGINT看当前策略需要现场就配handle SIGSEGV stop print nopass需要让程序自己处理就配handle SIGINT nostop pass。多线程下next跳到别的线程则是调度器抢占导致用set scheduler-locking on把执行权锁在当前线程。6. 进阶把断点变成日志点用脚本代替手点手敲命令适合一次性的现场排查但同一类问题复现第三次的时候就该把命令固化成脚本了。GDB 的commands能让断点在命中时自动执行一串动作配合silent和printf等于在不停程序的前提下打日志——这对复现概率低、一停就变样的时序问题尤其管用。(gdb) break stat_demo.c:38 (gdb) commands silent printf count%d total%ld p%s\n, count, total, p continue end这段配置的逻辑是断点命中后不打印 GDB 默认的停住信息silent用printf输出关心的几个值然后自动continue。程序照常跑完终端里留下一串采样记录。参数上commands后面跟断点编号才是精确绑定不跟编号时绑定最近设置的断点printf的格式符跟 C 语言一致但不能用%s去打一个空指针否则会在调试会话里再崩一次。把这套动作存成文件就能批量化# trace.gdb —— 用 gdb -q -batch -x trace.gdb ./stat_demo 执行 set pagination off set confirm off break sum_all commands silent printf enter sum_all, line%d\n, __LINE__ continue end rungdb -q -batch -x ./trace.gdb ./stat_demo-batch表示跑完就退出、不进入交互-x指定脚本文件。这条链路可以塞进构建流水线每次提交后自动跑一遍带日志断点的版本输出里出现异常值就报警。我用它抓到过一次只在特定输入下出现的计数溢出——手动复现了几十次没成功脚本跑了三分钟就把越界前的那几次累加值全打出来了。从那以后只要一个问题复现超过两次我都会先写个.gdb脚本再动手而不是继续在终端里重复敲next。这份 PPT 把命令体系列得很全正好当速查表用需要原件的按资源页说明取用即可。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。