Linux:调试器 - gdb
gbd基本概念
GDB (GNU Debugger) 是一个强大的命令行调试工具,用于调试各种编程语言(如C、C++、Java、Python等)编写的程序。使用 gdb可以帮助开发人员更快地定位和修复程序中的缺陷,提高代码质量和开发效率。它是 Linux/Unix 系统上最常用的调试工具之一。
先在Linux主机上安装gdb:
powershell
yum install -y gdb
该指令需要root权限,要么sudo进行提权,要么以root身份执行。
如果一个可执行程序想要被gdb调试,那么该可执行程序必须带有调试信息,也就是以debug形式发布。我们现在有一个test.c源文件:
如果直接使用gcc那么其编译出来就是release版本:
powershell
gcc -o test-r test.c
带上-g选项后,gcc会以debug形式编译:
powershell
gcc -g -o test-d test.c

可以看到的是,debug版本的可执行程序test-d明显比release版本的大。我们可以通过readelf指令来查看可执行文件中有没有调试信息,可执行文件也是有固定格式的,这个格式叫做ELF,而readelf指令就是用于查看可执行文件内部内容的。
先查看release版本的文件:
powershell
readelf -S test-r | grep debug
以上指令,管道左侧用于输出可执行文件内的内容,右侧用于筛选含debug的字段,最后该指令什么也没有输出,说明release版本内部不存在debug信息,也就是调试信息。
再查看debug版本的文件:
powershell
readelf -S test-d | grep debug
输出结果如下:

可见该文件内部确实有debug调试信息。
随后我们就可以直接用gdb来调试可执行程序了:
powershell
gdb test-d
当看到以下页面,说明成功开始调试了:

如果想退出,输入q或者ctrl + d。
gbd调试
我以以下代码为例,来进行调试示范:
c
#include <stdio.h>
int getNum(int n)
{
int sum = 0;
int j;
for(j = 1; j <= n; j++)
{
sum += j;
}
return sum;
}
int main()
{
int i, num = 0;
for(i = 0; i < 10; i++)
{
num += getNum(i);
}
printf("%d\n", num);
return 0;
}
浏览
l #:列出以#行为中心的10行代码
l:从上一次的最后一行开始,列出往后的10行代码
此处的l也可以改为list。
第一次执行l 1,就会列出从第1行开始的代码:

再次输入l,则会从上一次的后一行代码开始,也就是第11行开始:

输入l 16:

其不是从第16行开始,而是把第16行放在最中间,之前l 1从第一行开始,是因为第一行上面没有代码了。
l 函数名:列出某个函数的源代码
比如l main,就是列出main函数的代码:

不过其不是把main放在第一行,而是把main放在中心。
断点
b #:在行号为#处设置一个断点、
b 函数名:在函数的开头设置一个断点
b是break的简写,此处的b改为break也可以。

现在我们要在第22行设置断点,输入b 22:

其显示我们把断点设置在了第22行,断点序号为1。
再给getNum函数设置一个断点,b getNum:

其显示我们把断点设置在了第5行,断点序号为2。
如果想查看我们设置过的断点:
info b:查看断点信息

此时就列出了目前所有的断点信息,Num表示断点编号;Enb表示当前断点是否生效;What描述了该断点的信息。
d #:删除编号为#的断点
此处d为delete的缩写,把d换为delete也可以。
使用d 2,把编号为2的断点删掉:

此时再info b,就只剩下编号为1的断点了。
disable #:禁用编号为#的断点
使用disable 1,把1号断点禁止:

再次info b,可以看到一号断点的Enb属性变为n了,表示该断点失效了。
enable #:启用编号为#的断点
使用enable 1,把1号断点启用:

再次info b,可以看到一号断点的Enb属性变为y了,表示该断点启用了。
总结一下断点相关命令:
| 命令 | 功能 |
|---|---|
b # |
在行号为#处设置一个断点 |
b 函数名 |
在函数的开头设置一个断点 |
info b |
查看断点信息 |
d # |
删除编号为#的断点 |
disable # |
禁用编号为#的断点 |
enable # |
启用编号为#的断点 |
运行
r:运行程序
此处r是run的简写,使用run也可以

对当前程序使用r后,直接执行到了结尾,并输出结果165,exit normally表示程序正常退出。
现在我们使用b getNum在getNum函数上打一个断点,再次执行r指令:

可以看到,此时没有直接执行完程序,而是执行到断点处就停止了。我们再执行一次r:

其发出询问:"是否要从头开始执行",也就是说第一次使用r指令,会执行到下一个断点,如果没有断点就执行到程序结束,但是每次使用r都必须是从头开始执行的。因此r指令一般用于进入程序,后续的调试一般不用r。
c:执行到下一个断点
此处的c是continue的缩写,使用continue也可以。
对刚刚的程序执行c:

第一次执行r指令,到达第一个断点,也就是第一次调用getNum的时候,此时参数n = 0。第二次执行c指令,到达下一个断点,第二次调用getNum此时参数n = 1。因此c用于断点之间的跳转。
n:逐过程调试
现在我们删除原先的getNum断点,把断点打在第22行:

也就是语句num += getNum(i);处。
对该程序多次使用n:

第一次执行n,停在了for循环的语句;第二次执行n,停在了num += getNum(i);;第三次执行n,停在了for循环的语句。
逐过程调试的特点在于不会进入函数内部,把函数当成一个语句执行。
s:逐语句调试
示例:

一开始我们处于num += getNum(i);中,此时执行s指令,其直接跳转到了getNum函数的内部,到达其第一条语句int sum = 0;。
逐语句调试的特点在于会进入函数内部,详细展示函数内部的执行细节。
finish:执行到当前函数返回
示例:

一开始我们处于getNum函数的第一条语句int sum = 0;处,此时直接执行finish指令,跳转到了函数结束,并告知本次调用函数返回值为6。
总结:
| 命令 | 功能 |
|---|---|
r |
运行程序 |
c |
执行到下一个断点 |
n |
逐过程调试 |
s |
逐语句调试 |
finish |
执行到当前函数返回 |
变量
我们也可以在gdb中随时查看变量的值。
p #:输出变量值
示例:

现在处于某一次调用getNum的过程中,使用p sum得到当前sum = 15;p j得到当前j = 5;p n得到当前n = 8。
display #:跟踪名为#的变量,每次调试都会输出该变量的值
示例:
先跟踪n,sum,j三个变量:

支持c进行调试:

可以看到,其附带输出了n,sum,j的值。
每个变量前面都要一个数字,这是每个变量的编号。
undisplay #:取消对编号为#的变量的跟踪
示例:

一开始跟踪了n,sum,j三个变量,此时执行指令undisplay 2,就取消跟踪了sum变量。再次调试时,就没有sum变量了。
总结:
| 命令 | 功能 |
|---|---|
p # |
输出变量值 |
display # |
跟踪一个变量,每次调试都会输出该变量的值 |
undisplay # |
取消对变量的跟踪 |