Fork me on GitHub

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 删除断点
print 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 的单步调试,这些选项应该可以很容易理解和使用:

微信截图_20190423092532.png-34.7kB

断点小结

断点是调试最基本的方法之一,这一节主要介绍了断点相关的知识。主要是几个断点相关的命令

  • 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 是带图形界面的,有兴趣的可以自行尝试。
posted on 2019-04-23 16:54  anyux  阅读(1780)  评论(0)    收藏  举报