一、快速认识gdb
GDB 调试器详解
要将代码设置为debug模式
gcc SRC(源文件)-oBIN(可执行文件)-std=c99(设置为c99版本编译器)-g为什么需要 GDB?
初学者调试代码最常用的方式是printf大法——在代码各处插入printf打印变量值来定位问题:
printf("a = %d\n",a);// 临时调试printf("here1\n");// 看程序走到了哪printf("here2\n");// ... 调试完还得一个个删掉,烦不胜烦编译形成的文件,默认是release模式,加上-g选项就是debug模式
这种方式在简单场景下能应付,但面对复杂 bug 时力不从心:
源代码
- 效率低:每加一个打印就要重新编译、运行,循环往复
- 信息有限:只能看到你主动打印的变量,无法查看调用栈、寄存器、内存布局
- 影响程序行为:多线程程序中,插入
printf可能改变时序,导致 bug “消失”(Heisenbug) - 无法回溯:程序崩溃后没法"倒带"查看崩溃前的状态
GDB(GNU Debugger)解决了这些问题——它让你能在程序运行时暂停、检查、修改变量、单步执行,像"时间暂停器"一样观察程序的每一个细节。
二、快速上手:第一个调试会话
2.1 编译 Debug 版本
GDB 只能调试带有调试信息的程序。编译时必须加-g选项:
# Release 模式(默认)—— 不含调试信息,GDB 无法识别源码gcc code.c-ocode# Debug 模式 —— 包含源码映射,GDB 可以工作gcc code.c-ocode-g-std=c99| 编译选项 | 含义 | 说明 |
|---|---|---|
-g | 生成调试信息 | 在可执行文件中嵌入源码行号、变量名、类型等映射关系 |
-O0 | 关闭优化 | 建议调试时关闭优化,否则变量可能被优化掉,单步顺序可能错乱 |
-ggdb | GDB 专用调试信息 | 比-g包含更丰富的 GDB 特有调试数据 |
2.2 启动 GDB
# 方式一:启动时指定程序$ gdb ./code# 方式二:先启动 gdb,再加载程序$ gdb(gdb)file./code# 方式三:调试带参数的程序$ gdb--args./code arg1 arg22.3 示例源码
以下示例贯穿全文:
#include<stdio.h>intsum(intstart,intend)intsum(intstart,intend){intsum=0;for(inti=start;i<=end;i++){sum+=i;}returnsum;}intmain(){inta=sum(1,100);printf("%d",a);return0;}假设我有一个可执行文件(debug模式)code。执行gdb code进入调试
2.4 最基本操作:查看、运行、退出
查看源文件
$ gdb code(gdb)l0# 从第 0 行开始查看源码(list)(gdb)l# 继续列出后续代码(回车重复上一条命令)(gdb)b main# 在 main 函数打断点(gdb)r# 运行程序(run)(gdb)q# 退出 GDB(quit)list或者简写l后面跟数字表示从哪一行开始阅读,如l 0表示从头开始,我们会发现l指令不会以下列出所有代码,但是我们接着执行l就会按顺序继续列出代码直至代码完结
💡技巧:在 GDB 中直接按回车键会重复执行上一条命令,大幅减少输入量。
退出gdb
三、查看源码
在调试前,通常需要先浏览源码,定位关键行。
cgdb
| 命令 | 简写 | 说明 |
|---|---|---|
list | l | 显示当前位置周围 10 行源码 |
list 行号 | l 20 | 从第 20 行开始显示 |
list 函数名 | l main | 显示指定函数的源码 |
list 文件名:行号 | l code.c:10 | 显示指定文件的指定行 |
list +/list - | 显示下一段 / 上一段源码 | |
set listsize 20 | 设置一次显示的代码行数 |
(gdb)l main# 查看 main 函数(gdb)l0# 从头开始(gdb)l# 回车,继续往下看(gdb)l# 再回车,继续...# 直到显示 "Line number xxx out of range" 表示已经到文件末尾四、断点管理
断点是调试的核心——让程序在指定位置暂停,以便检查状态。
4.1 设置断点
# 在指定行打断点(gdb)b12## 二、调试命令------cgdb# 在函数入口打断点(gdb)b main(gdb)bsum# 在指定文件的某行打断点(多文件项目常用)(gdb)b code.c:154.2 查看断点
(gdb)info b# 或者 info breakpointsNum Type Disp Enb Address What1breakpoint keep y 0x0000000000401136insumat code.c:72breakpoint keep y 0x000000000040116ainmain at code.c:16| 字段 | 含义 |
|---|---|
| Num | 断点编号(后续删除/禁用时使用) |
| Type | 类型(breakpoint / watchpoint / catchpoint) |
| Disp | 命中后行为(keep 保留 / del 自动删除 / dis 自动禁用) |
| Enb | 是否启用(y 启用 / n 禁用) |
| What | 断点位置 |
打断点
4.3 删除与禁用断点
b 文件名:行号(或函数名)
# 删除指定编号的断点(gdb)d1# 删除编号为 1 的断点(gdb)d# 删除所有断点(需确认)info b# 禁用/启用断点(不删除,只是暂时关闭)(gdb)disable1# 禁用 1 号断点(gdb)enable1# 重新启用 1 号断点# 一次性禁用/启用所有断点(gdb)disable(gdb)enable⚠️注意:
d后面跟的是info b显示的断点编号,不是行号!初学者容易搞混。
删除断点
4.4 条件断点
d 文件名:编号
只在满足条件时才触发断点,避免在循环中反复停下:
# 方式一:新建条件断点(gdb)b10ifi==50# 当 i 等于 50 时才触发# 方式二:为已有断点添加条件(gdb)condition1i==50# 给 1 号断点附加条件### 调试# 查看断点条件(gdb)info b# 会显示 "stop only if i == 50"# 移除条件(gdb)condition1# 不带条件参数 = 清除条件4.5 临时断点
启动gdb,并没有启动调试的程序
当输入r或run之后才会让源程序执行起来
在没有断点的情况下,run就是执行程序
命中一次后自动删除的断点:
(gdb)tb main# temporary breakpoint(gdb)tbreak20# 等价写法4.6 断点命令列表
调试流程:
可以在断点触发时自动执行一系列命令:
- 单步执行,不进入函数,逐过程:n
- 单步执行,进入函数,逐语句:s
(gdb)b15(gdb)commands1Type commandsforbreakpoint1, one per line. End with a line saying just"end".>p i>psum>c>end# 每次命中 1 号断点时,自动打印 i 和 sum,然后继续运行五、程序执行控制
监视变量
GDB 启动后程序并不会自动运行,需要输入
r或run才会真正执行。
在没有断点的情况下,run就是让程序从头运行到结束。
p 变量名
5.1 命令速查
常显示:dispaly 变量名
| 命令 | 简写 | 说明 |
|---|---|---|
run | r | 启动程序(可带参数:r arg1 arg2) |
start | 启动并停在main函数第一条语句 | |
next | n | 单步执行,不进入函数内部(逐过程 / step over) |
step | s | 单步执行,进入函数内部(逐语句 / step into) |
continue | c | 继续执行,直到下一个断点 |
finish | fin | 执行完当前函数并跳出(返回到调用者) |
until 行号 | u 30 | 执行到指定行(跳出循环的利器) |
return | 强制当前函数立即返回(可带返回值) | |
quit | q | 退出 GDB(调试中的程序会被终止) |
kill | 终止当前调试的程序,但不退出 GDB |
5.2 调试流程示意
r (启动) program ──────────────────────> ● breakpoint │ n (不进函数) │ s (进入函数) ▼ 下一行代码 ──> finish (跳出) │ ▼ c (继续) 下一个断点 ──> ... ──> 程序结束删除监视变量
5.3 典型调试节奏
undisplay 编号
(gdb)b main# 1. 在入口打断点(gdb)r# 2. 启动,停在 main(gdb)n# 3. 逐过程单步,看看执行流(gdb)s# 4. 进入可疑函数内部(gdb)p var# 5. 查看变量,确认值对不对(gdb)n# 6. 继续单步...(gdb)c# 7. 确认没问题,全速继续六、查看与修改变量
跳出循环
6.1 打印变量值
until 行号 一直执行到这一行
(gdb)p a# 打印变量 a 的值$1=5050(gdb)psum# 打印函数返回值(如果刚执行完 sum)$2=5050(gdb)p i# 打印循环变量$3=50### 直接跳转到下一个断点(gdb)p/x a# 以十六进制格式打印$4=0x13bac
6.2 打印格式
| 格式符 | 含义 | 示例 |
|---|---|---|
p/d | 有符号十进制 | p/d flag→-1 |
p/x | 十六进制 | p/x 255→0xff |
p/o | 八进制 | p/o 8→010 |
p/t | 二进制 | p/t 5→101 |
p/c | 字符 | p/c 65→'A' |
p/s | 字符串 | p/s buf→"hello" |
p/a | 地址 | 以地址形式打印 |
6.3 常显示(display)
跳出函数
print每次都要手动输入,display会在每次单步后自动打印指定变量:
跳出函数
print每次都要手动输入,display会在每次单步后自动打印指定变量:
finish
(gdb)display i# 注册常显示变量 i(会分配一个编号)(gdb)displaysum# 再注册一个(gdb)n# 单步执行,自动显示 i 和 sum(gdb)info display# 查看所有常显示变量Auto-display expressions: Num Enb Expression1: y i2: ysum### 显示临时变量(gdb)undisplay1# 按编号删除常显示变量(gdb)delete display# 删除所有常显示变量6.4 查看局部变量
(gdb)info locals# 显示当前栈帧的所有局部变量i=10sum=55### watch功能(gdb)info args# 显示当前函数的参数start=1end=100watch 变量
6.5 查看寄存器和内存
(gdb)info registers# 查看所有 CPU 寄存器(gdb)p$rax# 查看单个寄存器(gdb)x/10x$rsp# 查看栈顶 10 个四字节(十六进制)(gdb)x/s 0x7fffffffe180# 以字符串形式查看某内存地址6.6 修改变量值(set var)
set var
当已经确定问题根因,想在调试过程中临时修改值来验证修复思路:
set var 变量=值
(gdb)setvar i=100# 将 i 的值改为 100(gdb)setvar flag=1# 修改标志位(gdb)p i# 确认修改生效$1=100设置值,如果已经有一个问题比较确定了,可以直接调试过程修改值
⚠️ 这只修改内存中的值,不会修改源码。调试结束后记得去改源代码!
七、监视点(watchpoint)
条件断点
监视点让程序在指定变量的值发生变化时自动暂停,是追踪"谁改了我的变量"问题的利器。
条件满足则触发断点
(gdb)watchvar# 当 var 被写入时暂停(硬件监视点,性能好)(gdb)rwatch var# 当 var 被读取时暂停(gdb)awatch var# 当 var 被读或被写时暂停b 行号 if条件 ---->新增条件断点(gdb)info watchpoints# 查看所有监视点(gdb)d3# 删除 3 号监视点(和删除断点方式相同)condition 编号 条件 —>为已经存在的断点添加条件
使用场景
// 某个全局变量莫名其妙被改了,不知道是谁干的intglobal_counter=0;(gdb)watchglobal_counter(gdb)r#Hardware watchpoint1:global_counter#Old value=0#New value=42#0x0000000000401152insome_function()at bug.c:23// ↑ 精准定位:bug.c 第 23 行把 global_counter 改成了 42💡限制:硬件监视点数量有限(通常 4 个),用完会自动降级为软件监视点(速度慢很多)。
八、函数调用栈
程序崩溃或停在断点时,最需要知道的是"我是怎么走到这里的"。
(gdb)bt# backtrace:打印完整调用栈#0 sum (start=1, end=100) at code.c:7#1 0x000000000040116a in main () at code.c:16(gdb)bt full# 打印调用栈 + 每层帧的局部变量(gdb)frame0# 切换到 #0 栈帧(gdb)frame1# 切换到 #1 栈帧(调用者)(gdb)up# 上移一层栈帧(向调用者方向)(gdb)down# 下移一层栈帧(向被调用者方向)(gdb)info frame# 查看当前栈帧的详细信息| 命令 | 说明 |
|---|---|
bt/backtrace | 显示调用栈 |
bt full | 显示调用栈 + 所有局部变量 |
frame N | 切换到 N 号栈帧 |
up/down | 在栈帧间上下移动 |
info frame | 当前栈帧详情(地址、返回地址、保存的寄存器等) |
info locals | 当前栈帧的局部变量 |
info args | 当前栈帧的函数参数 |
九、cgdb —— 带源码窗格的 GDB
cgdb是 GDB 的一个轻量级前端,在终端中提供分屏显示:上半部分是源码窗格,下半部分是 GDB 命令行。
# 安装 cgdb$sudoaptinstallcgdb# Debian/Ubuntu$sudoyuminstallcgdb# CentOS/RHEL# 使用方式与 GDB 完全相同$ cgdb ./codecgdb 操作技巧
| 按键 | 操作 |
|---|---|
i | 从源码窗格切换到 GDB 命令行(Insert 模式) |
Esc | 从命令行切回源码窗格 |
↑ ↓ | 在源码窗格中上下滚动 |
空格键 | 在当前行设置/取消断点 |
PageUp/PageDown | 翻页浏览源码 |
: | 进入 GDB 命令行模式 |
本质上 cgdb 就是 GDB 外面包了一层可视化界面,所有 GDB 命令在 cgdb 中完全适用。
十、GDB 常用命令速查表
10.1 启动与退出
| 命令 | 简写 | 说明 |
|---|---|---|
gdb ./prog | 启动 GDB 并加载程序 | |
gdb --args ./prog a b | 启动 GDB 并传递命令行参数 | |
file ./prog | 在 GDB 内加载程序 | |
quit | q | 退出 GDB |
kill | 终止当前调试的程序 |
10.2 源码查看
| 命令 | 简写 | 说明 |
|---|---|---|
list | l | 显示源码 |
list 行号 | l 20 | 从指定行开始显示 |
list 函数名 | l main | 显示函数源码 |
list 文件名:行号 | l code.c:20 | 显示指定文件的指定行 |
search 正则 | 向前搜索源码 | |
reverse-search 正则 | 反向搜索源码 |
10.3 断点
| 命令 | 简写 | 说明 |
|---|---|---|
break 行号 | b 20 | 在指定行打断点 |
break 函数名 | b main | 在函数入口打断点 |
break 文件名:行号 | b code.c:15 | 在指定文件的指定行打断点 |
break 行号 if 条件 | b 10 if i==100 | 条件断点 |
tbreak 行号 | tb 20 | 临时断点(命中一次自动删除) |
info breakpoints | info b | 查看所有断点 |
delete 编号 | d 1 | 删除指定断点 |
delete | d | 删除所有断点 |
disable 编号 | disable 1 | 禁用断点 |
enable 编号 | enable 1 | 启用断点 |
condition 编号 条件 | cond 1 i==50 | 为已有断点附加条件 |
ignore 编号 N | 忽略此断点 N 次后再触发 |
10.4 程序执行
| 命令 | 简写 | 说明 |
|---|---|---|
run | r | 运行程序 |
run arg1 arg2 | r a b | 带参数运行 |
start | 启动并停在main入口 | |
next | n | 逐过程(不进入函数) |
step | s | 逐语句(进入函数) |
continue | c | 继续执行到下一断点 |
finish | fin | 跳出当前函数 |
until 行号 | u 30 | 执行到指定行 |
return | 强制当前函数立即返回 | |
return 值 | return 0 | 强制返回并指定返回值 |
jump 行号 | j 25 | 跳转到指定行继续执行 |
10.5 查看数据
| 命令 | 简写 | 说明 |
|---|---|---|
print 变量 | p var | 打印变量值 |
print /x var | p/x var | 十六进制打印 |
display 变量 | disp var | 每次单步自动显示变量 |
info display | 查看所有常显示变量 | |
undisplay 编号 | undisp 1 | 删除常显示变量 |
info locals | 查看当前栈帧局部变量 | |
info args | 查看当前函数参数 | |
info registers | info reg | 查看 CPU 寄存器 |
print $rax | 查看单个寄存器 | |
watch 变量 | 设置写监视点 | |
rwatch 变量 | 设置读监视点 | |
awatch 变量 | 设置读写监视点 | |
info watchpoints | 查看所有监视点 |
10.6 查看内存
| 命令 | 说明 |
|---|---|
x/NFU addr | 查看内存(见下方格式说明) |
x/10x $rsp | 查看栈顶 10 个四字节(十六进制) |
x/s addr | 以字符串形式查看内存 |
x/i addr | 以汇编指令形式查看内存 |
x/NFU格式说明:
| 参数 | 含义 | 可选值 |
|---|---|---|
| N | 显示数量 | 正整数 |
| F | 显示格式 | x(十六进制)d(十进制)s(字符串)i(指令)c(字符) |
| U | 单位大小 | b(1字节)h(2字节)w(4字节)g(8字节) |
10.7 调用栈
| 命令 | 简写 | 说明 |
|---|---|---|
backtrace | bt | 显示调用栈 |
backtrace full | bt full | 显示调用栈 + 局部变量 |
frame N | f 0 | 切换栈帧 |
up | 上移栈帧 | |
down | 下移栈帧 | |
info frame | 当前栈帧详情 |
10.8 其他实用命令
| 命令 | 简写 | 说明 |
|---|---|---|
set var 变量=值 | 修改变量值 | |
disassemble | disas | 反汇编当前函数 |
shell 命令 | shell ls | 执行 shell 命令(不退出 GDB) |
show args | 显示程序的命令行参数 | |
set args a b | 设置程序的命令行参数 | |
help 命令 | h b | 查看命令帮助 |
apropos 关键词 | 搜索相关命令 |
十一、调试技巧与常见场景
11.1 调试段错误(Segfault)
$ ./program Segmentation fault(core dumped)$ gdb ./program core# 用 core dump 文件调试(gdb)bt# 直接看到崩溃位置和调用栈(gdb)frame0# 切换到崩溃帧(gdb)p ptr# 检查指针,通常是 NULL 或野指针如果系统没有生成 core dump,执行
ulimit -c unlimited开启。
11.2 调试正在运行的程序
# 找到目标进程 PID$psaux|grepmyprogram# 附加到运行中的进程$ gdb-p12345# 或者$ gdb ./myprogram12345# 调试完后用 detach 分离(不会终止目标进程)(gdb)detach11.3 循环调试技巧
# 在循环中逐次迭代太慢?用 until 跳出(gdb)b5(gdb)r(gdb)u20# 直接执行到第 20 行(循环结束之后)# 或者用条件断点 + continue(gdb)b8ifi==99(gdb)c# 直达第 99 次循环11.4 观察点追踪变量修改
# 场景:某个变量被意外修改,不知道是谁干的(gdb)watchsuspicious_var(gdb)r# 每次 suspicious_var 被写入都会停下,显示旧值 → 新值11.5 反向调试(Reverse Debugging)
GDB 支持"时光倒流"——回退执行(需要 GDB 7.0+):
(gdb)target record# 开启记录模式(gdb)r# 正常运行...# ...碰到 bug(gdb)reverse-next# 反向单步(rn)(gdb)reverse-step# 反向进入函数(rs)(gdb)reverse-continue# 反向继续(rc)(gdb)record stop# 关闭记录11.6 TUI 模式
GDB 自带文本界面,无需安装额外工具:
(gdb)tuienable# 或 gdb -tui ./code# 上下分屏:上 = 源码,下 = 命令# TUI 快捷键Ctrl + x, a# 切换 TUI 模式Ctrl + x, o# 切换焦点窗格Ctrl + x,2# 双窗格(源码 + 汇编)Ctrl + l# 刷新屏幕11.7 保存与恢复断点
(gdb)save breakpoints gdb.bp# 保存断点到文件# ...下次调试时恢复(gdb)sourcegdb.bp# 从文件加载断点11.8 打印数组和结构体
(gdb)p *array@10# 打印数组前 10 个元素(gdb)p *ptr@len# 打印动态数组(指针 + 长度)(gdb)p/s str# 以字符串形式打印(gdb)p(struct Foo)var# 强制类型转换后打印十二、总结
| 概念 | 一句话理解 |
|---|---|
-g选项 | 编译时嵌入调试信息,GDB 可以工作的前提 |
| 断点(breakpoint) | 让程序在指定位置暂停,方便检查状态 |
| 单步执行(next/step) | 一行一行执行代码,n不进入函数,s进入函数 |
| 打印(print) | 查看变量当前值,比printf灵活一万倍 |
| 常显示(display) | 每次单步自动打印变量,解放双手 |
| 监视点(watchpoint) | 变量值变化时自动暂停,追踪"谁改了它" |
| 调用栈(backtrace) | 回答"我是怎么走到这里的"——调试的指南针 |
| 条件断点 | 只在满足条件时停下,避免循环中反复中断 |
| cgdb / TUI | 自带源码窗格的终端调试器,比纯 GDB 直观 |
掌握 GDB 是从"猜测式调试"走向"精确式调试"的关键一步。虽然现代 IDE(VS Code、CLion)都有图形化调试界面,但它们底层调用的仍然是 GDB(或 LLDB)。理解 GDB 命令意味着你在任何 Linux 环境——哪怕是最
简朴的远程服务器——都能高效地定位问题。