为什么编译器没有警告超出范围的静态数组索引?

最近,我的一位同事通过写出堆栈上的静态数组(他在不增加数组大小的情况下添加了一个元素)而被严重咬了一口。 编译器不应该捕获这种错误吗? 以下代码使用gcc完全编译,即使使用-Wall -Wextra选项,但它显然是错误的:

 int main(void) { int a[10]; a[13] = 3; // oops, overwrote the return address return 0; } 

我很肯定这是不确定的行为,虽然我现在找不到C99标准的摘录。 但是在最简单的情况下,数组的大小称为编译时间并且索引在编译时是已知的,编译器是否应该至少发出警告?

海湾合作委员会对此提出警告。 但是你需要做两件事:

  1. 启用优化。 如果没有至少-O2,GCC就没有做足够的分析来了解a是什么,并且你跑掉了边缘。
  2. 更改您的示例以便实际使用[],否则GCC会生成无操作程序并完全放弃您的分配。

 $ cat foo.c int main(void) { int a[10]; a[13] = 3; // oops, overwrote the return address return a[1]; } $ gcc -Wall -Wextra -O2 -c foo.c foo.c: In function 'main': foo.c:4: warning: array subscript is above array bounds 

顺便说一句:如果你在测试程序中返回[13],那也不会有效,因为GCC再次优化了数组。

你有没有试过-fmudflap与GCC? 这些是运行时检查,但是很有用,因为大多数情况下,您无论如何都必须使用运行时计算的索引。 它会默默地继续工作,而不是默默地通知您这些错误。

-fmudflap -fmudflapth -fmudflapir对于支持它的前端(C和C ++),检测所有有风险的指针/数组解除引用操作,一些标准库字符串/堆函数,以及一些其他与范围/有效性测试相关的构造。 如此检测的模块应该不受缓冲区溢出,无效堆使用以及其他一些C / C ++编程错误的影响。 该指令依赖于一个单独的运行时库(libmudflap),如果在链接时给出-fmudflap,它将链接到一个程序中。 已检测程序的运行时行为由MUDFLAP_OPTIONS环境变量控制。 有关其选项,请参阅“env MUDFLAP_OPTIONS = -help a.out”。

如果您的程序是multithreading的,请使用-fmudflapth而不是-fmudflap进行编译和链接。 除-fmudflap或-fmudflapth外,如果检测应忽略指针读取,请使用-fmudflapir。 这样可以减少检测(因此执行速度更快),并且仍可以防止直接内存损坏写入,但允许错误地读取数据在程序中传播。

以下是mudflap为您提供的示例:

 [js@HOST2 cpp]$ gcc -fstack-protector-all -fmudflap -lmudflap mudf.c [js@HOST2 cpp]$ ./a.out ******* mudflap violation 1 (check/write): time=1229801723.191441 ptr=0xbfdd9c04 size=56 pc=0xb7fb126d location=`mudf.c:4:3 (main)' /usr/lib/libmudflap.so.0(__mf_check+0x3d) [0xb7fb126d] ./a.out(main+0xb9) [0x804887d] /usr/lib/libmudflap.so.0(__wrap_main+0x4f) [0xb7fb0a5f] Nearby object 1: checked region begins 0B into and ends 16B after mudflap object 0x8509cd8: name=`mudf.c:3:7 (main) a' bounds=[0xbfdd9c04,0xbfdd9c2b] size=40 area=stack check=0r/3w liveness=3 alloc time=1229801723.191433 pc=0xb7fb09fd number of nearby objects: 1 [js@HOST2 cpp]$ 

它有很多选择。 例如,它可以在违规时分叉gdb进程,可以显示程序泄漏的位置(使用-print-leaks )或检测未初始化的变量读取。 使用MUDFLAP_OPTIONS=-help ./a.out获取选项列表。 由于mudflap只输出地址而不是文件名和源代码行,我写了一个小小的gawk脚本:

 /^ / { file = gensub(/([^(]*).*/, "\\1", 1); addr = gensub(/.*\[([x[:xdigit:]]*)\]$/, "\\1", 1); if(file && addr) { cmd = "addr2line -e " file " " addr cmd | getline laddr print $0 " (" laddr ")" close (cmd) next; } } 1 # print all other lines 

将mudflap的输出传递到其中,它将显示每个回溯条目的源文件和行。

-fstack-protector[-all]

-fstack-protector发出额外的代码来检查缓冲区溢出,例如堆栈粉碎攻击。 这是通过向具有易受攻击对象的函数添加保护变量来完成的。 这包括调用alloca的函数,以及大于8字节的缓冲区的函数。 输入function时会初始化防护装置,然后在function退出时进行检查。 如果防护检查失败,则会打印错误消息并退出程序。

-fstack-protector-all类似-fstack-protector,但所有function都受到保护。

你是对的, 行为是不确定的 。 C99指针必须指向声明或堆分配的数据结构之外或仅仅一个元素。

我从来没有弄清楚gcc人如何决定何时发出警告。 我很震惊地得知 – -Wall 本身不会警告未初始化的变量; 至少你需要-O ,即使这样,警告有时也会被忽略。

我猜想因为无界数组在C中是如此常见,所以编译器可能在其表达式树中没有办法表示在编译时具有已知大小的数组。 因此,尽管声明中存在信息,但我猜想在使用时它已经丢失了。

是valgrind的推荐。 如果您使用C编程,则应始终在每个程序上运行valgrind,直到您无法再获得性能命中。

它不是静态数组。

是否未定义行为,它从数组的开头写入地址13个整数。 你的责任是什么? 由于合理的原因,有几种C技术故意错误分配数组。 在不完整的编译单元中,这种情况并不罕见。

根据您的标志设置,此程序的许多function都会被标记,例如从不使用该数组。 编译器可能很容易优化它而不是告诉你 – 一棵树落在森林里。

这是C方式。 这是你的arrays,你的记忆,做你想做的事。 🙂

(有许多lint工具可以帮助你找到这类东西;你应该自由地使用它们。虽然它们并不都是通过编译器完成的;编译和链接通常很乏味。)

C不这样做的原因是C没有这些信息。 像这样的陈述

 int a[10]; 

做两件事:它分配sizeof(int)*10个字节的空间(加上,可能还有一个用于对齐的小死空间),它在符号表中放入一个条目,从概念上讲,

 a : address of a[0] 

或以C语言

 a : &a[0] 

就这样。 事实上,在C中你可以将*(a+i)a[i]交换(几乎*)所有情况,但没有效果BY DEFINITION。 所以你的问题等同于问“为什么我可以在这个(地址)值中添加任何整数?”

*流行测验:这个例子什么,这不是真的吗?

C哲学是程序员永远是对的 。 因此,它会默默地允许您访问您在那里提供的任何内存地址,假设您始终知道自己在做什么,并且不会打扰您。

编译器不应该至少发出警告吗?

没有; C编译器通常不会执行数组边界检查。 正如您所提到的,这种明显的负面影响是未定义行为的错误,这很难找到。

在某些情况下,积极的一面是可能的小的性能优势。

我相信某些编译器在某些情况下会这样做。 例如,如果我的内存正确地为我服务,则较新的Microsoft编译器具有“缓冲区安全检查”选项,该选项将检测缓冲区溢出的微不足道的情况。

为什么不是所有编译器都这样做? (如前所述)编译器使用的内部表示不适合这种类型的静态分析,或者只是编写器优先级列表不够高。 说实话,无论如何都是一种耻辱。

gcc中有一些扩展(来自编译器方面) http://www.doc.ic.ac.uk/~awl03/projects/miro/

另一方面,夹板,鼠和其他一些静态代码分析工具都会发现。

您还可以在代码上使用valgrind并查看输出。 http://valgrind.org/

另一个广泛使用的图书馆似乎是libefence

这只是一个设计决策。 现在这导致了这件事。

关心弗里德里希

-fbounds-checking选项适用于gcc。

值得通过这篇文章http://www.doc.ic.ac.uk/~phjk/BoundsChecking.html

‘le dorfier’给出了你的问题的恰当答案,它是你的程序,它是C的行为方式。