EdwardWong
2022/09/26阅读:37主题:姹紫
GDB调试
借助gdb
调试器可以实现以下几个功能:
-
程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量
-
可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果),即支持断点调试
-
程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误
gdb
的安装
使用包管理器快速安装
在centos
系统中,采用sudo yum -y install gdb
在ubuntu
系统中,采用的sudi apt -y install gdb
源码安装gdb
使用源码安装gdb
时一定要提前安装gcc
编译器,因为会用到/configure
和make
命令。
-
step 1 : 官网下载
gdb
源码包,gdb链接 -
step 2: 使用
tar
命令解压该文件(以.tar.gz
为例)
tar -zxvf gdb-9.2.tar.gz
-
step 3: 进入
gdb-9.2
的目录文件,创建一个gdb_build_9.2
目录并进入,为后续下载并放置安装gdb
所需的依赖项做准备。 -
step 4: 执行
../configure
命令 -
step 5: 执行
make
命令进行编译, 此执行过程可能比较长 -
step 6: 使用命令
sudo make install
进行安装 -
step 7: 查看是否安装成功
gdb -v
gdb
调试c/c++
程序
调试的前期准备(编译)
只有在源程序被编译为可执行文件并执行时,gdb
才会排上用场。但是只使用gcc
命令编译生成的可执行文件,是无法借助gdb
调试的。
gcc main.c -o main.exe
这样是没有办法进行gdb
的调试的。因为在进行调试的时候需要包含调试的必要信息,例如各行代码所在的行号、程序中所有变量名称的列表(符号表),而上面的main.exe
是没有的
因此在进行调试前需要使用gcc -g
选项编译文件。 gcc main.c -o main.exe -g
同时gcc
编译器还可以加入-O
选项支持优化和-g
一起参与编译。 gcc
编译过程的优化程度可以分为5
个等级,即O0~O4
,O0
表示不优化,O4
优化级别最高。
优化,例如省略掉代码中从未使用过的变量,直接将常量表达式用结果代替。这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。
启动gdb
调试器
在生成包含调试信息的main.exe
可执行文件的基础上,启动gdb
调试器的指令如下:
method 1 调试具有调试信息的可执行文件
gdb main.exe
--> 会出现一些免责声明,可以通过--slient
选项将信息屏蔽掉, 如gdb main.exe --slient
method 2
gdb
+ file
或者exec-file
命令指定
method 3 调试正在执行的程序
gdb
可以调试正在运行的程序。 每一个正在运行的程序都有一个或多个进程来运行,系统为了方便管理当前系统中运行的诸多进程,每个进程都配有唯一的进程号(PID)。
可以通过pidof + 文件名
得到当前运行程序的进程号。
pidof main.exe
--> 1803
gdb attach pid
或gdb -p pid
或gdb 文件名 pid
当
gdb
调试器成功连接到指定进程时,程序会赞同执行,当退出调试让程序继续运行,需要首先通过detach
命令使调试器和程序分离,然后执行quit
退出调试。
调试执行异常崩溃的程序
除了以上 3 种情况外,C 或者 C++ 程序运行过程中常常会因为各种异常或者 Bug 而崩溃,比如内存访问越界(例如数组下标越界、输出字符串时该字符串没有 \0 结束符等)、非法使用空指针等,此时就需要调试程序。
在linux
操作系统中,当程序发生崩溃时,系统可以将发生崩溃的内存数据、调用堆栈情况等信息自动记录下来并存储到core
文件中。 这叫做核心转储(core dump)。gdb
对core
文件的分析和调试提供了非常强大的功能支持。
默认情况下,linux
系统不开启core dump
,可以借助ulimit -c
或ulimit -a
指令来查看当前其他是否开启。

如果core file size
对应的值为0,表示当前系统未开启core dump
功能,可以通过ulimit -c unlimited
开启,这样当程序发生异常时,系统就会自动生成相应的core
文件。**core
文件默认生成的位置与该程序所在目录相同**


segement fault
段错误,也就是当前程序访问了不可访问的存储空间,比如访问的不存在的空间,又或者是系统保护的内存空间。
core文件的内容如下

gdb
常见的调试指令


b +行号
--> 打断点
r
--> 执行被调试的程序,会自动在第一个断点处暂停执行
c
--> 当程序在某一个断点处停止运行后,使用该命令可以继续执行,知道遇到下一个断点或程序结束
n
--> 令程序一行一行的执行
p + 变量名
--> 打印指定变量的值
l
---> 显示源程序代码的内容,包括各行代码所在行号
q
--> 终止调试
下图列出了gdb
调试的一个简单例子:

启动程序命令run
run
命令与start
命令的区别:
默认情况下,run
指令会一直执行程序,直至程序结束,如果程序中手动设置有断点,则run
指令会执行程序至第一个断点处。
而start
指令会执行程序至main()
主函数的起始位置,即在main()
函数的第一行语句处停止执行,相当于在main()
主函数起始位置设置一个断点,然后在使用run
指令启动程序。
在进行调试前的准备工作包括以下内容:
-
启动
gdb
未指定要调试的目标程序,由于各种原因未找到所指定的目标程序,这种情况下需要手动指定 -
一些
c
或c++
程序需要接受一些参数(程序中用argc
或argv[]
接收) -
目标程序在执行过程中,可能需要临时设置
PATH
环境变量 -
gdb
默认将启动时所在的目录作为工作目录,如果程序所在的目录与gdb
启动时的工作目录不同时,需要进行调整 -
输入输出重定向

-
step 1 对已经启动的 gdb
调试器,先通过l
命令验证其是否已经找到了目标程序文件。
如果没有找到 --> 'No symbol table is loaded, use the file command'

-
step 2. 对于 int main(int argc, char* argv[])
需要给主函数传递参数。 有以下三种方法可以实现传递参数
-
未启动 gdb
调试器时,可以通过--args
选项指定
gdb --args main.exe a.txt
--> 调试main.exe
程序并为其传递a.txt
字符串。
-
已经启动 gdb
调试器,借助set args
命令指定目标调试程序启动所需要的数据。

-
通过 run
或start
命令启动目标程序并传递所需要的数据
run a.txt
或者start a.txt
-
step 3 改变工作路径
启动gdb
调试器后直接使用cd /tmp/demo
-
step 4 临时修改环境变量
通过path /tmp.demo
,只能临时修改
-
输入输出重定向
run > a.txt
只有调试所需的运行环境搭建好后,才可以使用run
或者start
命令

设置普通断点命令break
,tbreak
, rbreak
break
命令常见的语法格式:
1. break/b location
2. break/b... if cond
-
location
的方式

-
cond
的方式
cond
为某个表达式。 每次程序执行到...
位置时都计算cond
的值,如果为true
,则程序在该位置暂停,反之,程序继续执行.
b 7 if num>10
--> 如果num>10
,则运行到第7
行停止

tbreak
与break
命令相似,区别在于使用tbreak
命令打的断点只会作用1次,之后会自动消失。

rbreak
命令作用对象是c/c++
程序中的函数,它会在指定函数的开头位置打断点。此段点会一直存在,不会自动消失

其中regex
是一个正则表达式,程序中的函数名只要满足正则表达式,tbreak
命令就会在函数内部的开头位置打断点。

watch
命令设置观察断点监视变量值的变化
可以通过watch
命令来监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者bug
。 借助观察断点只要某个值发生了改变,程序就会停止执行。
命令格式:watch cond
cond
就是要监控的变量或表达式

info watchpoints
--> 查看当前建立的观察点的数量
info break
--> 查看所有设置的断点
使用watch
命令时需要注意的几点如下:
当监控的变量是一个局部变量时,一旦局部变量失效,则监控操作也随之失效。
如果监控的是一个指针变量(
*p
),则watch *p
和watch p
是有区别的,watch *p
是监控p
所指数据的变化情况,而watch p
是监控p
指针本身有没有改变指向。
watch
命令同样可以监控数组中元素值的变化情况。 例如watch a
只要a
数组中存储的数据发生改变,程序就停止执行。
catch
命令设置捕捉断点
捕捉断点用于监控程序运行时某一事件的发生,例如程序发生某种异常、某一个动态库被加载,一旦目标事件发生,程序停止执行。
命令如下: catch 监控事件

注意要想使用
catch
命令,gcc
编译器的版本最低为4.8
当
catch
命令捕捉到指定的event
事件时,程序暂停执行的位置往往位某个系统库中,这时候应该使用up
命令,即可返回event
事件的源代码处。
tcatch
与tbreak
相似,只会监控一次。



借助catch
命令设置了捕捉断点,只要发生监控事件异常,程序就会暂停执行。由此当程序执行时,其会暂停至libstdc++
库中的某个位置,借助up
指令可以得知该异常发生在源码的具体位置。
监控c/c++程序动态库的加载和卸载
首先通过ldd
命令查看程序运行期间所需的动态库。

设置条件断点condition
命令
例如break ...if cond
如果后面的condition
条件成立,那么就会出现断点。
同样的对于watch
观察断点和catch
捕捉断点都可以设置条件断点。
watch expr if cond
--> 参数expr
表示要观察的变量或表达式,参数cond
用于代指某个表达式。
上述创建条件断点break...if/watch...if
的方法不适用于捕捉断点,捕捉断点需要借助condition
命令为现有捕捉断点增添一个cond
表达式,才能使其变为条件断点。
condition
命令没有缩写形式,语法如下:
condition bnum expression
(为bnum编号的断点添加或修改条件表达式)或者condition bnum
(删除bnum编号断点的条件表达式,使其变成普通的无条件断点)。
参数bnum
用于代指目标断点的编号,参数expression
表示为断点添加或修改的条件表达式。


ignore
命令
ignore
命令也可以使一个断点成为条件断点,但这里的条件并非自定义的表达式,而仅为一个整数,它用来表示该断点失效的次数,也就是说,ignore
命令可以使目标断点暂时失去作用,当断点失效的次数超出了指定的次数后,断点的功能会自动恢复。
ignore bnum count

单步调试程序next
命令
gdb
调试器提供了三种可以实现单步调试程序的方法,即使用next
、until
和step
命令。
next
,until
和step
的区别在于
next
在遇到包含调用函数的语句时,无论函数内部包含多少行代码,next
指令都会一步执行完毕。
next/n count
--> 其中count
表示单步执行多少行代码。默认为1行。
step
命令在遇到函数的时候,会进入函数内部,并在函数第一行代码处停止执行
step/s count
--> count表示一次执行的行数。
until
命令的语法
1. until/u
2. until/u 行号
不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序.
带参数的
until
命令可以后跟行号,以指示gdb
调试器直接执行到指定位置后停止。
finish
命令和return
命令, jump
命令
如果在某个函数中调试了一段时间后,可能不需要在一步步执行到函数返回处,希望直接执行完当前函数,可以使用finish
命令,与finish
命令类似的还有return
命令,他们呢都可以结束当前执行的函数。
finish
和return
函数的区别在于:
finish
命令会执行函数到正常退出,而return
命令是立即结束执行当前函数并返回。另外。return
可以指定该函数的返回值。
jump 行号
--> 跳到某一行代码处
jump
命令可以略过某些代码,直接跳到行号处的代码继续执行程序。
print
命令和display
命令
print
命令不仅可以在调试过程中输出,还可以修改指定变量或者表达式的值p result=20
。
-
print
命令的高级用法
print [option--][/fmt] expr
--> option
参数和/fmt
或者expr
之间,必须使用--
分隔。当不指定任何选项时,print
和/fmt
之间不用添加空格
option
选项:

fmt
选项:

print
命令可以支持@
和::
运算符。@
运算符用于输出数组中指定区域的元素
print first@len
--> first
用于指定数组查看区域内的首个元素的值,参数len
用于指令自first
元素开始查看的元素个数。
int array[5]=[1,2,3,4]
print array[0]@2
当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助::
运算符明确指定要查看的目标变量或表达值。
print file::variable --> file 用于指定具体的文件名,文件名要用单引号括起来。
print function::variable --> function用于指定具体所在函数的函数名

print
main.c::num
--> 查看num
全局变量的值
print main::num
--> 查看num
局部变量的值

与print
不同的地方在于,display
在调试阶段会在程序暂停执行时自动帮我们打印出来,对于使用display
查看的目标变量及表达式,都会被记录在一张列表上,可以通过info display
打印出这张表.
display expr
display/fmt expr --> 以某种格式输出变量或者表达式
## 删除自动显示列表中的变量或表达式
`undisplay num` --> num表示目标变量或者表达式的编号
delete display num
## 禁用/激活变量或表达式
disable/enable display num


禁用和删除断点
-
查看当前已建好的断点
info breakpoint[n]
info break[n] --> n为某个断点的编号,表示查看指定断点而非全部断点
info watchpoint[n] --> 查看观察断点
-
删除断点
clear 行号或函数名
--> 如果为函数名时,表示删除位于该函数入口处的所有断点。
delete/d [breakpoints] [num]
--> 如果不指明num,则删除当前程序中存在的所有断点。breakpoints
参数可有可无,num
为指定断点编号。
-
禁用/激活断点
disable/enable [breakpoints] [num...]
--> breakpoints
可有可无,num
可以有多个参数,每个参数为要禁用断点的编号,如果不指明num
,disable
会禁用当前程序中所有的断点。
enable [breakpoints][num...]
--> 激活用num...
参数指定的多个断点,如果不指定num...
,表示激活所有禁用的断点
enable [breakpoints] once num...
--> 激活以num...
为编号的多个断点,但只能使用1次,之后会自动禁用
enable [breakpoints] count num...
--> 临时激活以num...
为编号的多个断点,断点可以使用count
次,之后进入禁用状态
enable [breakpoints] delete num...
--> 激活num...
为编号的多个断点,但断点只能使用1次,之后会永久删除。
多线程的调试
对于多线程编程的调试,之后会继续补充。
后台执行调试命令
调试命令在执行过程中,是没有办法使用其他gdb
调试命令的,因此可以采用后台执行的方式。
command&
--> command
和&
之间不需要空格
并非所有调试命令都支持后台执行,gdb
支持后台执行的调试命令:

gdb
的反向调试
所谓反向调试,指的是临时改变程序的执行方向,反向执行指定行数的代码。
反向调试的常用命令:

反向记录需要先记录信息record

下面是反向调试的例子:


gdb
编辑edit
和搜索源码search
edit [location]
edit [filename]:[location]

可以指定任意的编辑器来编辑文件,进入gdb
调试前,执行如下命令export EDITOR=/usr/bin/vim
,这样会自动进入vim
编辑器,从而对当前调试的程序进行修改。 如果想要永久生效,可以在~/.bashrc
修改。
search <regexp> --> 从当前行开始向前搜索
reverse-search<regexp> --> 向后搜索
``
作者介绍