GDB调试学习(三):观察点

简介: GDB调试学习(三):观察点

接着上一篇《GDB调试学习(二)》里面的步骤,经过调试我们知道,虽然sum已经赋了初值0,但仍需要在while (1)循环的开头加上sum = 0;

观察点调试实例

#include <stdio.h>
      int main(void)
      {
        int sum = 0, i = 0;
        char input[5];
        while (1) {
                      sum = 0;
                      scanf("%s", input);
                      for (i = 0; input[i] != '\0'; i++)
                              sum = sum*10 + input[i] - '0';
                      printf("input=%d\n", sum);
        }
        return 0;
      }

使用scanf函数是非常凶险的,即使修正了这个Bug也还存在很多问题。如果输入的字符串超长了会怎么样?我们知道数组访问越界是不会检查的,所以scanf会写出界。现象是这样的:

$ ./main
      123
      input=123
      67
      input=67
      12345
      input=123407

下面用调试器看看最后这个诡异的结果是怎么出来的。

$ gdb main
      ...
      (gdb) start
      Temporary breakpoint 1 at 0x804844d: file main.c, line 5.
      Starting program: /home/akaedu/main
      Temporary breakpoint 1, main () at main.c:5
      5               int sum = 0, i = 0;
      (gdb) n
      9                       sum = 0;
      (gdb)(直接回车)
      10                      scanf("%s", input);
      (gdb)(直接回车)
      12345
      11                      for (i = 0; input[i] != '\0'; i++)
      (gdb) p input
      $1 = "12345"

input数组只有5个元素,写出界的是scanf自动添的’\0’,用x命令查看会更清楚一些:

(gdb) x/7bx input
      0xbffff373: 0x31    0x32    0x33    0x34    0x35    0x00    0x00

x命令打印指定存储单元里保存的内容,后缀7bx是打印格式,7表示打印7组,b表示每个字节一组,x表示按十六进制格式打印[插图],x/7bx这条命令从input数组的第一个字节开始连续打印7个字节。

前5个字节是input数组的存储单元,打印的正是十六进制ASCII码的’1’到’5’,第6个字节是写出界的’\0’。根据运行结果,前4个字符转成数字都没错,第5个错了,也就是i从0到3的循环都没错,我们设一个条件断点从i等于4开始单步调试:

(gdb) l
      6               char input[5];
      7
      8               while (1) {
      9                       sum = 0;
      10                      scanf("%s", input);
      11                      for (i = 0; input[i] != '\0'; i++)
      12                              sum = sum*10 + input[i] - '0';
      13                      printf("input=%d\n", sum);
      14              }
      15              return 0;
      (gdb) b 12 if i == 4
      Breakpoint 2 at 0x8048484: file main.c, line 12.
      (gdb) c
      Continuing.
      Breakpoint 2, main () at main.c:12
      12                              sum = sum*10 + input[i] - '0';
      (gdb) p sum
      $2 = 1234

现在sum是1234没错,根据运行结果是123407,我们知道即将进行的这步计算肯定要出错,算出来应该是12340,那就是说input[4]肯定不是’5’了,事实证明这个推理是不严谨的:

(gdb) x/7bx input
      0xbffff373: 0x31    0x32    0x33    0x34    0x35    0x04    0x00

input[4]的确是0x35。再分析一下发现,产生123407这个结果还有另外一种可能,就是在下一次循环中123450不是加上而是减去一个数得到123407。可现在不是到字符串末尾了吗?

怎么会有下一次循环呢?注意到循环控制条件是input[i]!=‘\0’,而本来应该是0x00的位置现在莫名其妙地变成了0x04,因此循环不会结束。继续单步调试:

(gdb) n
      11                  for (i = 0; input[i] != '\0'; i++)
      (gdb) p sum
      $3 = 12345
      (gdb) n
      12                          sum = sum*10 + input[i] - '0';
      (gdb) x/7bx input
      0xbffff373: 0x31    0x32    0x33    0x34    0x35    0x05    0x00

进入下一次循环,原来的0x04又莫名其妙地变成了0x05,这是怎么回事?这个暂时解释不了,但123407这个结果可以解释了,是12345×10 + 0x05-0x30得到的,虽然多循环了一次,但下次一定会退出循环了,因为0x05的后面是’\0’。

input[4]后面那个字节到底是什么时候变的?可以用观察点(Watchpoint)来跟踪。

我们知道断点是当程序执行到某一代码行时中断,而观察点是当程序访问某个存储单元时中断,如果我们不知道某个存储单元是在哪里被改动的,这时候观察点尤其有用。下面删除原来设的断点,从头执行程序,重复上次的输入,用watch命令设置观察点,跟踪input[4]后面那个字节(可以用input[5]表示,虽然这是访问越界):

(gdb) delete breakpoints
      Delete all breakpoints? (y or n) y
      (gdb) start
      The program being debugged has been started already.
      Start it from the beginning? (y or n) y
      Temporary breakpoint 3 at 0x804844d: file main.c, line 5.
      Starting program: /home/akaedu/main
      Temporary breakpoint 3, main () at main.c:5
      5               int sum = 0, i = 0;
      (gdb) n
      9                       sum = 0;
      (gdb)(直接回车)
      10                      scanf("%s", input);
      (gdb)(直接回车)
      12345
      11                      for (i = 0; input[i] != '\0'; i++)
      (gdb) watch input[5]
      Hardware watchpoint 4: input[5]
      (gdb) i watchpoints
      Num    Type          Disp Enb Address   What
      4      hw watchpoint  keep y            input[5]
      (gdb) c
      Continuing.
      Hardware watchpoint 4: input[5]
      Old value = 0 '\000'
      New value = 1 '\001'
      0x080484ae in main () at main.c:11
      11                      for (i = 0; input[i] != '\0'; i++)
      (gdb) c
      Continuing.
      Hardware watchpoint 4: input[5]
      Old value = 1 '\001'
      New value = 2 '\002'
      0x080484ae in main () at main.c:11
      11                      for (i = 0; input[i] != '\0'; i++)
      (gdb) c
      Continuing.
      Hardware watchpoint 4: input[5]
      Old value = 2 '\002'
      New value = 3 '\003'
      0x080484ae in main () at main.c:11
      11                      for (i = 0; input[i] != '\0'; i++)

已经很明显了,每次都是回到for循环开头的时候改变了input[5]的值,而且是每次加1,而循环变量i正是在每次回到循环开头之前加1,原来input[5]就是变量i的存储单元,换句话说,i的存储单元是紧跟在input数组后面的。

修正这个Bug对初学者来说有一定难度。如果你发现了这个Bug却没想到数组访问越界这一点,也许一时想不出原因,就会先去处理另外一个更容易修正的Bug:如果输入的不是数字而是字母或别的符号也能算出结果来。这显然是不对的,可以在循环中加上判断条件检查非法字符:

while (1) {
          sum = 0;
          scanf("%s", input);
          for (i = 0; input[i] != '\0'; i++) {
                  if (input[i] < '0' || input[i] > '9') {
                  printf("Invalid input!\n");
                  sum = -1;
                  break;
                  }
                  sum = sum*10 + input[i] - '0';
          }
          printf("input=%d\n", sum);
      }

然后你会惊喜地发现,不仅输入字母会报错,输入超长也会报错:

$ ./main
      123a
      Invalid input!
      input=-1
      dead
      Invalid input!
      input=-1
      1234578
      Invalid input!
      input=-1
      1234567890abcdef
      Invalid input!
      input=-1
      23
      input=23

似乎是两个Bug一起解决掉了,但这是治标不治本的解决方法。

看起来输入超长的错误是不会出现了,但只要没有找到根本原因就不可能真的解决掉,等到条件一变,它可能又冒出来了,在下一节你会看到它又以一种新的形式冒出来了。

现在请思考一下为什么加上检查非法字符的代码之后输入超长也会报错。最后总结一下本节用到的gdb命令,如下表所示。

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
4月前
|
NoSQL Linux C语言
Linux GDB 调试
Linux GDB 调试
67 10
|
4月前
|
NoSQL Linux C语言
嵌入式GDB调试Linux C程序或交叉编译(开发板)
【8月更文挑战第24天】本文档介绍了如何在嵌入式环境下使用GDB调试Linux C程序及进行交叉编译。调试步骤包括:编译程序时加入`-g`选项以生成调试信息;启动GDB并加载程序;设置断点;运行程序至断点;单步执行代码;查看变量值;继续执行或退出GDB。对于交叉编译,需安装对应架构的交叉编译工具链,配置编译环境,使用工具链编译程序,并将程序传输到开发板进行调试。过程中可能遇到工具链不匹配等问题,需针对性解决。
112 3
|
4月前
|
NoSQL
技术分享:如何使用GDB调试不带调试信息的可执行程序
【8月更文挑战第27天】在软件开发和调试过程中,我们有时会遇到需要调试没有调试信息的可执行程序的情况。这可能是由于程序在编译时没有加入调试信息,或者调试信息被剥离了。然而,即使面对这样的挑战,GDB(GNU Debugger)仍然提供了一些方法和技术来帮助我们进行调试。以下将详细介绍如何使用GDB调试不带调试信息的可执行程序。
121 0
|
6月前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
49 1
|
6月前
|
NoSQL Linux C语言
Linux gdb调试的时候没有对应的c调试信息库怎么办?
Linux gdb调试的时候没有对应的c调试信息库怎么办?
36 0
|
6月前
|
NoSQL Linux C++
Linux C/C++ gdb调试正在运行的程序
Linux C/C++ gdb调试正在运行的程序
|
6月前
|
NoSQL Linux C++
Linux C/C++ gdb调试core文件
Linux C/C++ gdb调试core文件
|
6月前
|
NoSQL Linux C++
Linux C/C++ gdb调试
Linux C/C++ gdb调试
|
7月前
|
NoSQL Ubuntu 测试技术
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
【GDB自定义指令】core analyzer结合gdb的调试及自定义gdb指令详情
99 1
|
7月前
|
NoSQL 编译器 C语言
【GDB调试技巧】提高gdb的调试效率
【GDB调试技巧】提高gdb的调试效率
85 1