博客园  :: 首页  :: 新随笔  :: 订阅 订阅  :: 管理

GDB调试工具

Posted on 2020-10-16 17:23  面具下的戏命师  阅读(505)  评论(0编辑  收藏  举报

GDB调试工具

GDB 全称“GNU symbolic debugger”,是 Linux 下常用的程序调试器。当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。实际场景中,GDB 更常用来调试 C 和 C++ 程序。

总的来说,借助 GDB 调试器可以实现以下几个功能:

  1. 程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;
  2. 可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;
  3. 程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误

GDB调试C/C++程序

我们以一段可以正常运行的C程序来演示一下GDB的使用,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;
}

GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,GDB 才会派上用场。我们通过GCC来编译C代码并生成可执行文件。需要至于的时,直接通过gcc编译生成的可执行文件是无法调试的。原因是使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等。因此如果要生成满足GDB要求的可执行文件,需要使用 gcc -g 选项编译源文件。

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

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

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

启动GDB调试器

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

该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、--quiet)选项,可将比部分信息屏蔽掉。GDB 调试器启动成功的标志就是最终输出的 (gdb)。通过在 (gdb) 后面输入指令,即可调用 GDB 调试进行对应的调试工作。

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

表 1 GDB常用的调试指令
调试指令作 用
(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
终止调试。

如上所示,每一个指令既可以使用全拼,也可以使用其首字母表示。GDB 还提供有大量的选项,可以通过 help 选项来查看。

示例:

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

[root@all c]# gdb main -q
Reading symbols from /root/c/main...done.
(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 0x400488: file main.c, line 7.
(gdb) r                                    # 运行程序,遇到断点停止
Starting program: /root/c/main 

Breakpoint 1, main () at main.c:7
7        while (n <= 100)
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.149.el6.x86_64
(gdb) p n                                  # 查看代码中变量 n 的值
$1 = 1
(gdb) b 12                                 # 在程序第 12 行处打断点
Breakpoint 2 at 0x40049e: file main.c, line 12.
(gdb) c                                    # 继续执行程序
Continuing.

Breakpoint 2, main () at main.c:12
12        return 0;
(gdb) p n
$2 = 101
(gdb) q                                    # 退出调试
A debugging session is active.

    Inferior 1 [process 5633] will be killed.

Quit anyway? (y or n) y
[root@all c]#

调用GDB的几种方式

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

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

此方式启动的 GDB 调试器,由于事先未指定要调试的具体程序,因此需启动后借助 file 或者 exec-file 命令指定。

2)调试尚未执行的程序

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

gdb program

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

3) 调试正在执行的程序

在某些情况下,我们可能想调试一个当前已经启动的程序,但又不想重启该程序,就可以借助 GDB 调试器实现。
也就是说,GDB 可以调试正在运行的 C/C++ 程序。要知道,每个 C/C++ 程序执行时,操作系统会使用 1 个(甚至多个)进程来运行它,并且为了方便管理当前系统中运行的诸多进程,每个进程都配有唯一的进程号(PID)。
如果需要使用 GDB 调试正在运行的 C、C++ 程序,需要事先找到该程序运行所对应的进程号。查找方式很简单,执行如下命令即可,或者通过ps命令查询:

pidof  文件名

这里通过一个C程序来演示,代码如下:

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

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

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

 

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

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

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

 当调试完成后,如果想令当前程序进行执行,消除调试操作对它的影响,需手动将 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 -a 查看。

可以通过 ulimit -c unlimited 设置为不限制 core 文件的大小。当程序执行发生异常崩溃时,系统就可以自动生成相应的 core 文件。

示例C代码如下:

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

段错误又称为访问权限冲突,指的是当前程序访问了不可访问的存储空间,比如访问的不存在的空间,又或者是受系统保护的内存空间。此程序由于 p 指针初始化为 NULL,即不指向任何存储空间,但后续却执行*p=123操作,显然是不可行的。因此,该程序执行时会发生崩溃,Linux 系统会记录必要的崩溃信息,并存储到 core 文件中。

core文件的调试命令如下:

可以看到,程序发生崩溃的位置是在 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... 向可执行文件传递执行所需要的参数。

部分参数后续章节会详细介绍。