☰
GDB单步调试详解:从断点设置到core文件现场还原
2026/10/11 14:45:02 网站建设 项目流程

简介:GDB单步调试详解PPT.pdf 是一份面向C/C++及嵌入式开发者的GDB调试实战指南,适合正在学习Linux命令行调试、希望系统掌握程序运行控制与排错思路的初中级工程师。资源围绕GDB单步调试核心流程展开,详细讲解了编译时加入-g调试信息、通过gdb加载可执行文件,以及利用break设置断点、step/next逐行执行、continue恢复运行等基础操作,同时覆盖观察点、捕捉点、信号处理和线程中断等高级暂停机制,并演示了print、examine、backtrace等查看数据与调用栈的实用命令。资料以PDF格式呈现,共1个文件,压缩包大小2.34MB,内容精炼、结构清晰,适合作为日常调试时的随身参考。目前已有117人学习下载,对于需要快速定位程序崩溃、死循环或逻辑异常的开发者,这份PPT能帮助梳理完整的调试方法论,提升问题排查效率。

1. GDB 单步调试:先让断点停下来,再谈值对不对

GDB 单步调试是排查 C/C++ 逻辑错误最直接的手段,但很多人第一周就被劝退:编译时忘了加 -g,进到 GDB 里全是内存地址,源码对不上;或者 break 设了、run 也跑了,结果 next 和 step 分不清,循环里跳来跳去,最后还是退回 printf 打日志的老路。这份 GDB 单步调试详解 PPT 把断点、观察点、捕捉点、信号处理和单步执行串成完整链路,从 break linenum 到 print 再到 backtrace,每类命令都有清晰的适用场景。它适合刚学完 C/C++、一崩溃就不知道从哪查起的新手,也适合依赖 IDE 调试、想看清程序真实执行流的从业者。学完你至少能做到:让程序停在你想停的行,在停住的瞬间把变量值和调用栈全部摸清楚。

2. 用 -g 编译是前提:调试信息没有,GDB 只能看到裸地址

2.1 编译选项 -g:把变量名、函数名、行号写进可执行文件

很多人在第一个坑里就翻车了:用gcc test.c -o test编译,然后gdb test启动,进去敲list,GDB 直接回一句No symbol table is loaded。这不是 GDB 坏了,而是可执行文件里压根没有调试信息。GDB 能看到的符号——变量名、函数名、源码行号——全部来源于编译时写入二进制文件的 DWARF 调试信息,而这份信息默认是不生成的。

正确的编译命令至少要带一个 -g:

gcc -g -O0 -o test test.c g++ -g -O0 -o app app.cpp -std=c++11

逻辑说明:-g让编译器生成调试信息,包含源码行与机器指令的映射、变量名与寄存器和栈地址的映射;-O0关闭优化,避免变量被优化进寄存器或直接常量折叠,导致源码层面的单步执行与真实指令流脱节。参数说明:如果日后要调试线上 release 版本,可以用-O2 -g,但要接受两个代价——部分局部变量会被优化掉,print 时 GDB 提示value optimized out;代码行顺序也可能与源码不完全一致,next 跳行是常态。学习阶段一律-g -O0,这是成本最低的调试配置。

2.2 启动 GDB 的三种姿势:直接加载、file 补加载、core 与 attach

先说最常用的两种,raw 材料里都提到了。直接加载是gdb test,进入会话后还能用file命令补加载另一个可执行文件;如果程序崩溃生成了 core 文件,或者想调试一个正在运行的守护进程,那就得用第三种姿势。

# 方式一:直接加载可执行文件 gdb test # 方式二:先进 gdb,再用 file 命令补加载 gdb (gdb) file test # 方式三:调试 core dump 文件 gdb test core # 方式四:attach 到正在运行的进程 gdb -p 12345

逻辑说明:gdb test core里的 core 是程序崩溃时由内核转储的内存镜像,GDB 通过它还原出崩溃瞬间的调用栈、寄存器值和全局变量状态;gdb -p 12345会 attach 到 PID 为 12345 的进程,适合调试已经跑起来的服务程序。参数说明:针对正在运行的进程,attach 的同时自动暂停目标进程;调试完不要直接 quit,先detach再退出,否则目标进程会被 GDB 一并终止。这是线上服务调试的高危操作,记不住就先把detach写在便签上。

2.3 list 和 set args:动手前先把现场布置好

进入 GDB 还没设断点之前,先做两件事:确认源码能被正确列出、确认程序启动参数是对的。list 的灵活度比很多人以为的高,特别是多文件的工程:

(gdb) list (gdb) list 12,25 (gdb) list main (gdb) list test.c:16 (gdb) set args -b -x (gdb) show args

逻辑说明:list不带参数默认从当前停止位置或程序入口开始列 10 行源码;list 12,25显示第 12 到 25 行,设断点前先把行号范围看清楚;list test.c:16指定文件与行号,多文件工程里不会列错文件。set args -b -x设置了 run 时传给程序的参数,这里 -b 和 -x 只是示例,实际按你的程序需要填写。参数说明:show args查看当前缺省参数列表,set args后面不跟内容表示清空参数。有个容易被忽略的细节——不带参数的run会复用上一次设置的 args,如果你改了参数却不记得改过,反复 run 会出现“明明改了配置,程序行为却没变”的假象。

3. 暂停程序的四种方式:break、watch、catch、handle 怎么选

GDB 的核心能力是让程序按你的意愿停下来。停的方式不是只有断点一种,原始材料里列的这张表值得好好展开:断点按位置停、观察点按值变停、捕捉点按事件停、信号按信号到达停。选对了,定位效率差一个数量级。

3.1 break 断点:四种形式与条件断点 break...if

break 是最基础的暂停手段,语法上支持行号、函数名、内存地址,也支持文件名:行号和条件表达式。多文件工程里最忌讳只敲行号不敲文件名——GDB 只在当前文件里找行号,跨文件就找错地方。

(gdb) break 16 # 当前文件第 16 行 (gdb) break func # 进入 func 函数时 (gdb) break test.c:16 # 指定文件 test.c 第 16 行 (gdb) break 46 if testsize==100 # 条件断点 (gdb) break *0x08048456 # 按内存地址设断点

逻辑说明:break 16在源程序第 16 行处停下,执行到该行之前触发;break func在函数入口处停下,适合确认这个函数到底有没有被调用、入参是什么;break 46 if testsize==100是条件断点,每次执行到第 46 行都会判断条件,只有 testsize 等于 100 时才真正停下。参数说明:break *0x08048456这种按地址断的方式,在调试没有调试信息的二进制的场景更常用,比如你手里只有一个 stripped 的库,但这属于进阶玩法,普通应用层调试用不上。

断点设多了必须会管理,不然自己都记不住哪个断点是干嘛的:

(gdb) info break (gdb) disable 2 (gdb) enable 2 (gdb) delete 2 (gdb) clear 16

逻辑说明:info break显示所有断点,输出里 Num 是编号、Enb 是启用状态(y 启用 / n 禁用)、What 是断点位置描述。disable 2禁用 2 号断点但保留定义,enable 2重新启用,delete 2彻底删除,clear 16清除当前文件第 16 行上的所有断点。原则是:不确定还要不要的断点一律 disable,不要急着 delete,删了再想找回来就得重新设一遍。

3.2 watch 族观察点:变量被读、被写时自动停

break 关心“在哪一行停”,watch 关心“这个变量什么时候变了”。排查全局变量被谁改掉的问题,break 很难定位,watch 是正解。

(gdb) watch g_flag # g_flag 被写入时停下 (gdb) rwatch pBuf # pBuf 被读取时停下 (gdb) awatch sum # sum 被读或被写都停下 (gdb) info watchpoints

逻辑说明:watch g_flag监控变量的写入,只要有代码修改它,程序立刻停下,配合bt就能看到是谁在哪个函数里改的;rwatch pBuf监控读取,排查悬垂指针被读、读到已释放内存的场景时非常有用;awatch sum读写都监控,信息量大,但触发频繁,调试时如果被它吵得不耐烦就退回落 watch。参数说明:watch 后可以跟复杂表达式,比如watch pNode->count,但是 pNode 必须在当前帧可见,否则 GDB 报No symbol "pNode" in current context。硬件的 watchpoint 数量是有限的,x86 上一般四个左右,用完了 GDB 会自动降级成软件模拟单步检查,程序会慢到肉眼可见,这是正常现象。

3.3 catch 捕捉点:C++ 异常 throw/catch 的定位利器

C++ 程序里最折磨人的问题:异常从一个深处被 throw,外层 catch 的时候调用栈早就变了,光靠断点和 print 找不到抛出点。catch 专门解决这个。

(gdb) catch throw # 异常被抛出时停住 (gdb) catch catch # 异常被捕获时停住 (gdb) info catch

逻辑说明:执行到throw语句的瞬间,GDB 会立刻停住,这时候打印调用栈bt,看到的就是异常真正产生的现场——哪个文件的哪一行抛出来的、当时的局部变量是什么值,全都一目了然。catch catch则是在异常被某个 catch 块接住时停下,可以确认异常类型和捕获点。这对 C++ 异常路径分析几乎是必须的:不需要在代码里猜,也不需要打断点碰运气。参数说明:raw 材料里提到的catch exec、catch fork、catch load目前只在 HP-UX 上有实际效果,在 Linux 上意义不大,不用花时间研究。

3.4 handle 信号控制:SIGINT、SIGSEGV、SIGPIPE 的取舍

程序运行时会收到各种信号:SIGSEGV 段错误、SIGINT 键盘中断、SIGPIPE 管道断开。GDB 对这些信号的默认处理是“停住程序”,但有些信号你并不想停——比如网络服务里 SIGPIPE 频繁出现,每次都停就没法调了。

(gdb) handle SIGPIPE nostop print pass (gdb) handle SIGINT stop print (gdb) handle SIGSEGV stop print (gdb) handle SIGBUS nostop noprint pass

逻辑说明:handle后面跟信号名和参数组合,参数含义是:nostop表示收到信号时不停住程序,stop表示停住;print表示打一条消息告诉你信号到了,noprint不打;pass表示把信号交给被调试程序自己处理,nopass表示 GDB 截住信号不让程序处理。上面的配置让 SIGPIPE 到达时不打断、只提示、并且交给程序处理,SIGINT 和 SIGSEGV 则保持默认停住。参数说明:pass等同于noignore,nopass等同于ignore,不同 GDB 版本里两种写法都认。SIGINT 对应 Ctrl+C 信号,如果程序死循环,你需要在 GDB 里用 Ctrl+C 打断它,所以 SIGINT 务必保持 stop。

4. 单步执行与数据查看:next、step、finish、print、x、bt 的组合拳

程序停住之后,真正的调试才刚刚开始。这一章把 GDB 里最高频的六个命令讲透,它们是单步调试的基础操作。

4.1 next 不进入、step 进入:单步执行的核心差异

next 和 step 长的很像,行为差在“要不要钻进函数里”。用下面的例子感受差别:

#include <stdio.h> int func(int n) { int sum = 0; for (int i = 0; i < n; i++) sum += i; return sum; } int main() { int result = func(5); printf("%d\n", result); return 0; }

假设断点设在int result = func(5);这一行,执行next会整行跑完,直接停在printf那行,func 函数内部发生了什么你看不到;执行step则会进入 func 函数体,停在int sum = 0;。

(gdb) next # 简写 n:执行一行,不进入函数 (gdb) step # 简写 s:执行一行,进入函数 (gdb) continue # 简写 c:继续执行直到下一个断点或程序结束

逻辑说明:next的语义是“在当前函数内前进一行”,遇到函数调用就把整个调用当作一行执行完;step的语义是“前进一条语句”,遇到函数调用就钻进去,停在被调函数的第一条语句。参数说明:continue不是单步命令,但它是单步调试的出口——你想快速跑到下一个断点而不是一行行捱,就需要它。调试循环时,先 step 进去确认循环逻辑,再 continue 到下一次断点,最后 next 逐行收尾,这套组合能覆盖绝大多数场景。

4.2 finish 退出函数与 call 手动调用

钻进函数里之后想出来,除了 continue 等程序自然返回,还有更精准的finish。它执行到当前函数返回,并打印返回值:

(gdb) finish Run till exit from #0 func(n=250) at tst.c:5 0x080484e4 in main() at tst.c:24 Value returned is $1 = 31375

逻辑说明:finish一次性跑完当前函数剩余部分,停在调用处下一行。上面输出里Value returned is $1 = 31375是 GDB 把返回值存入历史变量$1,后续可以直接用print $1继续引用,不用重复调用。call则是在调试会话里直接调用一个函数,适合验证某个函数的中间结果而不改源码:

(gdb) call hash_calc("abc", 3) $2 = 918273

参数说明:call有副作用,调用free()或者修改全局变量的函数要格外小心——调试器里调用的效果和程序正常运行时一样,你把内存 free 了,程序后面再用就是悬垂指针。

4.3 print 的格式控制与数组查看

print 能打印变量、表达式、结构体,甚至可以打印数组片段和指定输出格式。后者在排查位掩码和缓冲区内容时特别有用。

(gdb) print i # 打印 i 的值 (gdb) print *array@10 # 从 array 指针开始连续打印 10 个元素 (gdb) print/x i # 十六进制格式 (gdb) print/t flags # 二进制格式 (gdb) print/c ch # 字符格式 (gdb) print/f fValue # 浮点格式 (gdb) ptype node # 打印结构体类型定义 (gdb) whatis i # 打印变量类型

逻辑说明:print *array@10是人为数组语法,array 是任意指针,@10 表示连续读取 10 个元素,适合把一块动态内存当数组看。格式符/x/d/u/o/t/a/c/f分别对应十六进制、十进制、无符号、八进制、二进制、地址、字符、浮点。参数说明:print/t flags看位掩码最直观——每一位的 0/1 直接对应标志位的开关状态,比看十六进制再心算快得多。ptype会展开结构体的完整定义和嵌套字段,比whatis的信息量大,不确定结构体长什么样时先ptype再print是标准操作。

4.4 x 查看内存与 bt 查看调用栈

print 面向变量,x(examine)面向内存。调试缓冲区溢出、指针越界这类问题,x 是唯一能直接看到内存原始内容的命令。

(gdb) x/10cw pFilePath # 按 4 字节单位、字符格式、连续 10 个单位 (gdb) x/20bx pBuf # 按单字节单位、十六进制、连续 20 个字节 (gdb) x/20d pBuf # 按十进制整数查看 (gdb) bt # 打印完整调用栈 (gdb) bt 3 # 只打印栈顶 3 层 (gdb) bt -5 # 只打印栈底 5 层

逻辑说明:x 命令的语法是x/[数量][格式][单位]地址。x/10cw pFilePath中,10 是数量,c 是字符格式,w 是 4 字节单位,pFilePath 是被查看内存的起点——pFilePath 本身是一个字符串指针,占 4 字节,连看 10 个单位就能看到整个字符串的内容和它在内存里的存储。x/20bx是排查字节型缓冲区时的惯用姿势,20 个字节按十六进制展开,一眼能看出 0x00 填充还是越界数据。bt在段错误出现时是第一个要敲的命令,它会列出从 main 到崩溃点经过的所有函数、每层的调用参数和源码行号,最下面的 #0 就是崩溃现场。参数说明:bt 3只看栈顶三层,bt -5只打印栈底五层,栈特别深的项目里可以按需裁剪输出,不用从头翻到尾。

5. 避坑:GDB 调试里六个容易翻车的典型问题

以下问题都是实际调试中反复出现的高频坑,每个都按现象、原因、解决三步说清。

5.1 现象:list 命令看不到源码,全是No such file or directory

原因:最常见的是编译时没加 -g,GDB 没有加载到调试信息;其次是启动 GDB 后源码文件被移动过,或者编译用的路径是相对路径,而当前工作目录已经不是编译时的目录。我的血泪经验是:有一半的“GDB 用不了”是编译命令少了一个 -g,另一半是编译机和调试机目录结构不一致。解决:先用gcc -g -O0重新编译,确认info files里能看到 debug info;如果编译信息正常但 list 仍然失败,用directory /path/to/src添加源码搜索路径。最后的手段是set substitute-path /old/path /new/path,把编译时的路径映射到当前路径。

5.2 现象:条件断点一直不满足,程序跑完整场都没停

原因:条件表达式写错了。break 10 if i==100是对的,写成break 10 if i=100是赋值而非判断,条件恒真,断点每次都会停;反过来,条件写成i==101而 i 根本不取这个值时,断点整个运行期间一次都不触发,你还以为条件断点有 bug。解决:先print i确认变量的实际值和类型,再看条件逻辑。修改一个已有断点的条件用condition 断点编号 新条件,不用删除重建。提醒一句:条件里引用的变量必须在断点所在位置可见,否则 GDB 会静默忽略,不报错也不触发。

5.3 现象:next 单步时一步跳过一大段代码,甚至整个函数

原因:带 -O2 及以上编译的程序,优化器会做指令重排、内联、循环展开,源码行与机器指令不再是线性对应。GDB 的源码级单步依赖行号表,优化后的行号表本身就乱。解决:学习调试一律用-O0;如果必须调试优化版本,切到汇编视图layout asm,用ni(next instruction)和si(step instruction)按指令级单步执行,这时候行号不可信,寄存器才是真实的。接受一个事实:优化后的程序里,局部变量可能不存在于内存而是直接在寄存器里,print 不出来不是你的调试技术不行,是编译器把它优化没了。

5.4 现象:print 大结构体或大数组时内容被截断,只显示一部分

原因:GDB 默认限制打印元素数量,大型数组或 STL 容器超过阈值就会被省略号截断。解决:用set print elements 0取消数量限制,set print pretty on让嵌套结构体按缩进逐层展开,set print array on让数组元素一行一个。这三条建议直接写进 ~/.gdbinit,省得每个会话手动敲。STL 容器打不全的另一层原因是 GDB 版本太老,GDB 10 以上对 C++11/14/17 容器的 pretty-printer 支持已经比较完善,老版本不行就升级。

5.5 现象:attach 到进程后退出 GDB,目标进程也被杀掉了

原因:attach 之后的退出流程不对。直接quit时,GDB 在某些场景下会 kill 掉被调试进程,特别是当目标进程因为断点已经处于停止状态时,这个风险很高。解决:退出前先执行detach,看到Detaching from program输出之后再用quit。我在调试线上服务时的固定操作顺序是:确认调试完成后,先detach,再continue确认目标进程恢复运行,最后才退出 GDB。这套流程没出过问题。

5.6 现象:watch 命令报错,提示无法设置观察点

原因:芯片或硬件平台不支持硬件 watchpoint,或者硬件调试寄存器已经被占用。嵌入式开发里这种限制尤其常见。解决:先执行show can-use-hw-watchpoints,如果值为 0,用set can-use-hw-watchpoints 1打开;如果硬件本来就不支持,GDB 会退回软件模拟,意味着每一步都停下来检查值,程序会非常慢。这通常是平台限制,不用死磕,改用断点加手动检查的笨办法,效率也可以接受。

6. 把 GDB 用出效率:.gdbinit、commands 脚本与 core 现场

6.1 .gdbinit 启动脚本:高频设置在启动时自动加载

每次进 GDB 都手动敲set pagination off太浪费生命。把常用设置写进 ~/.gdbinit,GDB 每次启动自动执行:

set pagination off set print elements 0 set print pretty on handle SIGPIPE nostop print pass

这四行的含义:set pagination off关掉分页,打印长调用栈时不会被--More--打断,省了持续按回车;set print elements 0解决大数组打印被截断的问题;set print pretty on让嵌套结构体按层级缩进,可读性好很多;handle 行把 SIGPIPE 设置为不停不干扰,调网络服务时必须要有。项目组如果多人共用一套调试约定,把 .gdbinit 放进代码仓库,大家同步一致的环境,比各自口头约定可靠得多。

6.2 commands 断点命令组:不改源码的临时 printf

断点命中后自动执行一串命令,这是 GDB 被低估的功能。它相当于给断点挂一个回调,自动打印关键信息再继续跑:

(gdb) break 46 (gdb) commands > silent > printf "i=%d, buf=%s\n", i, buf > continue > end

逻辑说明:给 46 行的断点绑定一组命令,命中时silent抑制默认的断点提示,printf输出你关心的变量,continue自动继续执行。这样实现了不重新编译就能拿到类似 printf 的日志输出——临时想看什么,改 commands 就行,比在源码里加 printf 然后重新编译循环高效得多。参数说明:删除断点命令组用commands 2重新定义或直接删掉对应断点。这套技巧在循环里跟踪变量演化时特别好用:断点设一次,每次命中自动打印,不用一遍遍手动 next。

6.3 core 文件调试:段错误后的后悔药

段错误发生后如果没留下现场,只能重新跑一遍碰运气。core 文件就是崩溃现场的快照,利用好等于事故后的后悔药:

ulimit -c unlimited # 允许当前 shell 生成 core 文件 ./test # 程序崩溃,生成 core 文件 gdb test core # 直接用 gdb 加载 (gdb) bt # 第一步:看调用栈 (gdb) info registers # 第二步:看寄存器状态 (gdb) frame 2 # 第三步:切换到指定层 (gdb) list # 第四步:查看对应源码

逻辑说明:ulimit -c unlimited是前提,Linux 默认常常是 0,不开的话崩溃了什么都不会留下;程序跑崩之后,当前目录或 /var/lib/systemd/coredump 下会出现 core 文件,gdb test core直接加载。进入 GDB 后的固定操作顺序:先bt看全貌,知道崩在哪几个函数里;再info registers确认 pc、sp 这些关键寄存器是不是指向了非法地址;然后frame 2切换到调用栈的某一层,list看那一层的源码,逐层排查入参是谁传进来的。参数说明:core 文件体积可能很大,生产环境里建议用 systemd-coredump 或core_pattern做统一收纳和管理,别让 core 文件裸奔在临时目录里被覆盖。

6.4 调试流程的固定验证步骤

最后分享我自己的固定习惯。接到一个新的工程,不管任务多急,我都会强制走一遍这套基础流程:确认编译命令带-g -O0,启动 GDB 后先list验证源码可见,设一个 main 入口断点,run 到断点后 print 一个全局变量确认数据加载正常,bt 确认调用栈第一层是预想的函数,然后才真正开始调试。这套验证花不了两分钟,但能排除掉一半以上的环境问题。从那以后,我每次收到编译好的二进制都会先问一句“带没带 -g”,这个习惯帮我省掉了无数次“GDB 怎么又用不了”的排查,希望帮到你。

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

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

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

立即咨询