王道C++班级参考资料
——
Linux部分卷2GNU工具集
节2GDB调试程序

最新版本V3.0
王道C++团队
COPYRIGHT ⓒ 2021-2024. 王道版权所有

什么是调试程序?

Gn!

代码写完了,可执行程序也成功生成了,但这并不意味着就万事大吉,直接完工并开始摸鱼了。在大多情况下,我们还需要调试程序。

那么什么是调试程序呢?

调试程序的本质,其实是信息确认的过程。

如何理解"信息确认"呢?

说得更清楚一点,调试程序实际上就是:

程序员先预判程序执行的行为和状态,然后再去和程序真实的行为和状态进行对比,确认信息。如果这个过程中,出现了不符合预期的意外情况,那么就有两种可能:

  1. 程序员的思路出了问题,求解方式一开始就错了,代码写对了也没用。

  2. 思路没问题,代码的逻辑写错了,或者代码的语法、细节(比如边界值)等出现了问题。

所以调试程序的最终目的,就是通过信息确认,判断出是"人的思路错了",还是"人的代码写错了",从而进行改正。

最后我们请大家思考一个问题,当你看了上面的描述后,你觉得调试程序的前提是什么?

实际上,程序员能够调试程序的前提是:

  1. 程序员要有清晰的求解思路,能够搞清楚自己想做什么

  2. 程序员还要能够掌控自己的代码,能够预判程序的状态和行为

所以大家会发现很多初级程序员是不会调试程序的,因为它们缺乏清晰的逻辑以及对代码的掌控力,而这些能力是需要我们在不断的学习过程中去加强的。

在以往Windows的环境下,我们可以借助VS的图形界面Debug模式来调试程序。在Linux环境下,我们也有类似的调试工具,那就是GDB,这一小节我们就主要学习使用GDB来调试程序。

GDB是什么?

Gn!

GDB(GNU Debugger)是GNU项目的一部分,虽然GDB是一个跨平台的支持多种语言的调试工具,但它主要还是用于在Linux操作系统下调试C/C++编程语言。

在Linux系统环境下,GDB可以基本上实现Windows下VS的Debug功能,比如:

  1. 断点设置

  2. 逐语句

  3. 逐过程

  4. 监视窗口(查看/修改变量取值)

  5. 查看内存

  6. ....

GDB调试程序的过程基本上和VS调试过程差不多,仍然可以总结为——"停一停、瞧一瞧,再走一走、再看一看"

总得来说,GDB肯定没有VS的调试功能好用(图形界面毕竟非常方便),但在Linux环境下GDB已经是比较好的选择了。而且GDB作为Linux环境下,C/C++程序员调试代码的常用工具,也是实际面试中比较常问常考的话题。

GDB快速入门

Gn!

在下面的小节中,我们将快速演示如何使用GDB调试一个程序,即GDB快速入门。但后续的提升和熟练使用,还需要大家多多练习。

若你还未安装GDB,可以使用下列指令进行安装:

这里给出一个用于GDB学习的简单代码,如下:

这个代码是存在Bug的,我们可以通过GDB调试找出这个Bug。

第一步:带调试信息编译代码

Gn!

编译过程会去掉代码中诸如变量名这样的调试信息,从汇编代码开始这些调试信息就被替换成了内存地址。

所以要想使用GDB调试程序。首先第一步就是:使用带"-g"的指令编译生成可执行程序。

比如指令:

注意:

  1. 调试程序时最好不要设置编译优化,一般保持默认就可以了,或者主动加上-O0选项。

  2. 这一步完成后,你会得到一个带调试信息的可执行程序hello,当然这个可执行程序也是可以正常启动执行的。

第二步:进入GDB调试界面

Gn!

进入GDB调试界面(控制台),可以认为是用GDB启动(调试)这个可执行程序,有以下两种方式:

注意:第二种方式是先进入gdb控制台,再选择调试的可执行程序,如下图所示:

进入GDB调试界面-图

成功进入GDB调试界面后,就可以使用各种指令进行程序的调试了,首先我们介绍一个最基础的指令——查看源代码

它的格式如下:

注意,大多数GDB指令都有长短两种写法,推荐直接使用短指令形式!

一些使用举例:

建议直接使用l简写的指令,没必要使用list这样的长指令。除此之外,还有一些比较零碎的指令,就都放这里给出了:

直接执行run指令,发现程序会直接执行完,这是正常的。因为我们还没有打断点!

第三步:打断点

Gn!

在VS当中调试程序起始于打断点,GDB中也是一样的,打断点可以使用以下指令:

常见用法举例:

设置好断点后,可以使用以下指令来查看断点信息:

断点信息的输出结果可能如下:

断点信息-图

这段信息从左往右内容是:

  1. Num:断点的唯一性标识编号,一次Debug过程断点编号会从1开始,且不会重置一直累加。

  2. Type:断点的类型,这里显示"breakpoint",表示断点都只是普通断点。

  3. Disp(Disposition,性格):它表示断点触发后,是否会继续触发。

    1. keep就表示它是一个持久的断点,只要不删除就一直存在,每次启动都会触发。

    2. 这个值如果是del就表示,该断点是一个一次性断点。

  4. Enb(enable):指示断点是否生效,可以通过dis指令设置该属性。

  5. What:指示断点在源代码的哪个位置

既然能打断点,那肯定也能删除断点,我们可以用 delete/d 指令删除断点:

比如:

如果不想删除断点的话,也可以暂时让断点失效,成为一个"哑断点",使用指令如下:

打了断点后,就可以使用run/r指令来启动调试程序了,此时程序就会在断点处暂停运行,下面则是一些常用的调试命令。

GDB常用调试指令

Gn!

逐语句/单步调试

step/s 命令可以用来进行单步调试,即遇到函数调用会进入函数。

跳出并执行完函数

我们可以使用 finish 命令执行完整个函数,并返回到函数调用处:

稍微需要注意的是:在main函数当中使用该指令是无效的,因为main一般处在调用栈的最底层。

逐过程

next/n 命令表示逐过程,也就是说遇到函数调用,它不会进入函数,而是把函数调用看作一条语句直接执行完毕。

一般来说,我们更建议使用逐过程来控制程序继续执行,step可能会进入一些库函数的执行,这一般是不需要的。

监视窗口/查看变量取值

print/p 命令可以打印表达式的值:

如:

print/p 命令还可以改变变量的值,使用格式是:

比如:

如果想要持续的,展示某个表达式的值,使用格式如下:

如果需要查看所有局部变量的值,局部变量窗口,使用格式如下:

继续,跳过一次断点:

continue/c 命令可以运行到逻辑上的下一个断点处:

忽略断点n次

我们可以用 ignore 命令来指定忽略某个断点多少次,这在调试循环的时候非常有用。使用格式如下:

常见用法:

注意:

  1. 设定忽略断点次数后,还需要按c继续跳过断点才会生效。

  2. 设定一次,仅生效一次。

  3. 跳过n次断点,那么程序会在(n + 1)次到达这个断点时停下来。

查看堆栈信息

一般我们可以在报错的位置使用该指令,用于查看程序的执行流程以及排查相关的问题。

查看内存,内存窗口

我们可以用 x 命令查看内存的值,类似VS当中的内存窗口,虽然不如VS那么直观,但在某些场景中会有奇效。

基本使用格式如下:

该指令会从数组名/指针/地址值..开始,向后以(内存数据的输出格式),展示(内存单元的个数) * (一个内存单元的大小)的内存数据。

其中:

内存单元的个数,直接输入一个整数即可。

内存数据的输出格式有:

  1. o(octal),八进制整数

  2. x(hex),十六进制整数

  3. d(decimal),十进制整数

  4. u(unsigned decimal),无符号整数

  5. t(binary),二进制整数

  6. f(float),浮点数

  7. c(char),字符

  8. a(address),地址值

  9. c(character):字符

  10. s(string),字符串

一个内存单元的大小的表示,有以下格式:

  1. b(byte),一个字节

  2. h(halfword, 2 bytes),二个字节

  3. w(word, 4 bytes),四个字节

  4. g(giant, 8 bytes),八个字节

演示代码案例:

常见用法:

注:如果忘记了查看内存指令如何使用,只需要使用help x查看帮助手册即可。

输入命令行参数

当main函数的形参列表是int argc, char *argv[]时,允许可执行程序传参命令行参数。如果想要使用GDB调试带命令行参数的可执行程序,有以下两种方式可以选择:

  1. 在启动GDB时,使用指令gdb --args ./a.out arg1 arg2 arg3...即可表示传递命令行参数,其中a.out表示可执行程序的名字。

  2. 如果已经启动了GDB,可以使用以下两种方式都可以传递命令行参数:

    1. 使用指令set args arg1 arg2....,其中指令部分是set args,后面的部分则是参数。

    2. 使用指令run/r arg1 arg2启动,也表示传递命令行参数。

以上,多多练习,熟悉后GDB用来调试程序,功能是完全足够的。

调试Coredump文件

Gn!

如果代码压根跑不起来,或者跑起来就会直接崩溃,我们还可以利用Core文件进行辅助调试。

通常情况下,程序异常终止时,会产生 Coredump 文件。Coredump 文件类似飞机上的"黑匣子",它会保留程序"失事"瞬间的一些信息,通常包含寄存器的状态、栈调用情况等。

Coredump 文件常用于辅助分析和 Debug,下面介绍一下这种调试手段。

首先,系统默认是不会生成Core文件的,这一点可以通过以下指令查看:

默认情况下,该指令输出的第一行就是:

core file size (blocks, -c) 0

表示此时系统允许生成的core文件最大是0个字节,即不允许生成。

所以我们需要用下列指令将core文件的大小设置为不受限制:

默认情况下,上述操作后可能还是无法生成Core文件,你可以切换到root用户或者使用sudo权限,然后补充一下core文件的配置信息到目标文件里。

具体的操作如下,先打开配置文件:

将下列信息补充到配置文件末尾(注意前面不要加#号)

紧跟着你还需要执行以下指令让配置信息生效:

这段配置信息的目的是给core文件设定一个固定的格式,这样设置后,再次执行报错可执行程序就会生成core文件了。

这里给出几段用于调试错误的参考代码:

使用这些代码正常生成带有调试信息的可执行程序,然后直接执行,就会在同目录下自动生成core文件了。

但是要注意:一般只有段错误才会生成对应core文件,像上面数组越界引发未定义行为是没有段错误的,也就不会生成core文件。

然后你就可以用指令:

查看报错的一些信息,此时再利用bt等指令就可以进行正常的程序调试了。

注:

实际上即便不依赖于core文件,直接在gdb中启动会报错的可执行程序,效果和使用core文件是一样的。比如:

GDB调试错误信息-图

接下来再通过查看堆栈信息、监视等功能,即可实现调试程序。

但在实际的生产环境中,你可能无法直接复现报错情况(或者很麻烦)。那么程序报错了,只能通过Core文件来检测程序的错误信息,进而修正代码。

GDB练习

Gn!

可以使用以下代码,利用GDB进行Debug调试的练习,找出并修改代码中的bug。

注意:

  1. 编译代码时要增加-Wall选项,这样可以排查出代码的一些隐藏的问题。使用gcc编译器时,要重视代码中的警告信息,而不是无视。

  2. gdb可以在随后的学习中,逐步练习,不要害怕更不要不想用这个东西。多练习才是王道!

GDB练习2

Gn!

可以使用以下代码,利用GDB进行Debug调试的练习,找出并修改代码中的bug。

为了更好的调试这段代码,我们再讲两个常用的GDB功能:

利用display,持续显示数组特定范围的取值:

注意:len是一个表示数组长度的变量,必须在代码中明确给出的变量。

观察断点:

如果你对数组中某个特定元素的变化很感兴趣,可以使用 watch 命令来设置断点监视该元素。

利用这两个功能,你可以很好的找到程序的bug,然后修正程序。

The End