【gdb调试】在ubuntu环境使用gdb调试一棵四层二叉树的数据结构详解

简介: 【gdb调试】在ubuntu环境使用gdb调试一棵四层二叉树的数据结构详解

🌞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): 单步执行程序,跳过函数内部的细节。
  • print (或 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 mainb 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 在程序运行时遇到段错误时所提供的信息:

  1. Program received signal SIGSEGV, Segmentation fault.
    这表示程序接收到了 SIGSEGV 信号,即段错误(Segmentation fault)信号。段错误通常发生在试图访问未分配给程序的内存或者访问已释放的内存时。
  2. 0x00005555555553d7 in main () at tree3_01.c:58:这部分提供了造成段错误的代码位置信息。其中:
  • 0x00005555555553d7 是导致段错误的指令的地址。
  • main () 表示段错误发生在 main 函数内部。
  • tree3_01.c:58 指明了出错的源文件以及代码所在的行数,即在文件 tree3_01.c 的第 58 行。
  1. *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

说明:

  1. GDB 无法访问内存地址 0x0 是因为这个地址通常被操作系统保留为无效地址,用来表示空指针或者未分配的内存。因此,当 GDB 尝试访问地址 0x0 时,操作系统会阻止这种访问,因为这个地址不属于程序的有效内存范围。
  2. 通常情况下,访问空指针会导致程序出现段错误(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技巧



相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
13天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
62 8
|
1月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
18 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
1月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
20 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
1月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
27 1
|
1月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
24 1
|
1月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
|
存储 算法
数据结构与算法学习十六:树的知识、二叉树、二叉树的遍历(前序、中序、后序、层次)、二叉树的查找(前序、中序、后序、层次)、二叉树的删除
这篇文章主要介绍了树和二叉树的基础知识,包括树的存储方式、二叉树的定义、遍历方法(前序、中序、后序、层次遍历),以及二叉树的查找和删除操作。
24 0
|
1月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
1月前
|
存储
【数据结构】二叉树零基础无压力上手,超详解
【数据结构】二叉树零基础无压力上手,超详解
28 0