Jon*_*Cav 240 c debugging buffer-overflow undefined-behavior
我最近开始学习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;
}
Run Code Online (Sandbox Code Playgroud)
在我的运行Ubuntu 14.04的笔记本电脑上,此代码不会中断.它运行完成.在我学校的运行CentOS 6.6的计算机上,它运行良好.在Windows 8.1上,循环永远不会终止.
更奇怪的是,当我将for循环条件编辑为:时i <= 11,代码只会在运行Ubuntu的笔记本电脑上终止.它永远不会在CentOS和Windows中终止.
任何人都可以解释内存中发生的事情以及运行相同代码的不同操作系统为什么会产生不同的结果?
编辑:我知道for循环超出范围.我是故意这样做的.我无法弄清楚不同操作系统和计算机之间的行为有何不同.
Que*_*onC 356
在我的运行Ubuntu 14.04的笔记本电脑上,此代码不会破坏它运行完成.在我学校的运行CentOS 6.6的计算机上,它运行良好.在Windows 8.1上,循环永远不会终止.
更奇怪的是,当我将
for循环的条件编辑为:时i <= 11,代码只会在运行Ubuntu的笔记本电脑上终止.CentOS和Windows永远不会终止.
你刚刚发现内存踩踏.你可以在这里阅读更多相关内容:什么是"内存踩踏"?
分配时int array[10],i;,这些变量进入内存(具体来说,它们是在堆栈上分配的,这是与函数关联的内存块). array[]并且i可能在记忆中彼此相邻.似乎在Windows 8.1上,i位于array[10].在CentOS上,i位于array[11].在Ubuntu上,它既没有位置(也许它在array[-1]?).
尝试将这些调试语句添加到您的代码中.您应该注意到在迭代10或11上,array[i]指向i.
#include <stdio.h>
int main()
{
int array[10],i;
printf ("array: %p, &i: %p\n", array, &i);
printf ("i is offset %d from array\n", &i - array);
for (i = 0; i <=11 ; i++)
{
printf ("%d: Writing 0 to address %p\n", i, &array[i]);
array[i]=0; /*code should never terminate*/
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
o11*_*11c 98
这些代码之间存在错误:
int array[10],i;
for (i = 0; i <=10 ; i++)
array[i]=0;
Run Code Online (Sandbox Code Playgroud)
由于array只有10个元素,在最后一次迭代中array[10] = 0;是缓冲区溢出.缓冲区溢出是未定义的行为,这意味着它们可能会格式化您的硬盘驱动器或导致恶魔飞出您的鼻子.
所有堆栈变量彼此相邻布局是相当常见的.如果i位于array[10]写入的位置,则UB将重置i为0,从而导致未终止的循环.
要修复,请将循环条件更改为i < 10.
Gil*_*il' 38
在应该是循环的最后一次运行时,你写入array[10],但是数组中只有10个元素,编号为0到9.C语言规范说这是"未定义的行为".这在实践中意味着你的程序将尝试写入int内存之后array的大小内存.然后发生什么取决于实际上是什么呢,这不仅取决于操作系统,还取决于编译器,编译器选项(例如优化设置),处理器体系结构,周围代码等等.它甚至可能因执行而异,例如由于地址空间随机化(可能不是在这个玩具示例中,但它确实发生在现实生活中).一些可能性包括:
i.循环永远不会终止,因为i重新启动为0.array它位于虚拟内存页面的末尾,并且未映射下一页.您在Windows上观察到的是编译器决定将变量i紧跟在数组后面的内存中,因此array[10] = 0最终分配给i.在Ubuntu和CentOS上,编译器没有放在i那里.几乎所有的C实现都在内存堆栈中对局部变量进行分组,但有一个主要的例外:一些局部变量可以完全放在寄存器中.即使变量在堆栈上,变量的顺序也由编译器决定,它可能不仅取决于源文件中的顺序,还取决于它们的类型(以避免浪费内存到会留下漏洞的对齐约束)在他们的名字上,在编译器的内部数据结构中使用的一些哈希值等.
如果你想知道你的编译器决定做什么,你可以告诉它向你展示汇编代码.哦,并学习解密汇编程序(它比编写它更容易).使用GCC(以及其他一些编译器,特别是在Unix世界中),传递选项-S以生成汇编代码而不是二进制代码.例如,这里是使用优化选项-O0(无优化)在amd64上使用GCC编译循环的汇编程序片段,并手动添加注释:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
Run Code Online (Sandbox Code Playgroud)
这里变量i是堆栈顶部下方52个字节,而数组在堆栈顶部下方48个字节处开始.所以这个编译器碰巧放在i数组之前; i如果你碰巧写信,你会被覆盖array[-1].如果array[i]=0改为array[9-i]=0,那么使用这些特定的编译器选项,您将在此特定平台上获得无限循环.
现在让我们用你的程序编译你的程序gcc -O1.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Run Code Online (Sandbox Code Playgroud)
那更短了!编译器不仅拒绝为堆栈位置分配i- 它只存储在寄存器中ebx- 但它没有为分配任何内存array或生成代码来设置其元素而烦恼,因为它注意到没有任何元素永远使用.
为了使这个例子更具说服力,让我们确保通过为编译器提供无法优化的东西来执行数组赋值.一种简单的方法是使用另一个文件中的数组 - 由于单独的编译,编译器不知道在另一个文件中发生了什么(除非它在链接时优化,哪个gcc -O0或哪个gcc -O1不优化).创建一个use_array.c包含的源文件
void use_array(int *array) {}
Run Code Online (Sandbox Code Playgroud)
并将您的源代码更改为
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
编译
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Run Code Online (Sandbox Code Playgroud)
这次汇编程序代码如下所示:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Run Code Online (Sandbox Code Playgroud)
现在数组在堆栈上,从顶部44个字节.怎么样i?它没有出现在任何地方!但循环计数器保存在寄存器中rbx.这不完全是i,但是地址array[i].编译器已经决定,由于i从未直接使用过的值,因此在每次循环运行期间执行算术计算存储0的位置没有意义.相反,地址是循环变量,并且确定边界的算法部分在编译时执行(每个数组元素乘以11个迭代乘以4个字节得到44),部分在运行时但在循环开始之前一次又一次地执行(执行减法以获得初始值).
即使在这个非常简单的例子中,我们已经看到了如何改变编译器选项(打开优化)或改变一些次要(array[i]to array[9-i])甚至改变一些显然不相关的东西(添加调用use_array)可以对可执行程序生成的内容产生重大影响由编译器做.编译器优化可以执行许多在调用未定义行为的程序上可能看起来不直观的事情.这就是为什么未定义的行为完全未定义的原因.当你偏离轨道时,在现实世界的程序中,很难理解代码的作用和应该做的事情之间的关系,即使对于有经验的程序员也是如此.
Yu *_*Hao 25
与Java不同,C不进行数组边界检查,即没有ArrayIndexOutOfBoundsException,确保数组索引有效的工作留给程序员.故意这样做会导致未定义的行为,任何事情都可能发生.
对于数组:
int array[10]
Run Code Online (Sandbox Code Playgroud)
指标仅在范围内有效0至9.但是,您正在尝试:
for (i = 0; i <=10 ; i++)
Run Code Online (Sandbox Code Playgroud)
访问array[10]此处,将条件更改为i < 10
Der*_*nes 19
你有一个边界违规,并且在非终止平台上,我相信你i在循环结束时无意中设置为零,以便它重新开始.
array[10]是无效的; 它包含10个元素,array[0]通过array[9],并且array[10]是第11个.你的循环应该写在之前 停止10,如下所示:
for (i = 0; i < 10; i++)
Run Code Online (Sandbox Code Playgroud)
如果array[10]土地是实施定义的,并且有趣的是,在你的两个平台上,它就会落地i,这些平台显然是直接布局的array. i设置为零,循环继续.对于您的其他平台,i可能位于之前array,或之后array可能有一些填充.
rak*_*rul 12
你申报int array[10]的手段array有指数0,以9(总10能容纳整数元素).但是以下循环,
for (i = 0; i <=10 ; i++)
Run Code Online (Sandbox Code Playgroud)
将循环0来10表示11时间.因此,i = 10它会溢出缓冲区并导致未定义的行为.
试试这个:
for (i = 0; i < 10 ; i++)
Run Code Online (Sandbox Code Playgroud)
要么,
for (i = 0; i <= 9 ; i++)
Run Code Online (Sandbox Code Playgroud)
它未定义array[10],并提供如前所述的未定义行为.想想这样:
我的杂货车里有10件物品.他们是:
0:一盒麦片
1:面包
2:牛奶
3:馅饼
4:鸡蛋
5:蛋糕
6:2升苏打
7:沙拉
8:汉堡
9:冰淇淋
cart[10]未定义,并且可能在某些编译器中给出一个超出范围的异常.但是,很多人显然没有.明显的第11项是实际上不在购物车中的项目.第11项指向,我将要称之为"恶作剧项目".它从来没有存在过,但它就在那里.
为什么有些编译器会给出i的指数array[10]或者array[11]甚至array[-1]是因为你的初始化/声明语句.一些编译器将此解释为:
ints 块array[10]和另一个int块.使它更容易,将它们放在彼此旁边."array[10]就不会指向i.i的array[-1](因为数组的索引不能或不应该是负的),或在完全不同的点分配,因为操作系统可以处理它,和它的安全.有些编译器希望事情变得更快,有些编译器更喜欢安全性.这完全取决于背景.例如,如果我正在为古老的BREW OS(基本手机的操作系统)开发应用程序,它就不会关心安全性.如果我正在为iPhone 6开发,那么它无论如何都可以快速运行,所以我需要强调安全性.(说真的,您是否阅读过Apple的App Store指南,或者阅读Swift和Swift 2.0的开发?)
由于您创建了一个大小为10的数组,因此for循环条件应如下所示:
int array[10],i;
for (i = 0; i <10 ; i++)
{
Run Code Online (Sandbox Code Playgroud)
目前您正在尝试使用内存访问未分配的位置array[10],这会导致未定义的行为.未定义的行为意味着您的程序将以不确定的方式运行,因此它可以在每次执行时提供不同的输出.
好吧,C编译器传统上不检查边界.如果您引用了不属于您的流程的位置,则可能会出现细分错误.但是,局部变量是在堆栈上分配的,并且根据内存的分配方式,array(array[10])之外的区域可能属于进程的内存段.因此,不会抛出分段故障陷阱,这就是您似乎遇到的情况.正如其他人所指出的那样,这是C中未定义的行为,您的代码可能被认为是不稳定的.由于您正在学习C,因此最好养成检查代码中边界的习惯.