
大家好,欢迎来到 huangjin007_ 的博客
⭐
个人主页:huangjin007_
🔥
文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持能从冰封的土地里培育出十万朵怒放的蔷薇
Linux 系统篇(十二) —— 调试器 gdb/cgdb使用
文章目录
- Linux 系统篇(十二) —— 调试器 gdb/cgdb使用
-
- 一、准备工作
-
- 1.1 一个简单的示例代码
- 1.2 Debug 与 Release 模式
- 二、GDB 基础命令
-
- 2.1 启动与退出
- 2.2 查看源代码:list(l)
- 2.3 设置断点:break(b)与断点管理
-
- 设置断点
- 查看断点:info breakpoints(info b)
- 删除与禁用断点
- 2.4 运行与单步执行
-
- 运行程序:run(r)
- 逐过程执行:next(n)
- 逐语句执行:step(s)
- 跳出函数:finish
- 跳到指定行:until
- 继续执行:continue(c)
- 2.5 查看与监控数据
-
- 打印变量:print(p)
- 自动显示:display
- 查看所有局部变量:info locals
- 查看调用栈:backtrace(bt)
- 2.6 动态修改变量:set var
- 三、常见调试技巧与进阶工具
-
- 3.1 CGDB
-
- 安装
- 基本操作与快捷键
- 3.2 监控变量变化:watch
- 3.3 用 set var 验证问题根源
- 3.4 条件断点
-
- 3.4.1 新建条件断点
- 3.4.2 给已有断点附加条件
- 四、常用命令速查表
- 结语:
一、准备工作
1.1 一个简单的示例代码
我们先准备一段 C 代码,之后所有的调试演示都会围绕它展开:
// mycmd.c
#include <stdio.h>
int Sum(int s, int e)
{
int result = 0;
for(int i = s; i <= e; i++)
{
result += i;
}
return result;
}
int main()
{
int start = 1;
int end = 100;
printf("I will begin\\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\\n", start, end, n);
return 0;
}
这是一个求和程序,Sum 函数计算从 s 到 e 的所有整数和,main 函数调用它并打印结果。
1.2 Debug 与 Release 模式
程序从源代码到可执行文件,通常有两种发布方式:debug 模式和 release 模式。
- Release 模式:面向最终用户,编译器会开启各种优化,同时剥离调试信息,让程序体积更小、运行更快。但缺点是无法再用调试器逐行跟踪源码。
- Debug 模式:面向开发人员,包含完整的符号表、源码映射等信息,虽然程序体积稍大,但能够支持断点、变量查看等功能。
在 Linux 下使用 gcc 或 g++ 编译时,默认生成的就是 release 模式。也就是说,如果你直接这样编译:
gcc mycmd.c -o mycmd
得到的 mycmd 是不带调试信息的,GDB 无法把它和源代码对应起来。要想进行调试,必须在编译时加上 -g 选项:
gcc mycmd.c -o mycmd -g
这就相当于告诉编译器:“请生成 debug 版本的程序”。我们怎么知道一个二进制文件是否包含调试信息呢?可以使用 file 命令来检查。
不加 -g 编译时:

加上 -g 编译后:

差别就在最后那句:with debug_info, not stripped,意思是这个文件带有调试信息,且符号表没有被移除(stripped)。
二、GDB 基础命令
2.1 启动与退出
要开始调试,只需在终端里输入:
gdb mycmd
随即进入 GDB 的交互界面,提示符变成 (gdb)。要退出调试,可以输入 quit(或简写 q),也可以按 Ctrl + D。
2.2 查看源代码:list(l)
进了 GDB 之后,第一件事往往是查看源码,确认自己要调试的函数在哪儿。list 命令(简写 l)就是用来显示源代码的。它有几种常用形式:
- list:从上次显示的位置继续,每次显示 10 行。
- list 10:显示第 10 行附近的 10 行代码。
- list main:显示 main 函数的完整代码。
- list mycmd.c:1:显示文件 mycmd.c 从第 1 行开始的代码。
2.3 设置断点:break(b)与断点管理
断点是调试的核心。没有断点,程序就会一口气跑完,那调试就无从谈起。
设置断点
- break 10(简写 b 10):在当前文件的第 10 行设置断点。
- break main:在 main 函数入口设置断点。
- break mycmd.c:20:明确指定在 mycmd.c 的第 20 行打断点。
我们以在第 20 行(int n = Sum(start, end);)设断点为例:

GDB 反馈了一个断点编号 1,并给出了内存地址和位置。
查看断点:info breakpoints(info b)
随时可以用 info b(或 info breakpoints)查看当前设置的所有断点:

- Num:断点编号,GDB 不退出时断点编号会一直递增,即使删除了也不会复用。
- Type:断点类型,常见为 breakpoint,还有 hw watchpoint 等。
- Enb:是否启用,y 表示启用,n 表示禁用。
- Address:断点在内存中的地址。
- What:断点的具体位置。
删除与禁用断点
- delete 1 (d 1)或 delete breakpoints 1:删除编号为 1 的断点。
- delete breakpoints:不带编号则删除所有断点(谨慎使用)。
- disable 1:禁用 1 号断点,状态变为 n。断点还在,只是暂时失效。
- enable 1:重新启用 1 号断点,状态恢复为 y。
当我们暂时不想让某个断点生效,但又不想删掉它时,先用 disable,等需要了再 enable。
2.4 运行与单步执行
运行程序:run(r)
设置好断点后,输入 run(或 r)即可启动程序。程序会一直执行,直到遇到第一个断点,或者程序正常结束 / 异常崩溃。如果在运行过程中想重新开始,可以再次执行 run,GDB 会询问你是否从头启动。
逐过程执行:next(n)
next(简写 n)用于单步执行,但不进入函数内部。如果当前行是一个函数调用(如 int n = Sum(start, end);),GDB 会把这个函数当作一个整体直接执行完,然后停在下一行。

一个小技巧:GDB 会记住你最近输入的命令。如果你输入了一次 n,之后只需要连续按回车键,它就会自动重复执行 n,省去反复敲字母的麻烦。
逐语句执行:step(s)
如果你怀疑问题出在 Sum 函数内部,就不能再用 n 跳过去了,而应该用 step(简写 s)进入函数内部。执行 step 时,如果当前行包含函数调用,GDB 会“钻”进那个函数,并停在函数的第一条语句上。

这里你会看到 GDB 进入了 Sum 函数,并显示当前执行到了第 6 行。
跳出函数:finish
当你已经进入某个函数,但不想再逐行执行到它返回,可以用 finish。这个命令会让程序一直执行到当前函数返回,然后自动停下来,并告诉你返回值是多少。

跳到指定行:until
如果你身处循环体内部,想要快速跳出循环,但又不想在循环末尾再设一个断点,until 行号 就是你的好帮手。它让程序继续执行,直到到达指定行才停下。
继续执行:continue(c)
让程序从当前停止的位置继续运行,直到遇到下一个断点或程序结束。如果你已经确认之前的部分没问题,希望程序直接跑到后面的断点,就用 continue。
2.5 查看与监控数据
断点停下来之后,最重要的就是检查当前状态:变量的值、表达式的计算结果等。
打印变量:print(p)
print 变量名 或 print 表达式 可以输出任意合法表达式的值。比如:
(gdb) p result
$1 = 0
(gdb) p start + end
$2 = 101
GDB 会把结果保存在历史变量 $1、$2 中,后续你也可以直接引用这些历史值。
自动显示:display
如果你想让某个变量在每次停下来时都自动显示,而不必反复敲 p,可以使用 display 变量名。设置好后,每次程序中断,GDB 都会打印该变量的当前值。当不再需要跟踪时,用 undisplay 编号 取消。例如:

编号 1 是 display 列表的序号,用于之后 undisplay 1 移除。
查看所有局部变量:info locals
当函数体较长、局部变量很多时,info locals 可以一次性列出当前栈帧内的所有局部变量及其值,省去一个个打印的麻烦。
查看调用栈:backtrace(bt)
程序为什么会执行到这里?是被谁调用的?backtrace(简写 bt)可以打印当前调用栈,从最底层的 main 一直到当前函数。

这表示当前在 Sum 函数,它是由 main 函数在第 20 行调用的。
2.6 动态修改变量:set var
有时候我们可能怀疑某个变量取值不对导致了 bug,想临时改成另一个值验证一下。set var 变量名=新值 就可以在运行时强制修改变量。
这一技巧在下一节会有完整的案例演示。
三、常见调试技巧与进阶工具
3.1 CGDB
很多人排斥 GDB 的原因是它只有一个黑乎乎的终端,不能同时看到源码和命令。CGDB(Curses GDB)恰好解决了这个问题。它本质上还是 GDB,但增加了一个上方的代码窗口,会随着调试过程高亮当前执行行,下方仍然是熟悉的 GDB 命令输入区。看起来就像一个简单的 IDE 调试器。
安装
- Ubuntu:sudo apt install -y cgdb
- CentOS:sudo yum install -y cgdb
基本操作与快捷键
启动方式和 GDB 完全相同:cgdb mycmd。
进入 CGDB 后,屏幕分为两部分:
- 上半部分:代码窗口,显示源码,当前行会被高亮。
- 下半部分:GDB 命令窗口,和纯 GDB 的界面完全一样,所有 GDB 命令照常输入。
CGDB 最大的便利在于代码窗口是可操作的。你可以按 ESC 键进入代码窗口,然后用方向键或 j/k滚动查看源码。需要输入命令时,按 i 键就能回到 GDB 命令窗口。
3.2 监控变量变化:watch
watch 是 GDB 提供的一种数据断点。普通断点在特定代码行停下,而 watch 则是监控某个变量(或表达式),只要它的值发生变化,程序立刻暂停,并告诉你旧值和新值分别是什么。
先将程序跑到准备监控的位置。以 Sum 函数中的 result 变量为例:

这里我们在第 6 行声明 result 之后立即设置了 watch result。GDB 创建了一个“硬件观察点”。
接着继续执行:

程序刚启动时 result 还是一个未初始化的随机值,当执行 int result = 0; 后,值变成了 0,变化被 GDB 截获并暂停。每次循环累加时,result 都会改变,因此也会反复触发观察点:

可以看到 result 从 0 变成了 1。继续执行,它会被依次累加到 3、6……
如果想停止监视,用 delete 2 删除该观察点即可。 
适用场景:当你有一个全局变量或关键局部变量,理论上不应该被修改,但运行时却发现它变了;或者你完全不知道是哪里改坏了它,就可以用 watch 捕获每一次修改。
3.3 用 set var 验证问题根源
有时候我们通过调试已经怀疑某个变量是罪魁祸首,但还需要最终确认。set var 允许我们在不修改源码、不重新编译的情况下,直接改变变量的值,看看程序行为是否符合预期。
我们用一个带 flag 标志位的新例子来演示。
// mycmd.c(修改版)
#include <stdio.h>
int flag = 0; // 这里的值不小心写错了,本意是 1 或 -1
// int flag = -1;
// int flag = 1;
int Sum(int s, int e)
{
int result = 0;
for(int i = s; i <= e; i++)
{
result += i;
}
return result * flag;
}
int main()
{
int start = 1;
int end = 100;
printf("I will begin\\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\\n", start, end, n);
return 0;
}
运行这个程序,结果竟是 0,而不是预期的 5050。为什么?我们开始调试。

显然 n 是 0。我们重新运行并进入 Sum 函数一探究竟。

经过几步,我们到达 return result * flag; 之前的那一行。使用 until 14 或直接多打几个 n,停在关键位置:

真相大白:result 本身计算正确,但 flag 是 0,导致乘积为 0。为了确认只要 flag 改成 1 就能恢复正常,我们使用 set var:

问题确认!随后我们可以回到源代码把 flag 改为正确的初始值。
3.4 条件断点
循环 100 次,我只想在第 30 次循环时停下来看结果,如果手动按 29 次 continue 会疯掉。条件断点就是为这种场景设计的。
3.4.1 新建条件断点
语法:b 行号 if 条件。注意这里要用 if 关键字。我们在循环体内的第 9 行设置条件断点,当 i==30 时停下:


然后 continue,程序会一口气跑到 i 等于 30 的那次循环才停下:

3.4.2 给已有断点附加条件
如果我们一开始没有加条件,后面才想到需要条件,可以使用 condition 断点编号 条件 来给已存在的断点追加条件,注意这里没有 if。例如:
先在第 9 行打一个普通断点:

然后让它只在 i==30 时触发:

效果和第一种方式完全一样,但语法不同。记住:新建时用 if,追加条件时不用 if,直接写 condition <编号> <表达式>。
四、常用命令速查表
| list | l | 显示源码,默认10行 | l main / l mycmd.c:1 |
| run | r | 从头开始执行程序 | r |
| break | b | 设置断点 | b 20 / b main / b mycmd.c:10 |
| info breakpoints | info b | 查看断点列表 | info b |
| delete | d | 删除断点 | d 1 / delete breakpoints |
| disable | dis | 禁用断点 | disable breakpoints 1 |
| enable | en | 启用断点 | enable breakpoints 1 |
| next | n | 逐过程,不进入函数 | n |
| step | s | 逐语句,进入函数 | s |
| finish | fin | 执行到当前函数返回 | finish |
| until | u | 执行到指定行 | until 20 |
| continue | c | 继续执行,直到下一断点 | c |
| p | 打印变量或表达式 | p result / p start+end | |
| display | disp | 自动显示变量值 | display i |
| undisplay | undisp | 取消自动显示 | undisplay 1 |
| info locals | i lo | 查看当前栈帧局部变量 | info locals |
| backtrace | bt | 查看调用栈 | bt |
| set var | — | 修改变量值 | set var flag=1 |
| watch | — | 监视变量变化 | watch result |
| condition | — | 给断点附加条件 | condition 2 i==30 |
| quit | q | 退出 GDB | q |
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _





