调试器gdb/cgdb

我们在之前已经学习了如何使用gcc/g++工具对c/c++源码进行编译,但是对于源码的操作还差一些。就是我们本文将提及的调试,我们将使用gdb和cgdb这两个工具进行调试代码。

我们先说gdb调试器(cgdb其实和gdb基本一样,多了一些方便的功能而已)。
这个gdb调试器其实是我们系统默认装的软件,我们使用指令gdb --version进行查看:
在这里插入图片描述
没安装也是不要紧的,使用包管理器yum安装一下即可。

release和debug

首先我们得解决第一个问题,即release和debug两个版本的代码的区别。
我们经常使用vs 2022等集成开发环境的都知道,代码是可以在两个版本下运行的,分别是release和debug版本。这两个版本的最大区别就是,debug版本是带有调试信息的,而release版本是没有的。所以release版本下我们是无法对代码进行调试工作的。

而且因为debug版本的代码编译器出来后是会有调试信息,所以debug平台下运行的可执行程序文件提体积会稍大一些。对于gcc/g++编译器而言,默认编译出的代码都是release版本的。但是我们在vs 2022下可以很轻松的通过对应的按钮,点击切换代码运行的版本,但是我们现在是在命令行下进行操作,应该如何操作呢?
这个很简单,只需要在编译的时候在后面加上-g选项即可。

我们这里随便写了一份c源码,我们分别使用和不使用-g选项进行编译:
在这里插入图片描述
如上图所示,我们确实可以发现,使用了-g选项进行编译的可执行文件的体积确实大一些,我们再使用gdb工具看看:

使用gdb调试GETSUM1:在这里插入图片描述
使用gdb调试GETSUM2:
在这里插入图片描述
这里我们先不管gdb下指令的使用,我们先看二者区别。

我们会发现,对默认版本(release版本)的代码,使用gdb是无法进行调试工作的,会报错No symbol table is loaded. Use the “file” command.
意思是没有加载符号表,我们不需要理解原理,只需要知道此时没办法使用gdb就可以了。

但是我们加了-g选项编译成debug版本的可执行程序后,就不会出现该问题。所以这验证了我们前面讲到的结论:只有debug版本才能进行调试。

快速认识gdb,引入cgdb

这个部分我们先来快速地认识一下gdb调试器(通过讲几个指令认识一下),然后引入一个新的工具——cgdb。

首先,我们得确保我们要调试的代码是没有报错的,否则没办法生成可执行文件的。然后需要保证调试的代码是debug版本,否则没有调试信息也是没办法进行调试的。

然后我们需要使用指令gdb 可执行文件,此时会进入gdb工具的调试界面。这里一定要注意gdb后面跟着的是可执行文件,因为我们要调试的是程序:
在这里插入图片描述
使用该指令后,我们会进入一个(gdb)的界面,这里的调试是通过命令行来进行操作的。一切的调试操作(如打断点、查看变量、查看代码等)都是通过命令行输入指令进行操作的。

在这里插入图片描述
首先,我们可以使用指令list 简写为l来查看当前需要被我们调试的所有的代码。然后我们会发现只显示了一小部分,只需要按下Enter键就可以下翻了:
在这里插入图片描述
直到翻到代码的最后一行,此时无法再进行翻滚,这个时候系统会给出提示。

我们还可以在l指令后面加行号,此时gdb就会展示以该行号为中心的大约10行代码:
在这里插入图片描述
这里我们就先看一下。所有的指令我们等一下都会在cgdb那里在讲一遍。


现在还面临着一个问题,就是如何退出gdb界面呢?其实很简单,使用指令quit 简写q或者使用快捷键ctrl + d就可以退出:
在这里插入图片描述
其他指令的退出方法在这里就不进行展示了,效果一样。


调试的时候我们还希望可以运行代码,使用指令run / r就可以运行代码:

//getSum.c源码

#include<stdio.h>

//求和 a ~ b
int GetSum(int s, int e){
  int result = 0;
  for(int i = s; i <= e; ++i){
    result += i;
  }
  return result;
}

int main(){
  int begin = 0, end = 100;
  printf("The Sum Will be Added from %d to %d\n", begin, end);
  int result = GetSum(begin, end);
  printf("The result is %d, [%d <-> %d]\n", result, begin, end);
  return 0;
}

在这里插入图片描述
我们会发现代码结果确实给运行出来了。


但是我们还需要打断点,这样子我们才能方便调试出代码是哪个部分出现了问题。这里我们就先简单的了解一下:
使用指令break / b 代码行号,就可以在那一行打上断点:
在这里插入图片描述
当然也可以使用指令break / b 函数名,可以在函数的开头设置断点:
在这里插入图片描述
如果想要一次性查看所有的断电,我们可以使用指令info break / b
在这里插入图片描述
里面具体的先不讲解,知道有这么个用法即可。


其实还有很多的操作,但是我们这里面临着一个很大的问题,即使用莫命令行特别麻烦。我们想要看到代码还要使用指令list来翻看。而且纯命令行下来进行调试工作非常的困难。

所以我们现在是迫切地需要一个能够显示出我们的调试操作的界面的工具。起码能够让我们能自由的翻看代码,一眼看出断点的位置等。所以这里需要使用一个新的工具——cgdb。
这个工具是需要我们自行下载的,使用指令:
sudo yum install cgdb -y #Centos系统
sudo apt install cgdb -y #Ubuntu系统

下载后查看版本:
在这里插入图片描述
cgdb其实我们可以理解为带有代码调试界面的gdb,所有的指令操作其实都和gdb是一样的。我们进入界面看看:
在这里插入图片描述
我们可以发现上半部分是代码,下半部分是指令操作界面。

我们是可以来回的上下视图的,按下i就是回到指令界面,按下Esc就是翻阅代码界面。但这里其实要注意的是,翻阅代码是可以用鼠标的。但是cgdb这个工具有一些问题,即使用鼠标的时候可能会出现冲突。所以我们尽量不要使用鼠标,其实真正实用的时候也确实不需要鼠标。

然后上面的绿色箭头指向的就是当前代码运行到的行数。至此,我们就可以很轻松的看着代码来进行调试工作了,这些指令和gdb的指令是一样的。只不过cgdb有配合代码界面,讲解起来更方便。所以后序的所有操作都将会在cgdb下进行讲解。

常见的指令(cgdb下操作)

在cgdb工具下,我们将更详细的讲解常用的指令并且配合图示。
首先,我们可以按下i进入下面的指令输入,也可以按Esc进入上半部分的代码展示图中进行手动翻阅代码(用鼠标/上下键j,k / pageup,pagedown)。但是cgdb这个工具使用鼠标进行翻阅确实比较难,所以我们其实不太需要。我们只需要在下面输入指令调试的时候可以看得见代码即可。实在需要翻阅可以使用上下键进行翻阅。


接下来就来仔细介绍一下cgdb下的一些常用指令(gdb也是一样的)。

1.查看代码list / l
这个指令我们早已介绍过,就是用来查看一小部分代码的,系统会大概展示10行左右的代码。然后我们可以手动按下Enter键,系统会继续往后展示:
在这里插入图片描述
但是这里很奇怪,为什么输入指令l展示的是13行开始的呢?其实这里要注意的l指令的定义,如果单纯使用l指令,就是默认把当前行开始的10行左右的代码进行展示。我们此时可以看上面的代码视图,发现绿色箭头指向13行,所以展示的是13行开始的代码。

还可以使用l + 数字查看指定行附近的代码。也可以是l + 函数名查看函数对应的源代码。这些前面部分展示过,就不再展示。况且我们可以手动翻阅代码。所以这个指令其实用不用都无所谓。


2.运行代码run / r
在没有打断点的情况下,我们输入该指令,会直接跑完这个代码并且输出结果:
在这里插入图片描述
run / r 指令就是用来运行代码的。


3.打断点break / b
调试,其实就是找出代码找出哪个部分出现了问题。然后通过断点让代码运行停在该位置,然后进行代码的查看(变量、代码逻辑、漏洞等)。

我们可以通过break / b 代码行号来进行打断点:
在这里插入图片描述
发现,在对应行号打上断点后,代码预览图中对应的行号会变成红色标识。这就跟我们在vs 2022中打断点后行前面有一个红色点一样。

我们可以使用run / r指令运行一下:
在这里插入图片描述
我们发现这里停在了断点3处,也就是15行。对于断点的编号这个等到断点的查看和删除再来解释。我们当前知道停在了断点处即可。

这个时候我们再按下r指令:
在这里插入图片描述
它会询问我们是否要从头开始调试,按下y就是yes,从头开始调试。按下n就是退出调试,回到正常的gdb命令输入。

这里只展示如何打断点,具体的代码调试逻辑我们放在后面讲。

还可以使用指令break / b + [文件名:]行号指定文件的对应行号进行打断点操作。这个可以用到多文件下进行操作,我们这里可以展示一下(把上面用到的代码拆分成多文件):

//GetSum.h
#pragma once
int GetSum(int s, int e);


//GetSum.c
#include"GetSum.h"
int GetSum(int s, int e){
  int result = 0;
  for(int i = s; i <= e; ++i){
    result += i;
  }
  return result;
}


//main.c
#include<stdio.h>
#include"GetSum.h"

int main(){
  int begin = 0, end = 100;
  printf("The Sum Will be Added from %d to %d\n", begin, end);
  int result = GetSum(begin, end);
  printf("The result is %d, [%d <-> %d]\n", result, begin, end);
  return 0;
}

我们进入cgdb调试:
在这里插入图片描述
我们当前一定是处在main所在的文件内的。因为main函数是所有c/c++程序的入口。我们这个时候可以使用指令b GetSum.c:1打上断点,然后运行:
在这里插入图片描述
此时我们会发现,我们确实停留在了GetSum.c这个文件的第一行中,然后就可以根据控制逻辑进行代码语句的调试了(如逐过程、逐语句等)。

还可以break / b 函数名在函数的开头设置断点:
在这里插入图片描述
此时我们会发现,第5行(GetSum函数开头)和第13行(main函数开头)都是被打上断点的。

但其实最主要的就是使用行号进行打断点,所以break直接加行号用的是最多的。至于给其他文件的代码打断点其实很简单。我们只需要控制一下代码的调试逻辑运行到其他文件内的时候再打断点也是可以的。


4.查看所有断点 info break / b
有时候我们想一次性查看当前所有的断点,我们可以使用指令info break / b进行查看:
在这里插入图片描述
重点掌握的是Num、Type、Enb、Address、What信息。

首先来讲第一列,这是断点的编号。断点编号是:
从我们进入gdb/cgdb开始,我们可以理解内部有一个计数器。每打上一个断点,计数器+1,+1后就是此时打的断点的编号。这里要注意的是,如果我们后序不退出cgdb,那么这个断点编号只会增加不会倒回,为了演示,这里我先删除断点1然后再添加回来:
在这里插入图片描述
我们会发现,重新给main打上断点,编号变成了3,而不是补回为1。这也是说明断点编号其实就是每次cgdb/gdb单次进入使用的时候打断点的顺序。
这里断点编号是有用的,因为后序对断点的一系列操作是无法对行号进行操作的,必须通过断点的编号来进行操作。这里先给出这个结论(重要!!!)。

Type信息表示的是断点的种类:
在这里插入图片描述
这里先看看断点种类也是有区别的即可(breakpoint和watchpoint)。具体的指令后序会讲解。

Enb信息代表的是断点的使能,这个就有点像开关一样,可以控制断点的有效性。即使能端有使能断点有效,反之断点无效。就和vs 2022中的金庸禁用断点一样。
在这里插入图片描述

Address信息就是断点所处的地址。这个只需了解一些即可。
What信息就是表示当前这个断点是在哪个文件的哪一行和哪个对应函数内。还有对于watchpoint断点类型指明跟随的变量是哪一个(先了解,等下会讲到)。


5.单步语句控制 step / s 、 next / n
这两个的区别其实都是对代码进行单步执行,但只不过一个进入函数,一个不会进入。

我们现在代码的第15行打一个断点,方便后序演示。
在这里插入图片描述

我们先来看step / s指令:
先使用指令run / r运行,然后输入s指令:
在这里插入图片描述
此时会发现绿色光标置于GetSum函数开头了。说明这个step / s指令会进入函数内部,就和我们在vs 2022内按下F11 逐语句是一样的,语句是一句句执行的。
然后我们后面可以继续按s进行下一句调试,也可以按下Enter键进行调试。系统会自动追踪我们刚刚输入的指令的。

我们现在再来看next / n指令:
这个指令是与step相反的,它不会进入函数:
在这里插入图片描述
这个就和vs 2022中的F10 逐过程是一样的,直接跳过函数了。


6.退出函数(执行完函数) finish
当然有时候进入一个函数内部,由于内部可能有循环次数较大的代码块。调试起来是很困难的。如果我们进入到了一个没有错误,但是调试比较麻烦的函数,我们可以使用指令finish进行退出:

进入函数GetSum:
在这里插入图片描述

使用finish指令:
在这里插入图片描述
此时会告诉我们返回值5050,并且绿色光标停留在该函数的调用位置。这是为什么呢?

其实是因为返回值的原因。了解过函数栈帧的都知道,函数只会在调用的时候在栈区开辟一块空间,内部会存有运行该函数所需要的相关变量等。但是返回值是很奇特的,因为函数一旦执行结束,就会自动销毁,内部所有的变量都会被销毁。那么这个返回值是怎么返回出来的呢?

其实是通过寄存器实现的。函数的return value语句其实就是把这个值给到寄存器存储,然后返回到函数调用的时候,如果有哪个变量进行接收,那就把寄存器里面的值再给这个变量。
在这里插入图片描述
我们在vs 2022中的反汇编调试界面下就可以看到这个原理。


7.看当前执行栈的各级函数调用及参数 backtrace / bt

使用bt指令是用来查看当前运行到的位置调用的函数栈帧有哪些,参数是什么:
在这里插入图片描述
这个调试在递归下还是很有用的,可以帮助我们看看当前递归到哪一层栈帧了。


8.执行多步语句 continue / c 、until + X(行号)
我们先来看continue / c指令:
在这里插入图片描述
continue指令就是连续的执行后序的语句:
在这里插入图片描述
但是我们会发现,如果后序有断点,会停留在断点处的。

如果后序没有断点,那么会直接跑完整个代码。
在这里插入图片描述
这个其实和vs 2022中的shift + F11是一样的,即跳出。把后面的过程跳过了。只不过vs 2022中的shift + F11进入函数后,再使用该按键就会跳出函数,回到函数的调用出,这一点和cgdb / gdb下的finish指令一样。

然后我们再来看untile + X指令:
这个指令其实很简单,就是将代码运行到指定行号:
在这里插入图片描述
比如这里我们为了跳过中间的循环过程,直接运行到第9行。

只不过要注意的是,使用该指令也是一样的,如果X行前面还有其它的断点,那么cgdb是不会跳过这个断点的,会停在断点处,如下图所示:

在这里插入图片描述


9.删除断点delete / d breakpoint
在这里插入图片描述
现在我们打了三个断点,我们想要删除断点,我们可以直接使用指令delete / d 或者 delete / d breakpoints进行删除所有断点:
在这里插入图片描述
此时系统会询问我们是否一次性删除所有断点。输入y表示yes删除所有断点,反之不做操作。

还可以使用delete / d [breakpoints] n(breakpoints可以不写)删除指定编号的断点:
在这里插入图片描述
这里要注意,除了打断点的时候可以用行号,其它的时候必须是用断点的编号进行操作的。如上图所示,15行的那个断点编号为1,需要使用指令d 1才能删除该断点。


10.对断点进行使能控制enable、 disable
我们前面讲到了断点的使能控制。其实就是是否禁用断点。

这个功能的作用还是蛮大的。比如有时候我们想要在某个位置打上断点,但是又希望在个别时候它不起作用,其他时候起作用,这个时候就可以考虑对断点进行禁用(disable),然后需要使用的时候再启用(enable)。

在这里插入图片描述
我们会发现,disable某个断点后,断点对应行号就变成了黄色。并且Enb信息变成了n,即该断点当前被禁用。想要使用使用指令enable + 断点编号就可以了。注意,这里也是一定要使用断点编号的!

我们尝试着运行一下看看:
在这里插入图片描述
会发现断点当前确实是不起作用的。


11.查看变量(如同vs 2022中的监视)
这个部分有很多指令,需要我们记忆并且熟练使用。

在调试的时候,很多时候我们是需要查看变量的。这样子才能更好地确定在某个代码块过程中是否是变量出了问题。

我们先来看print / p指令:
首先,可以使用print / p + 变量查看变量运行到此行时对应的值。
在这里插入图片描述
如上图,刚进入GetSum函数的时候,result的值为32767,其实就是随机值。然后经过定义和初始化后,result的值就变成0了。

但其实print指令不止是这个作用,还可以打印出表达式的结果:
在这里插入图片描述
也就是我们可以拿来做一些简单的表达式运算。

但是上面这些指令做不到像vs 2022中那样跟随的效果。即随着代码的执行,监视的变量的值会跟随着代码逻辑的变化而变化。如果是使用print指令查看需要持续变化的变量很不方便的。
所以对于变化多次的变量,需要使用指令display + 变量名,就可以持续变化了:
在这里插入图片描述
我们会发现result一致在跟随着变化。不想再看到也可以,使用undisplay就可以了,但是注意undisplay后面必须跟着编号,即变量前面显示的那个,否则如下图是报错所示。
在这里插入图片描述

如果是想要查看当前栈帧中的所有的局部变量,可以使用指令info locals
在这里插入图片描述
就可以把当前局部栈帧内的局部变量展示出来。


12.退出调试工具quit / q
如果想要退出cgdb / gdb调试器,可以使用指令quit / q进行退出。当然也可以使用快捷键ctrl + d进行退出。这个很简单,就不再讲了。

调试常用技巧

在前面的基础指令的基础上,我们在这里在讲解几个常用的调试的技巧。这些技巧在某些时刻是可以派上用场的。

watch指令

这个指令其实执行时监视一个表达式(如变量)的值。如果监视的表达式在程序运行期间的值发生变化,GDB 会暂停程序的执行,并通知使用者:
在这里插入图片描述
如图,这里设置了一个watchpoint,观察变量result的变化:
在这里插入图片描述
如上图所示,这个就是用来对比一个变量前后变化的。这点对于有些场景是很好用的。很可能我们的代码是在某个地方写错了,导致前后的逻辑出错。我们就可与以此进行变量追踪,查看附近的相关代码是哪里出问题了。

我们会发现watchpoint也是断点的一种,所以对断点的使能控制和删除都是和普通的断点是一样的,不想要的时候直接删除就好了:
在这里插入图片描述

set var设置变量

这个指令的语法是set var 变量=值,用来在调试代码的时候短暂休整变量的值的。

#include<stdio.h>

int flag = 0;
//int flag = 1;
//int flag = -1;

//求和 a ~ b
int GetSum(int s, int e){
  int result = 0;
  for(int i = s; i <= e; ++i){
    result += i;
  }
  return result * flag;
}

int main(){
  int begin = 0, end = 100;
  printf("The Sum Will be Added from %d to %d\n", begin, end);
  int result = GetSum(begin, end);
  printf("The result is %d, [%d <-> %d]\n", result, begin, end);
  return 0;
}

假设当前我们把代码改成上面这个样子,最后的答案需要根据符号的正负性flag返回。但是这里我们假装不知道我们把符号改成0了,最后的结果运行是:
在这里插入图片描述
我们发现答案是0,所以需要进行调试:
在这里插入图片描述
此时我们猜测可能是GetSum函数内部的问题,所以需要进入该函数内:
在这里插入图片描述
此时我们尝试着跑几次循环看看:
在这里插入图片描述
我们发现循环逻辑其实是没有问题的。所以我们猜测是不是返回值处出现了问题:
在这里插入图片描述
最后发现确实是返回值处出现了问题,flag变成0了。我们先尝试着把flag修改一下看看:
在这里插入图片描述
把flag改为1,然后执行完GetSum函数后这个结果就对了。所以我们只需要退出调试,用vim对源码进行修改就好了。这个只是简单的演示。

在实际开发中,可能问题会更麻烦,如果我们能够在内部进行短暂地修改,跑多几次就可以知道大致的问题,然后退出修改。要不然就需要修改一次调试一次,这是很麻烦的。

条件断点

当然,对于断点来说,还是有一些操作的,比如条件断点。这里分为两个部分,新增一个条件断点,给已有断点增加条件。

我们先来看新增断点:
指令语法:b 行号/文件名:行号/函数名 if i == 30(条件,这里举个例子)
在这里插入图片描述

如上图所示,当i==10的时候断点才会生效,也就是下面写的stop only if i ==10,我们来验证一下:
在这里插入图片描述
我们可以发现,确实是停下来了。这个特别适用于当我们发现某些特殊条件下代码会出错的时候,我们就可以用这个条件控制断点。碰到该条件就停下来,测试相关代码的功能。

当然也是可以给已有的断点追加条件的:
使用指令condition 当前已有断点编号 (不需要if)条件

在这里插入图片描述
假设当前11行的断点没有任何的条件,我们尝试运行几次:
在这里插入图片描述
每一次都会停下来。但是我们追加一个条件i == 10
在这里插入图片描述
此时断点又变成了条件断点了,只在i == 10的时候停下:
在这里插入图片描述

删除条件其实也很简单,直接condition 断点编号就可以了,我们可以理解为把断点的条件改成空:
在这里插入图片描述

总结

以上就是在使用gdb/cgdb工具中常用的调试技巧,具体可以归纳到下面这个表中:

指令作用样例
list/l显示源代码(每次10行)list
l 10
l 函数名显示函数代码l main
l 文件名:行号显示指定位置代码l test.c:5
run/r从头执行程序run
next/n单步执行(不进入函数)next
step/s单步执行(进入函数)step
break/b 行号设置行断点b 10
b test.c:5
b 函数名设置函数断点b main
info break/b查看断点信息info b
finish执行到函数返回finish
print/p 表达式打印表达式值p x+y
p 变量打印变量值p x
set var=值修改变量值set var i=10
continue/c继续执行c
delete/d删除所有断点d
d n删除指定断点d 1
disable b禁用所有断点disable b
enable b启用所有断点enable b
info breakpoints查看断点列表info breakpoints
display 变量持续显示变量display x
undisplay n取消持续显示undisplay 1
until 行号运行到指定行until 20
backtrace/bt查看调用栈bt
info locals查看局部变量info locals
quit退出GDBquit

这些指令确实比较多,但是已经涵盖了gdb工具最常用的指令了。熟练掌握即可。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐