软件调试 · 工具类 · GDB调试基础操作

1、引言

1.1、GDB简介

  GDB是GNU项目的一个调试器,它允许开发者在程序执行时或崩溃时查看程序内部发生的情况。

  GDB的主要功能包括启动程序、在特定条件下停止程序、检查程序停止时的状态以及在程序中更改内容以实验修复错误。

  更多信息可访问GDB的官网:GDB: The GNU Project Debugger

1.2、GDB支持的语言

  GDB支持多种编程语言,包括Ada、Assembly、C、C++、D、Fortran、Go、Objective-C、OpenCL、Modula-2、Pascal和Rust。

2、调试环境介绍

  本文所涉及到的编程环境为:

  1. wsl 2 + Ubuntu 22.04 LTS
  2. gcc version 11.4.0

在这里插入图片描述

3、安装GDB

  安装GDB前,先更新一些软件列表,其命令为:

sudo apt-get update

  更新结果:
在这里插入图片描述

  在更新完毕后,使用下面这条命令对GDB进行安装:

sudo apt-get install gdb -y

  更新结果:

在这里插入图片描述
  可以看出,安装GDB是一个十分简单的过程。安装完毕后,就可以使用GDB进行调试了。

4、测试代码

  在开始进行调试之前,需要有测试代码,本博文所用的测试代码如下所示:

#include <stdio.h>
#include <stdlib.h>

// 定义一个结构体
typedef struct
{
    int id;
    char name[20];
    float score;
} Student;

/**
 * @brief 打印学生信息
 *
 * @param s 指向Student结构的常量指针,代表要打印的学生信息
 */
void print_student(const Student *s)
{
    // 打印学生的学号、姓名和分数
    printf("学号:%d 姓名:%s 分数:%.1f\n", s->id, s->name, s->score);
}

/**
 * @brief 比较两个学生的分数
 *
 * @param a 指向第一个学生的指针
 * @param b 指向第二个学生的指针
 * @return int 比较结果,如果student_a的分数大于student_b则返回-1,
 *              如果小于则返回1,相等则返回0
 */
int compare(const void *a, const void *b)
{
    // 将void指针转换为Student指针
    const Student *student_a = (const Student *)a;
    const Student *student_b = (const Student *)b;
    // 比较两个学生的分数
    if (student_a->score > student_b->score)
    {
        return -1; // student_a的分数更高
    }
    else if (student_a->score < student_b->score)
    {
        return 1; // student_b的分数更高
    }
    else
    {
        return 0; // 分数相同
    }
}

/**
 * @brief 对学生数组进行排序
 *
 * @param students 学生数组
 * @param len 数组中学生的数量
 */
void sort_students(Student students[], int len)
{
    // 使用qsort函数对数组进行排序,比较函数为compare
    qsort(students, len, sizeof(Student), compare);
}

int main(int argc, char const *argv[])
{
    // 基本变量
    int a = 10;
    char ch = 'A';
    float f = 3.14f;

    // 数组
    int arr[5] = {5, 2, 8, 1, 9};
    char str[] = "Hello, GDB!";

    // 指针
    int *p = &a;
    char *p_str = str;

    // 结构体
    Student students[3] = {
        {1, "张三", 85.5f},
        {2, "李四", 92.0f},
        {3, "王五", 78.0f}};

    // 动态内存分配
    int *dyn_arr = (int *)malloc(5 * sizeof(int));
    for (int i = 0; i < 5; ++i)
    {
        dyn_arr[i] = i * i;
    }

    // 函数调用
    printf("基本变量:\n");
    printf("  整型:%d\n", a);
    printf("  字符型:%c\n", ch);
    printf("  浮点型:%.2f\n\n", f);


    printf("数组:");
    for (int i = 0; i < 5; ++i)
    {
        printf("%d ", arr[i]);
    }
    printf("\n\n");

    printf("字符串:%s\n\n", str);

    printf("指针:\n");
    printf("整型指针指向的值:%d\n", *p);
    printf("字符型指针指向的字符串:%s\n\n", p_str);

    printf("结构体:\n");
    for (int i = 0; i < 3; ++i)
    {
        print_student(&students[i]);
    }
    printf("\n");

    printf("动态内存分配的数组:\n");
    for (int i = 0; i < 5; ++i)
    {
        printf("%d ", dyn_arr[i]);
    }
    printf("\n\n");

    // 排序结构体数组
    sort_students(students, 3);
    printf("排序后的结构体数组:\n");
    for (int i = 0; i < 3; ++i)
    {
        print_student(&students[i]);
    }

    // 释放动态内存
    free(dyn_arr);

    return 0;
}

  使用gcc编译器进行编译,其命令为:

gcc -o main -Wall -std=c99 -g test.c

  记住!为了能生成调试信息,编译命令需加-g 参数

  运行结果为:

在这里插入图片描述

5、调试

5.1、基础操作

5.1.1、启动GDB

  启动GDB所使用的命令为gdb [程序名],如下所示:

在这里插入图片描述

5.1.2、运行程序

  直接运行程序的命令是 run ,效果如下所示:

在这里插入图片描述

可以看到,在程序输出相关内容之前,有三行信息:

Starting program: /home/orange/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

  这段GDB输出信息表示的是在使用GDB启动程序时,GDB检测到了程序使用了线程,并且已经启用了线程调试功能。

  具体来说,各部分的含义如下:

   [Thread debugging using libthread_db enabled]:这行表示GDB已经启用了线程调试功能,这是通过使用libthread_db库实现的。libthread_db是一个提供线程调试功能的库,它使得GDB能够跟踪和调试多线程程序。

   Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".:这行说明了GDB正在使用的libthread_db库的具体位置。在这个例子中,这个库位于/lib/x86_64-linux-gnu/目录下,并且库的文件名为libthread_db.so.1

  这有什么意义吗?

  事实上,如果程序是多线程的,那么就可以使用GDB来设置线程相关的断点,查看和操作各个线程的状态,这对于调试多线程程序是非常有用的。

  在程序的最后有一行这样的输出:

[Inferior 1 (process 2001) exited normally]

  这段GDB输出信息表示的是调试的程序(在这里称为“inferior”,即被调试的程序)已经正常退出了。

  具体来说含义如下:

   [Inferior 1 (process 2001) exited normally]:这行表示编号为1的被调试进程(如果同时调试多个进程,每个进程都会有一个编号)已经正常退出了。process 2001指的是这个进程的进程ID(PID),这是一个在系统中唯一标识进程的数字。exited normally意味着程序没有遇到错误或异常,而是按照预期完成了执行并退出。

5.1.4、清屏操作

  在调试过程中,输出的信息太多,扰乱了视觉,这时候可以按下Ctrl + L键进行清屏。

5.1.5、步进调试

5.1.5.1、start命令:运行程序后,停留在主函数第一行

  以本例程为例,run命令将会如果想从头到尾运行一遍程序,但更多的时候,我们更想进行步进调试,此时就需要使用start命令来进行调试,执行命令后,效果如下所示:

在这里插入图片描述
  上面所解释过的信息此处不再解释,让我们关注新出现的信息:

  1. Temporary breakpoint 1 at 0x555555555314: file test.c, line 64.

   这行表示GDB在指定的位置设置了一个临时的断点。Temporary breakpoint 1指的是这是第一个临时断点(如果设置了多个断点,它们将按设置顺序编号)。

   0x555555555314是断点设置在程序中的内存地址。这个地址是程序中main函数开始执行的地方。

   file test.c, line 64说明断点设置在test.c文件的第64行。这意味着当程序执行到test.c文件的第64行时,将会暂停。

  我们可以从源代码中看到,第64行正是主函数的函数头下一行:

在这里插入图片描述
  2. Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe3d8) at test.c:64

   这行表示程序在执行时遇到了之前设置的临时断点。Temporary breakpoint 1再次指出了这是第一个临时断点。

   main (argc=1, argv=0x7fffffffe3d8)main函数的参数。argc是传递给main函数的参数数量(在这个例子中是1),argv是一个指向参数字符串数组的指针(在这个例子中是0x7fffffffe3d8)。

   at test.c:64再次指出了断点位于test.c文件的第64行。

5.1.5.2、step命令:单步执行,会进入函数内部

  为了能够更好的进行举例,此处先提前使用一条命令break 126在第126行处设立一个断点,同时使用continue命令执行到断点处,这两条命令后面再解释,此处只需要知道程序在此处设立了一个断点并执行到断点处即可。效果如下:
在这里插入图片描述
  此时我们使用step命令即可进入函数中进行一步步调试,效果如下:

在这里插入图片描述
  可以查看到第60行(函数内的)的代码为:

在这里插入图片描述
  从上面的结果可以看到,程序进入了函数的内部。

5.1.5.3、next命令:单步执行,不会进入函数内部

  让我们使用start命令重启一下程序的调试,然后再执行continue命令执行到断点处,随后执行next命令观察跟step命令有何区别:

在这里插入图片描述
在这里插入图片描述

  从图中可以看到,使用了next命令之后直接执行函数并进入下一行,并不会进入到函数内部。此处部分代码如下图所示:

在这里插入图片描述

5.1.5.4、continue命令:继续执行程序直到下一个断点

  从上面的结果也可以看出来,continue的作用就是程序运行到程序的断点处停下来,等待程序员的进一步操作,此处不再举例。

5.1.6、list命令:输出断点处周围的代码

  使用start命令重启调试,而后直接使用list命令就能查看周围几行代码,list命令的基本用法是:

list [start[, end]]

  其中start是开始查看的行号,end是结束查看的行号。如果不指定end,GDB将默认显示从start开始的10行代码。

  举一些例子:

list 10:从第10行开始显示源代码,默认显示10行。
list 10, 20:显示从第10行到第20行的源代码。
list , 20:显示当前文件中从当前行到第20行的源代码。
list 10, :显示当前文件中从第10行到文件末尾的源代码。

  如果想在特定的文件中查看代码,可以这样做:

list [filename:]start[, end]

  举一些例子:

list test.c:10:显示test.c文件中从第10行开始的源代码。
list test.c:10, 20:显示test.c文件中从第10行到第20行的源代码。

  以本博文的例程为例,使用start命令重启调试后,直接运行list命令:

在这里插入图片描述

  查看当前文件的1~15行内容:

在这里插入图片描述

  显示test.c文件中从第50行到第75行的源代码:

在这里插入图片描述

5.1.7、断点

5.1.7.1、设置指定行的断点

  break命令可以设置断点,程序在运行到断点处会停下,等待程序员的进一步操作。
  设置指定行断点的命令格式为:

break [行号]

  比如在第66行、第67行和第68行设置断点:

在这里插入图片描述

5.1.7.2、查看所有断点信息

  查看断点信息的命令为:

info breakpoints

  具体效果为:
在这里插入图片描述

  这里解释一下输出信息:

  • Num:断点的编号。
  • Type:断点的类型,比如breakpoint、watchpoint等。
  • Disp:断点的处置方式,keep表示断点在程序重新启动时保持,del表示断点在程序停止时删除。
  • Enb:断点是否启用,y表示已启用,n表示已禁用。
  • Address:断点设置在程序中的内存地址。
  • What:断点所在的函数和文件位置。
5.1.7.3、在函数入口处设置断点

  在GDB中可以在函数入口处设置断点使用函数名。使用函数名设置断点时,GDB会在函数的第一条指令处停止执行。这是设置断点的常用方法,因为它不依赖于具体的行号,而是依赖于函数名,这样即使代码发生了变化,断点仍然有效。

  要设置一个函数入口处的断点,可以使用以下命令:

break function_name

  比如在本例中:

break print_student

  执行结果为:

在这里插入图片描述

5.1.7.4、删除断点

  删除断点可以使用delete 命令,格式为:

delete number

  number是行号,可以使用info breakpoints命令进行查看,比如要删除num 4的的断点:

在这里插入图片描述

  也可以连续删除:
在这里插入图片描述
  如果想全部删除,那么直接使用delete命令即可:

在这里插入图片描述

5.1.8 print:查看变量

5.1.8.1、查看基本变量

  在调试程序的时候,需要查看变量的值,如果是基本变量,可以直接使用以下命令格式:

print <表达式>

  举例一下,我们要观察GDB打印以下这些变量的值:

在这里插入图片描述

  那么使用start开始调试,会看到这三个变量的值都为以下这些情况:

在这里插入图片描述

  这是变量未初始化导致的,当我们在这些变量下面设置一个断点并运行到断点处,再观察输出:

在这里插入图片描述
  此时就能看到变量被初始化了。

5.1.8.2、查看数组元素的值

  除了可以打印基本变量的值,还可以打印数组元素的值,现在来看一下代码:

在这里插入图片描述

  此时在第72行处添加断点,并使用continue命令来执行到断点处,保证数组已经被初始化,而后再用print命令对数组元素的值来进行观察。

在这里插入图片描述

5.1.8.3、查看字符串

  想要观察字符串,此时还是需要设置一个断点,而后在运行到断点处,这时候在使用print命令去查看。

在这里插入图片描述

5.1.8.4、打印指针和它们指向的值

  一样是下断点,一样是运行到断点处,一样是使用print命令观察输出。此部分代码为:

在这里插入图片描述
  观察调试结果:

在这里插入图片描述

5.1.8.5、打印结构体的字段

  相关代码:

在这里插入图片描述
  调试结果:

在这里插入图片描述

5.1.8.6、打印数组的所有元素的值

  事实上,print命令不仅仅可以观察单个元素的值,还能看整个数组的值,比如在本例程中,arr、str和students都是数组,都能一次性查看所有成员的值:

在这里插入图片描述

  dyn_arr属于动态内存分配的数组,单独打印变量名得到的是内存地址,我们需要将dyn_arr的第一个元素强制转换为包含5个整数的数组,才能打印出元素的值:

在这里插入图片描述

5.1.9、退出GDB

  退出GDB的命令是q:

在这里插入图片描述


                                             未完待续

5.2 中级操作

5.2.1 条件断点

5.2.1.1 基本概念

  条件断点:在断点上附加一个布尔表达式,仅当这个表达式为真时,调试器才会中断程序。

5.2.1.2 使用场景

  当在只对程序中的某些特定情况感兴趣时,可以使用条件断点。例如,只想在数组越界或某个变量值达到特定条件时暂停程序。

5.2.1.3 优点

  提高调试效率,不必每次循环都中断,且可以针对复杂的逻辑设置条件,使得调试更加精准。

5.2.1.4 注意事项

  1、条件表达式的编写要遵循C语言或你使用的编程语言的语法规则。
  2、条件断点的计算和检查会增加调试器的开销,可能会稍微减慢程序的运行速度。

5.2.1.5 举例
  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值