GDB,GNU 项目调试器,通常以其命令 gdb 而闻名,允许查看另一个程序在执行时“内部”发生了什么,或者另一个程序在崩溃时正在做什么。
gdb是一个交互式控制台,可逐步浏览源代码,分析执行的内容,并基本上对错误应用程序中出现的问题进行逆向工程。
GDB 可以做4种主要的事情(以及支持这些事情的其他事情)来帮助捕获行为中的错误:
这些程序可能在与 GDB 相同的机器(本机)、另一台机器(远程)或模拟器上执行。
sudo apt-get install gdb
# 查看版本
(base) qiancj@qiancj-HP-ZBook-G8:~$ gdb --version
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 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.
gdb ./hello_randy
gdb attach pid
gdb filename corename
选择调试对象及其文件:
--args 可执行文件之后的参数被传递给下级
--core=COREFILE 分析核心转储 COREFILE。
--exec=EXECFILE 使用 EXECFILE 作为可执行文件。
--pid=PID 附加到正在运行的进程 PID。
--directory=DIR 在 DIR 中搜索源文件。
--se=FILE 使用FILE作为符号文件和可执行文件。
--symbols=SYMFILE 从 SYMFILE 中读取符号。
--readnow 首次访问时完全读取符号文件。
--readnever 不要读取符号文件。
--write 设置写入可执行文件和核心文件。
初始命令和命令文件:
--command=FILE, -x 从 FILE 执行 GDB 命令。
--init-command=FILE, -ix
与-x类似,但在加载inferior之前执行命令。
--eval-command=COMMAND, -ex
执行单个 GDB 命令。
可以多次使用并与 --command 结合使用。
--init-eval-command=COMMAND, -iex
与 -ex 类似,但在加载之前。
--nh 不要读取 ~/.gdbinit。
--nx 不要读取任何目录中的任何 .gdbinit 文件。
输出和用户界面控制:
--fullname emacs-GDB 接口使用的输出信息。
--interpreter=INTERP
选择特定的解释器/用户界面
--tty=TTY 正在调试的程序使用 TTY 进行输入/输出ged.
-w 使用 GUI 界面。
--nw 不要使用 GUI 界面。
--tui 使用终端用户界面。
--dbx DBX 兼容模式。
-q, --quiet, --silent
启动时不打印版本号。
操作模式:
--batch 处理选项后退出。
--batch-silent 与 --batch 类似,但抑制所有 gdb stdout 输出。
--return-child-result
GDB 退出代码将是子进程的退出代码。
--configuration 打印有关 GDB 配置的详细信息,然后退出。
--help 打印此消息然后退出。
--version 打印版本信息然后退出。
远程调试选项:
-b BAUDRATE 设置用于远程调试的串口波特率。
-l TIMEOUT 设置远程调试的超时(以秒为单位)。
其他选项:
--cd=DIR 将当前目录更改为 DIR。
--data-directory=DIR,-D
将 GDB 的数据目录设置为 DIR。
命令名称 | 命令缩写 | 命令说明 |
---|---|---|
run | r | 运行一个待调试的程序 |
continue | c | 让暂停的程序继续运行 |
next | n | 运行到下一行 |
step | s | 单步执行,遇到函数会进入 |
until | u | 运行到指定行停下来 |
finish | fi | 结束当前调用函数,回到上一层调用函数处 |
return | return | 结束当前调用函数并返回指定值,到上一层函数调用处 |
jump | j | 将当前程序执行流跳转到指定行或地址 |
p | 打印变量或寄存器值 | |
backtrace | bt | 查看当前线程的调用堆栈 |
frame | f | 切换到当前调用线程的指定堆栈 |
thread | thread | 切换到指定线程 |
break | b | 添加断点 |
tbreak | tb | 添加临时断点 |
delete | d | 删除断点 |
enable | enable | 启用某个断点 |
disable | disable | 禁用某个断点 |
watch | watch | 监视某一个变量或内存地址的值是否发生变化 |
list | l | 显示源码 |
info | i | 查看断点 / 线程等信息 |
ptype | ptype | 查看变量类型 |
disassemble | dis | 查看汇编代码 |
set args | set args | 设置程序启动命令行参数 |
show args | show args | 查看设置的命令行参数 |
假设有以下代码
#include //printf
#include //srand
#include
using namespace std;
int main() {
srand(time(NULL));
int alpha = rand() % 8;
cout << "Hello Randy." << endl;
int beta = 2;
printf("alpha is set to is %s\n", alpha);
printf("kiwi is set to is %s\n", beta);
return 0;
} // main
很明显,代码里面 printf 里面应该打印的是整型,但是匹配的字符是字符(%s)
但这是一个简单的问题,可以了解调试过程。编译并运行它以查看错误:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ g++ -o randy test_gdb.cpp
test_gdb.cpp: In function ‘int main()’:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ g++ -o randy test_gdb.cpp
test_gdb.cpp: In function ‘int main()’:
test_gdb.cpp:14:29: warning: format ‘%s’ expects argument of type ‘char*’, but argument 2 has type ‘int’ [-Wformat=]
14 | printf("alpha is set to is %s\n", alpha);
| ~^ ~~~~~
| | |
| char* int
| %d
test_gdb.cpp:15:28: warning: format ‘%s’ expects argument of type ‘char*’, but argument 2 has type ‘int’ [-Wformat=]
15 | printf("kiwi is set to is %s\n", beta);
| ~^ ~~~~
| | |
| char* int
| %d
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ ./randy
Hello Randy.
Segmentation fault (core dumped)
从此输出中,可以推测变量 alpha 设置正确,否则,不会期望它后面的代码行。
当然,这并不总是正确的,但这是一个很好的工作理论,如果使用 printf 作为日志和调试器,它基本上与你可能得出的结论相同。
从这里,可以假设错误位于成功打印的那一行之后的某行中。但是,目前尚不清楚该错误是在下一行还是在几行之后。
GNU 调试器是一个交互式的疑难解答,因此可以使用 gdb 命令来运行有缺陷的代码。
为了获得最佳结果,应该从包含调试符号的源代码重新编译有缺陷的应用程序。
加上-g
重新编译程序,然后运行程序
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ g++ -g -o randy test_gdb.cpp
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ gdb ./randy
--Type <RET> for more, q to quit, c to continue without paging--help
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./randy...
(gdb) start
Temporary breakpoint 1 at 0x1229: file test_gdb.cpp, line 8.
Starting program: /home/qiancj/codes/test/randy
Temporary breakpoint 1, main () at test_gdb.cpp:8
8 int main() {
(gdb) continue
Continuing.
Hello Randy.
Program received signal SIGSEGV, Segmentation fault.
__strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:65
65 ../sysdeps/x86_64/multiarch/strlen-avx2.S: No such file or directory.
可以在开启gdb后,一路next
,逐行打印代码,也能发现问题:
8 int main() {
(gdb) next
9 srand(time(NULL));
(gdb) next
10 int alpha = rand() % 8;
(gdb) next
11 cout << "Hello Randy." << endl;
(gdb) next
Hello world.
12 int beta = 2;
(gdb) next
14 printf("alpha is set to is %s\n", alpha);
(gdb) next
Program received signal SIGSEGV, Segmentation fault.
__strlen_avx2 () at ../sysdeps/x86_64/multiarch/strlen-avx2.S:65
65 ../sysdeps/x86_64/multiarch/strlen-avx2.S: No such file or directory.
也可以通过break
设置断点,可以在某一行设置断点,也可以在某个函数处设置断点
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test$ gdb ./randy
For help, type "help".
Type "apropos word" to search for commands related to "word"...
--Type <RET> for more, q to quit, c to continue without paging--c
Reading symbols from ./randy...
(gdb) break 11
Breakpoint 1 at 0x1259: file test_gdb.cpp, line 11.
(gdb) start
Temporary breakpoint 2 at 0x1229: file test_gdb.cpp, line 8.
Starting program: /home/qiancj/codes/test/randy
Temporary breakpoint 2, main () at test_gdb.cpp:8
warning: Source file is more recent than executable.
8 int main() {
(gdb) c
Continuing.
Breakpoint 1, main () at test_gdb.cpp:11
11 cout << "Hello Randy." << endl;
断点成功断在了11行,通过print
查看变量的值
(gdb) print alpha
$1 = 4
(gdb) print beta
$2 = 0
继续进行,可以步进代码行来到达将 beta
设置为一个值的位置:
(gdb) next
Hello world.
12 int beta = 2;
(gdb) print beta
$3 = 0
(gdb) next
14 printf("alpha is set to is %s\n", alpha);
(gdb) print beta
$4 = 2
可以通过watch
设置观察点(watch beta > 0
),当条件满足时,程序就会断住
(gdb) watch beta > 0
No symbol "beta" in current context.
(gdb) break 12
Breakpoint 1 at 0x1281: file test_gdb.cpp, line 12.
(gdb) continue
The program is not being run.
(gdb) start
Temporary breakpoint 2 at 0x1229: file test_gdb.cpp, line 8.
Starting program: /home/qiancj/codes/test/randy
Temporary breakpoint 2, main () at test_gdb.cpp:8
warning: Source file is more recent than executable.
8 int main() {
(gdb) c
Continuing.
Hello world.
Breakpoint 1, main () at test_gdb.cpp:12
12 int beta = 2;
(gdb) watch beta > 0
Hardware watchpoint 3: beta > 0
(gdb) c
Continuing.
Hardware watchpoint 3: beta > 0
Old value = false
New value = true
main () at test_gdb.cpp:14
14 printf("alpha is set to is %s\n", alpha);
用 next
手动步进完成代码的执行,或者可以用断点、观察点和捕捉点来控制代码的执行。
可以以不同形式查看 beta 的值
(gdb) print beta // 查看 beta 的值
$2 = 2
(gdb) print /o beta // 以8进制查看 beta的值
$3 = 02
(gdb) print /o &beta // 以8进制查看beta的内存地址
$4 = 03777777777753074
(gdb) whatis beta // 查看beta 的类型
type = int
(gdb) print /x beta // 以16进制查看 beta的值
$5 = 0x2
当通过whatis
查看beta类型的时候,会让我们想起来 printf 打印 beta 值的时候,使用的是%s
,所以代码中必须使用 %d
来代替 %s
(gdb) disassemble main
Dump of assembler code for function main():
0x0000555555555229 <+0>: endbr64
0x000055555555522d <+4>: push %rbp
0x000055555555522e <+5>: mov %rsp,%rbp
0x0000555555555231 <+8>: sub $0x10,%rsp
0x0000555555555235 <+12>: mov $0x0,%edi
0x000055555555523a <+17>: callq 0x5555555550f0 <time@plt>
0x000055555555523f <+22>: mov %eax,%edi
0x0000555555555241 <+24>: callq 0x555555555100 <srand@plt>
0x0000555555555246 <+29>: callq 0x5555555550d0 <rand@plt>
0x000055555555524b <+34>: cltd
0x000055555555524c <+35>: shr $0x1d,%edx
0x000055555555524f <+38>: add %edx,%eax
0x0000555555555251 <+40>: and $0x7,%eax
0x0000555555555254 <+43>: sub %edx,%eax
当代码编译但随后发现存在错误时,这尤其令人沮丧,但这是最棘手的错误的工作方式。
如果它们很容易被抓住,它们就不会是bug。使用 GDB 是追捕和消除它们的一种方法。
用了GDB,才体会到Visual Studio的调试功能做的多么人性化。
其实本质上可能都一样的,都是调用gdb软件,然后设置断点,查看变量值。