目录
- 1、引言
- 1.1、GDB简介
- 1.2、GDB支持的语言
- 2、调试环境介绍
- 3、安装GDB
- 4、测试代码
- 5、调试
- 5.1、基础操作
- 5.1.1、启动GDB
- 5.1.2、运行程序
- 5.1.4、清屏操作
- 5.1.5、步进调试
- 5.1.5.1、start命令:运行程序后,停留在主函数第一行
- 5.1.5.2、step命令:单步执行,会进入函数内部
- 5.1.5.3、next命令:单步执行,不会进入函数内部
- 5.1.5.4、continue命令:继续执行程序直到下一个断点
- 5.1.6、list命令:输出断点处周围的代码
- 5.1.7、断点
- 5.1.7.1、设置指定行的断点
- 5.1.7.2、查看所有断点信息
- 5.1.7.3、在函数入口处设置断点
- 5.1.7.4、删除断点
- 5.1.8 print:查看变量
- 5.1.8.1、查看基本变量
- 5.1.8.2、查看数组元素的值
- 5.1.8.3、查看字符串
- 5.1.8.4、打印指针和它们指向的值
- 5.1.8.5、打印结构体的字段
- 5.1.8.6、打印数组的所有元素的值
- 5.1.9、退出GDB
- 5.2 中级操作
- 5.2.1 条件断点
- 5.2.1.1 基本概念
- 5.2.1.2 使用场景
- 5.2.1.3 优点
- 5.2.1.4 注意事项
- 5.2.1.5 举例
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、调试环境介绍
本文所涉及到的编程环境为:
- wsl 2 + Ubuntu 22.04 LTS
- 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函数对数组进行排序,比较函数为compareqsort(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、条件断点的计算和检查会增加调试器的开销,可能会稍微减慢程序的运行速度。