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