gdb调试断点

“源码之前,了无秘密”,阅读源码让我们清楚的了解到程序的逻辑,但对于较大的系统来说,可能会涉及到多线程、系统的状态或者无法定位到关键代码,静态的阅读可能无法掌握所有的信息,此时就需要动态调试,动态调试有助于我们对程序的理解,可以看到每一步执行状态和相应的变化。用好动态调试,可以让我们事半功倍。

Linux环境下用的最多的调试工具是gdb,设置断点可以让程序在断点处暂停,供我们查看程序的状态。gdb提供三种类型断点,代码断点(breakpoint)、内存断点(watchpoint)和事件断点(catchpoint)。

代码断点

代码断点是最常用的,是用于设置断点到程序的特定地址,特定地址可以用代码行号、函数名、地址值等来指定,可以设置一次断点和条件断点。

用法

  • 设置普通断点

    break location

  • 设置一次断点,也称为临时断点,断下来后会自动将该断点删除

    tbreak location

  • 设置条件断点,只有满足condition条件后才会断下来,condition是boolean表达式

    break location if condition

举例

用下面最简单的程序来介绍如何使用普通断点和条件断点,该程序是输出0到9。

1 #include <stdio.h>
2
3 int main()
4 {
5    int i;
6    for (i = 0; i < 10; i++) {
7        printf("%d\n", i);
8    }
9    return 0;
10 }

我们先用普通断点断到main函数上,然后条件断点断到printf所在行如果i等于8时。

GNU gdb (Debian 8.3.1-1) 8.3.1
... ...
Type "apropos word" to search for commands related to "word"...
Reading symbols from watch...
(gdb) b main # 设置普通断点在main函数
Breakpoint 1 at 0x113d: file watch.c, line 6. # 断点编号为1
(gdb) b watch.c:7 if i == 8 # 设置条件断点在第七行如果i==8
Breakpoint 2 at 0x1146: file watch.c, line 7. # 断点编号为2
(gdb) run #  运行被调试程序
Starting program: /home/f/doing/debug/gdb/watch 

Breakpoint 1, main () at watch.c:6 # 执行到断点1后停下来
6           for (i = 0; i < 10; i++) {
(gdb) continue # continue是继续执行
Continuing.
0
1
2
3
4
5
6
7

Breakpoint 2, main () at watch.c:7 # 断点2满足条件,停下来
7               printf("%d\n", i);
(gdb) print i # print 查看变量值,可以看到等于8,满足我们的条件
$1 = 8

从上面简单例子可以看到,设置了断点后会为每个断点分配编号,用于后续跟踪和管理。条件断点可以帮助我们过滤想要的结果。

内存断点

代码断点是以代码为对象进行监控跟踪,而内存断点则是以内存为对象。对内存值进行监控,根据监控类型分为:监控内存值改变(watch),监控内存值被读取(rwatch)和监控内存值读取或写入(awatch)。

用法

  • 监控内存值改变

    watch expr [if condition]

  • 监控内存值被读取

    rwatch expr [if condition]

  • 监控内存值被读取和写入

    awatch expr [if condition]

expr 可以是变量也可以是表达式,但要确保有对应的内存地址,不能是常量。在使用变量时确保该变量在当前所在的上下文中。同样也可以添加if条件。

举例

同样用上面的例子,断点在i等于8处。

GNU gdb (Debian 8.3.1-1) 8.3.1
... ...
Reading symbols from watch...
(gdb) b main # 要监控变量i,由于i是在main方法中,首先要运行到main函数上下文
Breakpoint 1 at 0x113d: file watch.c, line 6.
(gdb) run  # 执行被调试程序
Starting program: /home/f/doing/debug/gdb/watch 

Breakpoint 1, main () at watch.c:6 # 执行到main函数处中断
6           for (i = 0; i < 10; i++) {
(gdb) watch i if i == 8 # 监控变量i,并让i==8时断下来
Hardware watchpoint 2: i
(gdb) c
Continuing.
0
1
2
3
4
5
6
7

Hardware watchpoint 2: i

Old value = 7
New value = 8
0x0000555555555160 in main () at watch.c:6 # i从7更改8被断下来
6           for (i = 0; i < 10; i++) {

使用watch进行断点i,i变量实质是 *(int *)&i,gdb监控的是i所在地址的四字节值,如果只想监控i的最低字节值改变,可以使用: watch *(unsigned char *)&i。如果只想监控特定地址addr,该地址没有对应的变量,则需要将地址转化成需要监控长度的类型,如监控4字节,则使用: watch *(int *)addr。

事件断点

事件断点用于监听特殊事件发生,如发生则中断下来,支持的事件有:

  • C++ exception,使用 catch exception [name]
  • Ada exception,使用 catch handlers [name]
  • exec事件, 使用 catch exec
  • fork事件, 使用 catch fork 或者 catch vfork
  • 加载和卸载动态so事件, 使用 catch load|unload [regexp]
  • 监听系统信号,使用 catch signal [signal]
  • 监听系统调用, 使用 catch syscall [name|number|group:groupname|g:groupname] …

可以看到事件断点可监听的事件较多,监听系统调用的使用场景可能较多,如查看某系统调用的参数或返回值。可以配置strace(用于监控程序所使用的系统调用)来使用。

举例

有些时候我们得到了打印消息,但是不知道代码在哪里或者打印消息存在哪里,我们可以使用监听系统调用write来跟踪,还是使用上面的例子,我们来寻找打印消息的内存地址。

GNU gdb (Debian 8.3.1-1) 8.3.1
... ...
Reading symbols from ./watch...
(gdb) b main
Breakpoint 1 at 0x113d: file watch.c, line 6.
(gdb) run
Starting program: /home/f/doing/debug/gdb/watch 

Breakpoint 1, main () at watch.c:6
6           for (i = 0; i < 10; i++) {
(gdb) catch syscall write # 事件断点在write系统调用上
Catchpoint 2 (syscall 'write' [1])
(gdb) continue
Continuing.

Catchpoint 2 (call to syscall write), 0x00007ffff7ec8904 in __GI___libc_write (fd=1, buf=0x5555555592a0, nbytes=2) at ../sysdeps/unix/sysv/linux/write.c:26
26      ../sysdeps/unix/sysv/linux/write.c: No such file or directory.
(gdb) x/2cb 0x5555555592a0 # 根据write的参数,buf和nbytes,我们查看下要打印的内容
0x5555555592a0: 48 '0'  10 '\n' # 打印内容为 0和换行符
(gdb) continue
Continuing.
0

断点管理

(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000055555555513d in main at watch.c:6
        breakpoint already hit 1 time
2       hw watchpoint  keep y                      i
3       catchpoint     keep y                      syscall "write" 
4       breakpoint     del  y   0x000055555555513d in main at watch.c:6

从上面断点的信息从左向右来看,每个断点都包含编号、类型、显示、enable状态、断点地址和描述。这三种断点设置后,都会配置一个编号,之后对在断点管理时可以使用该编号。

查看断点

  • 查看当前所有断点,使用命令:

    info breakpoints 或者 info b

  • 查看当前的内存断点,使用命令:

    info watchpoints

删除断点

使用delete或者clear,delete后面可以指定断点编号,删除指定断点,若不指定,则删除所有断点。而clear针对的是地址。

  • delete删除指定断点

    delete [断点编号]

  • delete删除连续断点

    delete 断点编号开始-断点编号结束

  • clear删除指定地址的断点

    clear location

启用和禁用

断点设置后,默认是开启状态,若要禁用可以使用disable,禁用之后要重新启动使用enable。

  • 禁用

    disable 断点编号

  • 启用

    enable 断点编号

断点后加入命令序列

gdb 支持断点后执行指定的命令序列,用于定制化需求。将命令序列放入到commands和end中间,并且是在设置断点后。

使用

设置断点
commands
定制命令1
定制命令2
...
定制命令N
end

举例

还是上面打印的例子,我们在printf输出i之后,1. 增加输出i+1000,2. 不中断下来。

GNU gdb (Debian 8.3.1-1) 8.3.1
... ...
Reading symbols from ./watch...
(gdb) b watch.c:7 # 断点在printf行
Breakpoint 1 at 0x1146: file watch.c, line 7.
(gdb) commands  # 绑定命令序列到该断点上
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>printf "%d\n", i+1000 # 输出i+1000
>continue # 继续执行
>end # 命令序列完成关键字
(gdb) run
Starting program: /home/f/doing/debug/gdb/watch 

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1000
0

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1001
1

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1002
2

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1003
3

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1004
4

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1005
5

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1006
6

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1007
7

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1008
8

Breakpoint 1, main () at watch.c:7
7               printf("%d\n", i);
1009
9
[Inferior 1 (process 128987) exited normally]

打赏

取消

感谢您的支持!

扫码支持
扫码支持
扫码打赏,您说多少就多少

打开支付宝或微信扫一扫,即可进行扫码打赏哦