在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
GDB调试时如何显示源代码?如何按文件、函数、行号设置断点?运行一个进程如何带入参?如何打印数组?如何打印指针指向内容?如何指定格式打印?读完本篇这些问题都将不再是问题~~
文章目录
1 编译版本
使用GDB,可执行程序中需要带调试信息,所以在编译版本时需要使用 -g 选项。
2 基础操作
2.1 基础指令一览
序号 | 操作 | 说明 | 示例 |
---|---|---|---|
1 | gdb [elf file name] | 进入GDB | gdb a.out |
2 | quit | 退出调试,可简写为q | q |
3 | show args | 显示进程main函数入参列表 | show args |
4 | set args | 设置进程main函数入参列表 | set args hello |
5 | break [[file name:]line number | function name] | 设置断点,可简写为b | b foo |
6 | info break | 查看已经设置的断点 | info b |
7 | run | 开始全速运行,可简写为r | r |
8 | continue | 继续全速运行,可简写为c | c |
9 | next | 单步执行,可简写为n | n |
2.2 查看源码
2.2.1 原理
查看源码的原理是在版本编译阶段就会记录下源文件所在路径(包括文件名),在查看源码时GDB会到该路径下寻找源码,如果找不到会到当前工作目录下寻找源文件。
2.2.2 查看源码的几种方式
- 查看当前文件当前行相关源码
# 查看当前行周边10行代码(多次按回车会继续显示)
list
# 查看当前文件指定行(例如行号为111)周边10行代码
list 111
# 查看指定行号范围(例如3-25行)代码
list 3, 25
- 查看指定函数源码
# 查看函数(foo)源码
list foo
- 查看指定文件内指定位置源码
# 查看指定文件(file.c)指定行号(11行)周边源码
list file.c:11
# 查看指定文件(file.c)内指定函数(foo)周边源码
list file.c:foo
2.2.3 配置
- 配置显示行数
# 显示当前配置(默认是10行)
show listsize
# 设置显示行数(设置为20行)
set listsize 20
- 配置源码路径
# 查看当前源码搜索路径
show directories
# 设置当前源码搜索路径(例如设置为当前路径)
set directorier .
其实,在不进入gdb的情况下也是可以查看elf文件中默认的源码路径的,查找指令和结果如下:
[xxx @localhost file]$ readelf a.out -p .debug_str
String dump of section '.debug_str':
[ 0] _IO_buf_end
[ c] _old_offset
[ 18] _IO_save_end
[ 25] temp
[ 2a] short int
[ 34] size_t
[ 3b] sizetype
[ 44] /home/usr_name/test/file # 该行信息记录了默认的源码路径
...
2.3 打印变量
2.3.1 打印普通变量和指针变量
gdb中变量以及指针变量的格式和C语言保持一致。
# 指令格式
p[/f] variable [@ number] # f为format;当variable为数组变量时可以使用 @number 指定打印个数
# 打印普通变量(例如变量名为i)
p i
# 打印一维指针变量(例如变量char *p_char = "hello world !")
p p_char
# 打印二维指针变量(例如变量char **pp_char = p_char)
p *pp_char
# 打印数组变量(char a[100])
p a 或 p/x a 或 p/x *a@10
format类型如下表:
类型 | 含义 |
---|---|
d | 有符号十进制 |
u | 无符号十进制 |
x | 十六进制(不扩展符号位) |
a | 十六进制(扩展符号位) |
o | 八进制 |
t | 二进制 |
c | 字符 |
f | 浮点 |
2.3.2 打印内存
打印内存使用 examine 指令。
# 指令格式
x/[n][f][u] addr # n代表要显示的内存单元个数;f表示要打印的格式;u表示要打印的单元类型
# 例子(pAddr指向一个地址)
x/128xb pAddr
unit类型如下表:
类型 | 含义 |
---|---|
b | byte:按照字节打印 |
h | half word:半字,按照双字节打印 |
w | word:字,按照四字节打印 |
g | 按照八字节打印 |
2.3.3 自动打印
为了提高调试速度,可以添加一些自动打印的变量。
# 每次暂停都自动打印变量(i)
display i
# 查看添加的自动显示变量
info display
# 删除自动打印变量(i)
delete display var_no #var_no为使用info display查询出来的变量i对应的编号
2.4 修改变量
在调试跟踪过程中有时需要修改变量以构造有利条件。比如,在跟踪一行循环内的代码时,即使设置了断点,可能需要按无数次next或者continue才能满足想要的条件,此时修改变量的功能就显得极为必要了。
修改变量的指令也非常简单:
# 修改变量i
print i = 1000 # 使用GDB调试C语言代码就用C语言赋值语句;其他语言修改为其他语言对应语法
3 示例
使用一个真实案例来演示GDB的基本使用。
3.1 源码及需要调试的问题
源码如下。源码有些长,但是逻辑是比较简单的。主要完成的功能是在一段内存中每取出2个字节,按照16进制打印到文件中,每打印2个字节换行。
/* 待调试的C语言源码 */
1 #include <stdio.h>
2
3
4 int main(int argc, void *argv[])
5 {
6 char *pBaseAddr = NULL;
7 char fileName[100] = {0};
8 FILE *pfile = NULL;
9 char buff[6] = {0};
10 int i = 0;
11 char testDate[1000];
12 int offset = 0;
13 unsigned int temp = 0;
14
15 if (argc != 2) {
16 printf("funcname offset. \n");
17 return -1;
18 }
19
20 if (NULL != argv) {
21 //printf("funcname is %s \n", *(char *)argv);
22 printf("offset = %s \n", argv[1]);
23 if (!(offset = atoi(argv[1])))
24 offset = 0;
25 }
26
27 for (i = 0; i < 1000; i++) {
28 testDate[i] = i;
29 }
30
31 /* 2.计算起始地址(基址+偏移) */
32 pBaseAddr = testDate + offset;
33 printf("pBaseAddr = %p .\n", pBaseAddr);
34
35 /* 3.写入文件 */
36 /* 3.1 打开文件 */
37 sprintf(fileName, "./%s", "log.bit" );
38 pfile = fopen(fileName, "w+");
39 if (NULL == pfile) {
40 printf("Can not open file %s .\n", fileName);
41 return -1;
42 }
43
44 /* 3.2 循环写入文件 */
45 for (i = 0; i < (1000-offset)/2; i++) {
46 temp = 0xffff & (*pBaseAddr << 8 | *(pBaseAddr + 1));
47 sprintf(buff, "%04x\n", temp);
48
49 if (!fwrite(buff, 1, 5, pfile)) {
50 printf("The %d th write failed .\n", i);
51 fclose(pfile);
52 return -1;
53 }
54 pBaseAddr += 2;
55
56 }
57
58 /* 3.3 关闭文件 */
59 fclose(pfile);
60 return 0;
61 }
该段代码运行之后的生成的文件内容如下。我们发现0x80、0x82等数字在保存过程中丢失了。我们接下来就是要通过GDB来跟踪定位这个问题。存入文件的是temp的值,temp的值又来自于pBaseAddr和pBaseAddr+1指向的值,所以只需要设置断点并观察这temp变量和pBaseAddr指向的内存就可以了。
0001
0203
...
7e7f
ff81
ff83
ff85
...
3.2 调试过程
- 启动GDB
注意编译版本时需要带-g选项。
[usr_name@localhost file]$ gdb a.out # 使用gdb拉起需要调试的进程
...
Reading symbols from /home/usr_name/test/file/a.out...done. # 看到这行打印说明启动成功
(gdb) # gdb命令提示符
- 设置入参
需要调试的进程有入参时需要设置好入参,不然会影响程序的运行和调试。
(gdb) show args # 查看给main函数的当前入参
Argument list to give program being debugged when it is started is "".
(gdb) set args 0 # 设置入参
(gdb) show args # 查询设置是否成功
Argument list to give program being debugged when it is started is "0".
(gdb)
- 设置断点
(gdb) b main # 设置第一个断点在起始位置
Breakpoint 1 at 0x4006e5: file fileopt.c, line 6.
(gdb) list 46 # 查看需要设置的第二个断点周围的代码
41 return -1;
42 }
43
44 /* 3.2 循环写入文件 */
45 for (i = 0; i < (1000-offset)/2; i++) {
46 temp = 0xffff & (*pBaseAddr << 8 | *(pBaseAddr + 1));
47 sprintf(buff, "%04x\n", temp);
48
49 if (!fwrite(buff, 1, 5, pfile)) {
50 printf("The %d th write failed .\n", i);
(gdb) b 46 # 在46行设置断点
Breakpoint 2 at 0x40085f: file fileopt.c, line 46.
(gdb) info b # 查看断点,设置成功
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000004006e5 in main at fileopt.c:6
2 breakpoint keep y 0x000000000040085f in main at fileopt.c:46
(gdb)
- 全速运行
(gdb) r
Starting program: /home/user_name/test/file/a.out 0 # 此时程序开始运行
Breakpoint 1, main (argc=2, argv=0x7fffffffe388) at fileopt.c:6 # 停在第一个断点处
6 char *pBaseAddr = NULL;
- 设置watch变量
(gdb) display i # 添加变量i到watch窗口
1: i = 32767
(gdb) display temp # 添加变量temp到watch窗口
2: temp = 0
(gdb) info display # 查看当前watch窗口内添加的变量
Auto-display expressions now in effect:
Num Enb Expression
2: y temp
1: y i
(gdb)
- 修改控制循环的变量
(gdb) c # continue 继续执行
Continuing.
offset = 0
pBaseAddr = 0x7fffffffde20 .
Breakpoint 2, main (argc=2, argv=0x7fffffffe388) at fileopt.c:46 # 停在断点处
46 temp = 0xffff & (*pBaseAddr << 8 | *(pBaseAddr + 1));
2: temp = 0 # 显示窗口变量
1: i = 0 # 显示窗口变量
(gdb) list # 列出断点处附近代码
41 return -1;
42 }
43
44 /* 3.2 循环写入文件 */
45 for (i = 0; i < (1000-offset)/2; i++) {
46 temp = 0xffff & (*pBaseAddr << 8 | *(pBaseAddr + 1));
47 sprintf(buff, "%04x\n", temp);
48
49 if (!fwrite(buff, 1, 5, pfile)) {
50 printf("The %d th write failed .\n", i);
(gdb) n # next执行下一步
47 sprintf(buff, "%04x\n", temp);
2: temp = 1 # 窗口变量实时显示
1: i = 0
继续调试,停留在第二个断点处,这里是一个for循环,我们知道问题出在第64次循环,因此,我们为了加快调试速度,需要修改循环相关变量,使问题尽快复现。
(gdb) p/x pBaseAddr = 0x7fffffffde9e # 直接修改指针地址(基址加126)
$9 = 0x7fffffffde9e
(gdb) c # 修改完指针变量的值后,继续执行到第二个断点
Continuing.
Breakpoint 2, main (argc=2, argv=0x7fffffffe388) at fileopt.c:46
46 temp = 0xffff & (*pBaseAddr << 8 | *(pBaseAddr + 1));
- 观察变量和内存
此时,观察我们最关心的两个变量 temp 和 pBaseAddr 指向的内存的值。
(gdb) p/x temp
$12 = 0xff81
(gdb) x/8xb pBaseAddr
0x7fffffffdea0: 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87
(gdb)
- 问题定位
到这里,问题已经明朗:内存中的值是正确的,但是在解析内存时解析成了有符号数,因此自动在高位扩展了多个0xf导致。
- 退出GDB
调试完成,退出GDB。
(gdb) q # 退出gdb
A debugging session is active.
Inferior 1 [process 41095] will be killed.
Quit anyway? (y or n) y
3.3 问题解决
问题定位后,解决就很简单了,只需要修改第6行代码如下,即可解决。
4 int main(int argc, void *argv[])
5 {
6 unsigned char *pBaseAddr = NULL; /* 指针解析格式从char修改为unsigned char */
4 总结
使用GDB调试的整体流程和思路与我们熟悉的界面调试是完全一致的,只不过需要记住一些指令而已。
对于工具的学习只有一个秘诀——就是多加使用和练习。
恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~