在软件开发过程中,调试是一个不可或缺的环节。GDB(GNU Debugger)和CGDB(基于GDB的图形化界面调试器)是Linux下常用的调试工具,它们可以帮助开发者深入了解程序的运行状态,快速定位并修复问题。本文将通过一个简单的示例程序,详细介绍如何使用GDB/CGDB进行调试。
一、示例程序
首先,我们来看一个简单的C语言示例程序mycmd.c
,该程序计算从start
到end
的整数之和,并输出结果。
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;
}
二、编译与调试准备
我们先来聊一个话题:软件发布的模式有两种:
- debug模式:Debug模式主要用于开发调试阶段,包含调试信息,方便开发者查找问题,但执行效率低、文件体积大。
- 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. 查看源代码
list
或l
:显示源代码,从上次位置开始,每次列出10行。list/l 10
:从第10行开始显示源代码。list/l 函数名
:列出指定函数的源代码,例如list/l main
。list/l 文件名:行号
:列出指定文件的源代码,例如list/l mycmd.c:1
。l 回车:
依次显示其余源代码。
3. 程序执行控制
run
或r
:从程序开始连续执行。next
或n
:单步执行,不进入函数内部。step
或s
:单步执行,进入函数内部。finish
:执行到当前函数返回,然后停止。
4. 设置断点
break
或b [文件名:]行号
:在指定行号设置断点,例如break 10
或break test.c:10
。
注意:只有打断点时是使用行号,其余对断电的控制是要使用编号的!!!
break 函数名
:在函数开头设置断点,例如break main
。info break
或info b
:查看当前所有断点的信息。
5. 查看变量和表达式
print
或p 表达式
:打印表达式的值,例如print start+end
。p 变量
:打印指定变量的值,例如p x
。set var 变量=值
:修改变量的值,例如set var i=10
。
6. 继续执行和停止
continue
或c
:从当前位置开始连续执行程序。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
。backtrace
或bt
:查看当前执行栈的各级函数调用及参数。
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进行调试:
-
编译并启动GDB
gcc -g -o array_sum array_sum.c gdb ./array_sum
-
设置断点 在
sumArray
函数的开始处设置断点,以便进入该函数时暂停程序。break sumArray
-
运行程序
run
-
查看变量和执行流程 程序在
sumArray
函数处暂停后,查看变量sum
和i
的初始值。info locals
输出显示
sum = 0
,i = 0
,这是正常的。 -
使用until命令 我们想快速执行完第一次循环迭代,使用until命令跳到循环的下一次迭代开始处。
until 7
这里假设循环体的代码在第7行。
-
检查变量值 再次查看变量值,发现
sum
已经加上了数组的第一个元素,i
也自增了。info locals
-
使用finish命令 为了确定问题是否在
sumArray
函数内,使用finish命令完成函数执行。finish
函数执行完成后,返回到
main
函数。查看返回值result
,发现它比预期的数组元素和要大。 -
分析问题 通过之前的调试步骤,我们怀疑问题出在
sumArray
函数的循环条件上。回顾代码,发现循环条件是i <= size
,这会导致数组越界访问。因为数组索引是从0开始的,应该使用i < size
。 -
修复错误 修改代码中的循环条件为
i < size
,重新编译并运行程序,这次输出结果正确。
通过这个过程,我们利用GDB的断点设置、变量查看、until和finish命令,成功定位并修复了程序中的逻辑错误。
五、总结
通过本文的介绍,相信你已经对GDB/CGDB调试器的使用有了基本的了解。在实际开发中,熟练掌握这些调试工具将大大提高你的开发效率,帮助你快速定位和修复程序中的问题。无论是简单的逻辑错误,还是复杂的内存泄漏或并发问题,GDB/CGDB都能为你提供强大的支持。希望本文能成为你在Linux开发旅程中的一个有用的参考。