欢迎光临
我们一直在努力

【Linux系统编程】(十)从入门到精通!Linux 调试器 gdb/cgdb 超全使用指南,程序员必备调试神器

目录

前言

一、gdb 调试基础:从环境准备到核心概念

1.1 为什么需要 gdb 调试?

1.2 gdb 调试的前提:编译调试版本

1.3 gdb 的启动与退出

启动 gdb

退出 gdb

二、gdb 核心命令:从基础到实战

2.1 查看源代码(list/l)

2.2 断点操作:调试的核心(break/b)

2.2.1 设置断点

2.2.2 查看断点信息

2.2.3 删除断点

2.2.4 禁用 / 启用断点

2.3 程序执行控制:运行、单步与继续

2.3.1 运行程序(run/r)

2.3.2 单步执行:逐过程与逐语句

2.3.3 继续执行(continue/c)

2.3.4 执行到函数返回(finish)

2.3.5 执行到指定行(until)

2.4 变量操作:查看、修改与监视

2.4.1 查看变量值(print/p)

2.4.2 修改变量值(set var)

2.4.3 跟踪显示变量(display/undisplay)

2.5 堆栈查看:定位崩溃位置(backtrace/bt)

2.6 查看局部变量(info locals)

三、gdb 高级技巧:条件断点、变量监视与更多

3.1 条件断点:精准定位特定场景

3.1.1 两种设置方式

3.1.2 实战示例:循环中定位特定值

3.2 变量监视(watch):捕捉变量变化

3.2.1 三种监视模式

3.2.2 实战示例:监视 result 变量变化

3.3 多线程调试:thread 命令

四、cgdb:gdb 的可视化增强版

4.1 cgdb 安装

CentOS 系统:

Ubuntu 系统:

4.2 cgdb 核心操作

4.3 cgdb 优势展示

五、常见问题与避坑指南

5.1 无法启动 gdb 调试

5.2 断点设置失败

5.3 变量无法查看

5.4 多文件调试路径问题

总结


前言

        作为 Linux 下 C/C++ 开发的核心工具,gdb 调试器是排查代码 bug、理解程序运行流程的必备技能。很多新手面对黑屏命令行调试望而却步,甚至资深开发者也可能只掌握基础用法。而 cgdb 作为 gdb 的增强版,更是解决了纯命令行调试看不到代码的痛点。本文将结合实战案例,从基础配置到高级技巧,全面拆解 gdb/cgdb 的使用方法,让你彻底掌握 Linux 下的调试精髓!下面就让我们正式开始吧!


一、gdb 调试基础:从环境准备到核心概念

1.1 为什么需要 gdb 调试?

        在开发过程中,我们难免会遇到代码逻辑错误、变量取值异常、程序崩溃等问题。printf 打印调试虽然简单,但存在诸多局限:需要手动添加打印语句、重新编译,无法实时观察变量变化,也难以定位崩溃点。而 gdb 调试器可以直接加载可执行程序,支持断点设置、单步执行、变量监视、堆栈查看等功能,让你像 "上帝视角" 一样看透程序运行的每一个细节。

1.2 gdb 调试的前提:编译调试版本

        Linux 下 gcc/g++ 默认生成的是 release 版本程序,不包含调试信息,无法使用 gdb 调试。因此,必须在编译时添加-g选项,生成包含调试信息的 debug 版本。

示例代码(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;
}

编译命令对比:

# 默认release版本,不支持gdb调试
gcc mycmd.c -o mycmd
file mycmd # 查看程序信息,输出"not stripped"但无debug_info

# debug版本,添加-g选项,支持gdb调试
gcc mycmd.c -o mycmd -g
file mycmd # 输出"with debug_info, not stripped",表明包含调试信息

1.3 gdb 的启动与退出

启动 gdb

        在终端中输入以下命令启动 gdb 并加载可执行程序:

gdb 可执行程序名
# 示例:gdb mycmd

        启动成功后,终端会显示 gdb 版本信息,并进入 gdb 命令行模式,提示符为(gdb)。

退出 gdb

        有两种常用退出方式:

  • 输入quit或q命令:(gdb) quit;
  • 使用快捷键Ctrl + D。

二、gdb 核心命令:从基础到实战

2.1 查看源代码(list/l)

        调试时需要对照源代码查看执行位置,list命令(简写l)可以显示程序源代码。

常用用法:

  • 显示当前位置附近的代码(默认每次 10 行):(gdb) l 或 (gdb) list
  • 显示指定行号附近的代码:(gdb) l 行号(示例:l 10 显示第 10 行附近代码)
  • 显示指定函数的代码:(gdb) l 函数名(示例:l main 显示 main 函数代码)
  • 显示指定文件的指定行代码:(gdb) l 文件名:行号(示例:l mycmd.c:1 显示 mycmd.c 第 1 行)
  • 实战示例:

    (gdb) l main # 查看main函数代码
    14
    15 int main()
    16 {
    17 int start = 1;
    18 int end = 100;
    19 printf("I will begin\\n");
    20 int n = Sum(start, end);
    21 printf("running done, result is: [%d-%d]=%d\\n", start, end, n);
    22 return 0;
    23 }
    24

    2.2 断点操作:调试的核心(break/b)

            断点是调试的核心功能,用于指定程序暂停执行的位置,方便观察变量状态和程序流程。

    2.2.1 设置断点

    常用断点设置方式:

  • 按行号设置断点:(gdb) b 行号(示例:b 20 在第 20 行设置断点)
  • 按函数名设置断点:(gdb) b 函数名(示例:b Sum 在 Sum 函数开头设置断点)
  • 按文件 + 行号设置断点(多文件项目):(gdb) b 文件名:行号(示例:b mycmd.c:10)
  • 2.2.2 查看断点信息

            使用info break或info b命令查看所有断点的详细信息:

    (gdb) b 20 # 设置断点
    Breakpoint 1 at 0x11c3: file mycmd.c, line 20.
    (gdb) info b # 查看断点信息
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x00005555555551c3 in main at mycmd.c:20

            输出说明:

    • Num:断点编号(后续操作断点的标识)
    • Type:断点类型(默认 breakpoint)
    • Disp:断点触发后的行为(keep 表示保留)
    • Enb:是否启用(y 表示启用,n 表示禁用)
    • Address:断点对应的内存地址
    • What:断点位置信息

    2.2.3 删除断点

    常用删除方式:

  • 删除所有断点:(gdb) delete breakpoints 或 (gdb) d
  • 删除指定编号的断点:(gdb) delete breakpoints 断点编号(示例:d 1 删除 1 号断点)
  • 2.2.4 禁用 / 启用断点

            无需删除断点时,可临时禁用或启用:

    • 禁用所有断点:(gdb) disable breakpoints
    • 启用所有断点:(gdb) enable breakpoints
    • 禁用指定断点:(gdb) disable breakpoints 断点编号
    • 启用指定断点:(gdb) enable breakpoints 断点编号

    2.3 程序执行控制:运行、单步与继续

            设置断点后,需要通过执行命令控制程序运行,逐步排查问题。

    2.3.1 运行程序(run/r)

            在 gdb 中输入run或r命令启动程序,程序会执行到第一个断点处暂停:

    (gdb) r
    Starting program: /home/whb/test/mycmd
    I will begin

    Breakpoint 1, main () at mycmd.c:20
    20 int n = Sum(start, end);

    2.3.2 单步执行:逐过程与逐语句

            单步执行是调试的核心操作,分为两种模式:

  • 逐过程(不进入函数):next或n(类似 VSCode 的 F10)

    执行当前行代码,如果是函数调用,不进入函数内部,直接执行完函数并返回结果

  • 逐语句(进入函数):step或s(类似 VSCode 的 F11)

    执行当前行代码,如果是函数调用,进入函数内部,停在函数第一行

  • 实战示例:

    (gdb) r # 启动程序到断点
    Breakpoint 1, main () at mycmd.c:20
    20 int n = Sum(start, end);
    (gdb) s # 逐语句执行,进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n # 逐过程执行,执行下一行
    6 int result = 0;
    (gdb) n # 继续逐过程执行
    7 for(int i = s; i <= e; i++)

    2.3.3 继续执行(continue/c)

            程序暂停在断点或单步执行后,使用continue或c命令让程序继续执行到下一个断点或程序结束:

    (gdb) c
    Continuing.
    running done, result is: [1-100]=5050
    [Inferior 1 (process 12345) exited normally]

    2.3.4 执行到函数返回(finish)

            在函数内部调试时,使用finish命令执行到当前函数返回,并显示返回值:

    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) finish # 执行到Sum函数返回
    Run till exit from #0 Sum (s=1, e=100) at mycmd.c:5
    0x00005555555551d2 in main () at mycmd.c:20
    20 int n = Sum(start, end);
    Value returned is $1 = 5050 # 显示Sum函数返回值

    2.3.5 执行到指定行(until)

            使用until 行号命令让程序执行到指定行,无需设置断点,适合快速跳转到目标位置:

    (gdb) until 16 # 执行到第16行
    Sum (s=1, e=100) at mycmd.c:16
    16 return result;

    2.4 变量操作:查看、修改与监视

            调试的核心目的是观察变量取值是否符合预期,gdb 提供了丰富的变量操作命令。

    2.4.1 查看变量值(print/p)

            使用print或p命令查看变量、表达式的值,支持直接计算表达式。

    常用用法:

  • 查看变量值:(gdb) p 变量名(示例:p result 查看 result 变量值);
  • 查看表达式值:(gdb) p 表达式(示例:p start+end 计算并显示 start+end 的值);
  • 连续查看变量(自动递增序号):多次使用p命令,变量值会被编号($1, $2…),可通过编号快速查看历史值。
  • 实战示例:

    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n
    6 int result = 0;
    (gdb) p result # 查看result变量值
    $1 = 0
    (gdb) n
    7 for(int i = s; i <= e; i++)
    (gdb) p i # 查看i变量值
    $2 = 1
    (gdb) p s+e # 查看表达式值
    $3 = 101

    2.4.2 修改变量值(set var)

            调试时如果发现变量取值异常,可以使用set var 变量名=值命令直接修改变量值,验证是否是该变量导致的问题。

    实战示例:假设我们修改 Sum 函数,添加一个 flag 变量控制返回值,故意设置 flag=0 导致结果异常:

    #include <stdio.h>

    int flag = 0; // 故意错误,导致返回值为0
    int Sum(int s, int e)
    {
    int result = 0;
    for(int i = s; i <= e; i++)
    {
    result += i;
    }
    return result*flag; // 乘以flag,结果为0
    }

    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;
    }

            调试时修改 flag 值:

    (gdb) r # 启动程序
    Starting program: /home/whb/test/mycmd
    I will begin
    Breakpoint 1, main () at mycmd.c:24
    24 int n = Sum(start, end);
    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:9
    9 {
    (gdb) n
    10 int result = 0;
    (gdb) n
    11 for(int i = s; i <= e; i++)
    (gdb) until 16 # 执行到return语句前
    Sum (s=1, e=100) at mycmd.c:16
    16 return result*flag;
    (gdb) p result # 查看result值,正确应为5050
    $1 = 5050
    (gdb) p flag # 查看flag值,发现为0
    $2 = 0
    (gdb) set var flag=1 # 修改flag值为1
    (gdb) p flag # 验证修改结果
    $3 = 1
    (gdb) n # 执行return语句
    17 }
    (gdb) n
    main () at mycmd.c:25
    25 printf("running done, result is: [%d-%d]=%d\\n", start, end, n);
    (gdb) n
    running done, result is: [1-100]=5050 # 结果正常,验证问题根源

    2.4.3 跟踪显示变量(display/undisplay)

            使用print命令需要手动重复输入,而display命令可以设置变量跟踪,每次程序暂停时自动显示变量值,适合需要持续观察的变量。

    常用用法:

  • 跟踪变量:(gdb) display 变量名(示例:display i 跟踪循环变量 i);
  • 查看跟踪列表:(gdb) info display;
  • 取消跟踪:(gdb) undisplay 跟踪编号(示例:undisplay 1 取消编号为 1 的跟踪)。
  • 实战示例:

    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n
    6 int result = 0;
    (gdb) n
    7 for(int i = s; i <= e; i++)
    (gdb) display i # 跟踪i变量
    1: i = 1
    (gdb) n
    9 result += i;
    1: i = 1
    (gdb) n
    7 for(int i = s; i <= e; i++)
    1: i = 1
    (gdb) n
    9 result += i;
    1: i = 2 # 自动显示i的最新值

    2.5 堆栈查看:定位崩溃位置(backtrace/bt)

            当程序崩溃时(如段错误),最关键的是定位崩溃发生的函数调用链。backtrace或bt命令可以显示当前的函数调用栈,包括每个函数的调用位置和参数。

    实战场景:假设程序崩溃,使用 bt 命令定位:

    (gdb) r # 启动程序,程序崩溃
    Starting program: /home/whb/test/crash_program

    Program received signal SIGSEGV, Segmentation fault.
    0x0000555555555189 in func2 (p=0x0) at crash.c:8
    8 *p = 10; # 空指针解引用导致崩溃
    (gdb) bt # 查看函数调用栈
    #0 0x0000555555555189 in func2 (p=0x0) at crash.c:8
    #1 0x000055555555519e in func1 () at crash.c:13
    #2 0x00005555555551b6 in main () at crash.c:19

            输出说明:

    • #0:当前崩溃的函数(func2),位置在 crash.c 第 8 行,参数 p=0x0(空指针);
    • #1:调用 func2 的函数(func1),位置在 crash.c 第 13 行;
    • #2:调用 func1 的函数(main),位置在 crash.c 第 19 行。

            通过调用栈可以快速定位崩溃的根源:main 调用 func1,func1 调用 func2 并传入空指针,func2 解引用空指针导致崩溃。

    2.6 查看局部变量(info locals)

            使用info locals或i locals命令查看当前函数的所有局部变量及其值,无需逐个输入 print 命令,高效便捷:

    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n
    6 int result = 0;
    (gdb) info locals # 查看当前函数局部变量
    result = 0
    i = 1

    三、gdb 高级技巧:条件断点、变量监视与更多

            掌握基础命令后,高级技巧能让调试效率翻倍,尤其适合复杂场景下的问题排查。

    3.1 条件断点:精准定位特定场景

            普通断点会在每次执行到该位置时暂停,而条件断点只在满足指定条件时才暂停,适合循环、分支等需要特定触发条件的场景。

    3.1.1 两种设置方式

  • 新增断点时直接添加条件:(gdb) b 行号/函数名 if 条件

    示例:b 9 if i == 30(在第 9 行设置断点,仅当 i=30 时暂停)。

  • 给已有断点添加条件:(gdb) condition 断点编号 条件

    示例:condition 2 i == 30(给 2 号断点添加条件 i=30)。

  • 3.1.2 实战示例:循环中定位特定值

            假设 Sum 函数循环计算 1 到 100 的和,我们需要在 i=30 时观察 result 的值:

    (gdb) r # 启动程序
    Breakpoint 1, main () at mycmd.c:20
    20 int n = Sum(start, end);
    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n
    6 int result = 0;
    (gdb) n
    7 for(int i = s; i <= e; i++)
    (gdb) b 9 if i == 30 # 新增条件断点,i=30时暂停
    Breakpoint 2 at 0x555555555186: file mycmd.c, line 9.
    (gdb) c # 继续执行
    Continuing.

    Breakpoint 2, Sum (s=1, e=100) at mycmd.c:9
    9 result += i;
    (gdb) p i # 验证i=30
    $1 = 30
    (gdb) p result # 查看此时result的值(1+2+…+29=435)
    $2 = 435

    3.2 变量监视(watch):捕捉变量变化

        watch命令用于监视变量或表达式的值,当值发生变化时,程序会自动暂停并提示旧值和新值,适合排查变量被意外修改的问题。

    3.2.1 三种监视模式

  • watch 变量名:监视变量值的读写变化(读写触发);
  • rwatch 变量名:仅监视变量被读时触发;
  • awatch 变量名:仅监视变量被修改时触发。
  • 3.2.2 实战示例:监视 result 变量变化

    (gdb) r # 启动程序
    Breakpoint 1, main () at mycmd.c:20
    20 int n = Sum(start, end);
    (gdb) s # 进入Sum函数
    Sum (s=1, e=100) at mycmd.c:5
    5 {
    (gdb) n
    6 int result = 0;
    (gdb) watch result # 监视result变量
    Hardware watchpoint 2: result
    (gdb) c # 继续执行
    Continuing.

    Hardware watchpoint 2: result
    Old value = 0
    New value = 1 # 第一次变化:result=0→1
    Sum (s=1, e=100) at mycmd.c:7
    7 for(int i = s; i <= e; i++)
    (gdb) c
    Continuing.

    Hardware watchpoint 2: result
    Old value = 1
    New value = 3 # 第二次变化:result=1→3
    Sum (s=1, e=100) at mycmd.c:7
    7 for(int i = s; i <= e; i++)

            通过 watch 命令,我们可以清晰看到 result 变量每次的变化情况,快速定位异常修改。

    3.3 多线程调试:thread 命令

            在多线程程序中,gdb 支持线程切换、设置线程专属断点等功能,核心命令如下:

    • 查看所有线程:info threads;
    • 切换到指定线程:thread 线程编号;
    • 设置线程专属断点:b 行号 thread 线程编号;
    • 仅当前线程执行:set scheduler-locking on(避免其他线程干扰)。

    四、cgdb:gdb 的可视化增强版

            gdb 虽然强大,但纯命令行模式看不到代码,需要频繁使用 list 命令查看,效率较低。cgdb 作为 gdb 的前端工具,支持分屏显示代码和 gdb 命令行,操作方式与 gdb 完全兼容,上手成本极低。

    4.1 cgdb 安装

    CentOS 系统:

    sudo yum install -y cgdb

    Ubuntu 系统:

    sudo apt-get install -y cgdb

    4.2 cgdb 核心操作

            cgdb 的命令与 gdb 完全一致,额外增加了分屏控制快捷键:

    • 启动 cgdb:cgdb 可执行程序名(示例:cgdb mycmd);
    • 切换代码屏 / 命令行屏:按Esc键进入代码屏,按i键回到命令行屏;
    • 代码屏滚动:使用方向键或j/k键上下滚动;
    • 退出 cgdb:与 gdb 一致,quit或Ctrl + D。

    4.3 cgdb 优势展示

            启动 cgdb 后,界面分为上下两部分:

    • 上半部分:实时显示源代码,当前执行行高亮显示;
    • 下半部分:gdb 命令行,支持所有 gdb 命令。

    实战体验:

    cgdb mycmd # 启动cgdb
    (cgdb) r # 运行程序
    Starting program: /home/whb/test/mycmd
    I will begin

    Breakpoint 1, main () at mycmd.c:20
    20 int n = Sum(start, end);

            此时上半部分代码屏会高亮显示第 20 行,无需输入 list 命令即可看到当前执行位置,调试过程中代码会自动跟随执行行滚动,体验远超纯 gdb。

    五、常见问题与避坑指南

    5.1 无法启动 gdb 调试

    • 问题:No debugging symbols found in 可执行程序;
    • 原因:编译时未添加-g选项,程序无调试信息;
    • 解决:重新编译,添加-g选项(gcc -g 源文件 -o 可执行程序)。

    5.2 断点设置失败

    • 问题:Breakpoint 1 at 0xXXXX: file 文件名.c, line 行号. (2 locations);
    • 原因:可能是代码被优化(如使用-O2优化选项),导致行号对应不上;
    • 解决:编译时关闭优化(去掉-O相关选项),仅保留-g选项。

    5.3 变量无法查看

    • 问题:No symbol "变量名" in current context.
    • 原因:变量超出作用域,或编译优化导致变量被消除。
    • 解决:确保在变量作用域内查看,关闭编译优化。

    5.4 多文件调试路径问题

    • 问题:多文件项目中,设置其他文件断点时提示No such file or directory。
    • 解决:使用file 文件名:行号格式设置断点,或编译时指定源文件路径。

    总结

            调试是编程的核心技能之一,熟练使用 gdb/cgdb 不仅能解决问题,更能帮助你深入理解程序运行机制。建议结合实际项目反复练习,将这些命令内化为肌肉记忆,成为高效的 Linux 开发者!

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux系统编程】(十)从入门到精通!Linux 调试器 gdb/cgdb 超全使用指南,程序员必备调试神器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址