GDB的基本介绍
H2 GDB基础知识:
GDB,是The GNU Project Debugger 的缩写,是 Linux 下功能全面的调试工具。GDB支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段。在Linux环境软件开发中,GDB 是主要的调试工具,用来调试 C 和 C++ 程序
在终端输入以下命令安装 GDB:
sudo apt-get install gdb -y
===BDG的进入和退出
下载源文件包:
wget http://labfile.oss.aliyuncs.com/courses/496/gdbtest.zip
unzip gdbtest.zip
如果要调试程序,需要在 gcc 编译可执行程序时加上 -g 参数,首先我们编译 bugging.c 程序,生成可执行文件:
cd gdbtest
gcc bugging.c -o bugging -g -m32
其中 -o指定输出文件名,默认编译为64位的程序,添加-m32选项可以编译为32位
如果在你的环境里编译报错,请安装 libc6-dev-i386 后再次编译:
sudo apt-get install libc6-dev-i386
输入 gdb bugging 进入 gdb 调试 bugging 程序的界面:
gdb bugging
在 gdb 命令行界面,输入run 执行待调试程序:
(gdb) run
在 gdb 命令行界面,输入quit 退出 gdb:
(gdb) run
GDB 命令行界面使用技巧
命令补全:
任何时候都可以使用 TAB 进行补全,如果只有一个待选选项则直接补全;否则会列出可选选项,继续键入命令,同时结合 TAB 即可快速输入命令。
部分 gdb 常用命令一览表:
| 命令 | 简写形式 | 数量 |
|---|---|---|
| list | l | 查看源码 |
| backtrace | bt、where | 打印函数栈信息 |
| next | 打印函数栈信息 | 打印函数栈信息 |
| step | s | 一次执行一行,遇到函数会进入 |
| finish | 运行到函数结束 | |
| continue | c | 继续运行 |
| info breakpoints | 显示断点信息 | |
| delete | d | 删除断点 |
| p | 打印表达式的值 | |
| run | r | 启动程序 |
| until | u | 执行到指定行 |
| info | i | 显示信息 |
| help | h | 帮助信息 |
查询用法:
在 gdb 命令行界面,输入 help command 可以查看命令的用法,command 是你想要查询的命令。
执行 Shell 命令:
在 gdb 命令行界面可以执行外部的 Shell 命令:
(gdb) !shell 命令
例如查看当前目录的文件:
(gdb) !ls
bugging linked_list.c test_linked_list.c
bugging.c linked_list.h
我们重新进入 debugging 调试界面:
list 命令用来显示源文件中的代码。
- list行号,显示某一行附近的代码
(gdb) list 2
1 /* bugging.c */
2
3 #include <stdio.h>
4
5 int foo(int n)
6 {
7
8 int sum;
9 int i;
- list文件名:行号,显示某一个文件某一附近的代码, 用于多个源文件的情况
(gdb) list bugging.c:2
1 /* bugging.c */
2
3 #include <stdio.h>
4
5 int foo(int n)
6 {
7
8 int sum;
9 int i;
- list 函数名,显示某个函数附近的代码
(gdb) list main
15
16 return sum;
17 }
18
19 int main(int argc, char** argv)
20 {
21 int result = 0;
22 int N = 100;
23
24 result = foo(N);
- list 文件名:函数名,显示某一个文件文件某个函数附近的代码,用于多个源文件的情况
(gdb) list bugging.c:main
15
16 return sum;
17 }
18
19 int main(int argc, char** argv)
20 {
21 int result = 0;
22 int N = 100;
23
24 result = foo(N);
设置断点
break 命令用来设置断点。
- break 行号,断点设置在该行开始处,注意:在该行代码未被执行
(gdb) break 19
Breakpoint 1 at 0x40055c: file bugging.c, line 19.
- break 文件名:行号,适用于有多个源文件的情况
- break 函数名,断点设置在该函数的开始处,断点所在行未被执行:
(gdb) break foo
Breakpoint 2 at 0x40052d: file bugging.c, line 11.
- break 文件名:函数名,适用于有多个源文件的情况
查看断点信息
info breakpoints 命令用于显示当前断点信息
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:19
2 breakpoint keep y 0x000000000040052d in foo at bugging.c:11
其中每一项的信息:
Num 列代表断点编号,该编号可以作为 delete/enable/disable 等控制断点命令的参数
Type 列代表断点类型,一般为breakpoint
Disp 列代表断点被命中后,该断点保留(keep)、删除(del)还是关闭(dis)
Enb 列代表该断点是 enable(y)还是disable(n)
Address 列代表该断点处虚拟内存的地址
What 列代表该断点在源文件中的信息
删除断点
delete 命令用于删除断点。
- delete Num, 删除指定断点,断点编号可通过info breakpoints获得:
(gdb) delete 2
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:19
- delete, 不带任何参数,默认删除所有断点
关闭和启用断点
disable 命令和 enable 命令分别用于关闭和启用断点:
disable 命令用于关闭断点,有些断点可能暂时不需要但又不想删除,便可以disable该断点。
enable 命令用于启用断点
- disable Num,关闭指定断点,断点编号可通过 info breakpoint 获得:
(gdb) break foo
Breakpoint 3 at 0x40052d: file bugging.c, line 11.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:19
3 breakpoint keep y 0x000000000040052d in foo at bugging.c:11
(gdb) disable 3
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:19
3 breakpoint keep n 0x000000000040052d in foo at bugging.c:11
- disable 命令和 enable 命令分别用于关闭和启用断点:
- enable Num,启用指定断点,断点编号可通过 info breakpoints 获得。
(gdb) enable 3
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:19
3 breakpoint keep y 0x000000000040052d in foo at bugging.c:11
- enable,不带任何参数,默认启用所有断点。
disable 和 enable 命令影响的是 info breakpoints 的 Enb 列,表示该断点是启用还是关闭
断点启用的更多方式
enable命令还可以用来设置断点被执行的次数,比如当断点设在循环中的时候,某断点可能多次被命中。
- enable once Num, 断点hit一次之后关闭该断点
- enable delete Num,断点 hit 之后删除该断点
(gdb) enable once 1
(gdb) enable delete 3
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint dis y 0x000000000040055c in main at bugging.c:19
3 breakpoint del y 0x000000000040052d in foo at bugging.c:11
粗体文本这两个命令影响的是 info breakpoints 的 Disp 列,表示该断点被命中之后的行为**
断点调试的一些命令
打印变量
调试的过程中需要观察变量或者表达式的值,所以先介绍两个基本的显示变量值的命令:
- info locals
打印当前断点处所在函数的所有局部变量的值,不包括函数参数 - print 变量或表达式
打印表达式的值,可显示当前函数的变量值、全局变量的值等
printf/FMT可以控制打印的格式,常见的有x(十六进制),t(二进制),c(显示为字符)等
启动程序
run 命令用于启动待调试程序,并运行到断点处停下
- run
不带任何参数,启动待调试程序,不传递参数- run 参数
有些程序需要跟参数,直接带上参数列表即可,会传递给 main 函数的 argc、argv变量
单步命令
next,step,finish,continue,until用于控制整个调试过程中,程序执行的流程
- next
next 单步执行,函数调用当做一条指令,不会进入被调用函数内部
next N,表示单步执行N次
- step
step 单步执行,会进入到函数调用内部
step N,表示单步执行N次
finish
执行程序到当前函数结束continue
执行程序开下个断点until
until N, 执行程序到源代码的某一行
下图是windows下图形IDE c-free的调试菜单,在 Linux 上 gdb 命令虽然没有图形化的显示,但如果理解了 gdb 的单步调试,这些选项应该可以很容易理解和使用:

断点小结
断点是调试最基本的方法之一,这一节主要介绍了断点相关的知识。主要是几个断点相关的命令
- list
- info breakpoints
- break
- delete
- disable 和 enable
- enable once 和 enable delete
- next,step,finish,continue,until
- info locals 和 print
不熟悉命令的时候,记得在 gdb 命令行下键入 help info breakpoints 等命令,查询帮助文档。
断点设置
本节将继续使用 bugging 程序,首先确认已生成了 debug 的可执行文件。在 main 函数处设置了一个断点,用于进行后续的单步调试。
gdb bugging
(gdb) break main
Breakpoint 1 at 0x40055c: file bugging.c, line 21.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040055c in main at bugging.c:21
正式开始调试程序
bugging程序简介:
- bugging示例程序是用来计算 1+2+3+...+100 的值的,预期结果为高斯数 5050
- 程序运行的结果和我们预期的不一致,仅仅从代码不易看出 bug 所在
- 接下来使用 gdb 单步调试该程序,找到 bug 所在
调试 bugging:
- info breakpoints 查看我们设置的断点:
(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x000000000040055c in main at bugging.c:21在main处设置了一个断点,位于源文件 bugging.c的21行
- run 运行程序,程序在第一次运行到断点会停止,等待下一条命令:
(gdb) run Starting program: /home/shiyanlou/gdbtest/bugging Breakpoint 1, main (argc=1, argv=0x7fffffffe4c8) at bugging.c:21
21 int result = 0;
启动bugging程序,停在断点处,位main函数的第一条指令处
- next 单步执行:
(gdb) next 22 int N = 100;
单步执行,相当于执行了 result 变量的初始化
- next:
(gdb) next
24 result = foo(N);
- step,也是单步执行,和next的区别还记得吗:
(gdb) step foo (n=100) at bugging.c:11 11 for (i=0; i<=n; i++)
单步进入,进入foo函数,停在foo函数的第一条指令处
- list 查看当前行附近的代码:
(gdb) list 6 { 7 8 int sum; 9 int i; 10 11 for (i=0; i<=n; i++) 12 { 13 sum = sum+i; 14 } 15查看下foo函数的源码
- info locals 打印出所有的局部变量:
(gdb) info locals sum = 4195821 i = 0打印下foo函数的所有局部变量,可以看到这些值是无规律的,因为没有初始化
- next:
(gdb) next 13 sum = sum+i;单步执行,进入循环体
- next:
(gdb) 11 for (i=0; i<=n; i++)单步执行,sum = sum + i; 程序继续执行到for语句的判断处
- info locals:
(gdb) info locals sum = 4195821 i = 0再次查看变量,i的值符合预期,sum的值依然不合理
- list foo 查看下 foo 函数源码:
(gdb) list foo 1 /* bugging.c */ 2 3 #include <stdio.h> 4 5 int foo(int n) 6 { 7 8 int sum; 9 int i; 10 (gdb) 11 for (i=0; i<=n; i++) 12 { 13 sum = sum+i; 14 } 15 16 return sum; 17 } 18 19 int main(int argc, char** argv) 20 {检查foo函数的代码,很容易发现定义了两个变量 sum 和 i,不在循环体被初化。而sum未被初始化
到这里已经基本定位程序 bug 所在了,sum 的值从进入循环体到执行一次循环结束都不对。bug 根源就是 sum 变量未初始化,导致错误的累加。我们修改 int sum = 0; 重新构建程序,便可以得到预期结果。
GDB函数栈
断点设置
本节将继续使用 bugging 程序,首先确认之前有执行过以下命令在 main 函数处设置了一个断点,用于进行后续的单步调试。
gdb bugging
(gdb) break foo
(gdb) info breakpoints
在 foo 函数处设置了一个断点
函数与函数栈
进程在内存空间会拥有一块叫做 stack 的区域,函数内部的局部变量、函数之间调用时参数的传递和返回值等等都会用到栈这种数据结构
- info proc mappings 可以查看待调试进程的内存分布情况
run
info proc mappings
process 198
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x400000 0x401000 0x1000 0x0 /home/shiyanlou/gdbtest/bugging
0x600000 0x601000 0x1000 0x0 /home/shiyanlou/gdbtest/bugging
0x601000 0x602000 0x1000 0x1000 /home/shiyanlou/gdbtest/bugging
0x7ffff7a0d000 0x7ffff7bcd000 0x1c0000 0x0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 0x200000 0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 0x4000 0x1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 0x2000 0x1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 0x4000 0x0
0x7ffff7dd7000 0x7ffff7dfd000 0x26000 0x0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fe6000 0x7ffff7fe9000 0x3000 0x0
0x7ffff7ff8000 0x7ffff7ffa000 0x2000 0x0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 0x2000 0x0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 0x1000 0x26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]
从进程的地址空间分配情况可以看到,有一块区域[stack],这就是该进程的 栈空间
- backtrace 查看函数调用栈的情况
backtrace、where、info stack
这三个命令都可以查看函数的调用情况
backgrace full、where full、info stack full
这三个命令查看函数调用情况的同时,打印所有局部变量的值
(gdb) backtrace
#0 foo (n=100) at bugging.c:11
#1 0x0000000000400574 in main (argc=1, argv=0x7fffffffe4c8) at bugging.c:24
(gdb) where
#0 foo (n=100) at bugging.c:11
#1 0x0000000000400574 in main (argc=1, argv=0x7fffffffe4c8) at bugging.c:24
(gdb) info stack
#0 foo (n=100) at bugging.c:11
#1 0x0000000000400574 in main (argc=1, argv=0x7fffffffe4c8) at bugging.c:24
(gdb) backtrace full
#0 foo (n=100) at bugging.c:11
sum = 4195821
i = 0
#1 0x0000000000400574 in main (argc=1, argv=0x7fffffffe4c8) at bugging.c:24
result = 0
N = 100
(gdb)
- 栈帧(stack frame)
1 是 main 函数用到的栈空间,这一部分可以称之为 main 函数的 stack frame
0 是 foo 函数用到的栈空间,同样可称之为 foo 函数的 stack frame, 0 代表之前执行停在 foo 函数内
- info frame Num 查看某个函数栈帧的详细信息
(gdb) info frame 0
Stack frame at 0x7fffffffe3c0:
rip = 0x40052d in foo (bugging.c:11); saved rip = 0x400574
called by frame at 0x7fffffffe3f0
source language c.
Arglist at 0x7fffffffe3b0, args: n=100
Locals at 0x7fffffffe3b0, Previous frame's sp is 0x7fffffffe3c0
Saved registers:
rbp at 0x7fffffffe3b0, rip at 0x7fffffffe3b8
(gdb) info frame 1
Stack frame at 0x7fffffffe3f0:
rip = 0x400574 in main (bugging.c:24); saved rip = 0x7ffff7a2d830
caller of frame at 0x7fffffffe3c0
source language c.
Arglist at 0x7fffffffe3e0, args: argc=1, argv=0x7fffffffe4c8
Locals at 0x7fffffffe3e0, Previous frame's sp is 0x7fffffffe3f0
Saved registers:
rbp at 0x7fffffffe3e0, rip at 0x7fffffffe3e8
本节并不详细介绍 函数与函数栈的理论知识,但是通过回溯栈,可以调试函数之间的调用关系、局部变量值的变化等。所以还是需要理解一些基本概念,可以看一下这篇文章:
函数调用过程中栈是怎么压入和弹出的?
函数栈小结
backtrace
backtrace full
info frame
不熟悉命令的时候,记得在 gdb 命令行下键入 help backtrace等命令,查询帮助文档
实验总结
本节实验通过对一个实例 buggging 程序的调试过程,学习了 GDB 的进入和退出,设置断点,单步调试以及函数栈等调试功能。在下一节中,我们将利用本节的知识完成一个更复杂的链表程序的 BUG 调试和修复。
编译运行程序
本节将使用 test_linked_list 程序,首先确认在实验楼环境中已经执行过以下命令:
- 构建 test_linked_list 可执行程序
gcc -g -o test_linked_list linked_list.c test_linked_list.c
阅读源码,linked_list.h 和 linked_list.c定义了线性数据结构链表,并且定义了一些对链表相关的操作。test_linked_list.c中的main函数对链接进行删除等测试。
- 运行该程序
./test_linked_list [16:17:07]
[1] 225 segmentation fault ./test_linked_list
程序运行发生了段错误,由于没有任何输出信息,不能定位bug所在。如果不会调试的基本手段,就需要修改代码,加上很多printf语句,重新构建程序,尝试定位问题。
使用 GDB 调试程序
调试的基本思路
- 使用 GDB 调试程度,启动待调试程序,先 run 一遍,查看挂在哪里
- 在程序挂掉的地方设置断点,单步调试,找到 bug 所在。
- 由于程序中有很多函数调用关系,合理设置断点 结合 backtrace 快速定位问题
进行 debug
在本程序的调试中,请先按照 2.2.1 的思路进行调试,这里列出的调试过程仅供遇到困难时参考
1.$ gdb test_linked_list
2. list main
3. run
4. backtrace
5. break core_dump_test
6. list core_dump_test
7. next 2
8. info locals
9. step
10. backtrace full
11. next 4
12. print p
13. next
debug 的步骤也可以按照自己习惯的顺序来。上述debug过程,在第8步就应该开始注意了h的值为0,第10步、第12步都表明局部变量 p 的值为0,最终 p->next 非法内存访问
实验总结
本节实验通过调试一个有 BUG 的链表程序,实践上一节中学习的 GDB 调试技术。如果有兴趣,可以使用更多 GDB 强大的功能对链表程序进行调试:
- test_linked_list 程序还可以进行 coredump 调试,有兴趣的可以自行尝试。
- GDB 的一个缺点是源码和调试过程分开,导致经常需要 list 查看代码。gdbtui 是带图形界面的,有兴趣的可以自行尝试。
浙公网安备 33010602011771号