欢迎光临
我们一直在努力

【Linux 系统篇(十二)】基础开发工具(七) —— 调试器 gdb/cgdb使用

在这里插入图片描述

大家好,欢迎来到 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
print 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

结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

赞(0)
未经允许不得转载:171主机测评 » 【Linux 系统篇(十二)】基础开发工具(七) —— 调试器 gdb/cgdb使用
分享到: 更多 (0)

评论 抢沙发

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