3

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

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);
}

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

好的谢谢。

4

6 回答 6

4

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

于 2013-03-07T01:34:13.013 回答
3

那么这怎么可能呢?

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

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

于 2013-03-07T01:39:26.467 回答
2

大多数实现不检查这些类型的错误。内存访问粒度通常非常大(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:警告:控制到达非空函数的结尾 [-Wreturn-type]

运行它时会发生以下情况:

$ ./a.out
*******
挡泥板违规1(检查/写入):时间=1362621592.763935 ptr=0x91f910 size=151
pc=0x7f43​​f08ae6a1 位置=`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 结束
挡泥板对象 0x91f960: name=`alloca region'
bounds=[0x91f910,0x91f919] size=10 area=heap check=0r/3w liveness=3
分配时间=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 并不完美,它不会捕捉到您的所有错误。

于 2013-03-07T02:04:32.547 回答
1

本机 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 的工具,自动验证就越困难。

于 2013-03-07T02:16:48.677 回答
0

在 C 规范下,访问数组末尾的元素是未定义的行为。未定义的行为意味着规范没有说明会发生什么——因此,理论上任何事情都可能发生。该程序可能会崩溃,也可能不会,或者它可能会在几个小时后在完全不相关的功能中崩溃,或者它可能会擦除您的硬盘驱动器(如果您不走运并且将正确的位插入正确的位置)。

未定义的行为不容易预测,绝对不应该依赖它。如果它调用未定义的行为,仅仅因为某些东西似乎有效并不能使它正确。

于 2013-03-07T01:42:44.560 回答
0

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

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

于 2013-03-07T01:42:48.267 回答