在案例中我使用c语言编写了一个简单的四层二叉树进行 GDB 调试练习。这个程序故意在后面引发了一个段错误,导致程序崩溃。文章将使用 GDB 来诊断这个问题。
建议阅读前先查看gdb的技巧 传送门:【GDB调试技巧】提高gdb的调试效率-CSDN博客
建议先配置一下.c文件使其显示行数【方便后续快速定位bug】。默认情况下,GDB 不会在每次调试时自动显示行号。
编辑 Vim 的配置文件 ~/.vimrc(如果不存在则创建它),并添加以下行:set number
详细步骤如下:
打开配置文件 ~/.vimrc
nano ~/.vimrc
文件内容添加
set number
效果图如下:
然后运行以下命令使其生效:
source ~/.bashrc
这样使用vim 打开文件就会显示行数了
使用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
在使用GNU调试器(GDB)时,以下是一些常用的命令:
run
(或 r
): 启动程序并开始调试。break
(或 b
): 在指定的位置设置断点。continue
(或 c
): 继续执行程序直到下一个断点。step
(或 s
): 单步执行程序,进入到函数中。next
(或 n
): 单步执行程序,跳过函数内部的细节。print
(或 p
): 打印变量的值。backtrace
(或 bt
): 打印函数调用栈。list
(或 l
): 显示源代码。info
(或 i
): 显示调试信息,比如当前位置、变量类型等。quit
(或 q
): 退出调试器。测试程序是一个简单的打印四层二叉树的c语言程序。
对于树TreeNode结构体和创建树节点createNode函数属于常规操作【不做分析】。
程序中的buildTree函数构建了一颗四层二叉树,并使用traverseTree函数先序遍历打印二叉树的数据结构:1 2 4 8 9 5 3 6 7
现在,启动 GDB 并加载程序:
gdb ./tree3_01
进入 GDB,可以执行下列步骤来逐步调试:
在程序出错的地方设置断点以停止程序执行,并检查变量。
break main
break main与b main等价。
这段输出是在 GDB 中设置断点的结果:
run
run和r等价
这个输出表明程序已经成功启动,并且停在了之前设置的断点处,也就是在 main 函数的第 49 行:
接着,输出显示了程序停在了 main 函数的第 49 行:
现在可以使用 GDB 的其他命令来查看程序状态,比如打印变量的值、单步执行等。
可以使用 print 命令,后跟想要打印的变量名。
print root
print root和p root等价
这会打印 root 变量的值,即指向树根节点的指针。在这里,我们期望 root 指向一个已经创建好的二叉树的根节点。
打印 root 变量的结果显示为 (TreeNode *) 0x0,这意味着 root 指针当前指向了内存地址 0x0,即空指针【也证明了run之后到达断点的第49行代码未执行】。
step
step和s等价
step 命令进入 buildTree() 函数后,GDB 显示了当前所在的位置和执行的下一行代码。
在buildTree函数内部单步执行用到的还是n,除非需要进入buildTree函数里面的其他函数才用到s。
此时树结构如下:
此时树结构如下:
此时树结构如下:
此时树结构如下:
连续多次单步执行 n 即可
next
next和n等价。
跟踪输出的详细过程如下:
跟踪递归输出显示的输出结果为:1 2 4 8 9 5 3 6 7
这和预期输出的结果保持一致。
单步执行 n 内容显示:
Program received signal SIGSEGV, Segmentation fault. 0x00005555555553d7 in main () at tree3_01.c:58 58 *ptr = 10; // 这里将会产生段错误
这个输出是 GDB 在程序运行时遇到段错误时所提供的信息:
0x00005555555553d7
是导致段错误的指令的地址。main ()
表示段错误发生在 main
函数内部。tree3_01.c:58
指明了出错的源文件以及代码所在的行数,即在文件 tree3_01.c
的第 58 行。10
写入指针 ptr
所指向的内存地址,但是 ptr
指向了一个空地址,因此导致了段错误。
现在我们需要进一步分析,为什么会发生段错误。可以使用以下几种方法:
在发生段错误之前,可以查看指针 ptr 的值,看它是否为 NULL。
p ptr
这个输出表示指针 ptr
的值是 0x0
,即空指针。
(int *)
表示这是一个指向整型数据的指针。0x0
是十六进制表示的地址,通常表示空指针。因此,(int *) 0x0
表示指针 ptr
当前指向内存地址为 0x0
,即空指针,那么后续执行的 *ptr = 10;
就会引发段错误。
x ptr
查看指针 ptr 所指向的地址中的内容。
x ptr
输出表示 GDB 尝试查看指针 ptr
所指向的内存地址上的内容时出现了问题:
0x0:
表示要查看的内存地址为 0x0
。Cannot access memory at address 0x0
意味着 GDB 无法访问内存地址 0x0
。说明:
0x0
是因为这个地址通常被操作系统保留为无效地址,用来表示空指针或者未分配的内存。因此,当 GDB 尝试访问地址 0x0
时,操作系统会阻止这种访问,因为这个地址不属于程序的有效内存范围。综合这些信息,由于 ptr
是空指针,即其指向的内存地址为 0x0,
会导致错误。
可以使用 backtrace
(或bt)命令来查看调用堆栈,确定是从哪个函数调用了 main 函数并传递了一个空指针。
bt
输出表示了当前的函数调用堆栈情况,其中:
#0
:表示当前所在的调用堆栈帧的索引,从 0 开始计数。0x00005555555553d7 in main () at tree3_01.c:58
:说明当前位于 main
函数内,位于文件 tree3_01.c
的第 58 行。输出表明程序在 main
函数的第 58 行出现了段错误(Segmentation fault),导致程序终止。
如果程序产生了核心转储文件,可以使用 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
则是指示了具体的行号。