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 # |
取消对变量的跟踪 |