接着上一篇《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命令,如下表所示。