我最近开始学习C,我正在上课,以C为主题 . 我正在玩循环,我遇到了一些我不知道如何解释的奇怪行为 .
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
在运行Ubuntu 14.04的笔记本电脑上,此代码不会中断 . 它运行完成 . 在我学校的运行CentOS 6.6的计算机上,它运行良好 . 在Windows 8.1上,循环永远不会终止 .
更奇怪的是,当我将 for
循环的条件编辑为: i <= 11
时,代码仅在运行Ubuntu的笔记本电脑上终止 . 它永远不会在CentOS和Windows中终止 .
任何人都可以解释内存中发生的事情以及为什么运行相同代码的不同操作系统会产生不同的结果?
编辑:我知道for循环超出范围 . 我故意这样做 . 我无法弄清楚不同操作系统和计算机之间的行为有何不同 .
14 回答
你刚刚发现内存踩踏 . 你可以在这里阅读更多相关信息:What is a “memory stomp”?
当你分配
int array[10],i;
时,那些变量进入内存(具体来说,它们被分配在堆栈上,这是与该函数关联的内存块) .array[]
和i
可能在内存中彼此相邻 . 似乎在Windows 8.1上,i
位于array[10]
. 在CentOS上,i
位于array[11]
. 在Ubuntu上,'s in neither spot (maybe it'在array[-1]
?) .尝试将这些调试语句添加到您的代码中 . 您应该注意到在迭代10或11上,
array[i]
指向i
.这些代码之间存在错误:
由于
array
只有10个元素,因此在最后一次迭代中array[10] = 0;
是缓冲区溢出 . 缓冲区溢出是 UNDEFINED BEHAVIOR ,这意味着它们可能会格式化您的硬盘驱动器或导致恶魔飞出您的鼻子 .所有堆栈变量彼此相邻布局是相当常见的 . 如果
i
位于array[10]
写入的位置,则UB会将i
重置为0
,从而导致未终止的循环 .要修复,请将循环条件更改为
i < 10
.在循环的最后一次运行中,你写入
array[10]
,但是数组中只有10个元素,编号为0到9.C语言规范说这是“未定义的行为” . 这在实践中意味着你的程序将尝试写入内存中紧跟在_791606之后的int
大小的内存 . 然后发生什么取决于实际上是什么呢,这不仅取决于操作系统,还取决于编译器,编译器选项(例如优化设置),处理器体系结构,周围代码甚至可能因执行而异,例如由于address space randomization(可能不是这个玩具的例子,但确实发生在现实生活中) . 一些可能性包括:未使用该位置 . 循环正常终止 .
该位置用于碰巧具有值0的内容 . 循环正常终止 .
该位置包含函数的返回地址 . 循环正常终止,但程序崩溃,因为它试图跳转到地址0 .
该位置包含变量
i
. 循环永远不会终止,因为i
在0处重新启动 .该位置包含一些其他变量 . 循环正常终止,但随后发生“有趣”的事情 .
该位置是无效的内存地址,例如因为
array
位于虚拟内存页面的末尾,并且未映射下一页 .Demons fly out of your nose . 幸运的是,大多数计算机缺少必要的硬件 .
您在Windows上观察到的是编译器决定将变量
i
紧跟在数组后面的内存中,因此array[10] = 0
最终分配给i
. 在Ubuntu和CentOS上,编译器没有在那里放置i
. 几乎所有C实现都在memory stack上对内存中的局部变量进行分组,但有一个主要的例外:一些局部变量可以完全放在registers中 . 即使变量在堆栈上,变量的顺序也由编译器决定,它可能不仅取决于源文件中的顺序,还取决于它们的类型(以避免浪费内存到会留下漏洞的对齐约束)在他们的名字上,在编译器的内部数据结构中使用的一些哈希值等 .如果你想知道你的编译器决定做什么,你可以告诉它向你展示汇编代码 . 哦,并学习解密汇编程序(比编写它更容易) . 使用GCC(以及其他一些编译器,特别是在Unix世界中),传递选项
-S
以生成汇编代码而不是二进制代码 . 例如,这里是使用优化选项-O0
(无优化)在amd64上使用GCC编译循环的汇编程序片段,并手动添加注释:这里变量
i
在堆栈顶部下方52个字节,而数组在堆栈顶部下方48个字节处开始 . 所以这个编译器碰巧在数组之前放置了i
;如果你碰巧写到array[-1]
,你会覆盖i
. 如果将array[i]=0
更改为array[9-i]=0
,则会使用这些特定的编译器选项在此特定平台上获得无限循环 .现在让我们用
gcc -O1
编译你的程序 .那更短了!编译器不仅拒绝为
i
分配堆栈位置 - 它只存储在寄存器ebx
中 - 但是它没有为array
分配任何内存,或者生成代码来设置其元素,因为它注意到没有这些元素的使用 .为了使这个例子更具说服力,让's ensure that the array assignments are performed by providing the compiler with something it isn'能够优化 . 一种简单的方法是使用另一个文件中的数组 - 由于单独的编译,编译器不知道在另一个文件中发生了什么(除非它在链接时优化,
gcc -O0
或gcc -O1
没有) . 创建一个包含的源文件use_array.c
并将您的源代码更改为
编译
这次汇编程序代码如下所示:
现在数组在堆栈中,从顶部44个字节 .
i
怎么样?它没有出现在任何地方!但循环计数器保存在寄存器rbx
中 . 它不完全是i
,而是array[i]
的地址 . 编译器已经决定,由于i
的值从未直接使用过,因此在每次运行循环期间执行算术计算存储0的位置没有意义 . 相反,地址是循环变量,并且确定边界的算法部分在编译时执行(乘以11个迭代乘以每个数组元素4个字节得到44),部分在运行时但在循环开始之前一次又一次(执行减法以获得初始值) .即使在这个非常简单的例子中,我们已经看到了如何改变编译器选项(打开优化)或改变一些次要的东西(
array[i]
到array[9-i]
)甚至改变一些显然不相关的东西(将调用添加到use_array
)可能会对编译器生成的可执行程序 . Compiler optimizations can do a lot of things that may appear unintuitive on programs that invoke undefined behavior . 这就是为什么未定义的行为完全未定义的原因 . 当你偏离轨道时,在现实世界的程序中,很难理解代码的作用和应该做的事情之间的关系,即使对于有经验的程序员也是如此 .与Java不同,C没有
ArrayIndexOutOfBoundsException
,确保数组索引有效的工作留给程序员 . 故意这样做会导致未定义的行为,任何事情都可能发生 .对于数组:
索引仅在
0
到9
范围内有效 . 但是,您正在尝试:访问
array[10]
此处,将条件更改为i < 10
你有一个边界违规,并且在非终止平台上,我相信你无意中在循环结束时将
i
设置为零,以便它重新开始 .array[10]
无效;它包含10个元素,array[0]
到array[9]
,array[10]
是第11个 . 您的循环应该写在10
之前停止,如下所示:array[10]
土地是实施定义的,有趣的是,在你的两个平台上,它落在i
上,这些平台显然是在array
之后直接布局的 .i
设置为零,循环继续 . 对于您的其他平台,i
可能位于array
之前,或者array
可能位于其之后 .您声明
int array[10]
表示array
具有索引0
至9
(它可以容纳的整数10
整数元素) . 但是以下循环,将
0
循环到10
表示11
时间 . 因此,当i = 10
它将溢出缓冲区并导致Undefined Behavior .所以试试这个:
要么,
它在
array[10]
未定义,并如前所述给出 undefined behavior . 想想这样:我的杂货车里有10件物品 . 他们是:
0:一盒麦片
1:面包
2:牛奶
3:馅饼
4:鸡蛋
5:蛋糕
6:2升苏打水
7:沙拉
8:汉堡包
9:冰淇淋
cart[10]
未定义,并且可能在某些编译器中提供超出范围的异常 . 但是,很多人显然没有 . 明显的第11项是一个项目 actually in the cart. 第11项是指向的,我打算称之为"poltergeist item."它从未存在,但它就在那里 .为什么一些编译器给
i
索引array[10]
或array[11]
甚至array[-1]
是因为你的初始化/声明语句 . 一些编译器将此解释为:“为
array[10]
分配了10个int
秒块,另一个int
块. to make it easier, 将它们放在彼此旁边 . ”与之前相同,但将其移动一两个空格,以便
array[10]
不指向i
.和以前一样,但在
array[-1]
分配i
(因为数组的索引可以't, or shouldn' t,为负数),或者在完全不同的位置分配它,因为操作系统可以处理它,并且它是 safer.有些编译器希望事情变得更快,有些编译器更喜欢安全性 . 这完全取决于背景 . 例如,如果我正在为古老的BREW OS(基本手机的操作系统)开发应用程序,它就不会关心安全性 . 如果我正在为iPhone 6开发,那么它无论如何都可以快速运行,所以我需要强调安全性 . (说真的,您是否阅读过Apple的App Store指南,或阅读Swift和Swift 2.0的开发?)
由于您创建了一个大小为10的数组,因此for循环条件应如下所示:
目前您正在尝试使用
array[10]
从内存中访问未分配的位置,并且它正在导致 undefined behavior . 未定义的行为意味着您的程序将以不确定的方式运行,因此它可以在每次执行时提供不同的输出 .好吧,C编译器传统上不检查边界 . 如果您引用的流程不是"belong",则可能会出现分段错误 . 但是,局部变量在堆栈上分配,并且根据内存的分配方式,数组之外的区域(
array[10]
)可能属于进程的内存段 . 因此,不会抛出任何分段故障陷阱,这就是您似乎遇到的情况 . 正如其他人所指出的,这是C中未定义的行为,您的代码可能被认为是不稳定的 . 由于您正在学习C,因此最好养成检查代码中边界的习惯 .除了内存可能被布置以便尝试写入
a[10]
实际上覆盖i
之外,优化编译器也可能确定无法在没有代码的情况下使用大于10的i
值来达到循环测试 . 首先访问不存在的数组元素a[10]
.由于尝试访问该元素将是未定义的行为,因此编译器对该程序在该点之后可能执行的操作没有任何义务 . 更具体地说,由于编译器没有义务生成代码以在任何可能大于10的情况下检查循环索引,因此它没有义务生成代码来检查循环索引;它可以改为假设
<=10
测试将始终产生真 . 请注意,即使代码读取a[10]
而不是编写它,也是如此 .当您迭代
i==9
时,您将'array items'分配给实际位于 past the array 的零,因此您将覆盖其他一些数据 . 很可能你会覆盖位于a[]
之后的i
变量 . 这样你只需 reset the i variable to zero ,从而重新启动循环 .如果你在循环中打印
i
,你可以发现自己:而不仅仅是
当然,这个结果很大程度上取决于变量的内存分配,而后者又取决于编译器及其设置,因此通常是 Undefined Behavior - 这就是为什么不同机器或不同操作系统或不同编译器上的结果可能不同的原因 .
错误在部分数组[10]中,w / c也是i的地址(int array [10],i;) . 当array [10]设置为0时,i将为0 w / c重置整个循环并导致无限循环 . 如果array [10]在0-10之间,则会有无限循环 . 正确的循环应该是(i = 0; i <10; i) int array [10],i; for(i = 0; i <= 10; i)array [i] = 0;
我会建议我在上面找到的东西:
尝试分配数组[i] = 20;
我想这应该在任何地方终止代码..(给你保持我<= 10或ll)
如果这个运行你可以坚定地决定这里指定的答案已经是正确的[答案与内存踩踏一个相关 . ]
这里有两件事是错的 . int i实际上是一个数组元素array [10],如堆栈中所示 . 因为你允许索引实际上使数组[10] = 0,所以循环索引i永远不会超过10.使它成为
for(i=0; i<10; i+=1)
.我是K&R会称之为'bad style' . 它以i的大小递增i,而不是1.我用于指针数学而i = 1用于代数 . 虽然这取决于编译器,但它对于可移植性来说并不是一个好的约定 .