C中的数组越界--为什么不会崩溃?

q9rjltbz  于 2023-05-16  发布在  其他
关注(0)|答案(6)|浏览(339)

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

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

说真的,试试看!它工作(至少在我的机器上)!但是,如果我试图更改索引太大的元素,例如索引20,000,它就不起作用了。所以编译器显然不够聪明,不能忽略这一行。
这怎么可能呢?
答:我可以用它来写入堆栈上其他变量消耗的内存,如下所示:

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

输出“可以”。这可不是什么好事

chhkpiq4

chhkpiq41#

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

bxfogqkk

bxfogqkk2#

这怎么可能呢?
因为在你的机器上,堆栈足够大,以至于堆栈上恰好有一个内存位置与&arr[150]对应,而且因为你的小示例程序在任何其他东西引用该位置之前退出,并且可能因为你覆盖了它而崩溃。
你使用的编译器不会检查是否有超过数组末尾的尝试(C99规范说,在你的示例程序中,arr[150]的结果是“undefined”,所以它可能会编译失败,但大多数C编译器不会)。

tyu7yeag

tyu7yeag3#

大多数实现不检查这类错误。内存访问粒度通常非常大(4 KiB边界),细粒度访问控制的成本意味着默认情况下不启用它。在现代操作系统上,错误导致崩溃的常见方式有两种:要么从未Map的页面读取或写入数据(即时segfault),要么覆盖导致其他地方崩溃的数据。如果你运气不好,那么缓冲区溢出不会崩溃(这是正确的,* 不幸 *),你将无法轻松诊断它。
但是,您可以打开检测。使用GCC时,编译时启用Mudflap。

$ gcc -fmudflap -Wall -Wextra test999.c -lmudflap
test999.c: In function ‘main’:
test999.c:3:9: warning: variable ‘arr’ set but not used [-Wunused-but-set-variable]
test999.c:5:1: warning: control reaches end of non-void function [-Wreturn-type]

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

$ ./a.out 
*******
mudflap violation 1 (check/write): time=1362621592.763935 ptr=0x91f910 size=151
pc=0x7f43f08ae6a1 location=`test999.c:4:13 (main)'
      /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_check+0x41) [0x7f43f08ae6a1]
      ./a.out(main+0xa6) [0x400a82]
      /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xfd) [0x7f43f0538ead]
Nearby object 1: checked region begins 0B into and ends 141B after
mudflap object 0x91f960: name=`alloca region'
bounds=[0x91f910,0x91f919] size=10 area=heap check=0r/3w liveness=3
alloc time=1362621592.763807 pc=0x7f43f08adda1
      /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_register+0x41) [0x7f43f08adda1]
      /usr/lib/x86_64-linux-gnu/libmudflap.so.0(__mf_wrap_alloca_indirect+0x1a4) [0x7f43f08afa54]
      ./a.out(main+0x45) [0x400a21]
      /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xfd) [0x7f43f0538ead]
number of nearby objects: 1

哦,看,它崩溃了。
请注意,Mudflap并不完美,它不会捕获所有错误。

l7wslrjt

l7wslrjt4#

原生C数组不进行边界检查。这将需要额外的指令和数据结构。C是为效率和精简而设计的,所以它没有指定牺牲性能来换取安全的特性。
你可以使用像valgrind这样的工具,它在一种模拟器中运行你的程序,并试图通过跟踪哪些字节被初始化,哪些没有来检测缓冲区溢出之类的事情。但它也不是绝对正确的,例如,如果溢出访问碰巧执行了对另一个变量的合法访问。
在引擎盖下,数组索引只是指针算术。当你说arr[ 150 ]时,你只是把sizeof的150倍加上一个元素,然后把它加到arr的地址上,以获得一个特定对象的地址。该地址只是一个数字,它可能是无意义的、无效的,或者本身就是一个算术溢出。其中一些条件会导致硬件在找不到要访问的内存或检测到类似病毒的活动时产生崩溃,但这些条件都不会导致软件生成的异常,因为没有软件钩子的空间。如果你想要一个安全的数组,你需要围绕加法原理来构建函数。
顺便说一句,您的示例中的数组在技术上甚至不是固定大小的。

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

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

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

为了完整起见:C标准没有指定边界检查,但也没有禁止。它福尔斯***未定义行为 * 的范畴,或者不需要生成错误消息的错误,并且可以有任何影响。**可以实现安全数组,存在各种近似的功能。C * 确实在这个方向上点头,例如,为了找到正确的越界索引来访问数组B中的任意对象A,取两个数组之间的差是非法的。但是这种语言是非常自由的,如果A和B是来自malloc的同一个内存块的一部分,那么它是法律的的。换句话说,使用越多的C特定内存技巧,即使使用面向C的工具,自动验证也会变得越困难。

rsl1atfo

rsl1atfo5#

在C规范下,访问数组末尾之后的元素是 *undefined behavior *。未定义的行为意味着规范没有说明会发生什么--因此,在理论上,任何事情都可能发生。程序可能会崩溃,也可能不会,或者它可能会在几个小时后在一个完全不相关的功能中崩溃,或者它可能会擦除你的硬盘(如果你运气不好,把正确的位插入正确的位置)。
未定义的行为不容易预测,绝对不应该依赖它。仅仅因为某些东西看起来工作并不意味着它是正确的,如果它调用了未定义的行为。

rn0zuynd

rn0zuynd6#

因为你很幸运。或者更不走运,因为这意味着更难找到bug。
只有当你开始使用 * 另一个进程 * 的内存(或者在某些情况下未分配的内存)时,运行时才会崩溃。应用程序在打开时会被分配一定量的内存,在本例中,这就足够了,您可以在自己的内存中随意乱用,但这会给自己带来调试工作的噩梦。

相关问题