超过C中的数组 – 为什么这不会崩溃?

我有这段代码,它运行得很好,我不知道为什么:

int main(){ int len = 10; char arr[len]; arr[150] = 'x'; } 

说真的,试试吧! 它工作(至少在我的机器上)! 但是,如果我尝试更改索引太大的元素(例如索引20,000),它就不起作用。 所以编译器显然不够聪明,只能忽略那一行。

那怎么可能呢? 我真的很困惑……


好的,谢谢你的所有答案!

所以我可以使用它来写入堆栈中其他变量所消耗的内存,如下所示:

 #include  main(){ char b[4] = "man"; char a[10]; a[10] = 'c'; puts(b); } 

输出“可以”。 这是一件非常糟糕的事情。

好的谢谢。

为了提高效率,C编译器通常不生成用于检查数组边界的代码。 越界数组访问导致“未定义的行为”,一种可能的结果是“它工作”。 它不能保证会导致崩溃或其他诊断,但是如果您使用的是具有虚拟内存支持的操作系统,并且您的arrays索引指向尚未映射到物理内存的虚拟内存位置,那么您的程序就更多了可能会崩溃。

那怎么可能呢?

因为堆栈在你的机器上足够大,以至于堆栈上的内存位置碰巧&arr[150]恰好对应的位置,并且因为你的小示例程序在引用该位置的任何其他内容之前退出可能因为你覆盖它而崩溃了。

你正在使用的编译器没有检查是否有超过数组末尾的尝试(C99规范说你的示例程序中的arr[150]的结果将是“未定义的”,所以它可能无法编译它,但大多数C编译器没有)。

大多数实现不检查这些类型的错误。 内存访问粒度通常非常大(4 KiB边界),并且更细粒度的访问控制的成本意味着默认情况下不启用它。 有两种常见的错误方法可以导致现代操作系统崩溃:您从未映射的页面读取或写入数据(即时段错误),或者覆盖导致其他地方崩溃的数据。 如果你运气不好,那么缓冲区溢出不会崩溃(这是正确的, 不幸的 ),你将无法轻易诊断它。

但是,您可以打开仪器。 使用GCC时,使用Mudflap进行编译。

 $ gcc -fmudflap -Wall -Wextra test999.c -lmudflap
 test999.c:在函数'main'中:
 test999.c:3:9:警告:变量'arr'设置但未使用[-Wunused-but-set-variable]
 test999.c:5:1:警告:控制到达非void函数结束[-Wreturn-type]

以下是运行时会发生的情况:

 $ ./a.out 
 *******
 mudflap违规1(检查/写入):时间= 1362621592.763935 ptr = 0x91f910 size = 151
 pc = 0x7f43​​f08ae6a1 location =`test999.c:4:13(main)'
       /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_check+0x41)[0x7f43​​f08ae6a1]
       ./a.out(main+0xa6)[0x400a82]
       /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xfd)[0x7f43​​f0538ead]
附近对象1:检查区域开始0B进入并结束141B
 mudflap对象0x91f960:name =`alloca region'
 bounds = [0x91f910,0x91f919] size = 10 area = heap check = 0r / 3w liveness = 3
 alloc time = 1362621592.763807 pc = 0x7f43​​f08adda1
       /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_register+0x41)[0x7f43​​f08adda1]
       /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_wrap_alloca_indirect+0x1a4)[0x7f43​​f08afa54]
       ./a.out(main+0x45)[0x400a21]
       /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xfd)[0x7f43​​f0538ead]
附近物体数量:1

哦,看,它崩溃了。

请注意,Mudflap并不完美,它不会捕获所有错误。

在C规范下,访问数组末尾的元素是未定义的行为 。 未定义的行为意味着规范没有说明会发生什么 – 因此,理论上任何事情都可能发生。 程序可能会崩溃,或者可能不会崩溃,或者可能会在几小时后在一个完全不相关的函数中崩溃,或者它可能会擦除你的硬盘(如果你运气不好并将正确的位戳到正确的位置)。

未定义的行为不容易预测,绝对不应该依赖它。 只是因为某些东西似乎工作不正确,如果它调用未定义的行为。

因为你很幸运 或者说不吉利,因为这意味着找到错误更难。

如果您开始使用另一个进程的内存(或在某些情况下未分配的内存),运行时将只会崩溃。 打开时,你的应用程序会获得一定的内存,在这种情况下就足够了,你可以随心所欲地在自己的内存中乱七八糟,但是你会给自己一个调试工作的噩梦。

本机C数组不会进行边界检查。 这将需要额外的指令和数据结构。 C旨在提高效率和精益度,因此它没有指定以安全性换取性能的function。

您可以使用像valgrind这样的工具,它在一种模拟器中运行您的程序,并尝试通过跟踪哪些字节被初始化以及哪些不是初始化来检测缓冲区溢出等事情。 但它并非绝对可靠,例如,如果溢出访问恰好执行对另一个变量的合法访问。

在引擎盖下,数组索引只是指针算术。 当你说arr[ 150 ] ,你只需要将一个元素的sizeof增加150倍,并将其添加到arr的地址以获取特定对象的地址。 该地址只是一个数字,它可能是无意义的,无效的,或者本身就是算术溢出。 这些条件中的一些导致硬件生成崩溃,当它无法找到要访问的内存或检测到类似病毒的活动时,但没有一个导致软件生成的exception,因为没有软件挂钩的空间。 如果你想要一个安全的数组,你需要围绕添加原则构建函数。

顺便说一句,您的示例中的数组在技术上甚至不是固定大小。

 int len = 10; /* variable of type int */ char arr[len]; /* variable-length array */ 

使用非const对象设置数组大小是自C99以来的一项新function。 你可以将len作为函数参数,用户输入等。这对于编译时分析会更好:

 const int len = 10; /* constant of type int */ char arr[len]; /* constant-length array */ 

为了完整起见:C标准没有指定边界检查,但也没有禁止。 它属于未定义行为的类别,或者不需要生成错误消息的错误,并且可以产生任何影响。 可以实现安全arrays,存在各种近似的特征。 C通过使其成为非法方式向这个方向点头,例如,为了找到正确的越界索引以从arraysB访问任意对象A,可以获取两个数组之间的差异。但语言非常自由 -如果A和B是malloc的同一个内存块的一部分,那么它是合法的。 换句话说,您使用的特定于C的内存技巧越多,即使使用面向C的工具,也会越来越难以进行自动validation。