Linux GDB 调试与 CGDB 使用

文章目录

    • [1. 准备一个结果错误的程序](#1. 准备一个结果错误的程序)
    • [2. 启动、断点与继续执行](#2. 启动、断点与继续执行)
    • [3. 单步执行与变量检查](#3. 单步执行与变量检查)
    • [4. 调用栈与当前栈帧](#4. 调用栈与当前栈帧)
    • [5. 条件断点与监视点](#5. 条件断点与监视点)
    • [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 使用门槛。

相关推荐
小羊没烦恼!1 小时前
关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
java·服务器·网络·学习·负载均衡
小Ti客栈1 小时前
Netty 零拷贝原理与实战
运维·服务器·网络
ai_coder_ai1 小时前
AIOps 智能运维架构设计及其应用
运维
静以修身zzz1 小时前
计算云服务
运维·网络·云计算
星恒随风2 小时前
Linux网络编程:应用层协议究竟是什么?从网络计算器到TCP Socket抽象基类完整实现
linux·网络·笔记·tcp/ip
万联WANFLOW2 小时前
异地组网速度忽快忽慢到底怎么回事?直连与中继的判断方法和处理步骤
运维·网络·网络协议·p2p
SDWAN_Cheap2 小时前
物理服务器跟云服务器有什么区别,使用于什么场景
运维·服务器
Smoothcloud润云2 小时前
GPU算力平台推荐:复现实验时,如何判断租用资源是否合适?
服务器·人工智能·深度学习·gpu算力·gpu
辰辰观影2 小时前
字幕导入教程:外挂字幕一键加载方法
网络·经验分享