调试器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开发旅程中的一个有用的参考。

相关推荐
予昊2 分钟前
Linux 资源控制实战分析:用 LXC 亲手造一个 “迷你虚拟机“
linux·运维·服务器
allforgood9 分钟前
管理网络安全
linux
灵晔君35 分钟前
【Linux】进程控制(一)——进程创建、进程终止与进程等待
linux·c语言·算法
wsad053244 分钟前
CentOS 10 上使用 Docker Compose 部署 Discuz X5.0 论坛完整教程
linux·docker·centos·discuz
张宇Joaquin1 小时前
openEuler编译和更换内核的方法
java·linux·服务器
小此方1 小时前
Re:Linux系统篇(五十六)线程篇 · 九:基于阻塞队列的生产者消费者模型与条件变量
linux·运维·服务器
迷途之人不知返1 小时前
自动化工程的构建
linux·自动化
Escalating_xu1 小时前
【Linux】库制作与链接原理:从静态库、动态库到 ELF、重定位、GOT 与 PLT
linux·运维·服务器
三言老师10 小时前
秘诀-如何远程SSN访问家里的八台电脑(frp 实操方案)
linux·运维·ssh
举手12 小时前
Dispatcher模块剖析
linux·c++