
文章目录
-
- [1. 准备一个结果错误的程序](#1. 准备一个结果错误的程序)
- [2. 启动、断点与继续执行](#2. 启动、断点与继续执行)
- [3. 单步执行与变量检查](#3. 单步执行与变量检查)
- [4. 调用栈与当前栈帧](#4. 调用栈与当前栈帧)
- [5. 条件断点与监视点](#5. 条件断点与监视点)
-
- [只在第 30 次累加前停止](#只在第 30 次累加前停止)
- 变量发生变化时停止
- [6. 验证原因与修复源码](#6. 验证原因与修复源码)
- [7. CGDB:保留源码窗口](#7. CGDB:保留源码窗口)
- [8. 小结](#8. 小结)
上一篇已经完成了 GCC/G++ 编译与 Makefile 构建。现在换一个问题:编译没有报错,程序也正常退出,可是本应得到 5050 的求和结果却是 0。
1. 准备一个结果错误的程序
在服务器的独立学习目录中创建源码:
bash
mkdir -p ~/linux-dev-study/debug
cd ~/linux-dev-study/debug
vim debug_sum.c
debug_sum.c 的完整内容如下:
c
#include <stdio.h>
int flag = 0;
int sum_range(int first, int last)
{
int total = 0;
for (int i = first; i <= last; ++i)
{
total += i;
}
return total * flag;
}
int main()
{
int first = 1;
int last = 100;
int result = sum_range(first, last);
printf("sum(%d..%d) = %d\n", first, last, result);
return 0;
}
这个例子的约定是:flag 只应取 1 或 -1,控制结果的符号;初始化为 0 是故意放入的错误。读源码虽然很快就能找到问题,但接下来要练习的是怎样用调试证据定位它。
编译与运行:
bash
gcc -std=c11 -g debug_sum.c -o debug_sum
./debug_sum
运行示例:

-g可生成调试信息,帮助 GDB 找到源码行、函数、变量等
2. 启动、断点与继续执行
在源码和程序所在目录启动:
bash
gdb ./debug_sum
看到 (gdb) 后,再输入的就是调试器命令,如下:

gdb
list 1,21
break debug_sum.c:19
info breakpoints
run

list 查看源码,break 设置断点,run 启动被调试程序。程序将会执行到第 19 行附近停下。

先检查输入:
gdb
print first
print last

关键值为 1 和 100。
输出带
$1、$2等编号,这是 GDB 的值历史编号,不是源码中的变量名。
若不需要逐步执行,使用:
gdb
continue
程序继续运行,直到遇到另一个有效断点、其他停止事件,或运行结束。
run则是从头启动一次运行,不等同于continue。
常用断点写法
| 命令 | 用途 |
|---|---|
break main |
在函数入口设置断点 |
break debug_sum.c:19 |
按文件与行号定位断点,适合多文件项目 |
info breakpoints |
查看断点编号、位置和启用状态 |
disable 1 / enable 1 |
禁用、启用编号 1 的断点 |
delete 1 |
删除编号 1 的断点 |
delete |
询问后删除所有断点等停止点 |
断点编号随会话中的创建操作变化,后面凡是用到编号,都以自己 info breakpoints 的结果为准。
3. 单步执行与变量检查
为了分别观察两种执行方式,可退出当前 GDB 后重新启动,每段示例从新的会话开始。quit 退出调试器。
next:越过函数调用
gdb
break debug_sum.c:19
run
next
print result

从调用行执行一次 next,通常停在第 20 行输出语句前,sum_range 已执行返回,result 为 0。
next 不逐行进入被调用函数,但函数仍然实际执行了。(可参考VS中的 F10 逐过程)
step:进入函数调用
下面重新执行 gdb 调试:
gdb
break debug_sum.c:19
run
step
list

step 会尝试进入 sum_range,随后一步步执行。
若只关心最终如何返回时,可以直接在返回语句上再设断点:
gdb
break debug_sum.c:11
continue
print total
print flag
print total * flag

此处分别观察到 5050、0、0。至此证据已经明确:累加过程得到正确的 total,返回表达式又乘了一个错误的标志值。
常用的检查命令
| 命令 | 适用场景 |
|---|---|
print 表达式,简写 p |
检查某个变量或简单表达式的值 |
info locals |
查看当前栈帧的局部变量 |
info args |
查看当前栈帧的函数参数 |
display 变量 |
每次停止时自动显示变量 |
info display |
查看自动显示项及编号 |
undisplay 编号 |
取消对应自动显示项 |
finish |
执行到当前函数返回,期间仍可能被断点等打断 |
until debug_sum.c:11 |
请求继续到指定位置;也可能因函数返回或其他停止事件提前结束 |
finish返回调用者时,GDB 可能还停在调用所在的源码行,返回值已经可见,但调用者赋值的机器指令未必全部完成。要检查result,应继续到下一条语句,而不是机械地认为"函数返回了,所有相关赋值就一定已经完成"。
4. 调用栈与当前栈帧
函数调用会形成层次:main 调用 sum_range,后者返回后继续执行 main。GDB 用栈帧表示这些尚未返回的函数调用及其上下文。
仍在第 11 行断点处,执行:
gdb
backtrace
frame 1
print first
print last
frame 0
print total

backtrace 可简写为 bt。本例中与源码相关的调用关系应包括:
text
#0 sum_range (...)
#1 main (...)

frame 1 后,在调用者 main 的上下文中检查变量;frame 0 回到 sum_range 的上下文。
为什么需要这一步?当一个函数被多个地方调用时,错误可能来自调用者传错参数。调用栈告诉我们"怎么走到这里",当前局部变量告诉我们"这里是什么状态"。
5. 条件断点与监视点
对 100 次循环一直按 next 没有必要。如果已经知道想观察什么,就让程序在对应条件出现时再停下。
只在第 30 次累加前停止
重新启动 GDB 进入调试:
gdb
break debug_sum.c:9 if i == 30
run
print i
print total
运行示例:

在第 9 行 total += i; 执行之前,i 为 30,total 为 435,即 1~29 的和。再继续执行:
gdb
next
print total

此时total就变成了 465,已经加上了 30。
条件必须要能在断点位置的上下文中求值;不能在 main 里直接要求检查另一个函数尚未存在的局部变量 i。
还有另一种做法是给已有断点添加条件,在单独新会话中:
gdb
break debug_sum.c:9
info breakpoints
condition 1 i == 30
run

这里查看得到刚创建的断点编号为 1。新增条件断点用 break 位置 if 条件,给已有断点加条件用 condition 编号 条件,后者不再写 if。
变量发生变化时停止
重新启动 GDB 调试:
gdb
break debug_sum.c:10
run
watch total
disable 1
continue

停在第 10 行时,total 已经初始化为 0,i 为 1。
第一次累加改变 total 时,监视点应报告旧值 0、新值 1;继续后还可观察到 1 变成 3。
watch默认关注表达式值的变化,并不等于"每次写入都停止";写入同一个值可能不触发。局部变量离开作用域后,相应监视也会受影响。
| 手段 | 解决的问题 |
|---|---|
| 普通断点 | 程序执行到哪里时停 |
| 条件断点 | 执行到某处且满足条件时停 |
display |
已经停下时,自动显示什么 |
watch |
哪个值发生变化时停 |
6. 验证原因与修复源码
已经怀疑了 flag = 0 是原因,所以我们现在就可以做一次可控验证。在新会话中执行:
gdb
break debug_sum.c:11
run
print total
print flag
set var flag = 1
print total * flag
continue
把本次运行中的 flag 改为 1 后,预期程序输出:

这也支持了我们的判断,但源码仍然是 int flag = 0;。再次 run 通常会重新初始化为 0,重新启动程序也是如此。
set var修改的是当前被调试程序的状态,既没有编辑.c文件,也没有修改磁盘上可执行文件的初始化定义。
7. CGDB:保留源码窗口
如果希望在终端中同时看到源码和调试命令,可以使用 CGDB。它提供基于终端的界面,底层仍使用 GDB,断点、next、print 等命令不需要重学。
如果使用时发现不存在,可以先按本系统的包管理器查询,仓库提供时再安装。例如:
bash
# yum 可用且仓库提供 cgdb 的系统
yum info cgdb
sudo yum install cgdb
bash
# Ubuntu、Debian 的相应仓库可用时
sudo apt install cgdb
启动方式与 gdb 一致:
bash
cgdb ./debug_sum
常用操作是按 Esc 把焦点切到源码窗口,再按 i 回到 GDB 命令窗口;源码窗口中可用类似 Vim 的移动键浏览。剩余操作基本与 gdb 保持一致,这里就不再过多演示。
8. 小结
-
GDB 是 Linux 下 C/C++ 程序命令行调试工具,编译代码需加
-g参数携带调试信息。 -
基础操作:
break设置断点,run从头运行,continue继续执行,next/step单步调试,print/p查看变量。 -
观察点
watch监控变量修改,变量值改变时自动暂停;display在每次暂停时自动打印变量,二者不要混淆。 -
断点管理命令:
info breakpoints查看断点,enable/disable启用 / 禁用断点,delete删除断点。 -
finish运行至当前函数返回,until运行至指定行,适合快速跳过循环或函数。 -
CGDB 是 GDB 的前端包装,保留全部 GDB 命令,同时提供源码窗口,可视化查看代码,降低了 GDB 使用门槛。
