前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【gdb调试】在ubuntu环境使用gdb调试一棵四层二叉树的数据结构详解

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

作者头像
SarPro
发布2024-04-23 08:25:51
650
发布2024-04-23 08:25:51
举报
文章被收录于专栏:【计网】Cisco【计网】Cisco

?1. 整体思路

在案例中我使用c语言编写了一个简单的四层二叉树进行 GDB 调试练习。这个程序故意在后面引发了一个段错误,导致程序崩溃。文章将使用 GDB 来诊断这个问题。

?2. 准备内容

建议阅读前先查看gdb的技巧 传送门:【GDB调试技巧】提高gdb的调试效率-CSDN博客

?2.1 配置.c文件

建议先配置一下.c文件使其显示行数【方便后续快速定位bug】。默认情况下,GDB 不会在每次调试时自动显示行号。

编辑 Vim 的配置文件 ~/.vimrc(如果不存在则创建它),并添加以下行:set number

详细步骤如下:

打开配置文件 ~/.vimrc

代码语言:javascript
复制
nano ~/.vimrc

文件内容添加

代码语言:javascript
复制
set number

效果图如下:

然后运行以下命令使其生效:

代码语言:javascript
复制
source ~/.bashrc

这样使用vim 打开文件就会显示行数了


?2.2 准备测试程序

使用vim文本编辑器新建一个.c文件

代码语言:javascript
复制
vim tree3_01.c

输入测试程序:

代码语言:javascript
复制
#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编译:

代码语言:javascript
复制
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 并加载程序:

代码语言:javascript
复制
gdb ./tree3_01

进入 GDB,可以执行下列步骤来逐步调试:


?1. 设置断点

在程序出错的地方设置断点以停止程序执行,并检查变量。

代码语言:javascript
复制
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. 启动程序并执行到断点处
代码语言:javascript
复制
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 命令,后跟想要打印的变量名。

代码语言:javascript
复制
print root

print root和p root等价

这会打印 root 变量的值,即指向树根节点的指针。在这里,我们期望 root 指向一个已经创建好的二叉树的根节点。

打印 root 变量的结果显示为 (TreeNode *) 0x0,这意味着 root 指针当前指向了内存地址 0x0,即空指针【也证明了run之后到达断点的第49行代码未执行】。

?4. 单步执行 s 进入buildTree函数内部
代码语言:javascript
复制
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函数内部:跟踪输出结果
代码语言:javascript
复制
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 行。
  3. *58 ptr = 10; // 这里将会产生段错误: 这是在发生段错误的位置处的代码。具体地,这行代码尝试将值 10 写入指针 ptr 所指向的内存地址,但是 ptr 指向了一个空地址,因此导致了段错误。

现在我们需要进一步分析,为什么会发生段错误。可以使用以下几种方法:


a. 查看指针 ptr 的值

在发生段错误之前,可以查看指针 ptr 的值,看它是否为 NULL。

代码语言:javascript
复制
p ptr

这个输出表示指针 ptr 的值是 0x0,即空指针。

  • (int *) 表示这是一个指向整型数据的指针。
  • 0x0 是十六进制表示的地址,通常表示空指针。

因此,(int *) 0x0 表示指针 ptr 当前指向内存地址为 0x0,即空指针,那么后续执行的 *ptr = 10; 就会引发段错误。


b. 查看 ptr 所指向的地址

x ptr 查看指针 ptr 所指向的地址中的内容。

代码语言:javascript
复制
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 函数并传递了一个空指针。

代码语言:javascript
复制
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文件
代码语言:javascript
复制
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) 命令来查看堆栈跟踪信息。

代码语言:javascript
复制
bt

这是 bt 命令的输出,表明当前程序执行时的函数调用栈:

  • #0: 表示当前栈帧的序号,这里是第一个栈帧。
  • 0x0000564e4be613d7: 这是当前正在执行的函数 main 的内存地址。
  • main (): 表示当前执行的函数是 main
  • at tree3_01.c:58: 表示 main 函数位于 tree3_01.c 文件中,并且是在第 58 行开始的。这里的 tree3_01.c 是源代码文件名,而 58 则是指示了具体的行号。

?4. gdb技巧

【GDB调试技巧】提高gdb的调试效率-CSDN博客

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-04-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ?1. 整体思路
  • ?2. 准备内容
    • ?2.1 配置.c文件
      • ?2.2 准备测试程序
        • ?2.3 GDB调试基础
        • ?3. GDB调试四层二叉树
          • ?3.1 测试程序分析
            • ?3.2 gdb分析
              • ?1. 设置断点
              • ?2. 启动程序并执行到断点处
              • ?3. 打印变量的值
              • ?4. 单步执行 s 进入buildTree函数内部
              • ?5. 单步执行 s 进入traverseTree函数内部:跟踪输出结果
              • ?6. 跟踪错误
          • ?4. gdb技巧
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com