🌞1. 整体思路
在案例中我使用c语言编写了一个简单的四层二叉树进行 GDB 调试练习。这个程序故意在后面引发了一个段错误,导致程序崩溃。文章将使用 GDB 来诊断这个问题。
🌞2. 准备内容
建议阅读前先查看gdb的技巧
传送门:【GDB调试技巧】提高gdb的调试效率-CSDN博客
🌼2.1 配置.c文件
建议先配置一下.c文件使其显示行数【方便后续快速定位bug】。默认情况下,GDB 不会在每次调试时自动显示行号。
编辑 Vim 的配置文件 ~/.vimrc(如果不存在则创建它),并添加以下行:set number
详细步骤如下:
打开配置文件 ~/.vimrc
nano ~/.vimrc
文件内容添加
set number
效果图如下:
然后运行以下命令使其生效:
source ~/.bashrc
这样使用vim 打开文件就会显示行数了
🌼2.2 准备测试程序
使用vim文本编辑器新建一个.c文件
vim tree3_01.c
输入测试程序:
#include <stdio.h> #include <stdlib.h> // 定义树节点 typedef struct TreeNode { int data; struct TreeNode *left; struct TreeNode *right; } TreeNode; // 创建一个新的树节点 TreeNode* createNode(int data) { TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode)); if (newNode == NULL) { fprintf(stderr, "Memory allocation failed.\n"); exit(EXIT_FAILURE); } newNode->data = data; newNode->left = NULL; newNode->right = NULL; return newNode; } // 构建四层树 TreeNode* buildTree() { TreeNode* root = createNode(1); root->left = createNode(2); root->right = createNode(3); root->left->left = createNode(4); root->left->right = createNode(5); root->right->left = createNode(6); root->right->right = createNode(7); root->left->left->left = createNode(8); root->left->left->right = createNode(9); return root; } // 递归遍历树并打印节点数据 void traverseTree(TreeNode* root) { if (root != NULL) { printf("%d ", root->data); traverseTree(root->left); traverseTree(root->right); } } int main() { // 构建树 TreeNode* root = buildTree(); // 打印树的结构 printf("Tree Structure:\n"); traverseTree(root); printf("\n"); // 故意制造一个段错误,导致core dump int* ptr = NULL; *ptr = 10; // 这里将会产生段错误 return 0; }
gcc编译:
gcc -g -o tree3_01 tree3_01.c
此时ls查看会出现可执行文件tree3_01
🌼2.3 GDB调试基础
在使用GNU调试器(GDB)时,以下是一些常用的命令:
run
(或r
): 启动程序并开始调试。break
(或b
): 在指定的位置设置断点。continue
(或c
): 继续执行程序直到下一个断点。step
(或s
): 单步执行程序,进入到函数中。next
(或n
): 单步执行程序,跳过函数内部的细节。p
): 打印变量的值。backtrace
(或bt
): 打印函数调用栈。list
(或l
): 显示源代码。info
(或i
): 显示调试信息,比如当前位置、变量类型等。quit
(或q
): 退出调试器。
🌞3. GDB调试四层二叉树
🌼3.1 测试程序分析
测试程序是一个简单的打印四层二叉树的c语言程序。
对于树TreeNode结构体和创建树节点createNode函数属于常规操作【不做分析】。
程序中的buildTree函数构建了一颗四层二叉树,并使用traverseTree函数先序遍历打印二叉树的数据结构:1 2 4 8 9 5 3 6 7
🌼3.2 gdb分析
现在,启动 GDB 并加载程序:
gdb ./tree3_01
进入 GDB,可以执行下列步骤来逐步调试:
🌻1. 设置断点
在程序出错的地方设置断点以停止程序执行,并检查变量。
break main
break main与b main等价。
这段输出是在 GDB 中设置断点的结果:
- (gdb): 这是 GDB 的提示符,表示它正在等待用户输入命令。
- break main: 这是用户输入的命令,表示在程序的 main 函数的起始处设置了一个断点。
- Breakpoint 1 at 0x1398: 这一行显示了断点的信息。Breakpoint 1 表示这是第一个断点。0x1398 是断点的地址,表示断点被设置在程序代码的内存地址 0x1398 处。
- file tree3_01.c, line 49: 这一行显示断点被设置位置在文件 tree3_01.c 的第 49 行处【还未执行】。
🌻2. 启动程序并执行到断点处
run
run和r等价
这个输出表明程序已经成功启动,并且停在了之前设置的断点处,也就是在 main 函数的第 49 行:
- Starting program: /root/host/my_program/tree3_01: 这是 GDB 启动程序时的输出,指示程序已经开始执行。
- [Thread debugging using libthread_db enabled]: 这个消息表明 GDB 正在使用 libthread_db 库进行线程调试,这是针对多线程程序的。
- Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1": 这条消息表明 GDB 正在使用指定的线程库进行调试。
接着,输出显示了程序停在了 main 函数的第 49 行:
- Breakpoint 1, main () at tree3_01.c:49: 这表示断点 1 已经触发,程序停在了 tree3_01.c 文件的第 49 行的 main 函数处。
- 49 TreeNode* root = buildTree();:表示tree3_01.c 文件的第 49 行的代码【此时该行代码未执行】。
现在可以使用 GDB 的其他命令来查看程序状态,比如打印变量的值、单步执行等。
🌻3. 打印变量的值
可以使用 print 命令,后跟想要打印的变量名。
print root
print root和p root等价
这会打印 root 变量的值,即指向树根节点的指针。在这里,我们期望 root 指向一个已经创建好的二叉树的根节点。
打印 root 变量的结果显示为 (TreeNode *) 0x0,这意味着 root 指针当前指向了内存地址 0x0,即空指针【也证明了run之后到达断点的第49行代码未执行】。
🌻4. 单步执行 s 进入buildTree函数内部
step
step和s等价
step 命令进入 buildTree() 函数后,GDB 显示了当前所在的位置和执行的下一行代码。
- buildTree () at tree3_01.c:26: 这行显示了当前所在的函数是buildTree以及函数参数为空。而 tree3_01.c:26 则表示这是在源文件 tree3_01.c 的第 26 行。
- 当前程序执行到了 buildTree() 函数的开头,即第 26 行【未执行】
在buildTree函数内部单步执行用到的还是n,除非需要进入buildTree函数里面的其他函数才用到s。
a. 第一层:根节点赋值
此时树结构如下:
b. 第二层:节点赋值
此时树结构如下:
c. 第三层:节点赋值
此时树结构如下:
d. 第四层:节点赋值
此时树结构如下:
e. 退出buildTree函数
连续多次单步执行 n 即可
🌻5. 单步执行 s 进入traverseTree函数内部:跟踪输出结果
next
next和n等价。
跟踪输出的详细过程如下:
跟踪递归输出显示的输出结果为:1 2 4 8 9 5 3 6 7
这和预期输出的结果保持一致。
🌻6. 跟踪错误
单步执行 n 内容显示:
Program received signal SIGSEGV, Segmentation fault.
0x00005555555553d7 in main () at tree3_01.c:58
58 *ptr = 10; // 这里将会产生段错误
这个输出是 GDB 在程序运行时遇到段错误时所提供的信息:
- Program received signal SIGSEGV, Segmentation fault.:
这表示程序接收到了 SIGSEGV 信号,即段错误(Segmentation fault)信号。段错误通常发生在试图访问未分配给程序的内存或者访问已释放的内存时。- 0x00005555555553d7 in main () at tree3_01.c:58:这部分提供了造成段错误的代码位置信息。其中:
0x00005555555553d7
是导致段错误的指令的地址。main ()
表示段错误发生在main
函数内部。tree3_01.c:58
指明了出错的源文件以及代码所在的行数,即在文件tree3_01.c
的第 58 行。
- *58 ptr = 10; // 这里将会产生段错误:
这是在发生段错误的位置处的代码。具体地,这行代码尝试将值10
写入指针ptr
所指向的内存地址,但是ptr
指向了一个空地址,因此导致了段错误。
现在我们需要进一步分析,为什么会发生段错误。可以使用以下几种方法:
a. 查看指针 ptr 的值
在发生段错误之前,可以查看指针 ptr 的值,看它是否为 NULL。
p ptr
这个输出表示指针 ptr
的值是 0x0
,即空指针。
(int *)
表示这是一个指向整型数据的指针。0x0
是十六进制表示的地址,通常表示空指针。
因此,(int *) 0x0
表示指针 ptr
当前指向内存地址为 0x0
,即空指针,那么后续执行的 *ptr = 10;
就会引发段错误。
b. 查看 ptr 所指向的地址
x ptr
查看指针 ptr 所指向的地址中的内容。
x ptr
输出表示 GDB 尝试查看指针 ptr
所指向的内存地址上的内容时出现了问题:
0x0:
表示要查看的内存地址为0x0
。Cannot access memory at address 0x0
意味着 GDB 无法访问内存地址0x0
。
说明:
- GDB 无法访问内存地址
0x0
是因为这个地址通常被操作系统保留为无效地址,用来表示空指针或者未分配的内存。因此,当 GDB 尝试访问地址0x0
时,操作系统会阻止这种访问,因为这个地址不属于程序的有效内存范围。 - 通常情况下,访问空指针会导致程序出现段错误(Segmentation fault),这是因为试图在未分配的内存地址上读取或写入数据会导致操作系统干预并终止程序的执行,以保证系统的稳定性和安全性。
综合这些信息,由于 ptr
是空指针,即其指向的内存地址为 0x0,
会导致错误。
c. 回溯调用堆栈
可以使用 backtrace
(或bt)命令来查看调用堆栈,确定是从哪个函数调用了 main 函数并传递了一个空指针。
bt
输出表示了当前的函数调用堆栈情况,其中:
#0
:表示当前所在的调用堆栈帧的索引,从 0 开始计数。0x00005555555553d7 in main () at tree3_01.c:58
:说明当前位于main
函数内,位于文件tree3_01.c
的第 58 行。
输出表明程序在 main
函数的第 58 行出现了段错误(Segmentation fault),导致程序终止。
d. 查看核心转储文件
如果程序产生了核心转储文件,可以使用 GDB 打开它并查看导致段错误的堆栈跟踪信息。
gdb program core
program是可执行文件
core是coredump文件
gdb tree3_01 /tmp/dump/cores/core_tree3_01.50497_1712891407
其中gdb tree3_01 /tmp/dump/cores/core_tree3_01.50497_1712891407等价于
gdb ./tree3_01 /tmp/dump/cores/core_tree3_01.50497_1712891407
然后使用 backtrace(或bt)
命令来查看堆栈跟踪信息。
bt
这是 bt
命令的输出,表明当前程序执行时的函数调用栈:
#0
: 表示当前栈帧的序号,这里是第一个栈帧。0x0000564e4be613d7
: 这是当前正在执行的函数main
的内存地址。main ()
: 表示当前执行的函数是main
。at tree3_01.c:58
: 表示main
函数位于tree3_01.c
文件中,并且是在第 58 行开始的。这里的tree3_01.c
是源代码文件名,而58
则是指示了具体的行号。
🌞4. gdb技巧