☰
GDB单步调试实战:从断点到core文件排错指南
2026/10/31 12:37:47 网站建设 项目流程

简介:这份PDF面向使用GNU工具链的C/C++开发者及计算机相关专业学生,聚焦命令行调试场景,系统梳理GDB单步调试的知识链路,帮助读者理解程序运行行为、定位变量与调用栈问题,并提升调试效率。内容从带-g选项编译、启动GDB与加载符号讲起,覆盖断点设置、run运行、step/next/continue单步控制、print/display变量查看及backtrace调用栈分析,并延伸到观察点、捕捉点、信号处理、线程中断、条件断点、临时断点、内存与寄存器查看、输出格式控制等进阶用法,适合课堂实验、课程设计或日常排错时查阅。资源包共1个PDF文件,大小约2.34MB,内容按命令主题组织,结构清晰,便于快速检索。目前已有118人学习,可作为从基础操作过渡到高级调试技巧的参考,也适合对照练习与查漏补缺,尤其适合需要命令行调试的初中级开发者随手翻阅。

1. 一份 GDB 单步调试 PPT:它填的是「会敲命令、但走不动程序」那道坎

第一次被一个 C++ 程序逼到墙角的人,往往不是不会敲gdb,而是断点停住之后不知道该按什么、该看哪一行、怎么判断这一行到底算错了没有。这份《最新 GDB 单步调试详解 PPT》PDF 就冲着这段路来:它不讲编译器原理,只把「编译带 -g、起调试器、下断点、单步、看变量、看栈」串成一条能照抄的顺序,顺带把观察点、捕捉点、信号处理、线程中断这些平时用得少、关键时刻救命的功能列全了。它更像一张可以放在第二个屏幕上的速查卡,而不是一本从头读到尾的教材。适合两类人:刚在 Linux 上写 C/C++、排错只会printf的新手;和用了几年 GDB 却一直停在break / run / print三件套、碰到多线程和 core 文件就发怵的熟手。关键词里那个「gnu c++ 软件/插件」的诉求,本质上就是想要一套能被反复套用、而不是每次重新搜的调试动作序列。

2. 从 -g 到第一个断点:符号表、启动方式和 run 的落点

调试这件事,成败一半在敲gdb之前就定了。程序编译出来如果没有调试信息,GDB 眼里只有一堆地址,你能停住,但停住之后看到的是一串十六进制,这就成了真正的黑匣子。所以这一章先把编译期的事情做对,再谈怎么把程序拉到断点上。

2.1 编译期就定胜负:-g 与 -O0 的取舍

-g干的事情是让编译器把符号表、行号映射、变量类型和作用域写进目标文件,GDB 才可能把「0x4005a6」翻译成「demo.c 的第 23 行」。不同优化等级下,这份映射的可靠程度差别很大,我一般默认-O0,等逻辑确认无误、要复现性能相关的问题时再回到-O2并接受行号会跳。

# 最常用的调试编译:保留调试信息,关掉优化,确保行号和语句一一对应 gcc -g -O0 -Wall -o demo demo.c # C++ 版本,注意扩展名和链接标准库的方式 g++ -g -O0 -std=c++17 -Wall -o demo demo.cpp # 需要看宏展开时用 -g3,调试信息里会额外保留宏定义 gcc -g3 -O0 -Wall -o demo demo.c # 多源文件工程:调试信息要加在每一个编译单元上,不能只加在链接那一步 gcc -g -O0 -c a.c b.c main.c gcc -g -O0 -o demo a.o b.o main.o

几个参数值得单独说清楚。-g是总开关,它生成的是 DWARF 格式的调试段,最终会被写进可执行文件的.debug_info、.debug_line这些 section 里。-g3比-g多保留宏定义,适合你需要info macro或者排查宏展开问题时用,代价是文件会明显变大。-O0的意义在于取消指令重排和变量寄存器化,让「一行源码」对应「一段连续可停的指令」;到了-O2,编译器可能把整个循环展开合并,GDB 报出来的行号会在几行之间来回跳,新手很容易以为自己程序逻辑写错了。-Wall不是调试必需的,但它能把一批本来要靠单步才能发现的问题提前暴露出来,省掉不少无效调试。

编译完之后先别急着进 GDB,花十秒验证一下符号到底进没进去,这一步能省掉后面半小时的困惑。

# 看文件是否被识别为带调试信息的可执行文件 file demo # 期望输出里出现 "not stripped",如果出现 "stripped" 说明符号被去掉了 # 直接看有没有 debug 段 readelf -S demo | grep -i debug # 看函数符号是否可见(-C 用于还原 C++ 的符号修饰) nm -C demo | head -n 20

如果file的输出里是stripped,那多半是构建流程里有一步strip,或者打包脚本对产物做了裁剪,这种产物拿去调是白费力气。readelf -S的输出里如果没有.debug_info,说明-g根本没生效,检查一下 Makefile 是不是把CFLAGS覆盖掉了——这是血泪经验里出现频率很高的一条,工程越大越容易踩。

2.2 三种启动姿势:可执行文件、core 文件、运行中进程

GDB 有三种进入方式,PPT 里都提到了,但用途差别很大,选错了会浪费很多时间。

启动方式命令形式典型场景注意点
直接调可执行文件gdb ./demo本地复现、逻辑调试最常用,符号自动加载
可执行文件加 coregdb ./demo core.1234线上崩溃、事后分析core 必须和二进制严格对应
附加到运行中进程gdb -p 12345或attach 12345服务型程序、没法重启的场景会暂停目标进程,注意影响

第一种最普通,进 GDB 之后还能用file换目标。第二种是事后复盘的主力,程序 core 掉之后现场已经没了,只剩一个内存快照,能不能读出东西全看符号和 core 是否匹配。第三种适合不能重启的长跑进程,但要有心理准备:附加进去的瞬间整个进程是停住的,如果它在对外提供服务,这段时间就是不可用状态,所以附加之后尽快把需要的信息抓完,然后detach放开。

除了启动方式,还有几个开关我几乎每次都会先设上,它们不改变程序行为,只改善你在 GDB 里的阅读体验。

# -q 抑制版权横幅,-tui 打开源码窗口(进去后 Ctrl+X A 可切换) $ gdb -q -tui ./demo (gdb) set pagination off # 关闭分页,backtrace 长的时候不用一直按回车 (gdb) set print pretty on # 结构体按缩进多行打印,比一行糊在一起好读 (gdb) set print elements 0 # 打印数组不截断,默认超过 200 个元素会显示 ... (gdb) set confirm off # 退出时不问 "Quit anyway?",脚本化时必开 (gdb) set args --input data.txt --level 3 (gdb) show args # 回看当前设置的运行参数

set args和show args这一对很容易被忽略。程序带参数运行时,直接在 shell 里写gdb ./demo --input data.txt也能传进去,但在 GDB 里反复run的时候,参数会沿用上一次的设置,改参数用set args更可控。set pagination off是新手最该先记住的一条,不加的话bt输出一长就卡在---Type <return> to continue---,很多人以为程序挂了。

2.3 第一个断点与 run:让程序停在你想要的那一行

list是进 GDB 之后第一个该敲的命令,不带参数从当前位置往下显示十行,带行号或函数名可以定位。养成先看源码再下断点的习惯,比盲打行号靠谱得多,尤其是源码改过而二进制没重编的时候。

(gdb) list # 从当前位置显示 10 行 (gdb) list demo.c:20,40 # 显示指定文件 20 到 40 行 (gdb) list swap # 显示 swap 函数附近的源码 (gdb) break demo.c:23 # 在 demo.c 第 23 行下断点 (gdb) break swap # 在 swap 函数入口下断点 (gdb) break swap if n > 100 # 条件断点:只有 n > 100 时才停 (gdb) break demo.c:23 thread 2 # 只在 2 号线程命中 (gdb) info breakpoints # 查看所有断点的编号、类型、启用状态、地址 (gdb) run # 运行到第一个断点

break的四类写法分别对应行号、函数名、文件名加行号、条件表达式,实际用起来最常翻车的是「行号对不对」。源码里空行、注释行、宏展开行在调试信息里的映射不一定符合直觉,稳妥的做法是先list确认目标行附近的内容,再下断点。info breakpoints输出里的Enb列是启用状态,y表示生效、n表示被disable掉了,这个字段一定要看,很多人以为自己设了断点,其实是上一轮调试时被禁用后没恢复。

run之前还有一个更温和的入口:start。它会临时在main函数入口停一下,效果相当于自动下一个只生效一次的断点。对于「我想从头跟一遍」的场景,start比break main+run省事,而且不会在断点列表里留下垃圾条目。程序停下来之后,GDB 会打印当前的行和即将执行的语句,这一刻才真正进入单步阶段。

3. 单步三件套与它的变体:next、step、finish 什么时候选哪个

停住之后才是真正考验判断力的地方。GDB 提供的单步命令不止三个,next、step、finish、until、advance、continue各有各的边界,选错了要么一头扎进标准库出不来,要么一步跨过出问题的那一行。

3.1 next 与 step 的边界:什么时候会掉进库函数

next执行一整行源码,遇到函数调用时把它当成一个整体,不进入函数体;step则相反,会钻进被调用的函数里。PPT 里用一句话概括了这个区别,但没讲清楚什么时候该用哪个。我的判断标准很朴素:这一行调用的是我自己写的、可能有问题的函数,就用step;调用的是标准库、第三方库或者已经验证过的工具函数,就用next。

(gdb) next # 简写 n,执行一行,不进入函数 (gdb) step # 简写 s,执行一行,进入函数内部 (gdb) continue # 简写 c,跑到下一个断点或程序结束 (gdb) stepi # 执行一条机器指令,反汇编调试时用 (gdb) nexti # 执行一条机器指令,不进入调用

step掉进库函数是新手最常见的翻车现场:本来想跟自己的parse_buffer,结果一步迈进malloc的内部分支,再想出来只能靠finish一层层退,或者干脆kill重来。有两种办法规避。第一种是finish,它会一直执行到当前函数返回,并打印返回值,这是退出误入函数最干脆的手段。第二种是skip,让 GDB 在单步时跳过指定函数或整个头文件路径,设置一次长期有效。

(gdb) skip function malloc # 单步时跳过 malloc (gdb) skip file /usr/include/* # 跳过系统头文件里的代码 (gdb) info skip # 查看当前跳过规则 (gdb) skip delete 1 # 删掉不需要的规则

skip在较新版本的 GDB 上才支持,如果敲了提示没有这个命令,就退回用finish手动退。另一个相关的开关是set step-mode on,它让step在没有调试信息的函数里也能停下来,适合排查「某个没有符号的库函数到底有没有被调用」这类问题,但在有符号的代码里开着它会让单步变碎,不建议常开。

3.2 finish、until、advance:三个省时间的跳法

一段几百行的循环,用next一行行按下去,按到手酸还不一定能到目标位置。GDB 有三个专门用来「跳」的命令,理解它们的差别能省下大量时间。

finish跳出当前函数,回到调用点并打印返回值,前面已经说过。until有两种用法:不带参数时,如果当前在一个循环里,它会一直执行到循环结束;带行号参数时,它会运行到指定行,但只在当前栈帧内有效,遇到函数调用不会进入。advance和until 行号类似,区别是advance在目标行位于当前函数之外时会失败,不能跨函数跳。

(gdb) finish # 执行到当前函数返回,打印返回值 (gdb) until # 跳出当前循环 (gdb) until 42 # 执行到本文件第 42 行 (gdb) advance handler # 运行到 handler 函数入口

这三个命令的共同前提是「程序还能继续跑」。如果当前行会崩溃,until和advance一样会把程序带到崩溃点,只是崩了之后没有断点缓冲区,现场得靠 core 或者bt现场抓。所以在往下跳之前,先确认这一段的输入数据是正常的,否则你跳过去看到的只是另一处尸体。

3.3 条件断点与 ignore:在循环里少按几百次 n

调试循环最痛苦的事情是:错误只发生在第 500 次迭代,而你从第 1 次开始按next。GDB 给了三种减少按键次数的办法,PPT 里提到了条件断点,另外两种更实用但容易被漏掉。

# 方式一:下断点时直接带条件 (gdb) break demo.c:23 if i == 500 # 方式二:先下普通断点,再补条件(适合事后想起来) (gdb) break demo.c:23 (gdb) condition 1 i == 500 # 1 是断点编号,用 info breakpoints 查 # 方式三:忽略前 N 次命中,第 N+1 次才停 (gdb) ignore 1 499 # 方式四:临时断点,命中一次自动删除 (gdb) tbreak demo.c:23

condition和ignore的区别值得说清楚:condition是在每次命中时求值表达式,只有为真才真正停住,程序仍然会在断点处被拦一下再放行,开销小但依赖于表达式能被求值;ignore是纯粹计数,前 499 次直接放过去,第 500 次才停,不需要表达式求值,在循环里更稳。条件断点里的表达式用的是 C 语法,=是比较,赋值要写set var,这一点和 GDB 命令行的其他部分不一致,容易写错。

还有一个比断点更轻的工具:dprintf。它不暂停程序,只在命中位置打印一段格式化内容然后继续跑,适合排查「某个值被谁改了」这类高频问题。

# 在 demo.c 第 23 行打印 i 和 sum,且不打断程序 (gdb) dprintf demo.c:23, "i=%d sum=%d\n", i, sum

用dprintf的时候要保证格式化串和参数类型对得上,参数错误不会报语法错,只会在运行时打印出乱码,这类问题排查起来反而费劲。

3.4 断点管理与自动化命令:让断点自己干活

断点多了之后必然要管理。disable、enable、delete、clear这一组命令 PPT 里列过,实际用起来最容易混淆的是delete和clear:前者按断点编号删,后者按源码行号清掉该行上的所有断点。

(gdb) info breakpoints # 看编号和状态 (gdb) disable 2 # 禁用 2 号断点,条目保留 (gdb) enable 2 # 重新启用 (gdb) delete 2 # 按编号删除 (gdb) delete # 不带编号删除全部,会二次确认 (gdb) clear demo.c:23 # 清掉 demo.c 第 23 行上的所有断点

更进一步的用法是给断点挂命令。命中某个断点之后自动打印几个变量、自动continue,这在跟踪高频路径时几乎是唯一可行的办法,因为手工按c根本跟不上。

(gdb) break demo.c:23 (gdb) commands 1 > silent > printf "i=%d sum=%d\n", i, sum > continue > end

commands后的silent用于压掉「Breakpoint 1, ...」那行默认提示,只保留自定义输出;最后的continue让程序自动往下走。三段缺一段效果都会打折:没有silent输出会混着断点信息,没有continue程序会停在那里等你按键。这一套配上前面说的ignore,基本上可以做到「放它跑十分钟,回来只看日志」。

4. 打印、观察点与调用栈:把「看一眼」变成能读出证据

单步只解决「走到哪」,真正的判断要靠「看到什么」。变量值、内存布局、调用栈这三层信息读不出来,单步就只是换个地方发呆。

4.1 print 的格式符和人为数组

print是 GDB 里功能最杂的命令之一,除了打变量值,还能打表达式、调函数、构造临时数组、查看历史值。PPT 里列了格式符,这里补上它们各自最容易用错的点。

格式符含义典型用途
/x十六进制看标志位、地址、掩码
/d有符号十进制普通整型变量
/u无符号十进制长度、计数类变量
/o八进制权限位
/t二进制逐位分析标志寄存器
/c字符单字节字符
/f浮点强制按浮点解释一段内存
/a地址形式看指针指向的符号
(gdb) print flags # 默认格式 (gdb) print/x flags # 十六进制 (gdb) print/t flags # 二进制,看第几位被置上 (gdb) p *arr@10 # 人为数组:把 arr 当成长度 10 的数组打印 (gdb) p $1 # 引用上一次打印的值(历史变量 $1、$2 ...) (gdb) p/x $pc # 程序计数器

人为数组@这个语法在 PPT 里出现过,很多人只记住写法没记住用途。当你手里只有一个指针、不知道长度,或者想看一段连续分配的内存内容时,p *ptr@N是最快的办法,比x命令更贴近「按类型看」的直觉。历史变量$1、$2也值得记,p *node打出地址之后,直接p *$1->next就能接着往下走,不用重复敲。

4.2 x 命令:n、f、u 三个参数怎么配

x是examine的简写,它和print的根本区别是:print按变量类型解释内存,x按你指定的格式和宽度解释内存。格式串是x/nfu addr三段拼起来的,n 是数量、f 是格式、u 是单位。

(gdb) x/10dw buf # 从 buf 开始,读 10 个,十进制,每个 4 字节 (gdb) x/16xb buf # 读 16 个字节,十六进制,每字节一个 (gdb) x/s name # 按字符串打印 (gdb) x/10i $pc # 从当前指令开始反汇编 10 条 (gdb) x/10cw pFilePath # 读 10 个字符,每个单元 4 字节

单位字母b、h、w、g分别是 1、2、4、8 字节,默认是w。这个参数最容易被忽略,结果是「明明变量是 int,打出来只有半个值」——因为单位没设对,GDB 按 4 字节读了一段本该是 8 字节的数据。PPT 里那个x/10cw pFilePath的例子,c是字符格式、w是四字节单位,含义是「按 4 字节为一组,每组取字符显示,共 10 组」,如果只想看一个 C 字符串,x/s更直接。

还有一个容易被低估的用法:x/10i $pc。程序崩溃或者停在奇怪位置时,先看接下来的十条指令,往往能判断出这行源码到底被编译成了什么样子,比对着源码猜有效得多。

4.3 观察点:watch、rwatch、awatch 与它的代价

断点是「执行到某处停」,观察点是「某个东西变了就停」。这两类完全不同的暂停机制,PPT 里分得很清楚,实际用起来观察点更考验判断,因为它可能慢得让人怀疑机器坏了。

(gdb) watch counter # counter 被写时停 (gdb) rwatch counter # counter 被读时停 (gdb) awatch counter # 读或写都停 (gdb) watch -location obj.field # 只监视这个字段,避免整个表达式反复求值 (gdb) info watchpoints # 查看当前观察点

watch的实现方式有两种:软件观察点每执行一步就重新求值表达式,慢;硬件观察点依赖 CPU 的调试寄存器,快但有数量上限(通常只有 4 个)。GDB 会优先尝试硬件方式,数量不够时退回软件方式。这就是为什么有的人加了三个watch之后程序跑得像卡死,有的人加十个也没事——取决于硬件寄存器的余量和表达式的复杂度。

watch -location这个写法值得单独提。直接watch obj.field时,GDB 监视的是「表达式 obj.field 的值」,如果obj是个指针或者表达式里有函数调用,每次单步都会重新求值,既慢又可能因为副作用给出错误结果。加-location之后监视的是那块内存地址,语义更接近「这个位置被改了」,行为也稳定得多。

4.4 backtrace 与 frame:顺着栈往回走

程序停住时,你看到的只是当前这一帧。真正的问题往往在上一层:参数传错了、返回值没检查、某个指针在上一个函数里就被置空了。backtrace把整个调用链摊开,frame让你切换到任意一层去查看当时的现场。

(gdb) backtrace # 简写 bt,打印调用栈 (gdb) bt 5 # 只打印栈顶 5 层 (gdb) bt -5 # 只打印栈底 5 层 (gdb) bt full # 连同每层的局部变量一起打印 (gdb) frame 2 # 切换到 2 号栈帧 (gdb) up # 上移一层 (gdb) down # 下移一层 (gdb) info args # 当前帧的参数 (gdb) info locals # 当前帧的局部变量

bt full是排查崩溃的第一选择,它会把每一层的局部变量都打出来,代价是输出很长,所以前面那条set pagination off就显得很重要。切换栈帧之后,print和info locals作用的都是那一帧的上下文,这一点新手经常搞混:切到frame 2之后p i打的是第 2 层的i,不是当前执行位置的i。

还有一条命令在 PPT 里被埋在info的长清单里,但实战价值很高:info registers和p $pc / $sp / $fp。当你面对的是一个没有源码或者源码和二进制对不上的崩溃现场时,寄存器是唯一还活着的线索,$pc指向出事指令,$sp和$fp能帮你判断栈有没有被打穿。

5. 避坑与排查:五个把新手卡在半路的现场

PPT 给的是「正确路径」,但真实调试里大部分时间花在「为什么我按文档做了却没反应」。这一章把五个高频现场按现象、原因、解决拆开,都是踩过之后才记住的类型。

5.1 编译与符号相关的两个坑

现象一:断点设了,run之后程序直接跑完退出,一次都没停。

原因通常有三种:编译时没加-g,GDB 认不出源码行;断点下在了根本没被执行到的分支上;或者程序被fork出去,断点只在父进程里生效。先看info breakpoints的地址列是不是0x0或者带<PENDING>字样,待定断点意味着符号还没加载,运行起来之后才会真正落位。

解决:确认二进制带调试信息(file输出里有not stripped),确认断点所在行确实在可执行路径上,必要时用start停在main再逐步往下走,看是从哪一步开始偏离预期。

现象二:源码明明改过了,GDB 里list出来的还是旧代码,断点位置也对不上。

原因是二进制和源码不同步——改完源码只重编了一个文件,或者根本没重编就重新gdb了。这类问题在多文件工程里特别常见,尤其是用脚本构建、增量编译规则写得不够严的时候。

解决:改完源码立刻重新构建,进 GDB 后先list看一眼目标行附近的内容对不对。如果怀疑符号缓存有问题,file ./demo重新加载一次,实在不行退出重进。这个动作我后来固定成习惯:进 GDB 第一件事不是下断点,是先list三行确认现场。

5.2 优化等级引发的三个坑

现象三:单步时行号在几行之间来回跳,明明没写循环却反复经过同一行。

这是-O2以上的典型表现。编译器会把相邻语句合并、把循环展开、把变量提到寄存器里,源码行和机器指令不再一一对应,GDB 只能按调试信息里的映射尽力而为,跳来跳去是正常现象,不是 GDB 的 bug。

解决:排查逻辑问题时用-O0重编一份专门用于调试的二进制,逻辑确认之后再切回优化版本验证。真要调试优化后的代码,改用stepi按指令单步,同时用disassemble /m看源码和汇编的混合视图,别跟行号较劲。

现象四:print一个明明还在用的变量,输出<optimized out>。

原因是这个变量在优化之后被放进了寄存器,或者在某个时间点之后就不再需要,编译器把它的内存位置回收了,调试信息里没有可读的地址。这不是变量丢了,是它的存储位置在编译期就被优化掉了。

解决:换到-O0版本复现;或者在当前帧用info registers找对应的寄存器(需要一点汇编基础);再或者用set var临时把值写进一个不会被优化掉的位置辅助观察。

现象五:加了两三个watch之后,程序慢到几乎不动,像死循环。

前面说过,软件观察点每执行一步都要重新求值,watch的表达式越复杂、嵌套越深,开销越大。三个软件观察点叠加,慢十倍都算正常。

解决:优先watch -location监视具体地址而非表达式;把观察点数量控制在硬件寄存器能承受的范围内;如果只是想临时看某个值的变化,改用dprintf打印,让程序全速跑,事后看日志。

5.3 运行期与多线程相关的坑

现象六:多线程程序里断点总是停在错误的线程,或者一停就整个进程卡住。

GDB 默认的调度方式是「全停」:任意一个线程命中断点,所有线程一起暂停。对于依赖线程间同步的逻辑,这一停就可能让另一个线程永远等不到信号,看起来像是程序死锁,其实是被调试器按住了。

解决:用info threads看线程列表,thread 2切到目标线程,break demo.c:23 thread 2让断点只对特定线程生效。需要单独放行某个线程时用set scheduler-locking on,只让当前线程跑,其他线程保持暂停。用完记得关掉,否则多线程逻辑会被调试器改造成串行执行,测出来的行为不可信。

现象七:程序收到SIGSEGV直接退出,还没来得及看现场就结束了。

默认情况下 GDB 会接管大部分信号并停住程序,但有些构建环境或者启动脚本会把信号处理改掉;另一种情况是崩溃发生在没有调试信息的位置,GDB 停住了你也看不出什么。

解决:先确认信号处理策略,handle SIGSEGV stop print确保它停在崩溃点而不是直接传给程序;停住后立刻bt full抓完整栈,再info registers记录现场;如果程序是 core 掉的,用ulimit -c unlimited打开 core 生成,事后再分析。信号这一组命令 PPT 里列得比较全,nostop、stop、print、noprint、pass、nopass六个参数的组合值得对着敲一遍,尤其是handle SIGPIPE nostop noprint pass这条,调试网络程序时不设它,管道断开就能把调试会话打断好几次。

6. 脚本化调试与 core 文件:把一次手工操作变成可复现流程

手工敲 GDB 命令的问题是每次都要重来一遍,尤其是那种「跑十分钟才崩」的 bug,重来一次的代价很高。GDB 支持把命令写进脚本文件,用-x加载,配合-batch可以做到无人值守跑完整个流程,输出重定向到文件,事后慢慢看。

# cmd.gdb:把一次完整的诊断流程固化成脚本 set pagination off set confirm off set print pretty on break demo.c:23 run --input data.txt bt full info registers thread apply all bt quit
# -batch 让 GDB 执行完脚本自动退出,适合放进 CI 或者定时任务 gdb -q -batch -x cmd.gdb ./demo > gdb_report.log 2>&1 # 分析 core 文件,同样可以脚本化 gdb -q -batch -x core.gdb ./demo core.1234 > core_report.log 2>&1

脚本里的每一条命令都对应一次手工操作,区别只在于它不会因为你手抖按错而中断。thread apply all bt在多线程场景下几乎是必加项,它把每个线程的栈都打出来,事后判断是哪个线程先动的手。-batch和-x的组合还有一个隐藏价值:把它挂到测试流程里,每次回归失败自动抓一份 GDB 报告,比事后凭记忆复现靠谱得多。

core 文件这条路要提前铺。默认很多环境是不生成 core 的,需要先ulimit -c unlimited放开大小限制,再确认/proc/sys/kernel/core_pattern指向的位置可写。程序崩溃之后拿到 core,用gdb ./demo core.1234打开,第一件事是bt full确认崩溃点和当时的局部变量,第二件事是info registers看指令指针和栈指针是否正常,第三件事是info proc mappings确认崩溃地址落在哪个模块里——如果落在一个共享库区间,问题很可能不在你的代码里。

(gdb) bt full # 完整调用栈 + 局部变量 (gdb) frame 0 # 回到崩溃帧 (gdb) info args # 看一眼参数有没有明显异常 (gdb) x/10i $pc # 崩溃点前后的指令 (gdb) info proc mappings # 崩溃地址属于哪个模块

这里有个前提必须强调:core 和二进制必须是同一次构建的产物,源码改了、重编了,旧 core 就对不上,符号会错位,读出来的栈是假的。我后来养成的习惯是每次发布留一份带符号的二进制存档,出问题的时候拿存档配 core,比现场重新构建再猜要稳得多。

从那以后我每次接手一个陌生的 C/C++ 工程,都强制走一遍这套动作:先用-g -O0编一份调试版,file和readelf -S确认符号进去了,进 GDB 先set pagination off再list确认源码同步,然后才下第一个断点。这套流程跑顺之后,单步调试就不再是碰运气,而是一件有固定入口、固定动作、固定出口的事。这份 PPT 我一般是当速查卡用,命令记不清的时候翻到对应那一页对一遍,比重新搜一遍快。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询