调试器gdb/cgdb的使用

在软件开发过程中,调试是一个不可或缺的环节。GDB(GNU Debugger)和CGDB(基于GDB的图形化界面调试器)是Linux下常用的调试工具,它们可以帮助开发者深入了解程序的运行状态,快速定位并修复问题。本文将通过一个简单的示例程序,详细介绍如何使用GDB/CGDB进行调试。

一、示例程序

首先,我们来看一个简单的C语言示例程序mycmd.c,该程序计算从startend的整数之和,并输出结果。

cpp 复制代码
// mycmd.c
#include <stdio.h>

int Sum(int s, int e)
{
    int result = 0;
    for (int i = s; i <= e; i++)
    {
        result += i;
    }
    return result;
}

int main()
{
    int start = 1;
    int end = 100;
    printf("I will begin\n");
    int n = Sum(start, end);
    printf("running done, result is: [%d-%d]=%d\n", start, end, n);
    return 0;
}

二、编译与调试准备

我们先来聊一个话题:软件发布的模式有两种:

  1. debug模式:Debug模式主要用于开发调试阶段,包含调试信息,方便开发者查找问题,但执行效率低、文件体积大。
  2. release模式:Release模式用于软件发布,经过优化,执行效率高、文件体积小,但不包含调试信息,难以调试。

要注意:Linux下我们编译好的代码是无法直接调试的:

在Linux环境下,使用GCC编译器编译源代码时,需要添加-g选项以生成包含调试信息的可执行文件。默认情况下,GCC编译器生成的是release模式的二进制程序,不包含调试信息,因此无法使用GDB进行调试。

也就是说:gcc/g++默认的工作模式是release模式

bash 复制代码
$ gcc mycmd.c -o mycmd # 默认模式,不支持调试
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=82f5cbaada10a9987d9f325384861a88d278b160, for GNU/Linux 3.2.0, not stripped

为了进行调试,我们需要重新编译源代码,并添加-g选项:

bash 复制代码
$ gcc mycmd.c -o mycmd -g # debug模式
$ file mycmd
mycmd: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3d5a2317809ef86c7827e9199cfefa622e3c187f, for GNU/Linux 3.2.0, with debug_info, not stripped

我们会发现加了-g选项进行gcc/g++编译,可执行程序的大小变大了,我们可以看到:

relaese下不包含debug调试信息。

注意注意!!!:程序要进行调试,必须是debug模式!!!😡(gdb操作携带调试信息的exe)

三、GDB/CGDB常用命令

补:安装GDB/CGDB
sudo apt update
sudo apt install gdb

sudo apt update
sudo apt install cgdb

1. 启动调试

使用gdb命令启动调试器,并指定要调试的可执行文件:千万不要去调试源文件!!!不然干嘛还区分release和debug😱

bash 复制代码
$ gdb mycmd

2. 查看源代码

  • listl:显示源代码,从上次位置开始,每次列出10行。
  • list/l 10:从第10行开始显示源代码。
  • list/l 函数名:列出指定函数的源代码,例如list/l main
  • list/l 文件名:行号:列出指定文件的源代码,例如list/l mycmd.c:1
  • l 回车: 依次显示其余源代码。

3. 程序执行控制

  • runr:从程序开始连续执行。
  • nextn:单步执行,不进入函数内部。
  • steps:单步执行,进入函数内部。
  • finish:执行到当前函数返回,然后停止。

4. 设置断点

  • breakb [文件名:]行号:在指定行号设置断点,例如break 10break test.c:10

注意:只有打断点时是使用行号,其余对断电的控制是要使用编号的!!!

  • break 函数名:在函数开头设置断点,例如break main
  • info breakinfo b:查看当前所有断点的信息。

5. 查看变量和表达式

  • printp 表达式:打印表达式的值,例如print start+end
  • p 变量:打印指定变量的值,例如p x
  • set var 变量=值:修改变量的值,例如set var i=10

6. 继续执行和停止

  • continuec:从当前位置开始连续执行程序。
  • delete breakpoints:删除所有断点。
  • delete breakpoints n:删除序/编号为n的断点,例如delete breakpoints 1/d 1

注意:在gdb当中,如果不退出gdb,断点编号依次递增,并不会因为删除断点后编号的覆盖(假设设置了3个断点,删除某几个断点后,在不退出gdb的情况下,再设置一个断点,该断点编号是4)

  • disable breakpoints:禁用所有断点。
  • enable breakpoints:启用所有断点。

7. 查看调试信息

  • info breakpoints/info b:查看当前设置的断点列表。
  • display 变量名:跟踪显示指定变量的值(每次停止时),地址等等,例如display x

这个就是我们的监视,是常显示/跟踪显示的效果。

  • undisplay 编号:取消对指定编号的变量的跟踪显示,例如undisplay 1
  • until 行号:执行到指定行号,例如until 20
  • backtracebt:查看当前执行栈的各级函数调用及参数。
  • info locals:查看当前栈帧的局部变量值。

locals就是指代所有的局部变量,可以用于display等,方便操作。

8. 退出调试

  • quit:退出GDB调试器。

我们实际操作发现GDB好难读,好难看,我们以后可以使用CGDB,因为CGDB的命令和GDB是一样的,而去CGDB结合了GDB的强大功能和图形化界面的易用性,使得调试过程更加直观、高效和便捷。无论是新手还是经验丰富的开发者,都可以通过CGDB快速上手并提高调试效率。如果你经常使用GDB进行调试,尝试使用CGDB可能会给你带来更好的调试体验。(动态呈现代码)

注意:gdb+回车是执行最近的指令,方便操作:比如逐语句,逐过程。

四、实际调试示例

假设我们想要调试上述示例程序mycmd.c,并查看Sum函数的执行过程。首先,我们启动CGDB调试器:

bash 复制代码
$ gdb mycmd

然后,我们在main函数和Sum函数的入口处分别设置断点:

bash 复制代码
(gdb) break main
(gdb) break Sum

接下来,我们开始运行程序:

bash 复制代码
(gdb) run

程序会在main函数的入口处停止。此时,我们可以查看当前的源代码:

bash 复制代码
(gdb) list

接着,我们使用next/n命令单步执行,直到进入Sum函数:(相当于逐过程F10)

bash 复制代码
(gdb) next

Sum函数内部,我们可以使用step/s命令逐行执行(相当于逐语句F11),并查看变量result的值:

bash 复制代码
(gdb) step
(gdb) print result

当我们完成对Sum函数的调试后,可以使用finish命令执行到函数返回:

bash 复制代码
(gdb) finish

最后,我们可以继续执行程序,直到程序结束:

bash 复制代码
(gdb) continue

如果需要退出调试器,可以使用quit命令:

bash 复制代码
(gdb) quit

我们真正知道调试的本质是什么吗?dgb只是一个让我们可视代码的能力,我们才是解决问题的主要,要知道为什么这个变量不是10而是0,为什么这个指针指向的是野指针?等等问题

我们应该找到问题,再查看代码上下文,在gdb中,是有很多用来找到问题的命令:

  • 断点的本质,是把代码进行块级别划分, 以块为单位进行快速定位区域!
  • finish->确定问题是否在函数内
  • until->局部区域快速执行

假设我们有一个C语言程序,其功能是计算一个整数数组中所有元素的和。程序代码如下:

cpp 复制代码
#include <stdio.h>

int sumArray(int arr[], int size) {
    int sum = 0;
    for (int i = 0; i <= size; i++) { // 这里存在逻辑错误,应该是 i < size
        sum += arr[i];
    }
    return sum;
}

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = sizeof(numbers) / sizeof(numbers[0]);
    int result = sumArray(numbers, size);
    printf("The sum is: %d\n", result);
    return 0;
}

编译并运行程序后,发现输出结果不正确。于是我们使用GDB进行调试:

  1. 编译并启动GDB

    gcc -g -o array_sum array_sum.c
    gdb ./array_sum
    
  2. 设置断点sumArray函数的开始处设置断点,以便进入该函数时暂停程序。

    break sumArray
    
  3. 运行程序

    run
    
  4. 查看变量和执行流程 程序在sumArray函数处暂停后,查看变量sumi的初始值。

    info locals
    

    输出显示sum = 0i = 0,这是正常的。

  5. 使用until命令 我们想快速执行完第一次循环迭代,使用until命令跳到循环的下一次迭代开始处。

    until 7
    

    这里假设循环体的代码在第7行。

  6. 检查变量值 再次查看变量值,发现sum已经加上了数组的第一个元素,i也自增了。

    info locals
    
  7. 使用finish命令 为了确定问题是否在sumArray函数内,使用finish命令完成函数执行。

    finish
    

    函数执行完成后,返回到main函数。查看返回值result,发现它比预期的数组元素和要大。

  8. 分析问题 通过之前的调试步骤,我们怀疑问题出在sumArray函数的循环条件上。回顾代码,发现循环条件是i <= size,这会导致数组越界访问。因为数组索引是从0开始的,应该使用i < size

  9. 修复错误 修改代码中的循环条件为i < size,重新编译并运行程序,这次输出结果正确。

通过这个过程,我们利用GDB的断点设置、变量查看、until和finish命令,成功定位并修复了程序中的逻辑错误。

五、总结

通过本文的介绍,相信你已经对GDB/CGDB调试器的使用有了基本的了解。在实际开发中,熟练掌握这些调试工具将大大提高你的开发效率,帮助你快速定位和修复程序中的问题。无论是简单的逻辑错误,还是复杂的内存泄漏或并发问题,GDB/CGDB都能为你提供强大的支持。希望本文能成为你在Linux开发旅程中的一个有用的参考。

相关推荐
BroccoliKing1 小时前
【Linux】编辑器之神vim使用教程
linux·编辑器·vim
唐可盐2 小时前
CentOS 7 下 MySQL 5.7 的详细安装与配置
linux·mysql·centos
每天敲200行代码3 小时前
Linux开发工具--vim编辑器-gcc/g++编译器-gdb调试器
linux·c++·编辑器·vim·gdb
流星白龙3 小时前
【Linux】5.Linux常见指令以及权限理解(3)
android·linux
carl.xu4 小时前
ubuntu 配置OpenOCD与RT-RT-thread环境的记录
linux·运维·ubuntu
如何学会学习?4 小时前
5 list 语法
linux·list·shell
九皇叔叔5 小时前
Linux 获取文本部分内容
linux·运维·服务器
重生之我是小白菜6 小时前
在 Linux 下Ubuntu创建同权限用户
linux·运维·ubuntu
sun0077007 小时前
键盘鼠标共享工具Barrier(kail与windows操作系统)
linux