OI 代码调试 - GDB 的使用
打算写一点不涉及算法的指南。这是第二篇。讲讲怎么使用 GDB 来调试代码。要调试代码,最简单的方式就是直接打印出来看,但有时候不想打印,这时候就需要依靠一些外部工具来进行调试了。
GDB 全名为 GNU symbolic debugger,是 GNU 开发的一个支持调试多种语言的调试器。
打开 GDB
GDB 是一个命令行的调试器,意味着其没有任何 UI,全部需要通过输入指令来完成调试操作。你应该习惯这种操作,并也习惯用命令行来编译 C++ 程序,而不是使用 IDE 自带的编译。
linux
linux 下你的发行版在安装 gcc 的时候就会附带 gdb,在终端输入 gdb 即可。
Windows
如果你使用的是 MinGW,那么在 bin 文件夹下就有一个 gdb.exe
,将这个文件夹加入 PATH
即可(如果你之前就可以用 g++
来命令行编译程序,那么你应该已经加入了)。如果你使用的是其他编译器,请自行查询。
加入 PATH
之后就可以在命令行直接输入 gdb
来调出程序了。
基础使用
首先,编译你的代码。在编译时要加入 -g
选项,将调试信息也编译进可执行文件中。注意 -g
选项不应该与优化选项同时开启(-O1
,-O2
,-O3
,因为优化选项会修改代码逻辑,无法单步执行)。
然后使用 gdb 可执行文件
来进行调试。(gdb 还有一个其他的使用方法,后文讲到 coredump 的时候会提到)
gdb 执行到一个停顿时,会显示当前的代码行数以及内容。这行代码还没有执行。
来看上图学习一下一些要点:
① 如果不用 -g
编译,就没有调试信息,不能调试。
② (小知识)gdb 的 -q
开关可以取消掉启动时的一些版权信息。
③ 在 gdb 的命令行交互中,输入 q
(quit)退出。
④ 使用 -g
才能正常调试。
⑤ 使用 b [函数名/行数]
设置断点。b main
即在 main
函数设置一个断点。我们习惯这么做,来让程序从刚开始运行的时候就停下来。(这些命令下文会再系统叙述一遍)
⑥ 使用 r
(run)运行程序。在 gdb 的调试过程中,你可以多次执行程序。)
⑦ 使用 n
(next)来下一步,单步执行。
⑧ 这句说明程序执行完了。接下来还可以再输入 r
,再执行一遍。
程序执行指令
有关运行程序的指令。
run
简写为 r
。开始执行程序。在进入 gdb 的一开始是不会执行程序的。只有输入了 r
才会开始执行。
r
可以用 <
重定向,这样你的程序就可以自动文件输入,而不是从标准输入流获取输入。
break
简写为 b
。有三个主要用法
b 行数
:在执行到这一行的时候停止执行。b 函数名
:在执行到这个函数的时候停止执行。b ... if 条件
:仅在条件成立的时候停止执行。
tbreak
简写为 tb
。和 break 一样,但是触发一次后自动销毁。
continue
简写为 c
。在程序暂停后,继续执行(直到下一个断点/阻塞/程序结束)。
next, step
分别简写为 n
,s
。单步执行。
区别为 next 遇到一个函数会直接执行完整个函数,而 step 会跳进这个函数体的内部。
until
简写为 u
。执行直到…
用法和 break 一样,即:
u 行数
:在执行到这一行的。u 函数名
:在执行到这个函数。u ... if 条件
:仅在条件成立的时候执行到…。
变量查看指令
除了让代码跑起来,我们还要看看这个变量到底是什么。
简写为 p
。
使用方法:p 表达式
,查看一个表达式的值。
display
简写为 disp
。
使用方法:disp 表达式
。和 print 不同,程序每次停下来(断点,单步执行)的时候都会显示所有你 disp 的变量。
实操
以这个代码为例:
1 |
|
我们执行了如下调试,请注意我使用的命令。
1 |
|
打印大数组的一部分
注意一下第 50 行 fib[1]@20
这个表达式。有的时候我们数组开的很大,不能直接打印出来(而我们的测试数据是很小的),这个时候可以 数组某一元素@个数
来获取部分数组。(是元素,不是指针)
例:(*arr)@5
,*(arr+5)@10
,arr[7]@8
。
进阶
一般来说,有这些指令已经够你调试代码了,但有些时候还不够。
查看调用栈
1 |
|
1 |
|
这时候,你发现遇到了段错误(现在是因为栈溢出了,这是很容易发现的,但段错误还有更多的原因)。遇到段错误,除了先静态差错之外,可以直接 gdb
然后 r
,这样程序就会在段错误的时候停下来。
1 |
|
这时候我们一般操作是先看调用栈(因为这份代码有递归)。
使用 backtrace
命令。简写为 bt
。查看调用栈。
一看就发现是栈溢出了。
核心转储
有时候在机器上会看到这样的话:段错误(核心已转储)/ Segmentation fault (core dumped)。
在之前却没有看到这样的话,这是为什么呢?
在某些机器上,默认是不开启核心转储的。使用 ulimit -c
查看目前的状态,使用 ulimit -c unlimited
来开启核心转储。核心转储可以把当前程序崩溃的前一秒的快照保存下来。
看一段程序:
1 |
|
这是一个非法内存写入的段错误。崩溃之后,会在当前目录生成一个 core
文件。
1 |
|
使用 gdb 可执行文件 核心文件
来调试。
1 |
|
可以看到,最后卡在一个指针赋值操作。我们尝试查看这个指针,发现指向 0,因此找出了错误。
结语
为什么要调试代码?因为代码的执行结果不符合预期。
为什么代码执行结果不符合预期?①你的算法本来就是错的。②你的代码写错了。
不谈你的想法就是错的情况(这种情况你可以用纸自己推一遍),那么哪里写错了呢?这就是有很多坑的地方,有的时候就是少初始化了一个变量,有的时候忘记修改一个变量,却调试了很久。
现在你可能对 gdb 的所有在 OI 中常用的功能有所掌握,但重点是自己多调试几次。仅凭几个样例是绝不能说清楚调试的时候所有技巧的,况且我来举例就是已经明确知道代码哪里有问题了的。自己上手调自己写错的代码,才能真正总结经验,避免踩坑。也就是说,本文仅为抛砖引玉,真正有关 gdb 还需要自己再多加练习,有想要做的操作多搜索,多用,就能熟练了。