本文主要介绍gdb的基础使用。若需了解一些技巧,请访问此篇博客:点这里
本篇教程适用于Windows,macOS及Linux,但由于Windows的自带终端很难用,所以体验可能不太好。Windows 10建议安装Windows Terminal以取得最佳体验。
1. 前言
你是否为C/C++下的调试而苦恼?你是否苦于Dev-C++调试烦人的问题(如调不了STL、结构体数组要一层一层展开)?那么,gdb很可能是你的最佳选择。
gdb是一个命令行下的、功能强大的调试器。看到命令行下,是不是有点害怕?没关系,本文最后会介绍一些图形前端,但建议先学习一些基础命令。
示例代码:(example.cpp,以下调试命令均以此代码为准)
博主太懒了,只写了个求阶乘
#include <iostream>
#include <cstdio>
using namespace std;
int f(int x){
int ans=1;
for(int i=1;i<=x;i++) ans*=i;
return ans;
}
int main(){
int a;
scanf("%d",&a);
printf("%d
",f(a));
return 0;
}
系统环境:Linux Mint 19.3 64位。
2. 调试
2.1 启动gdb,载入文件,打印源代码,退出gdb
首先,在编译选项里加上 -g
,以生成调试用的符号表。建议不要同时开 -O2
等优化选项,否则可能会有奇奇怪怪的问题。
打开终端,输入 gdb [可执行文件名]
,载入程序(注意,是可执行文件名(比如1.exe),不是你的源文件名)。比如这样:
> g++ example.cpp -o example -g
[编译,无提示]
> gdb ./example
然后,你可能会见到如下的界面:
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./example...done.
(gdb)
这样,你就进入gdb的命令行环境里了。
其中,第一行是版本信息,倒数第二行表示正载入符号表,最后一行 (gdb)
则是gdb的提示符。
请注意,若你倒数第二行有 (no debugging symbols found)
字样,请确保在编译选项里加上 -g
选项。
当然,如果你直接输入 gdb
启动,不加文件名,也可以。只是,你要使用 file
命令手动载入可执行文件。
以后出现的所有命令,都是在gdb的环境,而非系统shell的环境执行的。
命令:
file(简写fil)
格式:file 可执行文件名
作用:载入当前目录下的对应名称的可执行文件。
例子:
(gdb) file example
Load new symbol table from "example"? (y or n) y
Reading symbols from example...done.
命令:
list(简写为l)
格式:list [行号]
作用:打印给定行号周围10行的源代码。若不提供行号,则接续打印上次的源代码。
这里提到了一个简写的概念。什么是简写呢?简写是为了简化命令的。比如,打一个 list
还是有些麻烦的。这时,我们可以输入它的简写 l
。你可以认为一个命令与它的简写是完全等价的。以后若提到简写,不再解释。
例子:
(gdb) l 7
2 #include <cstdio>
3 using namespace std;
4 int f(int x){
5 int ans=1;
6 for(int i=1;i<=x;i++) ans*=i;
7 return ans;
8 }
9
10 int main(){
11 int a;
(gdb) l
12 scanf("%d",&a);
13 printf("%d
",f(a));
14 return 0;
15 }
(gdb)
最后,用 quit
命令退出gdb。
命令:
quit(简写为q)
格式:quit
(无参数)
作用:退出gdb。
2.2 设置断点
要调试程序,我们必须让它在某个地方停下来。否则,让它一直执行下去,那和普通的执行程序有什么区别呢?
因此,我们需要用 break
来设置断点。此后,程序将会在设定的断点处停下来。
命令:
break(简写为b)
格式:break 函数名|行号
作用:在给定函数名或行数处设置断点。
例子:
(gdb) break main //main函数处设置断点
Breakpoint 1 at 0x874: file example.cpp, line 10.
(gdb) break 11 //在第11行处设置断点
Breakpoint 2 at 0x883: file example.cpp, line 11.
当然可以用它的简写:
(gdb) b main
Breakpoint 1 at 0x874: file example.cpp, line 10.
(gdb) b 11
Breakpoint 2 at 0x883: file example.cpp, line 11.
2.3 运行
命令:
run(简写为r)
格式:run
(无参数)
作用:从头运行程序。
命令:
continue(简写为c)
格式:continue
(无参数)
作用:从当前位置继续运行程序,直到遇到下一个断点或程序运行完毕。
命令:
until(简写为u)
格式:until 行号
作用:从当前位置继续运行程序,直到指定行号处才停下来。
当然,有时候,你可能发现运行上述命令后gdb会停住。这有两种情况:
- 你的程序用了标准输入,gdb在等待输入。
- 数据规模太大或程序效率太低,以至于运行到断点的时间较长。
例子:
(gdb) r
Starting program: /home/acceptedzhs/example
Breakpoint 1, main () at example.cpp:10
10 int main(){
(gdb) c
Continuing.
Breakpoint 2, main () at example.cpp:12
12 scanf("%d",&a);
(gdb)
2.4 单步执行
很多时候,我们要一步一步地执行程序。无疑,反复地 break
与 continue
十分麻烦。gdb有两个命令 next
与 step
,可实现单步执行。
命令:
next(简写为n)
格式:next
(无参数)
作用:单步执行。若当前行有函数调用,则把这个函数作为一个整体执行(即不进入函数内部)。
命令:
step(简写为s)
格式:step
(无参数)
作用:单步执行。若当前行有函数调用,则进入该函数内部。
但是,又有人想偷懒了。反复敲 n
与 s
依然很麻烦。怎么办呢?
gdb有个特性:若什么都不输,直接按回车,则会执行上一次执行的命令。
所以,只要开始敲个 n
或 s
,然后一直敲回车就行了。
举个例子好了:
(gdb) b 13
Breakpoint 1 at 0x89b: file example.cpp, line 13.
(gdb) r
Starting program: /home/acceptedzhs/example
10
Breakpoint 1, main () at example.cpp:13
13 printf("%d
",f(a));
(gdb) n
3628800
14 return 0;
(gdb) [回车] //看到没,执行了上次的命令,即next
15 }
(gdb)
Starting program: /home/acceptedzhs/example
10
Breakpoint 1, main () at example.cpp:13
13 printf("%d
",f(a));
(gdb) s
f (x=10) at example.cpp:5 //step命令,进入了f函数内部
5 int ans=1;
(gdb)
2.5 输出变量/函数值
有时,我们想要打印某些变量或函数的值,看它是否符合期望。这又怎么办呢?
命令:
print(简写为p)
格式:print 变量名
作用:打印一次变量名/函数调用对应的值。
命令:
display(简写为disp)
格式:display 变量名
作用:设置在每一次停下来时(如到断点时,单步执行等)都打印该变量名/函数调用对应的值。
例子:
(gdb) b 13
Breakpoint 1 at 0x89b: file example.cpp, line 13.
(gdb) r
Starting program: /home/acceptedzhs/example
9
Breakpoint 1, main () at example.cpp:13
13 printf("%d
",f(a));
(gdb) p a
$1 = 9
(gdb) n
362880 //输出9!
14 return 0; //这只会显示一次,下一步就不会再打印该变量值了
(gdb) p f(2) //当然,调用函数也可以
$2 = 2
(gdb)
(gdb) b f
Breakpoint 1 at 0x841: file example.cpp, line 5.
(gdb) r
Starting program: /home/acceptedzhs/example
9
Breakpoint 1, f (x=9) at example.cpp:5
5 int ans=1;
(gdb) disp ans
1: ans = 0
(gdb) n
6 for(int i=1;i<=x;i++) ans*=i;
1: ans = 1
(gdb)
7 return ans;
1: ans = 362880 //每次停下来时,该变量都会显示
(gdb)
8 }
1: ans = 362880
(gdb)
有时,你可能会见到 <optimized out>
的提示。此时,请检查编译时是否开了优化(如 -O2
)。
2.6 查看某些信息
命令:
info(简写为i)
格式:info 类型
作用:打印对应类型的信息。
其中,类型可以是breakpoints(断点,简写b)、locals(局部变量,简写lo)、display(被设为总是显示的变量,简写disp)等。具体可以通过 help info
查看。
比如:
(gdb) b 13
Breakpoint 1 at 0x89b: file example.cpp, line 13.
(gdb) r
Starting program: /home/acceptedzhs/example
10 //程序的标准输入
Breakpoint 1, main () at example.cpp:13
13 printf("%d
",f(a));
(gdb) i lo
a = 10
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x000055555555489b in main() at example.cpp:13
breakpoint already hit 1 time
(gdb) disp a
1: a = 10
(gdb) i display
Auto-display expressions now in effect:
Num Enb Expression
1: y a
(gdb)
我们发现输出了很多信息。其中Num是编号。编号有什么用呢?我们待会儿就要见到。
2.7 删除/禁用/启用某些东西
命令:
disable(简写为dis)
格式:disable 类型 [编号]
作用:临时禁用某些类型的对应编号的东西,待会儿讲。
命令:
delete(简写为d)
格式:delete 类型 [编号]
作用:删除某些类型的对应编号的东西。
命令:
enable(简写为en)
格式:enable 类型 [编号]
作用:启用某些类型的对应编号的东西。
其中,类型就是讲述 info
命令时中的类型,编号就是 info
命令输出的一堆东西中的Num那一栏。
注意:delete可能用不了类型的简写。
举个例子:
(gdb) b 12
Breakpoint 1 at 0x883: file example.cpp, line 12.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555554883 in main() at example.cpp:12
(gdb) dis b 1 //禁用1号断点
(gdb) r
Starting program: /home/acceptedzhs/example
10
3628800 //不经过该断点了
[Inferior 1 (process 2695) exited normally]
(gdb) en b 1 //启用该断点
(gdb) r
Starting program: /home/acceptedzhs/example
Breakpoint 1, main () at example.cpp:12
12 scanf("%d",&a); //又经过该断点了
(gdb) d breakpoints 1 //删除
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/acceptedzhs/example
10
3628800 //又不经过断点了
[Inferior 1 (process 3516) exited normally]
(gdb)
2.8 获取帮助
有时,我们可能忘记某个命令的用法。这该怎么办呢?
命令:
help(简写为h)
格式:help 待查询的命令(待查询的命令可以用简写)
作用:显示待查询的命令的帮助。
例子:
(gdb) h b
Set breakpoint at specified location.
break [PROBE_MODIFIER] [LOCATION] [thread THREADNUM] [if CONDITION]
PROBE_MODIFIER shall be present if the command is to be placed in a
probe point. Accepted values are `-probe' (for a generic, automatically
guessed probe type), `-probe-stap' (for a SystemTap probe) or
`-probe-dtrace' (for a DTrace probe).
LOCATION may be a linespec, address, or explicit location as described
below.
With no LOCATION, uses current execution address of the selected
stack frame. This is useful for breaking on return to a stack frame.
...(省略若干行)...
3. 命令一览表
命令 | 简写 | 作用 |
---|---|---|
file |
fil |
载入可执行文件 |
list |
l |
打印源代码 |
quit |
q |
退出gdb |
break |
b |
设置断点 |
run |
r |
从头运行程序 |
continue |
c |
从当前位置继续运行程序 |
until |
u |
从当前位置继续运行,直到指定行号 |
next |
n |
单步执行 |
step |
s |
单步执行 |
print |
p |
打印一次值 |
display |
disp |
设置某个变量/函数总是显示 |
info |
i |
打印相关类型的信息 |
disable |
dis |
临时禁用某些东西 |
delete |
d |
删除某些东西 |
enable |
en |
启用某些东西 |
help |
h |
获取帮助 |
4. 图形界面?
gdb作为一个命令行调试器,对于某些人来说可能望而生畏。
所以,很多人为其开发了图形前端,以方便大家使用。
这里,我推荐nemiver、ddd、gdbgui。(貌似不支持win)
如果你是个Vim爱好者,vim-vebugger也不错。
对上面不满意?可以试试gdb自带的伪图形界面,只要启动gdb时加上 -tui
选项即可。
5. 结语
以上便是全部内容了。
蒟蒻写博客不易,恳请大佬点个赞!