随手记——GDB调试入门看这一篇就够了

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

GDB调试时如何显示源代码?如何按文件、函数、行号设置断点?运行一个进程如何带入参?如何打印数组?如何打印指针指向内容?如何指定格式打印?读完本篇这些问题都将不再是问题~~

1 编译版本

使用GDB,可执行程序中需要带调试信息,所以在编译版本时需要使用 -g 选项。

2 基础操作

2.1 基础指令一览

序号操作说明示例
1gdb [elf file name]进入GDBgdb a.out
2quit退出调试,可简写为qq
3show args显示进程main函数入参列表show args
4set args设置进程main函数入参列表set args hello
5break [[file name:]line number | function name]设置断点,可简写为bb foo
6info break查看已经设置的断点info b
7run开始全速运行,可简写为rr
8continue继续全速运行,可简写为cc
9next单步执行,可简写为nn

2.2 查看源码

2.2.1 原理

查看源码的原理是在版本编译阶段就会记录下源文件所在路径(包括文件名),在查看源码时GDB会到该路径下寻找源码,如果找不到会到当前工作目录下寻找源文件。

2.2.2 查看源码的几种方式
  1. 查看当前文件当前行相关源码
# 查看当前行周边10行代码(多次按回车会继续显示)
list
# 查看当前文件指定行(例如行号为111)周边10行代码
list 111
# 查看指定行号范围(例如3-25行)代码
list 3, 25
  1. 查看指定函数源码
# 查看函数(foo)源码
list foo
  1. 查看指定文件内指定位置源码
# 查看指定文件(file.c)指定行号(11行)周边源码
list file.c:11
# 查看指定文件(file.c)内指定函数(foo)周边源码
list file.c:foo
2.2.3 配置
  1. 配置显示行数
# 显示当前配置(默认是10行)
show listsize
# 设置显示行数(设置为20行)
set listsize 20
  1. 配置源码路径
# 查看当前源码搜索路径
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类型如下表:

类型含义
bbyte:按照字节打印
hhalf word:半字,按照双字节打印
wword:字,按照四字节打印
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 调试过程

  1. 启动GDB

注意编译版本时需要带-g选项。

[usr_name@localhost file]$ gdb a.out # 使用gdb拉起需要调试的进程
...
Reading symbols from /home/usr_name/test/file/a.out...done. # 看到这行打印说明启动成功
(gdb) # gdb命令提示符

  1. 设置入参

需要调试的进程有入参时需要设置好入参,不然会影响程序的运行和调试。

(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)

  1. 设置断点
(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)
  1. 全速运行
(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;

  1. 设置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)

  1. 修改控制循环的变量
(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));

  1. 观察变量和内存

此时,观察我们最关心的两个变量 temppBaseAddr 指向的内存的值。

(gdb) p/x temp
$12 = 0xff81
(gdb) x/8xb pBaseAddr
0x7fffffffdea0: 0x80    0x81    0x82    0x83    0x84    0x85    0x86    0x87
(gdb)

  1. 问题定位

到这里,问题已经明朗:内存中的值是正确的,但是在解析内存时解析成了有符号数,因此自动在高位扩展了多个0xf导致。

  1. 退出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调试的整体流程和思路与我们熟悉的界面调试是完全一致的,只不过需要记住一些指令而已。

对于工具的学习只有一个秘诀——就是多加使用和练习。

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值