当前位置: 首页 > news >正文

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 打满或无响应死循环 / 阻塞 IOgdb -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找到该指针被赋值的地方,为何没初始化/分配失败
0xcccccccc0xcdcdcdcd等整齐填充值未初始化内存 / 已释放内存(调试堆的填充标记)变量未初始化,或 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)程序主动 abortassert 失败;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 取消

二分定位法

毫无头绪时的通用打法:先确认输入在函数入口是对的,再确认输出在出口是错的,然后在中间打断点二分。

  1. break 可疑函数p所有入参——错在入口前还是入口后?
  2. 在可疑区段中点设断点,检查关键状态。
  3. 错在前半段就往前二分,错在后半段就往后二分。

每轮排除一半代码,几轮就能把问题压缩到十几行以内。

其他陷阱

(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 住的标准流程

  1. gdb -p <pid>挂到卡住的进程(也可gdb ./apprun到卡住再Ctrl-C)。
  2. thread apply all bt:找停在futex_wait/__lll_lock_wait/pthread_mutex_lock的线程——它们在等锁。
  3. 看这些线程的栈往上在哪个函数里加的锁,对照出两个线程以相反顺序抢同一组锁的模式。
  4. 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/16xwx/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 整块结构。

🎯收尾:最常用的三连永远是:btframe Ninfo locals。先看清自己在哪、手里有什么,再决定下一步。

http://www.cnnetsun.cn/news/4227758.html

相关文章:

  • 全文 - Isaac ROS 06 - Getting Started
  • 上线千舟报修云前后,贵州中建秀印高速公路有限公司后勤工作发生了什么?
  • 串口收发数据包
  • 2026 年 AI Agent 框架横评:10 大框架优缺点对比 + 选型指南
  • 具身智能之通用机器人RoboCat详解:一个能用少量示范快速学会新任务和新机器人的自我改进智能体
  • 电机模块注意事项
  • 随机前沿SFA结果解读:技术效率分布与前沿面估计
  • 2026年佛山桂城少儿美术培训机构哪家好
  • 前端:全景深度分析/前端岗位起源,以及“美国没有前端岗位”的真相
  • IMX6ULL MfgTool软件烧写系统
  • .NET 软件开发平台
  • Go命令工具全解:go build/run/test/fmt/mod
  • 二叉树的基本操作详解
  • 【RAG实战】LlamaIndex 深度集成:常用 Reader 全解析与自定义 Reader
  • 具身智能中融合TVA时空特征的VLA模型
  • 具身智能TVA-VLA分层规划提升长时序任务成功率
  • 面向具身智能的TVA-VLA增量学习防遗忘机制
  • 中国技术大败局TBL-20260809-048深度解剖报告V2.1 决策迭代版
  • 物理机异常重启怎么排查?定位根因,避免故障反复
  • can总线相关
  • 抖助手第066个开关:隐藏全屏观看的位置、验证方法与入口边界
  • Dify实战-人工输入节点-AI起草人工审批的工作流怎么做
  • 【大模型RAG生成式AI开发实战】《大模型RAG生成式AI开发实战》_114.[第12章 RAG评估体系] 生成评估:BLEU、ROUGE和BERTScore
  • 第10篇-通道架构与Telegram配置
  • Winform/Sunny.UI的DataGridView数据展示、uiPagination分页
  • 转:怎样招到优秀的核心人才
  • 【中国方言题库|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致
  • Raid卡命名及代表含义
  • C++进阶知识5.0
  • 深圳EMC现场测试辐射传导测试