C C++调试总结:IDE(visual studio和Dev C+,程序员如何解决中年危机

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

快速安装GDB

对于 RedHat 系列的 Linux 发行版,通过在命令行窗口中执行sudo yum -y install gdb指令,即可实现 GDB 调试器的安装。这里以 CentOS 为例,执行该指令的过程为:

[root@bogon ~]# gdb -v
bash: gdb: command not found <–当前系统中没有GDB
[root@bogon ~]# sudo yum -y install gdb <–安装 GDB
Loaded plugins: fastestmirror, refresh-packagekit, security
Loading mirror speeds from cached hostfile
… <-省略部分过程
Installed:
gdb.x86_64 0:7.2-92.el6

Complete!
[root@bogon ~]# gdb -v
GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6) <–安装成功

可以看到,GDB 安装成功。

对于 Debian 系列的 Linux 发行版,通过执行sudo apt -y install gdb指令,即可实现 GDB 的安装。感兴趣的读者可自行验证,这里不再过多赘述。

注意,使用 yum 或者 apt 更多情况需要借助网络下载所需使用的安装包,这也就意味着,如果读者所用系统没有网络环境,很有可能安装失败。

源码安装GDB

和使用 yum(apt)自动安装 GDB 不同,手动安装需提前到 GDB 官网下载相应的源码包,读者可直接点击 GDB源码包进行下载。值得一提的是,每个 GDB 版本都提供有 tar.gz 和 tar.xz 这 2 种格式的压缩包,这里以 tar.gz 格式为例教大家安装 GDB。

注意,在安装 GDB 之前,读者必须保证当前操作系统中有可以使用的编译器,比如最常用的 GCC 编译器(应同时支持 gcc 和 g++ 指令),有关 GCC 编译器的下载和安装,读者可阅读《GCC下载和安装》一文。另外,源码安装 GDB 需要用到 Makefile 相关的知识,读者可完全遵循以下步骤“照猫画虎”地安装 GDB。对 Makefile 感兴趣的读者,可前往《Makefile教程》做系统了解。

本节下载的 GDB 源码包为 gdb-9.2-tar.gz,接下来以 CentOS 系统为例(也同样适用于其它 Linux 发行版),给大家演示整个安装过程:

  1. 找到 gdb-9.2-tar.gz 文件,笔者将下载好的 gdb-9.2-tat.gz 放置在 /usr/local/src 目录下:

[root@bogon ~]# cd /usr/local/src
[root@bogon src]# ls
gdb-9.2.tar.gz

  1. 使用 tar 命令解压该文件,执行命令如下:

[root@bogon ~]# tar -zxvf gdb-9.2.tar.gz
–省略解压过程的输出结果
[root@bogon src]# ls
gdb-9.2 gdb-9.2.tar.gz

此步骤会得到 gdb-9.2.tar.gz 相应的解压文件 gdb-9.2 。

  1. 进入 gdb-9.2 目录文件,创建一个 gdb_build_9.2 目录并进入,为后续下载并放置安装 GDB 所需的依赖项做准备:

[root@bogon src]# cd gdb-9.2
[root@bogon gdb-9.2]# mkdir gdb-build-9.2
[root@bogon src]# cd gdb-build-9.2

  1. 在此基础上,继续执行如下指令:

[root@bogon gdb-build-9.2]# …/configure
… <–省略众多输出
configure: creating ./config.status
config.status: creating Makefile

  1. 执行 make 指令编译整个 GDB 源码文件,此过程可能会花费很长时间,读者耐心等待即可:

[root@bogon gdb-build-9.2]# make
… <-- 省略编译过程产生的输出结果

注意,如果编译过程中出现错误,极有可能是所用的 GCC 编译器版本过低导致的,可尝试升级 GCC 版本后再重新执行 make 命令。

  1. 确定整个编译过程没有出错之后,执行sudo make install指令(其中使用 sudo 指令是为了避免操作权限不够而导致安装失败),正式开始安装 GDB 调试器:

[root@bogon gdb-build-9.2]# sudo make install
… <-- 省略输出结果

以上过程全部成功执行,则表示 GDB 安装成功。通过再次执行 gdb -v 指令,可验证其是否被成功安装。

[root@bogon gdb-build-9.2]# gdb -v
GNU gdb (GDB) 9.2
Copyright © 2020 Free Software Foundation, Inc.
… <-- 省略部分输出

如何在Windows平台上安装GDB调试器?

前面介绍了如何将 GDB 调试器安装到 Linux 发行版系统中,其实它还能在 Windows 平台上使用。和前者不同,GDB 调试器无法直接安装到 Windows 平台上,如果想在 Windows 系统中使用 GDB 调试器,需要一个中间媒介,常用的就是 MinGW。

MinGw 全称 Minimalist GNU for Windows,应用于 Windows 平台,可以为我们提供一个功能有限的 Linux 系统环境以使用一些 GNU 工具,比如 GCC、GDB、gawk 等。也就是说,如果我们想将 GDB 安装到 Windows 平台,必须提前在该平台上打造出一个虚拟的 Linux 环境,MinGW 就可以帮我们完成这项工作。

由此,在 Windows 平台上安装 GDB 调试器的过程,分为如下 2 个步骤:

  1. 下载并安装 MinGW;
  2. 借助 MinGW 安装 GDB 调试器。

有关 MinGW 的下载和安装,我已经在《MinGW下载和安装教程》一节中做了详细的讲解,这里不再重复赘述(安装好的 MinGW 如图 1 所示)。本节要重点介绍的,就是当安装完成 MinGW 之后,如何安装 GDB 调试器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ck1ZdJOC-1641227989445)(http://c.biancheng.net/uploads/allimg/200818/1-200QP95301W5.gif)]
图 1 MinGW 安装完成 MinGW安装GDB调试器

MinGW安装GDB调试器

使用 MinGW 安装 GDB 调试器的方法有 2 种,一种是利用 MinGW 的在线安装功能,由 MinGW 帮我们下载 GDB 并安装;另一种是由我们手动到 MinGW 官网上下载 GDB 安装包(本质是一个压缩包),然后将其解压到 MinGW 安装目录中的适当位置,由此完成 GDB 调试器的安装。

1、在线安装GDB调试器

首先介绍较简单的第一种安装方法:

  1. 打开图 1 所示的 MinGW,在右侧框中找到 mingw32-gdb-bin,并勾选它,如图 2 所示:

在线安装GDB调试器
图 2 在线安装 GDB 调试器

  1. 菜单栏中依次选择Installation -> Apply Changes,会弹出如下对话框:

开始安装GDB调试器
图 3 开始安装 GDB 调试器

选择 “Apply”,MinGW 就会自动下载安装 GDB 调试器所需的包。此过程可能需要一段时间,读者耐心等待即可。看在如下对话框,证明 GDB 安装成功:

GDB调试器成功安装
图 4 GDB调试器成功安装

注意,在线安装过程中,GDB 安装包可能会下载失败。这种情况下,可以采用第 2 种安装方法。

2、手动安装GDB调试器

当 MinGW 提示 GDB 调试器安装包下载失败时,我们可以自行打开 MinGW 官网(如图 5 所示),下载所需版本的 GDB 调试器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bpn5zBkm-1641227989447)(http://c.biancheng.net/uploads/allimg/200818/1-200QQ101111S.gif)]
图 5 MinGW 官网下载 GDB 调试器安装包

本节,我们选择安装 7.6.1 版本的 GDB 调试器,点击gdb-7.6.1-1,然后找到gdb-7.6.1-1-mingw32-bin.tar.lzma,这是一个安装 GDB 的压缩包,直接点击即可开始下载:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbMpQmVw-1641227989447)(http://c.biancheng.net/uploads/allimg/200818/1-200QQ101422I.gif)]
图 6 下载 GDB 安装包

下载完成后,读者会得到gdb-7.6.1-1-mingw32-bin.tar.lzma压缩包,其内部包含一个gdb-7.6.1-1-mingw32-bin.tar的压缩包。所以,我们需要对下载的压缩包进行 2 次压缩,最终得到 bin 和 share 这 2 个文件夹。

将 bin 文件夹内的 2 个可执行文件 gdb.exe 和 gdbserver.exe 拷贝到 MinGW 安装目录中的 bin 文件夹内(本机 MinGW 安装目录为 E:\MinGW),即可完成 GDB 的安装。

3、修改PATH环境变量

注意,无论采用以上哪种安装方式,最终都需要将 MinGW 的 bin 文件夹所在路径添加到 PATH 环境变量中。以本机为例,我将 MinGW 安装到 E:\MinGW 路径下,因此需要将 E:\MinGW\bin 添加至 PATH 环境变量中,如图 7 所示:

设置PATH环境变量
图 7 设置 PATH 环境变量

PATH环境变量打开方法是:右击计算机(我的电脑) -> 属性 -> 高级系统设置 -> 环境变量,建议读者修改当前用户的 PATH 环境变量。

由此,GDB 调试器就算安装成功了。打开命令行窗口,输入gdb -v命令,如果输入如下信息:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m8kTCQgc-1641227989448)(http://c.biancheng.net/uploads/allimg/200818/1-200QQ10254163.gif)]
图 8 GDB 调试器安装成功的示意图

就证明 GDB 调试器安装成功,我们可以像在 Linux 系统中使用 GDB 那样,在 Windows 平台上使用 GDB 调试器。

GDB调试C/C++程序

通过前面的学习,读者已经了解了什么是 GDB,以及如何下载并安装它。从本节开始,我们将正式学习使用 GDB 调试 C、C++ 程序。

如下是一段能够正常编译运行的 C 语言程序:

#include <stdio.h>
int main ()
{
unsigned long long int n, sum;
n = 1;
sum = 0;
while (n <= 100)
{
sum = sum + n;
n = n + 1;
}
return 0;
}

此源码的完整存储路径为 /tmp/demo/main.c。

本节就以此程序为例,给大家演示 GDB 调试器的基本用法。

使用GDB的前期准备

通过前面的学习我们知道,GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,GDB 才会派上用场。

Linux 发行版中,经常使用 GCC 编译 C、C++ 程序(有关 GCC 编译器,读者可猛击《GCC编译器》系统学习)。但需要注意的是,仅使用 gcc(或 g++)命令编译生成的可执行文件,是无法借助 GDB 进行调试的。

以 main.c 源文件为例,正常情况下,使用 GCC 编译该源代码的指令如下:

[root@bogon demo]# ls
main.c
[root@bogon demo]# gcc main.c -o main.exe
[root@bogon demo]# ls
main.c main.exe

可以看到,这里已经生成了 main.c 对应的执行文件 main.exe,但值得一提的是,此文件不支持使用 GDB 进行调试。原因很简单,使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),而上面生成的 main.exe 则没有。

那么,如何生成符合 GDB 调试要求的可执行文件呢?很简单,只需要使用 gcc -g 选项编译源文件,即可生成满足 GDB 要求的可执行文件。仍以 main.c 源程序文件为例:

[root@bogon demo]# ls
main.c
[root@bogon demo]# gcc main.c -o main.exe -g
[root@bogon demo]# ls
main.c main.exe

由此生成的 main.exe,即可使用 GDB 进行调试。

较早以前的 C 语言编译器也允许使用 -gg 选项来产生调试信息,但是现在版本的 GDB 不再支持这种格式产生的调试信息,所以不建议使用 -gg 选项。

值得一提的是,GCC 编译器支持 -O(等于同 -O1,优化生成的目标文件)和 -g 一起参与编译。GCC 编译过程对进行优化的程度可分为 5 个等级,分别为 O0~O4,O0 表示不优化(默认选项),从 O1 ~ O4 优化级别越来越高,O4 最高。

所谓优化,例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。

而相对于 -O -g 选项,对 GDB 调试器更友好的是 -Og 选项,-Og 对代码所做的优化程序介于 O0 ~ O1 之间,真正可做到“在保持快速编译和良好调试体验的同时,提供较为合理的优化级别”。

解决了如何生成满足 GDB 调试器要求的可执行文件,接下来正式学习 GDB 调试器的使用。

启动GDB调试器

在生成包含调试信息的 main.exe 可执行文件的基础上,启动 GDB 调试器的指令如下:

[root@bogon demo]# gdb main.exe
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.

(gdb)

注意,该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、–quiet)选项,可将比部分信息屏蔽掉:

[root@bogon demo]# gdb main.exe --silent
Reading symbols from main.exe…(no debugging symbols found)…done.
(gdb)

无论使用以上哪种方式,最终都可以启动 GDB 调试器,启动成功的标志就是最终输出的 (gdb)。通过在 (gdb) 后面输入指令,即可调用 GDB 调试进行对应的调试工作。

GDB 调试器提供有大量的调试选项,可满足大部分场景中调试代码的需要。如表 1 所示,罗列了几个最常用的调试指令及各自的作用:

调试指令作 用
(gdb) break xxx (gdb) b xxx在源代码指定的某一行设置断点,其中 xxx 用于指定具体打断点的位置。
(gdb) run (gdb) r执行被调试的程序,其会自动在第一个断点处暂停执行。
(gdb) continue (gdb) c当程序在某一断点处停止运行后,使用该指令可以继续执行,直至遇到下一个断点或者程序结束。
(gdb) next (gdb) n令程序一行代码一行代码的执行。
(gdb) print xxx (gdb) p xxx打印指定变量的值,其中 xxx 指的就是某一变量名。
(gdb) list (gdb) l显示源程序代码的内容,包括各行代码所在的行号。
(gdb) quit (gdb) q终止调试。

如上所示,每一个指令既可以使用全拼,也可以使用其首字母表示。另外,表 1 中罗列的指令仅是冰山一角,GDB 还提供有大量的选项,可以通过 help 选项来查看。有关 help 选项的具体用法,读者可阅读《GDB查看命令》一节,这里不再做具体赘述。

仍以 main.exe 可执行程序为例,接下来为读者演示表 1 中部分选项的功能和用法:

(gdb) l <-- 显示带行号的源代码
1 #include <stdio.h>
2 int main ()
3 {
4 unsigned long long int n, sum;
5 n = 1;
6 sum = 0;
7 while (n <= 100)
8 {
9 sum = sum + n;
10 n = n + 1;
(gdb) <-- 默认情况下,l 选项只显示 10 行源代码,如果查看后续代码,安装 Enter 回车即可
11 }
12 return 0;
13 }
(gdb) b 7 <-- 在第 7 行源代码处打断点
Breakpoint 1 at 0x400504: file main.c, line 7.
(gdb) r <-- 运行程序,遇到断点停止
Starting program: /home/mozhiyan/demo1/main.exe

Breakpoint 1, main () at main.c:7
7 while (n <= 100)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-55.el7.x86_64
(gdb) p n <-- 查看代码中变量 n 的值
$1 = 1 <-- 当前 n 的值为 1,$1 表示该变量所在存储区的名称
(gdb) b 12 <-- 在程序第 12 行处打断点
Breakpoint 2 at 0x40051a: file main.c, line 12.
(gdb) c <-- 继续执行程序
Continuing.

Breakpoint 2, main () at main.c:12
12 return 0;
(gdb) p n <-- 查看当前 n 变量的值
$2 = 101 <-- 当前 n 的值为 101
(gdb) q <-- 退出调试
A debugging session is active.

Inferior 1 [process 3080] will be killed.

Quit anyway? (y or n) y <-- 确实是否退出调试,y 为退出,n 为不退出
[root@bogon demo]#

后续章节会对以上指令做详细的讲解,这里简单了解即可,不必深究。

调用GDB调试器的4种方式

GDB调试C/C++程序》一节演示了用 GDB 调试 C(或者 C++)程序的整个过程,其中对 main.exe 文件启动 GDB 调试,执行的指令为:

[root@bogon demo]# gdb main.exe
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.

(gdb)

要知道,这仅是调用 GDB 调试器最常用的一种方式,GDB 调试器还有其它的启动方式。并且,为了满足不同场景的需要,启动 GDB 调试器时还可以使用一些参数选项,从而控制它启动哪些服务或者不启动哪些服务。

调用GDB的方式

总的来说,调用 GDB 调试器的方法有 4 种。

  1. 直接使用 gdb 指令启动 GDB 调试器:

[root@bogon demo]# gdb
ubuntu64@ubuntu64-virtual-machine:~/demo$ gdb
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
Type “apropos word” to search for commands related to “word”.
(gdb)

此方式启动的 GDB 调试器,由于事先未指定要调试的具体程序,因此需启动后借助 file 或者 exec-file 命令指定(后续章节会做详细讲解)。

2) 调试尚未执行的程序

对于具备调试信息(使用 -g 选项编译而成)的可执行文件,调用 GDB 调试器的指令格式为:

gdb program

其中,program 为可执行文件的文件名,例如上节创建好的 main.exe。

3) 调试正在执行的程序

在某些情况下,我们可能想调试一个当前已经启动的程序,但又不想重启该程序,就可以借助 GDB 调试器实现。

也就是说,GDB 可以调试正在运行的 C、C++ 程序。要知道,每个 C 或者 C++ 程序执行时,操作系统会使用 1 个(甚至多个)进程来运行它,并且为了方便管理当前系统中运行的诸多进程,每个进程都配有唯一的进程号(PID)。

如果需要使用 GDB 调试正在运行的 C、C++ 程序,需要事先找到该程序运行所对应的进程号。查找方式很简单,执行如下命令即可:

pidof 文件名

比如,我们将上节创建的 main.c 源文件修改为:

#include <stdio.h>
int main()
{
int num = 1;
while(1)
{
num++;
}
return 0;
}

执行 gcc main.c -o main.exe -g 编译指令,获得该源程序对应的具备调试信息的 main.exe 可执行文件,并在此基础上执行:

[root@bogon demo]# ./main.exe
<–程序一直运行

显然,程序中存在死循环(5~8 行),它会一直执行。此时,借助 pidof 指令即可获取它对应的进程号:

[root@bogon demo]# pidof main.exe
1830

可以看到,当前正在执行的 main.exe 对应的进程号为 1830。在此基础上,可以调用 GDB 对该程序进行调试,调用指令有以下 3 种形式:

  1. gdb attach PID
  2. gdb 文件名 PID
  3. gdb -p PID

其中,PID 指的就是要调取的程序对应的进程号。

以调试进程号为 1830 的程序为例,执行如下指令:

[root@bogon demo]# gdb -p 1830
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
0x00005645319c613c in main () at main.c:6
6 num++;

注意,当 GDB 调试器成功连接到指定进程上时,程序执行会暂停。如上所示,程序暂停至第 6 行代码 num++ 的位置,此时可以通过断点调试、逐步运行等方式监控程序的执行过程。例如:

(gdb) l <-- 查看源码以及各行行号
1 #include<stdio.h>
2 int main()
3 {
4 int num = 1;
5 while(1){
6 num++;
7 }
8 return 0;
9 }
(gdb) b 6 <–在程序第 6 行代码处打断点
Breakpoint 1 at 0x5645319c6138: file main.c, line 6.
(gdb) c <–令程序进行执行,其会在下一个断点处停止
Continuing.

Breakpoint 1, main () at main.c:6
6 num++;
(gdb) p num <-- 查看当前 num 的值
$2 = 47100335

注意,当调试完成后,如果想令当前程序进行执行,消除调试操作对它的影响,需手动将 GDB 调试器与程序分离,分离过程分为 2 步:

  1. 执行 detach 指令,使 GDB 调试器和程序分离;
  2. 执行 quit(或 q)指令,退出 GDB 调试。
4) 调试执行异常崩溃的程序

除了以上 3 种情况外,C 或者 C++ 程序运行过程中常常会因为各种异常或者 Bug 而崩溃,比如内存访问越界(例如数组下标越界、输出字符串时该字符串没有 \0 结束符等)、非法使用空指针等,此时就需要调试程序。

值得一提的是,在 Linux 操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为 core 文件,Linux 系统所具备的这种功能又称为核心转储(core dump)。幸运的是,GDB 对 core 文件的分析和调试提供有非常强大的功能支持,当程序发生异常崩溃时,通过 GDB 调试产生的 core 文件,往往可以更快速的解决问题。

默认情况下,Linux 系统是不开启 core dump 这一功能的,读者可以借助执行ulimit -c指令来查看当前系统是否开启此功能:

[root@bogon demo]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited

其中,如果 core file size(core 文件大小)对应的值为 0,表示当前系统未开启 core dump 功能。这种情况下,可以通过执行如下指令改变 core 文件的大小:

[root@bogon demo]# ulimit -c unlimited
[root@bogon demo]# ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited

其中,unlimited 表示不限制 core 文件的大小。

由此,当程序执行发生异常崩溃时,系统就可以自动生成相应的 core 文件。

举个例子,修改 main.c 源程序文件中的代码为:

#include <stdio.h>
int main()
{
char *p = NULL;
*p = 123;
return 0;
}

重新编译,即执行如下指令:

[root@bogon demo]# gcc main.c -o main.exe -g
[root@bogon demo]# ./main.exe
Segmentation fault (core dumped) <–发生段错误,并生成了 core 文件
[root@bogon demo]# ls
core main.c main.exe

段错误又称为访问权限冲突,指的是当前程序访问了不可访问的存储空间,比如访问的不存在的空间,又或者是受系统保护的内存空间。

观察此程序不难发现,由于 p 指针初始化为 NULL,即不指向任何存储空间,但后续却执行*p=123操作,显然是不可行的。因此,该程序执行时会发生崩溃,Linux 系统会记录必要的崩溃信息,并存储到 core 文件中。

默认情况下,core 文件的生成位置同该程序所在的目录相同。当然我们也可以指定 core 文件的生成的位置,感兴趣的读者可自行研究,这里不再介绍。

对于 core 文件的调试,其调用 GDB 调试器的指令为:

[root@bogon demo]# gdb main.exe core
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.

Reading symbols from main.exe…
[New LWP 4296]

warning: Unexpected size of section .reg-xstate/4296' in core file. Core was generated by./main.exe’.
Program terminated with signal SIGSEGV, Segmentation fault.

warning: Unexpected size of section `.reg-xstate/4296’ in core file.
#0 0x00005583b933013d in main () at main.c:5
5 *p = 123;

可以看到,程序发生崩溃的位置是在 main.c 中的第 5 行。甚至于,对于 core 文件中记录的崩溃信息,可以使用 where、print、bt 等指令查看,有关这些指令的功能和用法,由于并非本节重点,这里不再具体赘述,后续章节会做详细讲解。

GDB调试器启动可用参数

表 1 罗列了一些在启动 GDB 调试器时常用的指令参数,以及它们各自的功能。

参 数功 能
-pid number -p number调试进程 ID 为 number 的程序。
-symbols file -s file仅从指定 file 文件中读取符号表。
-q -quiet -silent取消启动 GDB 调试器时打印的介绍信息和版权信息
-cd directory以 directory 作为启动 GDB 调试器的工作目录,而非当前所在目录。
–args 参数1 参数2…向可执行文件传递执行所需要的参数。

其中有些参数,我们已经在前面的学习给大家做了具体的演示,这里不再重复赘述,读者可自行尝试使用。除此之外,启动 GDB 调试器时还有其它参数指令可以使用,感兴趣的读者可查阅 GDB 官网做系统了解。有关表 1 以及 GDB 调试器支持的其它指令,后续章节用到时会做详细讲解。

gdb run(r)命令:启动程序

使用 GDB 调试器调试程序的过程,其实就是借助 GDB 调试器来监控程序的执行流程,进而发现程序中导致异常或者 Bug 的代码。通过前面章节的学习,读者已经学会了如何启动 GDB 调试器,在此基础上,本节继续为大家讲解如何在 GDB 调试器中启动(运行)程序,以及启动程序过程中的一些注意事项。

根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。也就是说,run 和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:

  • 默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run 指令会执行程序至第一个断点处;
  • start 指令会执行程序至 main() 主函数的起始位置,即在 main() 函数的第一行语句处停止执行(该行代码尚未执行)。

可以这样理解,使用 start 指令启动程序,完全等价于先在 main() 主函数起始位置设置一个断点,然后再使用 run 指令启动程序。另外,程序执行过程中使用 run 或者 start 指令,表示的是重新启动程序。

问一个问题,GDB 调试器启动后是否就可以直接使用 run 或者 start 指令了呢?答案当然是否定的。我们知道,启动 GDB 调试器的方式有多种,其中简单的方法就是直接使用 gdb 指令,例如:

[root@bogon demo]# gdb
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
Type “apropos word” to search for commands related to “word”.
(gdb)

注意,使用此方式启动的 GDB 调试器,尚未指定要调试的目标程序,何谈使用 run 或者 start 指令呢?

不仅如此,在进行 run 或者 start 指令启动目标程序之前,还可能需要做一些必要的准备工作,大致包括以下几个方面:

  • 如果启动 GDB 调试器时未指定要调试的目标程序,或者由于各种原因 GDB 调试器并为找到所指定的目标程序,这种情况下就需要再次手动指定;
  • 有些 C 或者 C++ 程序的执行,需要接收一些参数(程序中用 argc 和 argv[] 接收);
  • 目标程序在执行过程中,可能需要临时设置 PATH 环境变量;
  • 默认情况下,GDB 调试器将启动时所在的目录作为工作目录,但很多情况下,该目录并不符合要求,需要在启动程序手动为 GDB 调试器指定工作目录。
  • 默认情况下,GDB 调试器启动程序后,会接收键盘临时输入的数据,并将执行结果会打印在屏幕上。但 GDB 调试器允许对执行程序的输入和输出进行重定向,使其从文件或其它终端接收输入,或者将执行结果输出到文件或其它终端。

假设使用 GDB 调试器调试如下程序:

//存储路径为 /tmp/demo/main.c
//其生成的可执行文件为 main.exe,位于同一路径下
#include<stdio.h>
int main(int argc,char* argv[])
{
FILE * fp;
if((fp = fopen(argv[1],“r”)) == NULL){
printf(“file open fail”);
}
else{
printf(“file open true”);
}
return 0;
}

要知道,命令行窗口打开时默认位于 ~ (表示当前用户的主目录)路径下,假设我们就位于此目录中使用 gdb 命令启动 GDB 调试器,则在执行 main.exe 之前,有以下几项操作要做:

  1. 首先,对于已启动的 GDB 调试器,我们可以先通过 l (小写的 L)指令验证其是否已找到指定的目标程序文件:

[root@bogon ~]# gdb -q <-- 使用 -q 选项,可以省略不必要的输出信息
(gdb) l
No symbol table is loaded. Use the “file” command.

可以看到,对于找不到目标程序文件的 GDB 调试器,l 指令的执行结果显示“无法加载符号表”。这种情况下,我们就必须手动为其指定要调试的目标程序,例如:

(gdb) file /tmp/demo/main.exe
Reading symbols from /tmp/demo/main.exe…
(gdb) l
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 FILE * fp;
5 if((fp = fopen(argv[1],“r”)) == NULL){
6 printf(“file open fail”);
7 }
8 else{
9 printf(“file open true”);
10 }
(gdb)
11 return 0;
12 }
(gdb)

可以看到,通过借助 file 命令,则无需重启 GDB 调试器也能指定要调试的目标程序文件。

除了 file 指令外,GDB 调试器还提供有其它的指定目标调试文件的指令,感兴趣的读者可千万 GDB 官网做详细了解,后续章节在用到时也会做详细的讲解。

  1. 通过分析 main.c 中程序的逻辑不难发现,要想其正确执行,必须在执行程序的同时给它传递一个目标文件的文件名。

总的来说,为 GDB 调试器指定的目标程序传递参数,常用的方法有 3 种:
1、启动 GDB 调试器时,可以在指定目标调试程序的同时,使用 --args 选项指定需要传递给该程序的数据。仍以 main.exe 程序为例:

[root@bogon demo]# gdb --args main.exe a.txt

整个指令的意思是:启动 GDB 调试器调试 main.exe 程序,并为其传递 “a.txt” 这个字符串(其会被 argv[] 字符串数组接收)。

2、GDB 调试器启动后,可以借助 set args 命令指定目标调试程序启动所需要的数据。仍以 main.exe 为例:

(gdb) set args a.txt

该命令表示将 “a.txt” 传递给将要调试的目标程序。

3、除此之外,还可以使用 run 或者 start 启动目标程序时,指定其所需要的数据。例如:

(gdb) run a.txt
(gdb) start a.txt

以上 2 条命令都可以将 “a.txt” 传递给要调试的程序。

  1. 要知道,对于调试 /tmp/demo/ 路径下的 main.exe 文件,将其作为 GDB 调试器的工作目录,一定程度上可以提高我们的调试效率。反之,如果 GDB 调试器的工作目录和目标调试文件不在同一目录,则很多时候需要额外指明要操作文件的存储路径(例如第 1) 种情况中用 file 指令指明调试文件时就必须指明其存储位置)。

默认情况下,GDB 调试器的工作目录为启动时所使用的目录。例如在 ~ 路径下启动的 GDB 调试器,其工作目录就为 ~(当前用户的 home 目录)。当然,GDB 调试器提供有修改工作目录的指令,即 cd 指令。例如,将 GDB 调试器的工作目录修改为 /tmp/demo,则执行指令为:

(gdb) cd /tmp/demo

由此,GDB 调试器的工作目录就变成了 /tmp/demo。

  1. 某些场景中,目标调试程序的执行还需要临时修改 PATH 环境变量,此时就可以借助 path 指令,例如:

(gdb) path /temp/demo
Executable and object file path: /temp/demo:/usr/local/sbin:/usr/local/bin…

注意,此修改方式只是临时的,退出 GDB 调试后会失效。

  1. 默认情况下,GDB 调试的程序会接收 set args 等方式指定的参数,同时会将输出结果打印到屏幕上。而通过对输入、输出重定向,可以令调试程序接收指定文件或者终端提供的数据,也可以将执行结果输出到文件或者某个终端上。

例如,将 main.exe 文件的执行结果输出到 a.txt 文件中,执行如下命令:

(gdb) run > a.txt

由此,在 GDB 调试的工作目录下就会生成一个 a.txt 文件,其中存储的即为 main.exe 的执行结果。

总的来说,只有将调试程序所需的运行环境搭建好后,才能使用 run 或者 start 命令开始调试。如下是一个完整的实例,演示了 GDB 调试 mian.exe 之前所做的准备工作:

[root@bogon demo]# pwd <–显示当前工作路径
/tmp/demo
[root@bogon demo]# ls <-- 显示当前路径下的文件
a.txt main.c main.exe
[root@bogon demo]# cd ~ <-- 进入 home 目录
[root@bogon ~]# gdb -q <-- 开启 GDB 调试器
(gdb) cd /tmp/demo <-- 修改 GDB 调试器的工作目录
Working directory /tmp/demo.
(gdb) file main.exe <-- 指定要调试的目标文件
Reading symbols from main.exe…
(gdb) set args a.txt <-- 指定传递的数据
(gdb) run <-- 运行程序
Starting program: /tmp/demo/main.exe a.txt
file open true[Inferior 1 (process 43065) exited normally]

GDB break(b):设置断点

默认情况下,程序不会进入调试模式,代码会瞬间从开头执行到末尾。要想观察程序运行的内部细节(例如某变量值的变化情况),可以借助 GDB 调试器在程序中的某个地方设置断点,这样当程序执行到这个地方时就会停下来。

所谓断点(BreakPoint),读者可以理解为障碍物,人遇到障碍物不能行走,程序遇到断点就暂停执行。

在 GDB 调试器中对 C、C++ 程序打断点,最常用的就是 break 命令,有些场景中还会用到 tbreak 或者 rbreak 命令,本节将对这 3 个命令的功能和用法做详细的讲解。

为了让大家更好地了解给程序打断点的作用,这里以一段完整的 C 语言程序为例:

#include<stdio.h>
int main(int argc,char* argv[])
{
int num = 1;
while(num<100)
{
num *= 2;
}
printf(“num=%d”,num);
return 0;
}

程序存储在/tmp/demo/main.c文件中,并已经生成了具备调试信息的 main.exe 可执行文件:

[root@bogon demo]# ls
main.c main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe…
(gdb)

GDB break命令

break 命令(可以用 b 代替)常用的语法格式有以下 2 种。

1、(gdb) break location // b location
2、(gdb) break … if cond // b … if cond

  1. 第一种格式中,location 用于指定打断点的具体位置,其表示方式有多种,如表 1 所示。
location 的值含 义
linenumlinenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。
filename:linenumfilename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。
+ offset - offsetoffset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处(第 2 行)打断点。
functionfunction 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。
filename:functionfilename 表示远程文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。
  1. 第二种格式中,… 可以是表 1 中所有参数的值,用于指定打断点的具体位置;cond 为某个表达式。整体的含义为:每次程序执行到 … 位置时都计算 cond 的值,如果为 True,则程序在该位置暂停;反之,程序继续执行。

如下演示了以上 2 种打断点方式的具体用法:

(gdb) l
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 int num = 1;
5 while(num<100)
6 {
7 num *= 2;
8 }
9 printf(“num=%d”,num);
10 return 0;
(gdb)
11 }
(gdb) b 4 <-- 程序第 4 行打断点
Breakpoint 1 at 0x1138: file main.c, line 4.
(gdb) r <-- 运行程序,至第 4 行暂停
Starting program: /home/ubuntu64/demo/main.exe

Breakpoint 1, main (argc=1, argv=0x7fffffffe078) at main.c:4
4 int num = 1;
(gdb) b +1 <-- 在第 4 行的基础上,在第 5 行代码处打断点
Breakpoint 2 at 0x55555555513f: file main.c, line 5.
(gdb) c <-- 继续执行程序,至第 5 行暂停
Continuing.

Breakpoint 2, main (argc=1, argv=0x7fffffffe078) at main.c:5
5 while(num<100)
(gdb) b 7 if num>10 <-- 如果 num>10 在第 7 行打断点
Breakpoint 3 at 0x555555555141: file main.c, line 7.
(gdb) c <-- 继续执行
Continuing.

Breakpoint 3, main (argc=1, argv=0x7fffffffe078) at main.c:7
7 num *= 2; <-- 程序在第 7 行暂停
(gdb) p num <-- p 命令查看 num 当前的值
$1 = 16 <-- num=16

有关 c 和 p 命令,后续章节会做详细讲解,这里会用即可,不必深究。

GDB tbreak命令

tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。

tbreak 命令的使用格式和 break 完全相同,有以下 2 种:

1、(gdb) tbreak location
2、(gdb) tbreak … if cond

其中,location、… 和 cond 的含义都和 break 命令中的参数含义相同,即表 1 也同样适用于 tbreak 命令。

仍以 main.exe 为例,如下演示了 tbreak 命令的用法:

(gdb) tbreak 7 if num>10
Temporary breakpoint 1 at 0x1165: file main.c, line 7.
(gdb) r
Starting program: /home/ubuntu64/demo/main.exe

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:7
7 num *= 2;
(gdb) p num
$1 = 16
(gdb) c <-- 继续执行程序,则原使用 tbreak 在第 7 行打的断点将不再起作用
Continuing.
num=128[Inferior 1 (process 6534) exited normally]
(gdb)

可以看到,自num=16开始,后续循环过程中 num 的值始终大于 10,则num>10表达式的值永远为 True,理应在第 7 行暂停多次。但由于打断点采用的是 tbreak 命令,因此断点的作用只起 1 次。

GDB rbreak 命令

和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。

tbreak 命令的使用语法格式为:

(gdb) tbreak regex

其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,tbreak 命令就会其内部的开头位置打断点。值得一提的是,tbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。

这里我们对 main.c 源文件的程序做如下修改:

(gdb) l <-- 显示源码
1 #include<stdio.h>
2 void rb_one(){
3 printf(“rb_one\n”);
4 }
5 void rb_second(){
6 printf(“rb_second”);
7 }
8 int main(int argc,char* argv[])
9 {
10 rb_one();
(gdb)
11 rb_second();
12 return 0;
13 }
(gdb) rbreak rb_* <–匹配所有以 rb_ 开头的函数
Breakpoint 1 at 0x1169: file main.c, line 2.
void rb_one();
Breakpoint 2 at 0x1180: file main.c, line 5.
void rb_second();
(gdb) r
Starting program: /home/ubuntu64/demo/main.exe

Breakpoint 1, rb_one () at main.c:2
2 void rb_one(){
(gdb) c
Continuing.
rb_one

Breakpoint 2, rb_second () at main.c:5
5 void rb_second(){
(gdb) c
Continuing.
rb_second[Inferior 1 (process 7882) exited normally]
(gdb)

可以看到,通过执行rbreak rb_*指令,找到了程序中所有以 tb_* 开头的函数,并在这些函数内部的开头位置打上了断点(如上所示,分别为第 2 行和第 5 行)。

总结

在 GDB 调试器中,为程序的适当位置打断点,是最常用的调试程序的方法。不过,本节仅介绍了如何使用 break(tbreak、rbreak)在程序中打断点,实际场景中还可以使用 catch 或者 watch 中断程序的运行,有关它们的功能和用法,会在后续章节中给大家做详细讲解。

GDB watch命令:监控变量值的变化

GDB break命令》一节,给大家介绍了使用 break 命令在程序某一行的位置打断点。但还有一些场景,我们需要监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者 Bug。这种情况下,break 命令显然不再适用,推荐大家使用 watch 命令。

要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点,关于捕捉断点,后续章节会做详细讲解。

使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。

所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。

对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,watch 命令的语法非常简单,如下所示:

(gdb) watch cond

其中,conde 指的就是要监控的变量或表达式。

和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:

  • rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
  • awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。

强调一下,watch 命令的功能是:只有当被监控变量(表达式)的值发生改变,程序才会停止运行。

举个例子:

(gdb) l <–列出要调试的程序源码
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 int num = 1;
5 while(num<=100)
6 {
7 num *= 2;
8 }
9 printf(“%d”,num);
10 return 0;
(gdb)
11 }
(gdb) b 4 <-- 使用 break 命令打断点
Breakpoint 1 at 0x115c: file main.c, line 4.
(gdb) r <-- 执行程序
Starting program: /home/ubuntu64/demo/main.exe

Breakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:4
4 int num = 1;
(gdb) watch num <-- 监控程序中 num 变量的值
Hardware watchpoint 2: num
(gdb) c <-- 继续执行,当 num 值发生改变时,程序才停止执行
Continuing.

Hardware watchpoint 2: num

Old value = 0
New value = 2
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb) c <-- num 值发生了改变,继续执行程序
Continuing.

Hardware watchpoint 2: num

Old value = 2
New value = 4
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb)

有关代码中蓝色部分的含义,本文后续会做详细解释。

可以看到在程序运行过程中,通过借助 watch 命令监控 num 的值,后续只要 num 的值发生改变,程序都会停止。感兴趣的读者,可自行尝试使用 awatch 和 rwatch 命令,这里不再给出具体的示例。

如果我们想查看当前建立的观察点的数量,借助如下指令即可:

(gdb) info watchpoints

值得一提的是,对于使用 watch(rwatch、awatch)命令监控 C、C++ 程序中变量或者表达式的值,有以下几点需要注意:

  • 当监控的变量(表达式)为局部变量(表达式)时,一旦局部变量(表达式)失效,则监控操作也随即失效;
  • 如果监控的是一个指针变量(例如 *p),则 watch *p 和 watch p 是有区别的,前者监控的是 p 所指数据的变化情况,而后者监控的是 p 指针本身有没有改变指向;
  • 这 3 个监控命令还可以用于监控数组中元素值的变化情况,例如对于 a[10] 这个数组,watch a 表示只要 a 数组中存储的数据发生改变,程序就会停止执行。

如果读者只想学习如何使用 watch 命令,则读者这里即可。反之,如果想了解 watch 命令底层是如何实现的,可以继续往下阅读。

watch命令的实现原理

watch 命令实现监控机制的方式有 2 种,一种是为目标变量(表达式)设置硬件观察点,另一种是为目标变量(表达式)设置软件观察点。

所谓软件观点(software watchpoint),即用 watch 命令监控目标变量(表达式)后,GDB 调试器会以单步执行的方式运行程序,并且每行代码执行完毕后,都会检测该目标变量(表达式)的值是否发生改变,如果改变则程序执行停止。

可想而知,这种“实时”的判别方式,一定程度上会影响程序的执行效率。但从另一个角度看,调试程序的目的并非是为了获得运行结果,而是查找导致程序异常或 Bug 的代码,因此即便软件观察点会影响执行效率,一定程度上也是可以接受的。

所谓硬件观察点(Hardware watchpoint),和前者最大的不同是,它在实现监控机制的同时不影响程序的执行效率。简单的理解,系统会为 GDB 调试器提供少量的寄存器(例如 32 位的 Intel x86 处理器提供有 4 个调试寄存器),每个寄存器都可以作为一个观察点协助 GDB 完成监控任务。

需要注意的是,基于寄存器个数的限制,如果调试环境中设立的硬件观察点太多,则有些可能会失去作用,这种情况下,GDB 调试器会发出如下警告:

Hardware watchpoint num: Could not insert watchpoint

解决方案也很简单,就是删除或者禁用一部分硬件观察点。

除此之外,受到寄存器数量的限制,可能会出现:无法使用硬件观察点监控数据类型占用字节数较多的变量(表达式)。比如说,某些操作系统中,GDB 调试器最多只能监控 4 个字节长度的数据,这意味着 C、C++ 中 double 类型的数据是无法使用硬件观察点监测的。这种情况下,可以考虑将其换成占用字符串少的 float 类型。

目前,大多数 PowerPC 或者基于 x86 的操作系统,都支持采用硬件观点。并且 GDB 调试器在建立观察断点时,会优先尝试建立硬件观察点,只有当前环境不支持硬件观察点时,才会建立软件观察点。借助如下指令,即可强制 GDB 调试器只建立软件观察点:

set can-use-hw-watchpoints 0

注意,在执行此命令之前建立的硬件观察点,不会受此命令的影响。

注意,awatch 和 rwatch 命令只能设置硬件观察点,如果系统不支持或者借助如上命令禁用,则 GDB 调试器会打印如下信息:

Expression cannot be implemented with read/access watchpoint.

GDB catch命令:建立捕捉断点

要知道,GDB 调试器支持在被调试程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点,其中普通断点用 break 命令建立(可阅读《GDB break》一节),观察断点用 watch 命令建立(可阅读《GDB watch》一节),本节将讲解如何使用 catch 命令建立捕捉断点。

和前 2 种断点不同,普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。

用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。

建立捕捉断点的方式很简单,就是使用 catch 命令,其基本格式为:

(gdb) catch event

其中,event 参数表示要监控的具体事件。对于使用 GDB 调试 C、C++ 程序,常用的 event 事件类型如表 1 所示。

event 事件含 义
throw [exception]当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。
catch [exception]当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。
load [regexp] unload [regexp]其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。

除表中罗列的以外,event 参数还有其它一些写法,感兴趣的读者可查看 GDB官网进行了解,这里不再过多赘述。

注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:

  1. 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
  2. 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
  3. catch 无法捕获以交互方式引发的异常。

如同 break 命令和 tbreak 命令的关系一样(前者的断点是永久的,后者是一次性的),catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。

接下来就以下面的 C++ 程序为例,给大家演示 catch(tcatch)命令的用法:

#include
using namespace std;
int main(){
int num = 1;
while(num <= 5){
try{
throw 100;
}catch(int e){
num++;
cout << “next” << endl;
}
}
cout << “over” << endl;
return 0;
}

此程序存储于 ~/demo/main.cpp 文件中( ~ 表示当前登陆用户的主目录)。

在此基础上,对 main.cpp 文件进行编译,并启动对该程序的调试:

[root@bogon demo]$ ls
main.cpp
[root@bogon demo]# g++ main.cpp -o main.exe -g
[root@bogon demo]$ ls
main.cpp main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe…done.
(gdb)

通过观察程序可以看出,当前程序中通过 throw 手动抛出了 int 异常,此异常能够被 catch 成功捕获。假设我们使用 catch 命令监控:只要程序中引发 int 异常,程序就停止执行:

(gdb) catch throw int <-- 指定捕获“throw int”事件
Catchpoint 1 (throw)
(gdb) r <-- 执行程序
Starting program: ~/demo/main.exe

Catchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6 <-- 程序暂停执行
(gdb) up <-- 回到源码
#1 0x0000555555555287 in main () at main.cpp:8
8 throw 100;
(gdb) c <-- 继续执行程序
Continuing.
next

Catchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x0000555555555287 in main () at main.cpp:8
8 throw 100;
(gdb)

如上所示,借助 catch 命令设置了一个捕获断点,该断点用于监控 throw int 事件,只要发生程序就会暂停执行。由此当程序执行时,其会暂停至 libstdc++ 库中的某个位置,借助 up 指令我们可以得知该异常发生在源代码文件中的位置。

同理,我们也可以监控 main.cpp 程序中发生的 catch event 事件:

(gdb) catch catch int
Catchpoint 1 (catch)
(gdb) r
Starting program: ~/demo/main.exe

Catchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb) c
Continuing.
next

Catchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb)

甚至于,在个别场景中,还可以使用 catch 命令监控 C、C++ 程序动态库的加载和卸载。就以 main.exe 为例,其运行所需加载的动态库可以使用 ldd 命令查看,例如:

[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fffbc1ff000)
libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003e75000000)
libm.so.6 => /lib64/libm.so.6 (0x00000037eee00000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003e74c00000)
libc.so.6 => /lib64/libc.so.6 (0x00000037ee200000)
/lib64/ld-linux-x86-64.so.2 (0x00000037eda00000)

就以监控 libstdc++.so.6 为例,在 GDB 调试器中,通过执行如下指令,即可监控该动态库的加载:

(gdb) catch load libstdc++.so.6
Catchpoint 1 (load)
(gdb) r
Starting program: ~/demo/main.exe

Catchpoint 1
Inferior loaded /lib/x86_64-linux-gnu/libstdc++.so.6
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libm.so.6
0x00007ffff7fd37a5 in ?? () from /lib64/ld-linux-x86-64.so.2

以上实例仅演示了 catch 命令的用法,而 tcatch 命令的语法格式和 catch 完全相同,读者可自行尝试使用 tcatch 命令,观察它的功能。

GDB条件断点(condition命令)详解

前面章节给大家介绍了 GDB 调试器中普通断点、观察断点以及捕捉断点的功能和用法。其中值得一提的是,对于普通断点的建立,可以使用如下格式的 break 命令:

(gdb) break … if cond

… 参数用于指定生成断点的具体位置;cond 参数用于代指某个表达式。通过此方式建立的普通断点,只有当表达式 cond 成立(值为 True)时,才会发挥它的作用;反之,断点并不会使程序停止执行。

类似上面这种,以某个表达式的是否成立作为条件,从而决定自身是否生效的断点,又称为条件断点。除了普通断点外,观察断点和捕捉断点也可以成为条件断点。

需要说明的是,创建普通条件断点的方式,也同样适用于观察条件断点。通过执行如下命令,即可直接生成一个观察条件断点:

(gdb) watch expr if cond

参数 expr 表示要观察的变量或表达式;参数 cond 用于代指某个表达式。

但是,以上创建条件断点的方法,不适用于捕捉断点。换句话说,捕捉条件断点无法直接生成,需要借助 condition 命令为现有捕捉断点增添一个 cond 表达式,才能使其变成条件断点。

总的来说,借助 condition 命令,我们可以将现有的普通断点、观察断点以及捕捉断点变成条件断点;而普通条件断点和观察条件断点,可以分别通过 break if 命令和 watch if 命令直接生成。

接下来,我将给大家详细地讲解 condition 命令的用法。

GDB condition命令

严格来说,condition 命令的功能是:既可以为现有的普通断点、观察断点以及捕捉断点添加条件表达式,也可以对条件断点的条件表达式进行修改。

condition 命令没有缩写形式,使用方法很简单,语法格式如下:

(gdb) condition bnum expression
(gdb) condition bnum

参数 bnum 用于代指目标断点的编号;参数 expression 表示为断点添加或修改的条件表达式。

以上 2 种语法格式中,第 1 种用于为 bnum 编号的断点添加或修改 expression 条件表达式;第 2 种用于删除 bnum 编号断点的条件表达式,使其变成普通的无条件断点。

举个例子,这里以调试如下 C++ 程序为例:

#include
using namespace std;
int main(){
int num = 1;
while(num<20){
try{
throw num;
}catch(int &e){
num++;
}
}
cout << num << endl;
return 0;
}

程序存储位置为~/demo/main.cpp,并已经生成了可供 GDB 调试器使用的执行文件:

[root@bogon demo]# ls
main.cpp main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from ~/demo/main.exe…done.
(gdb) l
1 #include
2 using namespace std;
3 int main(){
4 int num = 1;
5 while(num<20){
6 try{
7 throw num;
8 }catch(int &e){
9 num++;
10 }
(gdb)
11 }
12 cout << num << endl;
13 return 0;
14 }
(gdb)

接下来,为读者演示 condition 命令的功能和用法:

(gdb) b 9 <–添加普通断点
Breakpoint 1 at 0x12d0: file main.cpp, line 9.
(gdb) r
Starting program: /home/test/demo/main.exe

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) rwatch num <-- 添加观察断点
Hardware read watchpoint 2: num
(gdb) catch throw int <-- 添加捕捉断点
Catchpoint 3 (throw)
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555552d0 in main() at main.cpp:9 breakpoint already hit 1 time
2 read watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb) condition 1 num3 <-- 为普通断点添加条件表达式
(gdb) condition 2 num
5 <-- 为观察断点添加条件表达式
(gdb) condition 3 num==7 <-- 为捕捉断点添加条件表达式
(gdb) c
Continuing.

Breakpoint 1, main () at main.cpp:9 <-- 普通条件断点触发
9 num++;
(gdb) p num
$1 = 3
(gdb) c
Continuing.

Hardware read watchpoint 2: num <-- 观察条件断点触发

Value = 5
0x000055555555526f in main () at main.cpp:7
7 throw num;
(gdb) c
Continuing.

Catchpoint 3 (exception thrown), 0x00007ffff7e81762 in __cxa_throw () <-- 捕捉条件断点触发
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x0000555555555285 in main () at main.cpp:7
7 throw num;
(gdb) p num
$2 = 7
(gdb)

可以看到,通过借助 condition 命令为不同类型断点设置条件表达式,只有当条件表达式成立(值为 True)时,相应的断点才会触发从而使程序暂停运行。

GDB ignore命令

ignore 命令也可以使一个断点成为条件断点,但这里的“条件”并非自定义的表达式,而仅为一个整数,它用来表示该断点失效的次数。也就会说,ignore 命令可以使目标断点暂时失去作用,当断点失效的次数超过指定次数时,断点的功能会自动恢复。

ignore 命令也没有缩写形式,其语法格式如下:

ignore bnum count

参数 bnum 为某个断点的编号;参数 count 用于指定该断点失效的次数。

仍以 main.exe 为例:

(gdb) b 9
Breakpoint 1 at 0x400a33: file main.cpp, line 9.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) p num
$1 = 1
(gdb) ignore 1 3
Will ignore next 3 crossings of breakpoint 1.
(gdb) c
Continuing.

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) p num
$2 = 5
(gdb)

可以看到,执行 ignore 命令之前,num 变量的值为 1。借助 ignore 命令使编号为 1(作用于第 9 行)的断点失效 3 次后,继续执行程序,最终程序仍暂停至第 9 行,此时 num 的值变为 5。这这恰恰证明了 num 从 1 递增至 5 的过程中,编号为 1 的断点失效了 3 次。

GDB单步调试程序

调用GDB调试器的几种方式》一节中提到,借助 next 命令可以控制 GDB 单步执行程序。所谓单步调试,就是通过一行一行的执行程序,观察整个程序的执行流程,进而尝试发现一些存在的异常或者 Bug。

根据实际场景的需要,GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB 调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。

本节就来一一给大家讲解这 3 个命令的功能和用法。讲解过程中,将以调试如下 C 语言程序为例:

#include <stdio.h>
int print(int num){
int ret = num * num;
return ret;
}
int myfunc(int num){
int i = 1;
int sum = 0;
while(i <= num){
sum += print(i);
i++;
}
return sum;
}
int main(){
int num =0;
scanf(“%d”, &num);
int result = myfunc(num);
printf(“%d”, result);
return 0;
}

此程序存储在~/demo/main.c源文件中(~ 表示当前用户的主目录),功能是根据用户输入的 num 值,输出 12+22+…+num2 的值。

GDB next 命令

next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。

next 命令可以缩写为 n 命令,使用方法也很简单,语法格式如下:

(gdb) next count

参数 count 表示单步执行多少行代码,默认为 1 行。

举个例子:

(gdb) b 16
Breakpoint 2 at 0x40058b: file main.c, line 16.
(gdb) r
Starting program: /root/demo/main.exe

Breakpoint 2, main () at main.c:16
16 int num =0;
(gdb) n 2 <-- 单步执行 2 次
3
18 int result = myfunc(num);
(gdb) n
19 printf(“%d”, result);
(gdb) n
20 return 0;
(gdb)

可以看到,当程序单步执行第 18 行时,继续执行 next 指令,下一次将要执行的是第 19 行代码,而非 myfunc() 函数内部的代码。

GDB step命令

通常情况下,step 命令和 next 命令的功能相同,都是单步执行程序。不同之处在于,当 step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。

step 命令可以缩写为 s 命令,用法和 next 命令相同,语法格式如下:

(gdb) step count

参数 count 表示一次执行的行数,默认为 1 行。

仍以 main.exe 为例:

(gdb) b 18
Breakpoint 1 at 0x4005ab: file main.c, line 18.
(gdb) r
Starting program: ~/demo/main.exe
Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step <-- step 命令进入 myfunc() 函数内部执行
myfunc (num=0) at main.c:7
7 int i = 1;

可以看到,当程序暂停到包含 mufunc() 函数的代码行处时(此时该行代码尚未执行),如果使用 step 命令,则 GDB 在执行该函数代码的同时,会进入 mufunc() 函数内部,并暂停在函数内部的第一行代码处。反之如果使用 next 命令,则程序下一次会执行第 19 行代码,而不是第 7 行,这就是它们最大的不同之处。

GDB until命令

until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:

1、(gdb) until
2、(gdb) until location

其中,参数 location 为某一行代码的行号。

不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。

以 main.exe 中 myfunc() 函数的循环为例:

(gdb) b 17
Breakpoint 1 at 0x1201: file main.c, line 17.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:17
17 scanf(“%d”, &num);
(gdb) u
3
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) u
8 int sum = 0;
(gdb) u
9 while(i <= num){
(gdb) u
10 sum += print(i);
(gdb) u
11 i++;
(gdb) u <-- 执行 i++ 操作
9 while(i <= num){
(gdb) u <-- 快速执行完循环体
13 return sum;
(gdb) p sum
$1 = 14

可以看到,这里当程序单步执行完第 11 行代码时,借助 until 命令快速执行完了整个循环体,并在第 13 行代码处停止执行。根据 p 命令输出的 num 变量的值可以确认,整个循环过程确定完整地执行完了。

until 命令还可以后跟某行代码的行号,以指示 GDB 调试器直接执行至指定位置后停止。举个例子:

(gdb) r
Starting program:~/demo/main.exe

Breakpoint 1, main () at main.c:17
17 scanf(“%d”, &num);
(gdb) until 19 <-- 执行至第 19 行停止
3
main () at main.c:19
19 printf(“%d”, result);
(gdb) p result
$3 = 14

可以看到,通过执行 until 19 命令,GDB 调试器直接从第 17 行代码处执行至指定的第 19 行。

如何使用GDB进行断点调试?

前面利用 3 节的内容,分别介绍了 GDB 调试器支持在被调试程序中打断点的 3 种方法,即 break、watch 以及 catch 命令。在此基础上,本节给大家讲解:如何借助断点对程序进行调试?

通过在程序的适当位置打断点,观察程序执行至该位置时某些变量(或表达式)的值,进而不断缩小导致程序出现异常或 Bug 的语句的搜索范围,并最终找到,整个过程就称为断点调试。

值得一提的是,整个断点调试的过程,除了要借助 break、watch 或者 catch 命令以外,还要借助其它一些命令,例如在前面章节中,我们已经使用过的 print 命令(查看变量的值)、continue 命令(使程序继续执行)等。

表 1 罗列了断点调试程序过程中,常用的一些命令以及各自的含义。

命令(缩写)功 能
run(r)启动或者重启一个程序。
list(l)显示带有行号的源码。
continue(c)让暂停的程序继续运行。
next(n)单步调试程序,即手动控制代码一行一行地执行。
step(s)如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。
until(u) until location(u location)当你厌倦了在一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。
finish(fi)结束当前正在执行的函数,并在跳出函数后暂停程序的执行。
return(return)结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。
jump(j)使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。
print(p)打印指定变量的值。
quit(q)退出 GDB 调试器。

其中,next、step 以及 until 命令,已经在 《GDB单步调试》一节中做了详细介绍,本节不再赘述。另外,表 1 中罗列的这些命令读者无需死记硬背,因为实际使用 GDB 调试器时,它们都会常常用到,熟能生巧。

为了搞清楚表 1 中这些命令的功能和用法,这里仍以上节创建的 C 语言程序为例:

#include <stdio.h>
int print(int num){
int ret = num * num;
return ret;
}
int myfunc(int num){
int i = 1;
int sum = 0;
while(i <= num){
sum += print(i);
i++;
}
return sum;
}
int main(){
int num =0;
scanf(“%d”, &num);
int result = myfunc(num);
printf(“%d”, result);
return 0;
}

此程序存储在~/demo/main.c源文件中(~ 表示当前用户的主目录)。

  1. 首先,表 1 中 run、continue、list、next、print 以及 quit 命令的用法都非常简单,唯一需要注意的一点是,run 命令除了可以启动程序的执行,还可以在任何时候重新启动程序。

例如,以 main.c 为例:

[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe…
(gdb) l <-- list 命令的缩写,罗列出带有行号的源码
1 #include <stdio.h>
2 int print(int num){
3 int ret = num * num;
4 return ret;

15 int main(){
16 int num =0;
17 scanf(“%d”, &num);
18 int result = myfunc(num);
19 printf(“%d”, result);
20 return 0;
(gdb)
21 }
(gdb) b 16 <-- break 命令的缩写,在程序第 16 行处打普通断点
Breakpoint 1 at 0x11fa: file main.c, line 16.
(gdb) r <-- run 命令的缩写,启动程序
Starting program: /home/test/demo/main.exe
Breakpoint 1, main () at main.c:16
16 int num =0;
(gdb) next <-- 单步执行程序,即执行一行代码
17 scanf(“%d”, &num);
(gdb) next <-- 继续单步执行,此时需要输入一个整数传递给 num
3
18 int result = myfunc(num);
(gdb) p num <-- print 命令的缩写,显示 num 的值
$1 = 3 <-- $1 表示 num 变量所在存储区的名称,这里指的是 num 的值为 3
(gdb) n <-- 继续单步执行
19 printf(“%d”, result);
(gdb) n <-- 单步执行
20 return 0;
(gdb) q <-- 退出调试
A debugging session is active.

Inferior 1 [process 4576] will be killed.

Quit anyway? (y or n) y <-- 由于程序执行尚未结束,GDB 会进行再次确认
[root@bogon demo]#

事实上,以上很多命令还有其它的语法格式,只是不常用,这里不再过多赘述,感兴趣的读者可自行查阅 GDB 官网手册

接下来,重点给大家介绍表 1 中另外几个命令的用法。

GDB finish和return命令

实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令。与 finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。

finish 命令和 return 命令的区别是,finish 命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。

仍以 main.exe 为例,如下演示了 finish 命令的用法:

(gdb) b 18
Breakpoint 1 at 0x4005ab: file main.c, line 18.
(gdb) r
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) finish
Run till exit from #0 myfunc (num=3) at main.c:9
0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
Value returned is $1 = 14
(gdb)

可以看到,当程序运行至第 9 行处使用 finish 命令,GDB 调试器会执行完 myfunc() 函数中的剩余代码,并在执行完函数后停止。接下来重新执行程序,观察 return 命令的功能:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) return 5
Make myfunc return now? (y or n) y
#0 0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
(gdb) n
19 printf(“%d”, result);
(gdb) p result
$3 = 5

可以看到,同样程序执行至第 9 行,借助 return 命令会立即终止执行 myfunc() 函数,同时手动指定该函数的返回值为 5。因此,最终 result 变量的值为 5,而不再是 14。

GDB jump命令

jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:

(gdb) jump location

其中,location 通常为某一行代码的行号。

也就是说,jump 命令可以略过某些代码,直接跳到 location 处的代码继续执行程序。这意味着,如果你跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump 跳转到的位置后续没有断点,那么 GDB 会直接执行自跳转处开始的后续代码。

举个例子:

(gdb) b 16
Breakpoint 1 at 0x40058b: file main.c, line 16.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:16
16 int num = 0;
(gdb) jump 19
Continuing at 0x4005b8.
0
Program exited normally.

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
有 return 命令,它们都可以结束当前执行的函数。

finish 命令和 return 命令的区别是,finish 命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。

仍以 main.exe 为例,如下演示了 finish 命令的用法:

(gdb) b 18
Breakpoint 1 at 0x4005ab: file main.c, line 18.
(gdb) r
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) finish
Run till exit from #0 myfunc (num=3) at main.c:9
0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
Value returned is $1 = 14
(gdb)

可以看到,当程序运行至第 9 行处使用 finish 命令,GDB 调试器会执行完 myfunc() 函数中的剩余代码,并在执行完函数后停止。接下来重新执行程序,观察 return 命令的功能:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) return 5
Make myfunc return now? (y or n) y
#0 0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
(gdb) n
19 printf(“%d”, result);
(gdb) p result
$3 = 5

可以看到,同样程序执行至第 9 行,借助 return 命令会立即终止执行 myfunc() 函数,同时手动指定该函数的返回值为 5。因此,最终 result 变量的值为 5,而不再是 14。

GDB jump命令

jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:

(gdb) jump location

其中,location 通常为某一行代码的行号。

也就是说,jump 命令可以略过某些代码,直接跳到 location 处的代码继续执行程序。这意味着,如果你跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump 跳转到的位置后续没有断点,那么 GDB 会直接执行自跳转处开始的后续代码。

举个例子:

(gdb) b 16
Breakpoint 1 at 0x40058b: file main.c, line 16.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:16
16 int num = 0;
(gdb) jump 19
Continuing at 0x4005b8.
0
Program exited normally.

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-lXmPDyEd-1713287752276)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值