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命令,如下表所示。

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

TrustZone_Hcoco

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

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

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

打赏作者

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

抵扣说明:

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

余额充值