【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
目录
相关文章
|
7天前
|
存储 算法 Java
算法系列之数据结构-二叉树
树是一种重要的非线性数据结构,广泛应用于各种算法和应用中。本文介绍了树的基本概念、常见类型(如二叉树、满二叉树、完全二叉树、平衡二叉树、B树等)及其在Java中的实现。通过递归方法实现了二叉树的前序、中序、后序和层次遍历,并展示了具体的代码示例和运行结果。掌握树结构有助于提高编程能力,优化算法设计。
40 9
 算法系列之数据结构-二叉树
|
2月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
61 12
|
2月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
60 10
|
2月前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
60 2
|
3月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
4月前
|
传感器 算法
数据结构之环境监测系统(深度优先搜索)
环境监测系统采用深度优先搜索(DFS)算法,实现实时监测和分析环境参数,如温度、湿度等。系统通过构建传感器网络图结构,利用DFS遍历网络,检测异常数据。当温度超过预设阈值时,系统将发出警告。此系统适用于工业生产、室内空调控制、农业温室管理等多种场景,提供高效的环境监测解决方案。
77 12
|
4月前
|
NoSQL 编译器 C语言
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。
C语言调试是开发中的重要技能,涵盖基本技巧如打印输出、断点调试和单步执行,以及使用GCC、GDB、Visual Studio和Eclipse CDT等工具。高级技巧包括内存检查、性能分析和符号调试。通过实践案例学习如何有效定位和解决问题,同时注意保持耐心、合理利用工具、记录过程并避免过度调试,以提高编程能力和开发效率。
81 1
|
4月前
|
机器学习/深度学习 存储 算法
数据结构实验之二叉树实验基础
本实验旨在掌握二叉树的基本特性和遍历算法,包括先序、中序、后序的递归与非递归遍历方法。通过编程实践,加深对二叉树结构的理解,学习如何计算二叉树的深度、叶子节点数等属性。实验内容涉及创建二叉树、实现各种遍历算法及求解特定节点数量。
139 4
|
4月前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
241 8
|
5月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆