GDB 调试手册
程序崩溃或行为异常时,GDB 能把你直接送到出错的那一行。本手册按真实排查流程组织:从可调试构建、复现崩溃,到读栈、查内存,再用断点与观察点逼出逻辑错误。
关键词:bt看崩溃栈 ·watch抓变量改写者 ·core事后验尸 · C / C++ · Linux / WSL / MSYS2
目录
- 00 症状速查
- 01 可调试的构建
- 02 启动与复现
- 03 五步定位崩溃点
- 04 Core dump 事后分析
- 05 逻辑异常排查
- 06 多线程调试
- 07 难复现问题与进阶
- 08 命令速查表
- 09 效率配置
00 症状速查
拿到一个问题,先判断它属于哪一类,再走对应章节的流程。
| 你看到的现象 | 多半是 | 第一步动作 | 章节 |
|---|---|---|---|
Segmentation fault (core dumped) | 非法内存访问:空指针、野指针、越界、栈溢出 | run复现 →bt | §03 |
Aborted/assert失败 /double free or corruption | 断言失败、double free、堆被写坏 | bt找到 abort 的调用者 | §03 |
| 程序不崩,但结果不对 | 逻辑异常:分支走错、变量被意外改写 | 条件断点 +watch | §05 |
| 偶发崩溃,难以复现 | 时序、竞争条件、依赖外部输入 | 带条件的断点/观察点,或挂 ASan 长跑 | §07 |
| 多线程程序卡死不动 | 死锁 / 阻塞式等待 | gdb -p挂上去 →thread apply all bt | §06 |
| 程序 hang 住、CPU 打满或无响应 | 死循环 / 阻塞 IO | gdb -p挂上去 →bt看停在哪 | §06 |
01 可调试的构建
GDB 的一切能力都建立在符号信息之上。编译时不带-g,后面所有技巧都无从谈起。
gcc-g-O0-oapp main.c util.c# 调试版黄金组合g++-g-O0-oapp *.cpp gcc-ggdb3-O0-oapp main.c# -ggdb3 连宏定义都能展开:p MACRO_NAME-g:生成符号与行号信息。可以-g3加满。-O0:关闭优化。-O2会内联函数、重排代码、优化掉变量,调试时会看到<optimized out>,栈帧也与源码对不上。- 不要
strip调试用二进制;线上版本可以另编一个带符号的同版本二进制用于分析 core。 - 判断符号是否就位:
file app输出应含with debug_info;如果bt只有地址没有文件名行号,就是没带-g。
💡Windows 用户:优先在 WSL 里
sudo apt install gdb,或 MSYS2 装mingw-w64-ucrt-x86_64-gdb。MSVC(PDB 符号)编译的程序 GDB 读不了,请用 Visual Studio 或 WinDbg。
02 启动与复现
进入 GDB 的几种方式
gdb ./app# 最常用gdb--args./app-cprod.yaml# 命令行直接带程序参数gdb-p12345# 挂到正在运行的进程gdb ./app core.12345# 分析崩溃转储,见 §04gdb-tui./app# 带源码窗口的界面,见 §07在 GDB 里运行程序
(gdb) run -c prod.yaml # run 后面的参数传给你的程序 (gdb) set args -c a.yaml # 或预先设置参数,之后直接 run (gdb) start # 启动并停在 main 入口(临时断点) (gdb) continue # 从暂停处继续,直到断点或崩溃程序在 GDB 里崩溃时不会直接退出,而是停在出事的那一行——这就是最完整的现场。此时 GDB 会打印:
Program received signal SIGSEGV, Segmentation fault. 0x0000555555555151 in fill (dst=0x0) at app.c:5 5 strcpy(dst, "prod");信号处理
info signals查看 GDB 对每种信号的处理方式。网络程序常被SIGPIPE打断调试,可以让 GDB 忽略:
(gdb) handle SIGPIPE nostop noprint pass # 不拦截,直接交给程序处理03 五步定位崩溃点
以这个会崩溃的小程序为例:fill()收到一个 NULL 指针。
/* app.c */voidfill(char*dst){strcpy(dst,"prod");}// 第 1 行:崩溃发生在 strcpy 内部intmain(void){char*name=NULL;fill(name);// 第 4 行:祸根在这里}第一步:复现并看调用栈(Backtrace)
run触发崩溃后,第一件事永远是bt(backtrace)。栈顶是崩溃点(常在 libc 内部),往下找到第一个属于你自己代码的帧。bt full会连带打印每一帧的局部变量。
第二步:切换到自己的栈帧(Frame)
frame 1跳到 1 号帧(也可up/down逐层移动)。切过去之后,GDB 会显示该帧对应的源码行。
第三步:检查这一帧的变量(Inspect)
info args看入参,info locals看局部变量,p 变量逐个确认。本例中p dst显示0x0——空指针实锤。
第四步:追问坏值从哪里来(Trace back)
崩溃帧只是终点,坏值往往产生在更早的地方。用up回到调用方帧继续检查(本例main帧里能看到name = NULL);如果来源更远,在赋值处设断点重新run,或直接用 §05 的watch。
第五步:验证假设(Verify)
修复后重新编译再跑一遍确认。也可以先用set var name = buf在 GDB 里临时改值继续运行,验证「改对就不崩」来佐证判断,再动代码。
读懂内存里的值
第三步里怎么判断一个指针「坏在哪」?经验法则:
| 看到的值 | 通常含义 | 排查方向 |
|---|---|---|
0x0或极小地址(< 0x1000) | NULL 指针 + 成员偏移,如p->field | 找到该指针被赋值的地方,为何没初始化/分配失败 |
0xcccccccc、0xcdcdcdcd等整齐填充值 | 未初始化内存 / 已释放内存(调试堆的填充标记) | 变量未初始化,或 use-after-free |
| 地址看似合理,内容却是垃圾 | 指针指向的对象已被释放或从未构造 | 对照分配与释放的调用时序 |
bt中同一组帧无限重复 | 栈溢出,几乎必是递归没有出口 | 检查递归终止条件 |
bt地址混乱、帧不可读 | 返回地址被写坏,典型是栈上缓冲区溢出 | 用x/32a $rsp看栈内存,找越界的写操作 |
查看数据的命令
(gdb) p node # 打印变量(自动按类型) (gdb) p *node # 解引用,看指向的对象 (gdb) p node->next->val # 表达式随便写 (gdb) p/x value # 十六进制;还有 /d 十进制 /t 二进制 /c 字符 (gdb) ptype node # 看类型定义 (gdb) p arr[2]@5 # 打印数组中连续 5 个元素 (gdb) x/16xw buf # 按内存看:16 个 word,十六进制 (gdb) x/4i $pc # 反汇编当前指令附近的 4 条指令 (gdb) x/s str # 按字符串看一段内存
x的格式:x / 数量 格式 大小,如x/16xw= 16 个 word 十六进制;大小可选b(1B)h(2B)w(4B)g(8B)
常见崩溃信号对照
| 信号 | 含义 | 最常见的原因 |
|---|---|---|
| SIGSEGV (11) | 非法内存读写 | 空/野指针解引用、数组越界、写只读段 |
| SIGABRT (6) | 程序主动 abort | assert 失败;glibc 检测到 double free / 堆破坏;未捕获异常 |
| SIGFPE (8) | 算术异常 | 除以零、INT_MIN / -1溢出 |
| SIGBUS (7) | 总线错误 | 未对齐访问;mmap 的文件被截断后继续访问 |
| SIGILL (4) | 非法指令 | 函数指针被写坏跳到非代码区、二进制损坏 |
| SIGTRAP (5) | 陷阱 | 正常命中断点时也是它,不必惊慌 |
04 Core dump 事后分析
程序已经崩了、进程没了?只要留下 core 转储文件,就能事后还原完整现场——所有寄存器、栈、内存都冻结在崩溃瞬间。
ulimit-cunlimited# 允许生成 core(默认常为 0 = 不生成)cat/proc/sys/kernel/core_pattern# 看 core 写到哪里./app# Segmentation fault (core dumped)gdb-q./app core# 加载二进制 + core(gdb)bt# 之后与 §03 完全相同- systemd 系统上 core 常被
systemd-coredump收走:用coredumpctl list查找,coredumpctl gdb <PID>直接进入调试。 - 二进制必须与 core 同一次构建。分析线上崩溃时,把对应版本的带符号二进制(
-g未 strip)找来再加载。 - core 里不能
run/continue,但可以随意bt、切帧、p/x——只读现场,随便翻。
⚠️注意:core 文件可能包含敏感数据(内存里的密钥、用户数据),分析完按需清理,不要随手提交进仓库。
05 逻辑异常排查
程序不崩,只是算错了、走错分支、状态被改坏。思路:把程序停在你怀疑的位置,检查状态;或者设下陷阱,等错误发生的那一刻自动停下。
断点:停在关键位置
(gdb) break main.c:42 # 文件:行号 (gdb) break parse_header # 函数名(C++ 可写完整签名区分重载) (gdb) break process if len > 1024 # 条件断点:只在可疑输入时停 (gdb) condition 2 i == 100 # 给 2 号断点补条件 (gdb) ignore 2 50 # 前 50 次命中直接跳过(循环第 51 次才停) (gdb) tbreak oneshot_init # 一次性断点,命中后自动删除 (gdb) info breakpoints # 列表;delete 2 删除 / disable 2 禁用断点还能自动执行命令——把 GDB 变成条件日志打印器,比插 printf 重编译快得多:
(gdb) break send_packet (gdb) commands > silent > printf "send: len=%d seq=%d\n", len, seq > continue > end每次命中
send_packet就打印一行并继续跑,程序几乎不受打扰。
单步控制
| 命令(缩写) | 行为 | 什么时候用 |
|---|---|---|
step (s) | 单步,进入函数内部 | 怀疑当前调用的函数有问题 |
next (n) | 单步,跨过函数调用 | 逐行走查当前函数 |
finish | 跑完当前函数并停在返回处 | 误入不想看的函数,赶紧出去 |
until (u) | 跳出当前循环 | 循环第 3 次迭代才出错,不想手动 n 三百次 |
continue (c) | 跑到下一个断点 | — |
return | 强制当前函数立即返回 | 跳过可疑代码段做对照实验 |
观察点:谁改了我的变量
逻辑错误最经典的问法是「这个变量是什么时候、被谁改成这个值的」。观察点就是为此而生:变量一被写(或被读)就自动断下来,并显示动手的那行代码。
(gdb) start (gdb) watch total # total 一被写入就停 (gdb) rwatch buffer # buffer 被读取时停 (gdb) awatch flags # 读或写都停 (gdb) watch *(int*)0x7fff5ab0 # 也可以盯一个内存地址 (gdb) watch arr[3] if i > 10 # 观察点同样支持条件 (gdb) info watchpoints (gdb) continue # 命中后 bt 立刻看到是谁写的💡提示:x86 硬件观察点通常只有 4 个,且必须在变量作用域内设置(先走到它已分配的位置)。数量超限或对象过大时 GDB 退回软件轮询实现,会显著变慢——用完记得
delete。
每次停下自动显示
(gdb) display state # 每次程序停下都打印 state (gdb) display/i $pc # 也可以显示寄存器、反汇编 (gdb) info display # undisplay 1 取消二分定位法
毫无头绪时的通用打法:先确认输入在函数入口是对的,再确认输出在出口是错的,然后在中间打断点二分。
break 可疑函数,p所有入参——错在入口前还是入口后?- 在可疑区段中点设断点,检查关键状态。
- 错在前半段就往前二分,错在后半段就往后二分。
每轮排除一半代码,几轮就能把问题压缩到十几行以内。
其他陷阱
(gdb) catch throw # C++:任何 throw 时停下 (gdb) catch syscall write # 调用 write 系统调用时停下 (gdb) set var retry = 3 # 现场直接改变量值做实验 (gdb) p (void)dump_state() # 现场调用函数查看内部状态06 多线程调试
多线程程序的崩溃和死锁,关键是把每个线程各自停在哪、在等谁看清楚。
(gdb) info threads # 所有线程,* 号是当前线程 (gdb) thread 3 # 切到 3 号线程,再 bt/p 就是它的现场 (gdb) thread apply all bt # 所有线程的调用栈一次打完——死锁排查第一招 (gdb) set scheduler-locking on # 单步时冻结其他线程,防止它们抢跑破坏现场 (gdb) set scheduler-locking step # 折中:仅 step/next 期间锁定死锁 / hang 住的标准流程
gdb -p <pid>挂到卡住的进程(也可gdb ./app后run到卡住再Ctrl-C)。thread apply all bt:找停在futex_wait/__lll_lock_wait/pthread_mutex_lock的线程——它们在等锁。- 看这些线程的栈往上在哪个函数里加的锁,对照出两个线程以相反顺序抢同一组锁的模式。
p mutex.__data.__owner可查 glibc 互斥锁当前持有者的线程 ID。
💡竞态:数据竞争导致的偶发崩溃:给共享变量的写入加
watch,或在可疑临界区两端设断点配合scheduler-locking手动构造交错。更省事的做法见 §07 的 ThreadSanitizer。
07 难复现问题与进阶
逆向调试:让时间倒流
「变量变错了,但不知道是哪一步改的」——录下执行过程,然后倒着单步:
(gdb) start (gdb) record full # 从此刻开始录制(程序会变慢,录满会停,可 set record full limit 调大) (gdb) continue # 正常跑,直到发现状态不对 (gdb) p counter $1 = -1 # 什么时候变负的? (gdb) reverse-step # 单步倒退 (gdb) reverse-next # 倒退但跨过函数调用 (gdb) reverse-continue # 倒退到上一个断点/观察点配合
watch更佳:record 期间设watch counter,再reverse-continue,直接倒回它被改写的那一行。支持 Linux x86,性能开销大,适合缩小范围后使用。
GDB + Sanitizer:偶发内存错误的最佳搭档
ASan/TSan 负责在第一时间报告非法访问/竞争的精确位置和分配释放历史,GDB 负责在原地检查具体变量值:
gcc-g-O1-fsanitize=address-oapp main.c# 内存错误(越界/use-after-free/double free)gcc-g-O1-fsanitize=thread-oapp main.c# 数据竞争(gdb) run # Sanitizer 触发 abort 后,bt 看两份调用栈:出错点 + 当初的分配/释放点Valgrind(valgrind --tool=memcheck ./app)不需要重新编译,适合不能改构建的场景,但慢得多。
挂到活进程 / 远程调试
gdb-p12345# 挂到运行中的进程;结束时 detach 放行# 远程机器(目标机):gdbserver :1234 ./app# 开发机:gdb ./app-ex"target remote 目标机IP:1234"TUI 源码窗口模式
gdb -tui ./app # 或运行中按 Ctrl-x a 切换 (gdb) layout src # 源码+命令双窗格;layout split 再加反汇编;focus cmd 切键盘焦点嫌终端不够用就换图形前端:gdb -i=mi之上接 VS Code(CodeLLDB/cppdbg)、CLion、DDD 等,命令内核完全一样。
08 命令速查表
启动 / 运行 / 停止
| 命令 | 作用 |
|---|---|
run [args]/r | 启动程序(可带参数) |
start | 启动并停在 main 开头 |
continue/c | 继续运行到下一个断点 |
step/s | 单步进入函数 |
next/n | 单步跨过函数 |
finish | 运行至当前函数返回 |
until | 跳出当前循环 |
Ctrl-C | 中断正在运行的程序 |
quit/q | 退出 GDB |
断点 / 观察点 / 捕获点
| 命令 | 作用 |
|---|---|
break 位置 [if 条件] | 设断点;位置可为 file:line、函数名、*地址 |
tbreak | 一次性断点 |
condition N 表达式 | 给 N 号断点加条件 |
ignore N 次数 | 前 N 次命中跳过 |
commands N ... end | 命中断点 N 时自动执行的命令 |
watch / rwatch / awatch | 写 / 读 / 读写 观察点 |
catch throw / syscall | 捕获异常抛出、系统调用等事件 |
info breakpoints | 列出所有断点;delete / disable / enable 管理 |
查看数据 / 栈 / 内存
| 命令 | 作用 |
|---|---|
p 表达式 | 打印值;p/x十六进制、p/t二进制 |
ptype | 查看类型定义 |
display 表达式 | 每次停下自动打印 |
x/NFS 地址 | 检查内存,如x/16xw、x/s |
bt/bt full | 调用栈(含局部变量) |
frame N/up/down | 切换栈帧 |
info locals / args | 当前帧的局部变量 / 参数 |
info functions / source / line | 符号、源文件、行号信息 |
disas | 反汇编当前函数 |
线程 / 录制 / 杂项
| 命令 | 作用 |
|---|---|
info threads/thread N | 列出 / 切换线程 |
thread apply all bt | 所有线程打栈(死锁必用) |
set scheduler-locking on|step|off | 单步时是否冻结其他线程 |
record full/record stop | 开始 / 停止录制执行 |
reverse-step / reverse-next / reverse-continue | 逆向单步 / 逆向继续 |
set var 名 = 值 | 修改变量值 |
set args / show args | 设置 / 查看程序参数 |
handle SIGXXX nostop noprint | 让 GDB 忽略某信号 |
attach pid/detach | 挂到 / 离开进程 |
09 效率配置
把常用设置写进用户目录下的~/.gdbinit,每次启动自动生效:
# ~/.gdbinit set history save on # 保存命令历史(↑ 能翻上次会话的命令) set history size unlimited set print pretty on # 结构体换行缩进打印 set print array on # 数组元素换行打印 set pagination off # 长输出不再 --Type <RET>-- 分页- STL 容器(
std::vector/std::string等)在较新的 GDB + GCC 环境下自带 pretty printer,直接p vec就能看到内容。 - 项目目录里的
.gdbinit默认不会自动加载(安全考虑),需要时set auto-load local-gdbinit on。 - GDB 内建 Python:
python print("hi"),复杂场景可以写脚本批量分析,例如遍历链表节点、dump 整块结构。
🎯收尾:最常用的三连永远是:
bt→frame N→info locals。先看清自己在哪、手里有什么,再决定下一步。
