原址
v2-ee61d20f0a4ad8e97432d71b3dc6c91d_b.jpg
lQLPJxZoswpqXSDNAr7NAtCwwBFomvz_WKkCrJsTu8DnAA_720_702.png

GCC 简介

在为 Linux 开发应用程序时,绝大多数情况下使用的都是 C 语言,因此几乎每一位 Linux 程序员面临的首要问题都是如何灵活运用 C 编译器。目前 Linux 下最常用的 C 语言编译器是 GCC。GCC 编译器是 GNU 开源组织发布的 UNIX/Linux 下功能强大、性能优越的多平台编译器,它可以将 C、C++ 等多种语言编写的源程序编译、链接成可执行文件。而 GDB 是 GNU 推出的功能强大的程序调试器,可以说 GCC 与 GDB 是在 Linux 环境下进行 C 程序开发不可或缺的工具,也是 Linux 程序员必须掌握的技能之一。本实验我们将对 GCC 进行简单的介绍,学习如何初步使用 GCC。

GCC 发展历史

GNU 计划是由理查德·斯托曼在 1983 年 9 月 27 日公开发起的自由软件集体协作计划,主要目的是创建一个名叫 GNU’s Not Unix(GNU) 的完全免费的操作系统。该操作系统将包括绝大多数自由软件基金会所开发的其他软件,以对抗所有商业软件,而这个操作系统的核心(kernel)就叫 HURD。

但是 GNU 在开发完全免费的操作系统上并未取得成功,直到 20 世纪 90 年代由林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds)开发了 Linux 操作系统,GNU 才算在免费操作系统上完成了任务。虽然 GNU 计划在开发免费操作系统上不成功,但是却成功开发几个广为流传的 GNU 软件,其中最著名的是 GNU C Complier(GCC)。
GCC 这个软件成为历史上最优秀的 C 语言编译器, 其执行效率与一般的编译器相比平均效率要高 20%~30%,使得那些靠贩卖编译器的公司大吃苦头,因为它们无法研制出与 GCC 同样优秀,却又完全免费、并开放源代码的编译器来。它还有一个十分特殊而且不同寻常的意义:几乎所有的自由软件都是通过它编译的。可以说,它是自由软件发展的基石与标杆。

现在,GCC 已经可以支持 7 种编程语言和 30 种编程结构,是学术界最受欢迎的编译工具。其他 GNU 软件还包括 GNU emacs、GNU Debugger(GDB)、GNU Bash 以及大部分 Linux 系统的程序库和工具等。目前,GCC 已发展到了 8.x 的版本,几乎所有开源软件和自由软件中都会用到,因此它的编译性能会直接影响到 Linux、Firefox、OpenOffice.org、Apache 以及一些数不清的小项目的开发。GCC 无疑处在开源软件的核心地位。

作为自由软件的旗舰项目,Richard Stallman 在十多年前刚开始写作 GCC 的时候,还只是把它当作一个 C 程序语言的编译器。GCC 的意思也只是 GNU C Compiler 而已。经过这么多年的发展,GCC 已经不仅仅能支持 C 语言,它现在还支持 Ada、C++、Java、Objective-C、Pascal、COBOL 以及函数式编程和逻辑编程的 Mercury 语言等。因此,现在的 GCC 已经变成了 GNU Compiler Collection,也即是 GNU 编译器套件的意思了。这个名称同时也说明了 GCC 对于各种硬件平台无所不在的支持,甚至包括一些生僻的硬件平台。

GCC 不仅功能非常强大,结构也异常灵活。最值得称道的一点就是,它可以通过不同的前端模块来支持各种语言,如 Java、Fortran、Pascal、Modula-3 和 Ada 语言等。总而言之,GUN 虽然没有开发出操作系统,但是却开发出了很多系统级的自由软件,GCC 就是其中之一。

GCC 编译器简介

GCC 可以编译如 C、C++、Object C、Java、Fortran、Pascal、Modula-3、和 Ada 等多种语言,而且 GCC 又是一个交叉平台编译器,它能够在当前 CPU 平台上为多种不同体系结构的硬件平台开发软件,因此尤其适合在嵌入式软件领域的开发编译。

在使用 GCC 编译程序时,编译过程可以被细分为四个阶段:

  • 预处理(Pre-Processing)
  • 编译(Compiling)
  • 汇编(Assembing)
  • 链接(Linking)

GCC 提供了 30 多条警告信息和三个警告级别,使用它们有助于增强程序的稳定性和可移植性,此外,GCC 还对标准的 C 和 C++语言进行了大量的扩展,提高程序的执行效率,有助于编译器进行代码优化,能够减轻编程的工作量。GCC 通过文件后缀名来区别输入文件的类别。

初步使用 GCC

GCC 仅仅是一个编译器,没有界面,必须在命令行模式下使用。通过gcc命令就可以将源文件编译成可执行文件。GCC 既可以一次性完成 C 语言源文件的编译,也可以分步骤完成,我们先完整演示如何一次性完成源文件的编译,让大家对 GCC 的使用有一个初步的了解。

以下面简单的 C 语言程序为例,打开 Sublime Text 输入代码,保存到桌面上,命名为 demo.c

  1. #include <stdio.h>
  2. int main()
  3. {
  4. puts("shiyanlou");
  5. return 0;
  6. }

GDB 基础入门实战 - 图3

GDB 基础入门实战 - 图4

生成可执行程序

在 Linux 下编译 C 语言程序,一般的 Linux 发行版都内建的有 GCC,无需用户再自行安装。打开 Xfce 终端,使用以下命令查看 GCC 版本:

  1. $ gcc -v

GDB 基础入门实战 - 图5
输入以下命令,生成可执行程序文件:

  1. $ cd Desktop #进入源文件所在目录
  2. $ gcc demo.c #在 gcc 命令后面紧跟源文件

我们能够发现在桌面上多了一个名为 a.out 的文件,这就是最终生成的可执行文件。
GDB 基础入门实战 - 图6
这样我们就完成了编译和链接的过程。值得一提的是,Linux 不像 Windows 那样以文件后缀来区分可执行文件,Linux 下的可执行文件后缀理论上可以是任意的,这里的 .out 只是用来表明它是 GCC 的输出文件。不管源文件的名字是什么,GCC 生成的可执行文件的默认名字始终是 a.out
如果不想使用默认的文件名,那么可以通过 -o 选项来自定义文件名:

  1. $ gcc demo.c -o demo.out

这样生成的可执行程序的名字就是 demo.out。可执行文件也可以不带后缀,因为 Linux 下可执行文件的后缀仅仅是一种形式上的:

  1. $ gcc demo.c -o demo

这样生成的可执行程序的名字就是 demo
通过 -o 选项也可以将可执行文件输出到其他目录,并不一定非得在当前目录下,例如:

  1. $ gcc democ -o ../demo.out

表示将可执行文件输出到上一层目录,并命名为 demo.out../ 表示上一层目录,如果不写,默认也是当前目录。这里的上一层目录是 shiyanlou,打开该目录能够看到生成的可执行文件 demo.out
GDB 基础入门实战 - 图7

运行可执行程序

既然我们已经生成了可执行程序,现在我们来学着如何运行它。执行以下命令:

  1. $ ./a.out

./ 表示当前目录,整条命令的意思是运行当前目录下的 a.out 程序。如果不写 ./,Linux 会到系统路径下查找 a.out,而系统路径下显然不存在这个程序,所以会运行失败。
GDB 基础入门实战 - 图8

注意:如果程序没有执行权限,可以使用 sudo chmod 777 a.out 命令来增加权限。

GCC 编译过程

上一实验我们学习了如何使用 GCC 一次性完成编译和链接的整个过程,一般来说我们在学习 C 语言过程中都这么做,因为这样最方便。从程序员的角度看,只需简单地执行一条 GCC 命令就可以了,但从编译器的角度来看,却需要完成一系列非常繁杂的工作。为了更好地理解 GCC 的工作过程,我们可以把 GCC 编译过程分成几个步骤单独进行,并观察每一步的运行结果。

预处理阶段

GCC 预处理阶段第一个任务是头文件展开,例如一开始的 #include <stdio.h>,那么预处理阶段就会把这个 stdio.h 文件加载到你的 .c 中去。

第二个任务就是宏定义和条件编译处理,ANSI 标准可用的预处理宏定义和条件编译命令主要有这些:

  1. #define
  2. #error
  3. #include
  4. #if
  5. #else
  6. #endif
  7. #ifdef
  8. #ifndef
  9. #undef
  10. #line
  11. #pragma
  12. ...

该阶段会生成一个中间文件 *.i,但实际工作中通常不会专门生成这种文件,基本上用不到,如果非要生成这种文件,可以使用 -E 参数让 GCC 在预处理结束后停止编译过程。

我们还是使用上一节实验的 C 语言源程序并以 demo.c 命名保存在桌面:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. puts("shiyanlou");
  5. return 0;
  6. }

打开终端,执行以下命令,让 GCC 在预处理结束后停止编译过程。

  1. $ cd Desktop
  2. $ gcc -E demo.c -o demo.i

此时桌面上就会生成 demo.i 文件。
GDB 基础入门实战 - 图9

编译阶段

在编译阶段,GCC 把预处理后的结果编译成汇编或者目标模块。输入的是中间文件 *.i,编译后生成汇编语言文件 *.S,这个阶段对应的 GCC 命令如下:

  1. $ gcc -S demo.i -o demo.S

此时桌面上就会生成 demo.S 文件。
GDB 基础入门实战 - 图10

汇编阶段

在汇编阶段,编译器把编译出来的结果汇编成具体 CPU 上的目标代码模块。输入汇编文件 *.S,输出机器语言 *.O。这个阶段可以通过使用参数 -C 来完成。

  1. $ gcc -C demo.S -o demo.o

GDB 基础入门实战 - 图11

链接阶段

链接阶段把多个目标代码模块连接生成一个大的目标模块,输入机器代码文件 *.o,汇集成一个可执行的二进制代码文件。

这一步骤可以通过以下命令完成:

  1. $ gcc demo.o -o demo

或者

  1. $ gcc demo.c -o demo (推荐)

在线上环境里执行 gcc demo.o -o demo 命令时会发生如下报错:
GDB 基础入门实战 - 图12
这里的 GCC 包中的 crt1.o 功能简单理解为:crt 是 libc 的基本包之一,它提供访问计算机的基本功能,包含了像 printfputs 等方法,这就是为什么它经常包含在最基本的 C 应用程序中。

在这里因为线上环境版本问题会有报错,所以我们执行第二条命令 gcc demo.c -o demo,这并不影响我们的学习,因为在本小节我们需要理解的是 GCC 编译过程以及它的基础原理,在平常使用中我们并不会像这样分步地对程序进行编译。
GDB 基础入门实战 - 图13#

GCC 警告提示和优化

本小节主要学习 GCC 的警告提示和代码优化功能。GCC 包含完整的出错检查和警告提示功能,它们可以帮助 Linux 程序员写出更加专业和优美的代码。
我们千万不能小瞧这两个功能,在很多情况下,含有警告信息的代码往往会有意想不到的运行结果。代码优化则能通过编译器分析源码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行功能。GCC 提供的警告提示和代码优化功能十分强大,我们接下来就对此进行介绍。

GCC 警告提示功能

我们先来看一段能让 GCC 产生警告的代码,将这一段代码保存到桌面上。

  1. //demo.c
  2. #include <stdio.h>
  3. void main(void)
  4. {
  5. long long int var = 2020;
  6. printf("This is a bad code!\n");
  7. }

这段代码有以下问题:

  • main 函数的返回值被声明为 void,但实际上应该是 int
  • 使用了 GNU 语法扩展,即使用 long long 来声明 64 位整数,不符合 ANSI/ISO C 语言标准。
  • main 函数在终止前没有调用 return 语句。

下面来看看 GCC 是如何帮助程序员来发现这些错误的,当 GCC 在编译不符合 ANSI/ISO C 语言标准的源代码时,如果加上了 -pedantic 选项,那么使用了扩展语法的地方将产生相应的警告信息。
打开终端,执行以下命令:

  1. $ gcc -pedantic demo.c -o demo

GDB 基础入门实战 - 图14
现在我们对上面的程序进行修改,将 main 函数的返回值声明为 intvar 变量定义为长整形(long int),在 main 函数的最后增加返回语句return 0; ,如下所示:

  1. #include <stdio.h>
  2. int main(void)
  3. {
  4. long int var = 2020;
  5. printf("This is a bad code!\n");
  6. return 0;
  7. }

然后再次使用 GCC 的 -pedantic 进行编译。
这个时候可能就有人有疑问了,明明有三个错误,为什么终端提示只有一个呢?需要注意的是,-pedantic 编译选项并不能保证被编译程序与 ANSI/ISO C 语言标准完全兼容,它仅仅只能用来帮助程序员离这个目标越来越近。换句话说,-pedantic 选项能够帮助大家发现一些不符合 ANSI/ISO C 语言标准的代码,但不是全部。事实上只有 ANSI/ISO C 语言标准中要求进行编译器诊断的那些问题才有可能被 GCC 发现并提出警告。
除了-pedantic 之外,GCC 还有一些其他编译选项也能够产生有用的警告信息。这些选项大多以 -W 开头,其中最有价值的当数 -Wall 了,使用它能使 GCC 阐释尽可能多的警告信息:

  1. $ gcc -Wall demo.c -o demo

GDB 基础入门实战 - 图15
GCC 给出的警告信息虽然从严格意义上说不能算作是错误,但很可能错误就藏身于这些地方。我们应该避免产生警告信息,所以我建议在用 GCC 编译源代码时始终带上 -Wall 选项,这对找出常见的隐式编程错误很有帮助。

GCC 代码优化

代码优化是指编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。GCC 提供的代码优化功能非常强大,它通过编译选项 -On 来控制优化代码的生成,其中 n 是一个代表优化级别的整数。对于不同版本的 GCC 来讲,n 的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从 0 变化到 23

编译时使用选项 -O 可以告诉 GCC 同时减小目标代码的长度和执行时间,其效果等价于 -O1。选项 -O2 告诉 GCC 除了完成 -O1 级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项 -O3 则除了完成 -O2 级别的优化之外,还包括循环展开和其他一些与处理器特性相关的优化工作。通常来说,数字越大优化的等级越高,同时也意味着程序的运行速度越快。许多程序员喜欢使用 -O2 选项,因为它在优化长度、编译时间和代码大小之间取得了一个比较理想的平衡点。

现在我们来看一段效率很低的代码:

  1. #include<stdio.h>
  2. int main(void)
  3. {
  4. double counter;
  5. double result;
  6. double temp;
  7. for(counter = 0; counter < 2020.0*2020.0*2020.0/20.0+2020; counter += (5-1)/4)
  8. {
  9. temp = counter / 1979;
  10. result = counter;
  11. }
  12. printf("Result is %lf\n", result);
  13. return 0;
  14. }

在终端输入以下命令查看运行时间:

  1. $ gcc demo.c -o demo #不加任何优化选项进行编译
  2. $ time ./demo #借助Linux提供的time命令,统计出改程序在运行时所需的时间

GDB 基础入门实战 - 图16

可以明显感受到不加任何优化选项进行编译时运行的缓慢,现在我们使用优化选项 -O 来对代码进行优化处理:

  1. $ gcc -O demo.c -o demo
  2. $ time ./demo

GDB 基础入门实战 - 图17

从运行结果可以看出,我们使用优化选项来对代码进行优化处理后,程序的性能得到了很大幅度的改善,从原来将近 6s 降低到 1s。如果我们使用优化的等级更高的选项 -O2-O3
GDB 基础入门实战 - 图18
你会发现-O2-O3-O 的效果相差并不大,这是代表他们没有任何区别吗?并不是。因为我们在这里只是一个很简单的程序,如果在大型的项目中,数字越大则优化的等级越高。

尽管 GCC 代码优化功能很强大,但是我们仍然要要求能写出高质量代码,这样编译器就不会做更多的工作。优化虽然能给程序带来更好的执行性能,但在一些场合中应避免优化代码:

  • 程序开发的时候:优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化。
  • 资源受限的时候:一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果。
  • 跟踪调试的时候:在对代码进行优化的时候,某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。

GCC 常用选项

学习到这里,GCC 的使用我们就告一段落了,下一节实验将会开始 GDB 的学习之旅。通过之前的学习,我们学习了一些 GCC 的选项,GCC 是一个功能强大的编译器,其编译选项非常多,有些选项通常不会用到,因此将所有的编译选项全部列出讲解是不明智的。下面只对一些 GCC 编译器的常用选项进行讲解,这些选项在实际编程过程中非常实用。GCC 的常用选项如下表所示。

选项名 作用
-c 通知 GCC 取消连接步骤,即编译源码并在最后生成目标文件。
-Dmacro 定义指定的宏,使它能够通过源码中的 ``#ifdef 进行检验。
-E 不经过编译预处理程序的输出而输送至标准输出。
-g3 获得有关调试程序的详细信息,它不能与 ``-o 选项联合使用。
-Idirectory 在包含文件搜索路径的起点处添加指定目录。
-llibrary 提示连接程序在创建最终可执行文件时包含指定的库。
-O -O2 -O3 将优化状态打开,该选项不能与 ``-g 选项联合使用。当出现多个优化时,以最后一个为准。
-O0 关闭所有优化选项。
-S 要求编译程序生成来自源代码的汇编程序输出。
-v 启动所有警报。
.h 预处理文件(标头文件)。
-Wall 在发生警报时取消编译操作,即将警报看作是错误。
-w 禁止所有的报警。
-share 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库。
-shared 产生共享对象文件。
-g 在编译结果中加入调试信息。
-ggdb 加入 GDB 调试器能识别的格式。

GDB 简介

即便对于一个经验丰富的程序员,在编写程序的时候也避免不了出错。有的时候我们发现,代码能够编译通过,并且没有语法错误,但是运行结果却不对,就算反复检查了很多遍,依然不知道哪里出了问题。这个时候,就需要调试程序了。

一个功能强大的调试器不仅为程序员提供了跟踪程序执行的手段,而且还可以帮助程序员找到解决问题的方法。本小节开始我们将开始学习 GDB 调试器,Linux 下的 GDB 是一个用来调试 C、C++ 程序的功能强大的调试器,它能够在程序运行的过程中观察程序的 内部结构和内存使用情况,使用 GDB 来跟踪程序中的错误从而减少程序员的工作量。

GDB 概述

GDB 是 GNU 开源组织发布的一个强大的 UNIX 下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像 VC、BCB 等 IDE 的调试,但如果你是在 UNIX 平台下做软件,你会发现 GDB 这个调试工具有比 VC、BCB 的图形化调试器更强大的功能。

一般来说,GDB 主要帮忙你完成下面几个方面的功能:

  • 设置断点(断电可以是条件表达式),使程序在指定的代码行上暂停执行,便于观察。
  • 单步执行程序,便于调试。
  • 查看程序中变量值得变化。
  • 动态改变程序的执行环境。
  • 分析崩溃程序产生的 core 文件。

在终端输入以下命令安装 GDB:

  1. sudo apt-get update
  2. sudo apt-get install gdb

GDB 基础入门实战 - 图19

GDB 是一个命令行方式的调试工具,它不同于我们在 Windows 下常见的 Turbo C、VC 等图形化程序开发工具。GDB 使用非常简单,只要在 Linux 的命令行提示符下输入 gdb,系统便会启动 GDB:
GDB 基础入门实战 - 图20

想要退出可以输入 quit 命令。我们也可以在 gdb 后面给出文件名,直接指定想要调试的程序,GDB 就会自动调用这个可执行文件进行调试,命令形式如下:

  1. $ gdb filename

告诉 GDB 装入名为 filename 的可执行文件进行调试。
另外,为了使 GDB 正常工作,必须使程序在编译的时候包含调试信息,这需要在 GCC 编译时加上 -g 或者 -ggbb 选项。调试信息包好了程序中的每个变量的类型和在可执行文件中的地址映射以及源代码的行号。而 GDB 正是利用这些信息是源代码和机器码相关联。

GDB 常用命令

GDB 支持很多的命令使用户能实现不同的功能,有简单的文件装入命令,有允许程序员检查所调用的堆栈内容的复杂命令,为方便本节后续内容的讲解和方便学员查阅,这里先将 GDB 常用命令列出:

命令 含义描述
file 装入想要的调试的可执行文件。
run 执行当前被调试的程序。
kill 终止正在调试的程序。
step 执行一行源代码而且进入函数内部。
next 执行一行源代码但不进入函数内部。
break 在代码里设置断点,这将使程序执行到这里时被挂起。
print 打印表达式或变量的值,或打印内存中某个变量开始的一段连续区域的值,还以用来对变量进行赋值。
display 设置自动显示的表达式或变量,当程序停住或在单步追踪时,这些变量会自动显示其当前值。
list 列出产生执行文件的源代码的一部分。
quit 退出 GDB。
watch 使你能监视一个变量的值而不管它何时被改变。
backtrace(或 bt) 回溯追踪。
frame n 定位到发生错误的代码段,n 为backtrace 命令的输出结果中的行号。
examine 查看内存地址中的值。
jump 是程序跳转执行。
signal 产生信号量。
return 强制函数返回。
call 强制调用函数。
make 使用户不退出 GDB 就可以重新产生可执行文件。
shell 使用户不离开 GDB 就执行 Linux 的 shell 命令。

GDB 调试初步

我们先通过一个具体的简单实例来向大家介绍如何使用 GDB 调试器来分析程序中的错误,帮助学员快速入门。下面是一段 C 语言代码:

  1. #include <stdio.h>
  2. int main(void){
  3. int input = 0;
  4. printf("input an interger:\n");
  5. scanf("%d",input);
  6. printf("the interger you input is%d\n",input);
  7. return 0;
  8. }

我们先把这段代码保存在桌面上,命名为 demo.c
GDB 基础入门实战 - 图21
然后使用 GCC 编译 demo.c,并且加上 -ggdb 调试选项:

  1. $ cd Desktop
  2. $ ls #查看当前文件
  3. $ gcc -ggdb demo.c -o demo

GDB 基础入门实战 - 图22
为了更快的发现错误的所在,我们使用 GDB 进行一个简单的调试:
GDB 基础入门实战 - 图23
当出现提示符(gdb)的时候,表示调试器已经做好了准备可以进行调试了,现在可以通过 run 命令来让程序开始在 GDB 的监控下运行。
GDB 基础入门实战 - 图24
分析一下 GDB 给出的输出结果,不难看出,程序是由于段错误而导致异常中止的,说明内存操作出了问题,具体发生问题的地方是在调用 _IO_vfscanf——internal() 的时候。为了得到更有价值的信息,可以使用 GDB 提供的溯源跟踪命令即 backtrace 命令,执行结果如下:
GDB 基础入门实战 - 图25
通过以上的信息不难看出 GDB 已经将错误信息定位到了第五行,现在可以仔细检查一下了。
GDB 基础入门实战 - 图26
使用 GDB 提供的 frame 命令可以定位到发生错误代码的代码段,该命令后面跟着的数值是在 backtrace 中找到的行号。
GDB 基础入门实战 - 图27
通过上面的调试我们可以确定发生错误的信息是 input,之后我们将 input 改为 &input 即可。
修改后退出 GDB,然后执行以下命令重新编译并运行:

  1. $ gcc -ggdb demo.c -o demo
  2. $ ./demo

GDB 基础入门实战 - 图28
至此,一个简易的调试完成,可能你暂时没有感受的 GDB 带来的便利,因为我们只是对 GDB 的使用有一个初步的认识,下一节开始我们将对其进行详细的介绍。

GDB 常用命令详解

GDB 的命令有很多,善于使用 GDB 命令能够提升我们调试的效率。本小节我们将对 GDB 常用命令进行详细的介绍,让学员学会如何使用这些命令以及在什么场景使用它们是本实验的核心内容。

代码实例

将以下代码保存在桌面上,命名为 demo.c

  1. #include<stdio.h>
  2. int add(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. int sum[10] ={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  9. int i;
  10. int array1[10] ={11, 22, 33, 44, 55, 66, 77, 88, 99, 00};
  11. int array2[10] ={1, 2, 3, 4, 5, 4, 3, 2, 1, 0};
  12. for (i = 0; i < 10; i++)
  13. {
  14. sum[i] = add(array1[i], array2[i]);
  15. }
  16. }

使用命令 gcc –ggdb demo.c –o demo 编译上述程序,得到包含调试信息的二进制文件 demo,执行 gdb demo 命令进入调试状态:
GDB 基础入门实战 - 图29

list 命令

在 GDB 中运行 list 命令(缩写 l)可以列出代码,list 的具体形式包括:

  • list <linenum> :显示程序第 linenum 行周围的 10 行代码,如:

    1. list 10

    GDB 基础入门实战 - 图30

  • list <function>:显示函数名为 function 的函数周围的 10 行,如:

    1. list main
    2. list add

    GDB 基础入门实战 - 图31

  • list:输出从上次调用 list 命令开始往后的 10 行程序代码(如果不够 10 行则会输出到最后)。

    1. list

    GDB 基础入门实战 - 图32

  • list -:输出从上次调用 list 命令开始往前的 10 行程序代码。

    1. list 14
    2. list -

    GDB 基础入门实战 - 图33

    run 命令

    将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

    1. #include <stdio.h>
    2. /* 函数声明 */
    3. int max(int num1, int num2);
    4. int main ()
    5. {
    6. /* 局部变量定义 */
    7. int a,b,ret;
    8. scanf("%d",&a);
    9. scanf("%d",&b);
    10. /* 调用函数来获取最大值 */
    11. ret = max(a, b);
    12. printf( "Max value is : %d\n", ret );
    13. return 0;
    14. }
    15. /* 函数返回两个数中较大的那个数 */
    16. int max(int num1, int num2)
    17. {
    18. /* 局部变量声明 */
    19. int result;
    20. if (num1 > num2)
    21. result = num1;
    22. else
    23. result = num2;
    24. return result;
    25. }

这个时候我们执行 run 命令运行程序:
GDB 基础入门实战 - 图34
出现空白后,依次输入参数并回车。

break 命令

添加断点

在 GDB 中用 break 命令来设置断点,设置断点的方法包括:

  • break <function> :在进入指定函数时停住。
  • break <linenum> :在指定行号停住。
  • break +offset/break -offset :在当前行号的前面或后面的 offset 行停住,offset 为自然数。
  • break filename:linenum :在源文件 filenamelinenum 行处停住。
  • break *address :在程序运行的内存地址处停住。
  • breakbreak命令没有参数时,表示在下一条指令处停住。
  • break ... if <condition> :“…” 可以是上述的 breakbreak +offset / break –offset 中的参数,condition 表示条件,在条件成立时停住。比如在循环体中,可以设置 break if i=100,表示当 i 为 100 时停住程序。

这里使用 run 命令的 C 程序代码,没有保存的可以将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

  1. #include <stdio.h>
  2. /* 函数声明 */
  3. int max(int num1, int num2);
  4. int main ()
  5. {
  6. /* 局部变量定义 */
  7. int a,b,ret;
  8. scanf("%d",&a);
  9. scanf("%d",&b);
  10. /* 调用函数来获取最大值 */
  11. ret = max(a, b);
  12. printf( "Max value is : %d\n", ret );
  13. return 0;
  14. }
  15. /* 函数返回两个数中较大的那个数 */
  16. int max(int num1, int num2)
  17. {
  18. /* 局部变量声明 */
  19. int result;
  20. if (num1 > num2)
  21. result = num1;
  22. else
  23. result = num2;
  24. return result;
  25. }

这里我们只介绍两种使用最多的断点调试,其他的请学员自行动手学习。首先是 break <function> ,指定想要断点的函数,在进入指定函数时停住。输入 run 命令运行,依次写入参数,程序会在运行到指定函数时停住。想要执行下一步,可以输入 next 命令,其简写为 n
GDB 基础入门实战 - 图35
然后我们介绍 break <linenum>,在指定行数停住。
GDB 基础入门实战 - 图36

查看断点

在 GDB 下查看断点使用命令 info break 或者简写 i b
GDB 基础入门实战 - 图37
打印出了刚刚添加的 max 函数的断点信息:编号、类型、显示状态、是否启用、地址、其他信息。

删除断点

在 GDB 上删除断点使用命令 delet 断点Num,简写 d 断点num
GDB 基础入门实战 - 图38
删除后再次查看断点,提示当前没有断点,即删除成功。

禁用断点

在 GDB 下禁用断点使用命令 disable num
GDB 基础入门实战 - 图39
可以看到字段 Enb 已经变为 n 了,表示这个断点已经被禁用。

info 命令

info 命令可以在调试时用来查看寄存器、断点、观察点和信号等信息,常用命令如下:

  • info registers :查看除了浮点寄存器以外的寄存器。
  • info all-registers :查看所有寄存器,包括浮点寄存器。
  • info registers :查看所指定的寄存器。
  • info break 查看断点信息。
  • info watchpoints 列出当前所设置的所有观察点。
  • info line 命令来查看源代码在内存中的地址(后面可以跟行号、函数名、文件名:行号、文件名:函数名等多种形式)。
  • info threads 可以看多线程。

其他关于 info 的命令可以输入 info 进行查看:
GDB 基础入门实战 - 图40

单步命令

在调试过程中,next 命令用于单步执行。next 的单步执行不会进入函数的内部,与 next 对应的 step(缩写 s)命令则在单步执行一个函数时,会进入其内部。

  1. #include<stdio.h>
  2. int add(int a, int b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. int sum[10] ={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  9. int i;
  10. int array1[10] ={11, 22, 33, 44, 55, 66, 77, 88, 99, 00};
  11. int array2[10] ={1, 2, 3, 4, 5, 4, 3, 2, 1, 0};
  12. for (i = 0; i < 10; i++)
  13. {
  14. sum[i] = add(array1[i], array2[i]);
  15. }
  16. }

下面演示了 step 命令的执行情况,在 15 行的 add() 函数调用处执行 step 会进入其内部的 return a+b; 语句:
GDB 基础入门实战 - 图41
然后我们来看与 next 命令的区别:
GDB 基础入门实战 - 图42

next 的单步执行不会进入函数的内部。单步执行的更复杂用法有以下几种,学员们可以自行尝试。

  • step <count>:单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有 debug 信息)。step 后面不加 count 表示一条条地执行,加表示执行后面的 count 条指令,然后再停住。
  • next <count>:单步跟踪,如果有函数调用,它不会进入该函数。同样地,next 后面不加 count 表示一条条地执行,加表示执行后面的 count 条指令,然后再停住。
  • set step-modeset step-mode on 用于打开 step-mode 模式,这样,在进行单步跟踪时,程序不会因为没有 debug 信息而不停住,这个参数的设置可便于查看机器码。set step-mod off 用于关闭 step-mode 模式。
  • finish:运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
  • until:一直在循环体内执行单步,退不出来是一件令人烦恼的事情,until 命令可以运行程序直到退出循环体。

continue 命令

当程序被停住后,可以使用 continue 命令(缩写 c)恢复程序的运行直到程序结束,或到达下一个断点。
将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. int a=0;
  5. for(int i=0; i<10; i++)
  6. a+=i;
  7. }

为了方便理解,我们搭配着 watch 命令进行使用,在使用 watch 时步骤如下:

  1. 使用 break 命令在要观察的变量所在处设置断点。
  2. 使用 run 命令执行,直到断点。
  3. 使用 watch 命令设置观察点。
  4. 使用 continue 命令观察设置的观察点是否有变化。

GDB 基础入门实战 - 图43

print 命令

在调试程序时,当程序被停住时,可以使用 print 命令(缩写为 p),print 的输出格式包括:

  • x :按十六进制格式显示变量。
  • d :按十进制格式显示变量。
  • u :按十六进制格式显示无符号整型。
  • o :按八进制格式显示变量。
  • t :按二进制格式显示变量。
  • a :按十六进制格式显示变量。
  • c :按字符格式显示变量。
  • f :按浮点数格式显示变量。

将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

  1. #include<stdio.h>
  2. int main()
  3. {
  4. int a[] = {1,2,3,4,5,6};
  5. return 0;
  6. }

GDB 基础入门实战 - 图44
我们现在来理解这些是什么意思:

  • print a[4]:打印 a[4] 的值。
  • print a+4:打印 a[4] 的地址。
  • print &a[4]:打印 a[4] 的地址。
  • x a[4]:访问 a[4] 值所代表的内存,即打印 a[4] 值代表内存里面的值。
  • x a+4:访问指针 a+4 代表内存里面的值,即 5。
  • x &a[4]:访问指针 a+4 代表内存里面的值,即 5。
  • x &(a+4):访问 a+4 指针所在的地址,不存在。

print 就是打印给定变量(参数是什么,就打印什么),x 打印给定变量代表的内存地址里的值(即 x 后面的参数 是地址值,打印的是地址所在内存单元的值)。

GDB 查看命令

GDB 中调试的命令非常的多,我们暂时只对上述的命令进行详细介绍,具体可以通过 help 命令查看。

查看命令的种类

查看各个种类的命令可以进入到 GDB 的命令行模式中,使用 help 命令查看,使用方式:

  1. (gdb) help

GDB 基础入门实战 - 图45

查看具体某个类型中的命令

使用 help 命令,向我们展示了命令总体被划分成了 12 种,其中每一种又会包含许多的命令,查看各个种类种的命令使用方法:

  1. (gdb) help <class>

其中 <class> 表示 help 命令显示的 GDB 中的命令的种类,例如:
GDB 基础入门实战 - 图46
列举的只是 breakpoints 这个种类中的一小部分,关于 breakpoints 相关的命令非常多,可以输入 return 查看。

命令的具体使用方式

如果我们想知道具体某条命令的使用方法,仍然可以使用 help 命令,使用方法如下:

  1. (gdb) help <command>

<command> 表示的是具体的一条命令,会显示出这条命令的含义以及使用方式,例如:
GDB 基础入门实战 - 图47

GDB 多线程与多进程调试

本小节我们将学习 GDB 的多线程与多进程调试。多线程顾名思义就是实现多个线程并发执行,简单的说就是同时处理多项任务。我们在开发过程中会经常使用到多线程,当然出现的问题也是不可避免的。在 C 语言中创建多进程程序需要使用 fork 相关的一些函数,调用一次 fork 函数就会创建一个进程。多进程调试时,我们需要对调试的进程和未调试的进程进行设置。

GDB 调试多线程

多线程调试的主要任务是准确及时地捕捉被调试程序线程状态的变化的事件,并且 GDB 针对根据捕捉到的事件做出相应的操作,其实最终的结果就是维护一个叫 thread list 的链表。

查看线程的相关信息

使用 GDB 调试多线程的程序时,可以使用下面的命令获取线程的信息,命令展示如下:

  1. info threads

显示可以调试的所有线程,GDB 会为每个线程分配一个 Id,编号一般从 1 开始,当前调试的线程编号前有 *

调试多线程

调试多线程的程序和调试普通的单线程的程序是不同的。当我们调试多线程中的某一个线程时,需要对其他线程的状态做一些设置,包括主线程调试时其他线程是否运行以及运行时需要执行的命令。下面是调试多线程命令的详细介绍:

  1. 调试线程时,可以做到切换线程,使用命令:

    1. thread [ID]
  2. 通过线程的编号切换到指定的线程:

    1. thread 3 //切换到编号为 3 的线程。
  3. 运行中的线程指定执行命令,命令格式:

    1. thread apply [ID……] [command]
  4. 这个命令可以指定多个 ID,使用 all 表示指定所有的线程。command 表示执行的命令:

    1. (gdb) thread apply 1 continue // 1号线程执行continue命令。
    2. (gdb) thread apply 1 2 continue //1号和二号线程执行continue命令
    3. (gdb) thread apply all continue //所有的线程执行continue命令
  5. 锁定执行的线程,命令格式:

    1. set scheduler-locking [mode]
  6. 该命令表示当调试一个线程时其他的线程是否继续执行。mode 有三种选项,分别是:offonstep。当 modeoff 时,表示不锁定线程,这是 GDB 的默认选项。当 mode 的值为 on 时,表示锁定其他的线程,只有当前线程执行。当 modestep 时,表示如果使用 step 单步执行,只有被调试的程序执行。

    在线程中设置断点

    我们可以设置断点在所有的线程上或是在某个特定的线程,使用以下命令:

    1. break <linespec> thread [ID]
    2. break <linespec> thread [ID] if ...

    linespec 指定了断点设置在的源程序的行号,ID 表示线程的编号。

    实例

    下面我们以以下多线程的程序为例,在 GDB 模式下测试各种命令。由于 pthread 不是 Linux 下的默认的库,也就是在链接的时候,无法找到 phread 库中哥函数的入口地址,于是链接会失败。在 GCC 编译的时候,附加要加 -lpthread 参数即可解决。
    将以下代码保存在桌面上,命名为 demo.c,执行命令 gcc -ggdb demo.c -o demo -pthread,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

    1. #include <stdio.h>
    2. #include <string.h>
    3. #include <stdlib.h>
    4. #include <pthread.h>
    5. static void *thread_job(void *s)
    6. {
    7. printf("this is 1\n");
    8. }
    9. static void *thread_job1(void *s)
    10. {
    11. printf("this is 2\n");
    12. }
    13. int main(void)
    14. {
    15. pthread_t tid,tid1;
    16. pthread_create(&tid, NULL, thread_job, NULL);
    17. pthread_create(&tid1, NULL, thread_job1, NULL);
    18. pthread_join(tid,NULL);
    19. pthread_join(tid1,NULL);
    20. exit(0);
    21. }

    GDB 基础入门实战 - 图48
    在需要调试的地方打下断点,run 运行到断点处:
    GDB 基础入门实战 - 图49
    run 运行到断点处,info threads 可以查看被调试的线程:
    GDB 基础入门实战 - 图50
    使用 thread Id 切换线程号:
    GDB 基础入门实战 - 图51
    可以看到线程号从 2 变成了 1。
    使用thread apply all bt 让所有线程打印堆栈信息:
    GDB 基础入门实战 - 图52
    最后执行 c 到最后,调试结束:
    GDB 基础入门实战 - 图53

GDB 调试多进程

GDB 是 linux 系统上常用的 C/C++ 调试工具,功能十分强大。对于较为复杂的系统,比如多进程系统,如何使用 GDB 调试呢?考虑下面这个三进程系统:
GDB 基础入门实战 - 图54
Proc2 是 Proc1 的子进程,Proc3 又是 Proc2 的子进程。如何使用 GDB 调试 proc2 或者 proc3 呢?
实际上,GDB 没有对多进程程序调试提供直接支持。例如,使用 GDB 调试某个进程,如果该进程 fork 了子进程,GDB 会继续调试该进程,子进程会不受干扰地运行下去。如果你事先在子进程代码里设定了断点,子进程会收到 SIGTRAP 信号并终止。那么该如何调试子进程呢?其实我们可以利用 GDB 的特点或者其他一些辅助手段来达到目的。此外,GDB 也在较新内核上加入一些多进程调试支持。
在 C 语言中创建多进程程序需要使用 fork 相关的一些函数,调用一次 fork 函数就会创建一个进程。多进程调试时,我们需要对调试的进程和未调试的进程进行设置。下面介绍的一些命令是我们在调试时经常使用到的。

  1. GDB 默认调试的是父进程,我们可以设置调试的进程,使用命令:

    set follow-fork-mode <mode>
    
  2. 其中 mode 为设置调试的进程:可以是 child,也可以是 parent。当 modeparent 时,程序在调用 fork 后调试父进程,子进程不会受到影响。当 modechild 时,程序在调用 fork 后调试子进程,父进程不会受到影响。

  3. 查看 GDB 中设置的 follow-fork-mode 可以使用命令:

    show follow-fork-mode
    
  4. 在 GDB 中调试多进程时,可以只调试一个进程,也可以同时调试两个进程,这个和 GDB 中的 detach-on-fork 的设置有关,使用命令:

    set detach-on-fork <mode>
    
  5. mode 可以为 on,也可以为 off。当 modeon 时,表示程序只调试一个进程(可以是父进程、子进程),这是 GDB 的默认设置。当 modeoff 时,父子进程都在 GDB 的控制之下,其中一个进程正常的调试,另一个会被设置为暂停状态。

  6. 查看 GDB 中设置的 detach-on-fork 可以使用命令:

    show detach-on-fork
    
  7. GDB 将每一个被调试程序的执行状态记录在一个名为 inferior 的结构中。一般情况下一个 inferior 对应一个进程,每个不同的 inferior 有不同的地址空间。inferior 有时候会在进程没有启动的时候就存在。

  8. 查看当前调试的所有的 inferior,使用命令:

    info inferiors
    
  9. 当前调试的进程前有 *

  10. 切换进程使用命令 inferior

    inferior <num>
    
  11. 表示切换到 idnuminferior

    inferior 2
    
  12. 切换到 2 号进程。

    实际操作

    下面我们以以下多进程的程序为例,在 GDB 模式下测试各种命令。将以下代码保存在桌面上,命名为 demo.c,执行命令gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/wait.h>
    int main(void)
    {
     pid_t pid;
     pid = fork();
     if(pid < 0)
     {
     perror("fork()");
     }
     if(pid == 0)
     {
         printf("this is child,pid = %d\n",getpid());
     }
     else
     {
         printf("this is parent,pid = %d\n",getpid());
     }
     exit(0);
    }
    

    show follow-fork-mode

    查看当前调试的 fork 模式,如下图,默认为父进程,如果想设置为子进程,可以使用 set follow-fork-mode child
    GDB 基础入门实战 - 图55

    show detach-on-fork

    查看 detach-on-fork 的模式。设置为 on 表示只调试父子进程中的一个,off 表示父子进程都在 GDB 的控制之下,其中一个进程正常调试另一个进程会被设置为暂停状态。
    GDB 基础入门实战 - 图56

    info inferiors

    显示 GDB 调试的所有进程。inferior [进程编号] 可以切换到特定的 inferiors 进行调试。其中 * 代表正在调试的进程。
    GDB 基础入门实战 - 图57

    设置捕捉点

    捕捉点是一种特殊类型的断点,用来在设置在某些事件发生时中断程序,使用 catch 命令可以捕获,当调用 fork 函数会产生中断。
    GDB 基础入门实战 - 图58

    实例

    总的相关操作及调试信息如下:

    (gdb) show follow-fork-mode  //显示默认的 follow-fork-mode 配置
    (gdb) show detach-on-fork //显示默认的 detach-on-fork  配置
    (gdb) set follow-fork-mode child //设置
    follow-fork-mode 为 child
    (gdb) set detach-on-fork off //设置 detach-on-fork off 为 off
    (gdb) catch fork  //设置捕获点中断
    (gdb) run  //运行程序
    (gdb) s
    (gdb) info inferiors   //显示程序运行的进程
    (gdb) inferior 1   //切换到一号进程
    (gdb) info inferiors
    (gdb) c  //执行第一个进程
    (gdb) info inferiors
    

    GDB 基础入门实战 - 图59

GDB 查看栈信息

当我们阅读代码和查找 BUG 时,往往有一个烦恼,就是我们不知道函数的调用顺序。而这些函数调用顺序对应我们理解程序结构,程序运行过程是很有帮助的。但是程序的调用过程往往是很复杂的,而且可能是多层嵌套,跨文件调用的。这时候如果靠人工去查找,这将是一件非常大工作量的事情。GDB 中有办法帮助我们做到查看函数调用的过程吗?本小节我们将会对此进行学习。

基础知识

首先我们需要知道,函数调用信息存放在哪?只有知道函数调用信息,我们才能进行信息提取这一步。答案是,关于函数的信息都存放在栈中。
GDB 基础入门实战 - 图60
使用 GDB 调试程序时,当程序发生中断,我们首先应该知道程序在哪里产生中断以及产生中断的原因是什么?函数发生调用时,相关的调试信息就已经产生,并且被存储在一块被称为栈帧的数据里。

栈帧是在调用栈的内存区域里分配的,是调用栈划分的连续的区块,简称为栈。每个帧是一个函数调用另一个函数的相关数据,包含了传递给本地用函数的参数,这个函数的本地变量和这个函数的执行地址。

在函数开始的时候栈中只有一个帧,是 main 函数的,这个帧称为初始帧或者是最外层的帧。每当一个函数被调用,就产生一个新的栈帧。当函数返回时,这个调用所属的帧就被销毁了。如果调用的是递归函数,那么同一个函数就可能有多个帧。当前正在执行的函数调用的帧成为最内层的帧,这是最近创建的帧,同时还有别的帧存在。程序内部的栈帧用地址标识,一个栈帧有许多的字节组成,每个字节都有自己的地址。

GDB 为所有现存的栈帧编号,从最内层帧 0 开始,1 是这个函数调用的帧,以此类推。这些编号并不真正存在于程序里,他们是由 GDB 分配,用于 GDB 的命令来区分栈帧。

显示栈帧信息

显示栈帧信息的命令主要有 framebacktrace

frame 命令

frame 的命令格式如下:

frame

使用 frame 命令会打印出当前调用栈的信息,这些信息包含:栈帧的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。命令可以缩写为 f

backtrace 命令

backtrace 的命令格式如下:

backtrace <n>

不带参数:打印当前调用函数的栈帧信息,每个栈帧显示一行。 带参数:n 为正整数时,表示打印栈顶 n 层的栈信息;n 为负整数时,那么表示打印栈底 n 层的栈信息。
如果我们想要获取更详细的当前栈帧层的信息,可以使用命令:

info frame

打印出的大多数都是运行时的内地址。比如:函数地址,被调用的函数地址,当前函数是由什么样的语言写成的、函数参数地址及值、局部变量的地址。info 命令的其他使用方式:

命令 功能说明
info registers 查看当前寄存器的值。
info args 查看当前函数参数的值。
info locals 查看当前局部变量的值。
info frame 查看当前栈帧的详细信息。
info variables 查看程序中的变量符合。
info functions 查看程序中的函数符号。

切换到其他栈帧

切换到任意的栈帧

切换到任意的栈帧使用 frame 相关的命令格式如下:

frame <n>

n 表示栈帧的标号,这个命令可以从一个堆栈帧转到另一个,并打印所选的堆栈帧。

(gdb) frame 3

切换到标号为 3 的栈帧。

从当前的栈帧层向上移动

命令格式:

up <n>

n 表示栈帧的标号,在堆栈里上移 n 帧,对于正数向外层的帧移动,更高编号的帧,存在更长的时间的帧。

(gdb) up 3

移动到编号为当前栈帧的编号加 3 的栈帧。

从当前的栈帧层向下移动

命令格式:

down <n>

n 表示栈帧的标号,在堆栈里下移 n 帧。对于正数 n,向内层的帧移动,更低编号的帧,新创建的帧。

(gdb) down 3

移动到编号为当前栈帧的编号减 3 的栈帧。

实例

将以下代码保存在桌面上,命名为 demo.c,执行命令gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

#include <stdio.h>
int sum(int n)
{
    int ret = 0;
    if( n > 0 )
    {
        ret = n + sum(n-1);
    }
    return ret;
}
int main()
{
    int s = 0;
    s = sum(10);
    printf("sum = %d\n", s);
    return 0;
}

设置断点:设置到递归结束标志的位置

(gdb) start
(gdb) break sum if n==0 //设置sum函数中, n==0 时的数据断点。
(gdb) info break //查看断点信息

GDB 基础入门实战 - 图61

查看函数调用过程

(gdb) continue
(gdb) backtrace //查看函数调用的顺序

GDB 基础入门实战 - 图62

分析函数调用过程

(gdb) next
(gdb) next
(gdb) info args //查看当前函数参数的值
(gdb) frame 7 //切换栈编号为7的上下文中
(gdb) info args //查看栈编号为7时函数参数的值
(gdb) info locals //查看当前局部变量ret的值
(gdb) info frame //查看当前栈帧的详细信息
(gdb) bt 2 //显示栈顶的两层的信息
(gdb) bt -2 //显示栈底的两层的信息
(gdb) frame 0
(gdb) up 1
(gdb) down 1
(gdb) info registers //查看当前寄存器的值
(gdb) info frame //查看当前栈帧的详细信息
Saved registers:
  rbp at 0x7fffffffdf50, rip at 0x7fffffffdf58
(gdb) x /1wx 0x7fffffffdf50 //查看ebp地址中的值
(gdb) next
(gdb) next
(gdb) info args
(gdb) info registers //查看栈帧编号为1的寄存器值
(gdb) info locals
ret = 1 //计算结果

GDB 基础入门实战 - 图63
GDB 基础入门实战 - 图64
GDB 基础入门实战 - 图65

GDB 实战

通过之前的学习,我们已经能够使用 GDB 最常见的用法了,本小节将带领大家使用 GDB 一步一步去调试一个 C 语言程序。本实验可能无法使用之前所学的全部命令,因为使用命令需要大家结合实际情况去判断,之前的教程已经把使用 GDB 命令的场景给大家介绍了,我相信在实际情况中大家也能自己很好的使用。

GDB 学习实例

将以下代码保存在桌面上,命名为 demo.c,执行命令 gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:

#include <stdio.h>
int add_range(int low, int high)
{
    int i, sum;
    for (i = low; i <= high; i++)
        sum = sum + i;
    return sum;
}
int main(void)
{
    int result[100];
    result[0] = add_range(1, 10);
    result[1] = add_range(1, 100);
    printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
    return 0;
}

运行程序,结果如下:
GDB 基础入门实战 - 图66
从运行结果来看,这个答案肯定是错误的,因为我们完成的功能是计算 1 加到 10 和 1 加到 100 的和,打印出来的结果应该是 55 和 5050。现在我们开始利用 GDB 对程序进行调试。
首先用 start 命令开始执行程序:

(gdb) start

GDB 基础入门实战 - 图67
gdb 停在 main 函数之后的第一条语句处等待我们发命令,gdb 列出的这条语句是即将执行的下一条语句。我们可以用 next 命令(简写为 n)控制这些语句一条一条地执行:

(gdb) n
(gdb) n
(gdb) n

GDB 基础入门实战 - 图68
虽然程序正常打印,并且正常退出,但是并没有找到程序的问题所在。因为错误不在 main 函数中而在 add_range 函数中,现在用 start 命令重新来过,这次用 step 命令(简写为 s)钻进 add_range 函数中去跟踪执行:

(gdb) start
(gdb) s
(gdb) s

GDB 基础入门实战 - 图69
这次停在了 add_range 函数中变量定义之后的第一条语句处。在函数中有几种查看状态的办法,backtrace 命令(简写为 bt )可以查看函数调用的栈帧:

(gdb) bt

GDB 基础入门实战 - 图70
可见当前的 add_range 函数是被 main 函数调用的,main 传进来的参数是 low=1, high=10main 函数的栈帧编号为 1,add_range 的栈帧编号为 0。现在可以用 info 命令(简写为 i)查看 add_range 函数局部变量的值:

(gdb) i locals

GDB 基础入门实战 - 图71
如果想查看 main 函数当前局部变量的值也可以做到,先用 frame 命令(简写为 f)选择 1 号栈帧然后再查看局部变量:

(gdb) f 1
(gdb) i locals

GDB 基础入门实战 - 图72
注意到 result 数组中有很多元素有杂乱无章的值,我们知道未经初始化的局部变量具有不确定的值。到目前为止一切正常。用 sn 往下走几步,然后用 print 命令(简写为 p)打印出变量 sum 的值:

(gdb) s
(gdb) s
(gdb) s
(gdb) s
(gdb) p sum

GDB 基础入门实战 - 图73
第一次循环 i 是 1,第二次循环 i 是 2,加起来是 3。这里的 $1 表示 gdb 保存着这些中间结果,$ 后面的编号会自动增长,在命令中可以用 $1$2$3 等编号代替相应的值。由于我们本来就知道第一次调用的结果是正确的,再往下跟也没意义了,可以用 finish 命令让程序一直运行到从当前函数返回为止:

(gdb) finish

GDB 基础入门实战 - 图74
返回值是 55,当前正准备执行赋值操作,用 s 命令赋值,然后查看 result 数组:

(gdb) s
(gdb) p result

GDB 基础入门实战 - 图75
第一个值 55 确实赋给了 result 数组的第 0 个元素。下面用 s 命令进入第二次 add_range 调用,进入之后首先查看参数和局部变量:

(gdb) s
(gdb) bt
(gdb) i locals

GDB 基础入门实战 - 图76
由于局部变量 isum 没初始化,所以具有不确定的值,又由于两次调用是挨着的,isum 正好取了上次调用时的值。i 的初值不是 0 倒没关系,在 for 循环中会赋值为 0 的,但 sum 如果初值不是 0,累加得到的结果就错了。
我们已经找到错误原因,可以退出 gdb 修改源代码了。如果我们不想浪费这次调试机会,可以在 gdb 中马上把 sum 的初值改为 0 继续运行,看看这一处改了之后还有没有别的 bug:

(gdb) set var sum=0
(gdb) finish
(gdb) n
(gdb) n

GDB 基础入门实战 - 图77
这样结果就对了,修改变量的值除了用 set 命令之外也可以用 print 命令,因为 print 命令后面跟的是表达式,学员们可以自行尝试。