接着上一节的代码,在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
Breakpoint 1 at 0x80483b5: file main.c, line 5.
Starting program: /home/akaedu/main
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/7b input
0xbfb8f0a7: 0x31 0x32 0x33 0x34 0x35 0x00 0x00

x命令打印指定存储单元的内容。7b是打印格式,b表示每个字节一组,7表示打印7组,从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 0x80483e6: 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/7b input
0xbfb8f0a7: 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/7b input
0xbfb8f0a7: 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
Breakpoint 1 at 0x80483b5: file main.c, line 5.
Starting program: /home/akaedu/main
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 2: input[5]
(gdb) i watchpoints
Num Type Disp Enb Address What
2 hw watchpoint keep y input[5]
(gdb) c
Continuing.
Hardware watchpoint 2: input[5] Old value = 0 '\0'
New value = 1 '\001'
0x0804840c in main () at main.c:11
11 for (i = 0; input[i] != '\0'; i++)
(gdb) c
Continuing.
Hardware watchpoint 2: input[5] Old value = 1 '\001'
New value = 2 '\002'
0x0804840c in main () at main.c:11
11 for (i = 0; input[i] != '\0'; i++)
(gdb) c
Continuing.
Hardware watchpoint 2: input[5] Old value = 2 '\002'
New value = 3 '\003'
0x0804840c 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的命令:

不得不承认,在有些平台和操作系统上也未必得到这个结果,产生Bug的往往都是一些平台相关的问题,举这样的例子才比较像是真实软件开发中遇到的Bug,如果您的程序跑不出我这样的结果,那这一节您就凑合着看吧。


前面提出修正Bug的方法是在循环中加上判断条件,如果不是数字就报错退出,不仅输入字母可以报错退出,输入超长的字符串也会报错退出。表面上看这个程序无论怎么运行都不出错了,但假如我们把while (1)循环去掉,每次执行程序只转换一个数: 

#include <stdio.h>

int main(void)
{
int sum = 0, i = 0;
char input[5]; 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);
return 0;
}

然后输入一个超长的字符串,看看会发生什么:

$ ./main
1234567890
Invalid input!
input=-1

看起来正常。再来一次,这次输个更长的:

$ ./main
1234567890abcdef
Invalid input!
input=-1
Segmentation fault

又出段错误了。我们按同样的方法用gdb调试看看:

$ gdb main
...
(gdb) r
Starting program: /home/akaedu/main
1234567890abcdef
Invalid input!
input=-1 Program received signal SIGSEGV, Segmentation fault.
0x0804848e in main () at main.c:19
19 }
(gdb) l
14 }
15 sum = sum*10 + input[i] - '0';
16 }
17 printf("input=%d\n", sum);
18 return 0;
19 }

gdb指出,段错误发生在第19行。可是这一行什么都没有啊,只有表示main函数结束的}括号。这可以算是一条规律:如果某个函数的局部变量发生访问越界,有可能并不立即产生段错误,而是在函数返回时产生段错误

 

linux下的gdb调试工具--内存调试的更多相关文章

  1. linux下的gdb调试工具--断点调试

    到目前为止我们的调试手段只有一种: 根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以 ...

  2. linux下使用gdb对php源码调试

    title: linux下使用gdb对php源码调试 date: 2018-02-11 17:59:08 tags: --- linux下使用gdb进行php调试 调试了一些php的漏洞,记录一下大概 ...

  3. Linux下使用GDB进行调试

    Linux下使用GDB进行调试的常用命令记于此. $ sudo su # g++ -g test.cpp -o test -pthread # gdb test         <------- ...

  4. linux下如何产生core,调试core

    linux下如何产生core,调试core 摘自:http://blog.163.com/redhumor@126/blog/static/19554784201131791239753/ 在程序不寻 ...

  5. Linux下使用GDB调试程序

    问题描述:          Linux下使用GDB调试程序 问题解决:          (1)生成调试文件 注:         使用命令   gdb IOStream.c   -o IOStre ...

  6. Linux下的串口调试工具——Xgcom

    Linux下的串口调试工具——Xgcom xgcom的下载网址:https://code.google.com/archive/p/xgcom/downloads (1)安装必须的库 apt-get ...

  7. linux下对进程按照内存使用情况进行排序

    linux下对进程按照内存使用情况进行排序的命令为:ps aux --sort -rss 详细解说参见 http://alvinalexander.com/linux/unix-linux-proce ...

  8. 浅析Linux下进程间通信:共享内存

    浅析Linux下进程间通信:共享内存 共享内存允许两个或多个进程共享一给定的存储区.因为数据不需要在客户进程和服务器进程之间复制,所以它是最快的一种IPC.使用共享内存要注意的是,多个进程之间对一给定 ...

  9. linux下的CPU、内存、IO、网络的压力测试

    linux下的CPU.内存.IO.网络的压力测试  要远程测试其实很简单了,把结果放到一个微服务里直接在web里查看就可以了,或者同步到其他服务器上 一.对CPU进行简单测试: 1.通过bc命令计算特 ...

随机推荐

  1. Winform单例模式与传值

    单例模式(singleton)的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类. 在多窗体界面中,如果要加入一个“关于”的窗体,用于显 ...

  2. 字符和字符串处理-ANSI字符和Unicode字符

    我们知道,C语言用char数据类型表示一个8位的ANSI字符,默认在代码中声明一个字符串时,C编译器会把字符串中的字符转换成由8位char数据类型构成的一个数组: // An 8-bit charac ...

  3. push() & concat()

    eg. var arr = []; arr.push(1); arr.push([2, 3]); arr.push(4, 5); arr = arr.concat(6); arr = arr.conc ...

  4. mysql的四种隔离级别

    一.READ UNCOMMITTED(未提交读) 在READ UNCOMMITTED级别,事务中的修改,即使未提交,对其他事务也都是可见的.事务可以读取未提交的数据,这也被称为脏读( Dirty RE ...

  5. yii2不用composer使用redis

    1.下载redis https://github.com/yiisoft/yii2-redis 2.下载解压放到 basic\vendor\yiisoft\yii2-redis 3.编辑文件: bas ...

  6. 正式学习React(五) Reactjs 的 PropTypes 使用方法

    propTypes 使用來規範元件Props的型別與必需狀態 var Test = React.createClass({ propTypes: { // required requiredFunc: ...

  7. 构建混合云:配置Azure site to site VPN连接(3)

    9. 那么我们来创建网关,创建网关的时候需要注意,看看你的设备是否支持动态网关,在本示例中的Cisco ASA 5550不支持动态网关,所以我们只能创建静态网关: 该创建会花费一定的时间,稍等即可. ...

  8. ARM和X86功耗差别的深层原因探讨

    ARM和X86功耗差别的深层原因探讨 ARM和X86功耗的差别一直是个很热的话题.ARM可以做的很低,甚至1瓦都不到.而X86服务器的芯片可以达到100-200瓦,就算是嵌入式处理器Atom系列也需要 ...

  9. android HttpGet 另开线程获取网络数据问题

    android跨线程通讯可以使用android.os.Handler-android.os.Message这两类对象完成. public static void getResultForHttpGet ...

  10. codevs1039 数的划分

    题目描述 Description 将整数n分成k份,且每份不能为空,任意两种划分方案不能相同(不考虑顺序). 例如:n=7,k=3,下面三种划分方案被认为是相同的. 1 1 5 1 5 1 5 1 1 ...