为什么编译器不警告越界静态数组索引?

Why do compilers not warn about out-of-bounds static array indices?

提问人:Adam Rosenfield 提问时间:12/20/2008 更新时间:12/10/2016 访问量:9723

问:

我的一位同事最近因为在堆栈上越界写入静态数组而受到严重咬伤(他在不增加数组大小的情况下向其中添加了一个元素)。编译器不应该捕获这种错误吗?以下代码使用 gcc 干净地编译,即使使用选项也是如此,但它显然是错误的:-Wall -Wextra

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

我确信这是未定义的行为,尽管我目前找不到 C99 标准的摘录。但是在最简单的情况下,数组的大小被称为编译时,索引在编译时是已知的,编译器不应该至少发出警告吗?

C 数组 警告

评论

0赞 shoosh 12/20/2008
"...在不增加数组大小的情况下......“——我想知道他是如何实现的,因为它是一个静态数组......
0赞 Adam Rosenfield 12/20/2008
他的代码就像“int a[2];a[0] = 0;a[1] = 1;“,然后他添加了”a[2] = 2;“,而没有将 a 的大小增加到 3。
0赞 Robert Gamble 12/21/2008
@Adam:参见§6.5.6p8,即(理解a[13] = *(a+13):“如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素,则计算不应产生溢出;否则,行为是未定义的。

答:

0赞 Friedrich 12/20/2008 #1

gcc 中对此(从编译器端)有一些扩展 http://www.doc.ic.ac.uk/~awl03/projects/miro/

另一方面,Splint、RAT 和相当多的其他静态代码分析工具将具有 发现。

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

另一个广泛使用的库似乎是 Libefence

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

问候 弗里德里希

评论

0赞 reuben 12/20/2008
在 Windows 世界中,最新版本的 Visual Studio 附带了静态分析工具,以帮助在编译时捕获此类问题。这是我在简短搜索后找到的参考资料:msdn.microsoft.com/en-us/library/d3bbz7tz.aspx
7赞 Norman Ramsey 12/20/2008 #2

你是对的,行为是未定义的。C99 指针必须指向声明或堆分配的数据结构内部或外部的一个元素。

我一直无法弄清楚人们是如何决定何时发出警告的。我很震惊地发现,它本身不会警告未初始化的变量;至少你需要 ,即便如此,警告有时也会被省略。gcc-Wall-O

我推测,由于无界数组在 C 中非常常见,编译器可能在其表达式树中没有一种方法来表示在编译时具有已知大小的数组。因此,尽管声明中存在信息,但我推测在使用时它已经丢失了。

支持valgrind的建议。如果你是用 C 语言编程的,你应该在每个程序上运行 valgrind,直到你再也无法承受性能的打击。

5赞 dkretz 12/20/2008 #3

它不是一个静态数组。

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

根据您的标志设置,此程序的许多功能将被标记,例如从不使用数组的事实。编译器可能很容易地优化它,而不是告诉你 - 一棵树倒在森林里。

这是 C 方式。它是你的数组,你的内存,用它做你想做的事。:)

(有很多 lint 工具可以帮助你找到这种东西;你应该自由地使用它们。不过,它们并不都通过编译器工作;编译和链接通常很乏味。

评论

0赞 Evan Teran 12/20/2008
它实际上是未定义的行为。你是对的,它正在写入数组的地址 13,并且有几种 C 技术使用这样做......但这只是意味着在许多情况下可以可靠地预测结果。
0赞 Evan Teran 12/20/2008
事实上,仅仅指向 NULL 以外的任何位置和正确分配的“对象”(或超出数组边界的一个元素)本身在技术上是未定义的行为。(虽然我同意,如果你只是指点,就不会发生任何不好的事情)。
0赞 Lawrence Dol 12/20/2008
@Evan:在 C 语言中,它不是未定义的。使用 arr[13]=1 只是 *(arr+13)=1 上的句法糖 - C 从未做出过区分。我似乎想起了许多做这种事情的合法事情,比如一个尾部元素为 arr[0] 的结构体,其中所需的空间是 malloc'd。
0赞 Thomas Padron-McCarthy 12/20/2008
是的,实际的指针操作不是问题,但是arr+13指向哪里?如果更改该内存位置会发生什么?这就是本例中未定义的内容!毕竟,这是可以存储变量send_dirty_email_to_your_mother_flag的地方。
0赞 jalf 12/20/2008
Software Monkey:标准中哪里说这是合法的?在 C++ 中,正如 Evan 所说,它显然是未定义的。如果 C 允许它,无论它是否在实践中使用(在很多 C 代码中都有),我都会感到惊讶
2赞 Paul 12/20/2008 #4

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

不可以;C 编译器通常不进行数组边界检查。正如您提到的,这样做的明显负面影响是具有未定义行为的错误,这可能很难找到。

这样做的积极方面是在某些情况下可能具有较小的性能优势。

评论

3赞 Adam Rosenfield 12/20/2008
唯一的性能优势是在编译期间 - 这对运行时性能绝对没有影响。而且我非常怀疑编译过程中的性能影响是不可忽视的。
0赞 Robert Gamble 12/20/2008
一些编译器可以将边界检查代码插入到程序中,以便在运行时进行检查,在这种情况下,性能肯定会受到影响,但正如 Adam 所说,在编译时产生警告的效果最多只是一次很小的命中。
2赞 Evan Teran 12/20/2008 #5

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

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

0赞 FL4SOF 12/20/2008 #6

-fbounds-checking 选项可用于 gcc。

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

不过,“le dorfier”已经对你的问题给出了恰当的答案,这是你的程序,它是 C 的行为方式。

28赞 derobert 12/20/2008 #7

GCC确实对此发出了警告。但是你需要做两件事:

  1. 启用优化。如果没有至少 -O2,GCC 就没有做足够的分析来知道什么是,并且你跑出了边缘。a
  2. 更改示例,以便实际使用 a[],否则 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

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

评论

1赞 Adam Rosenfield 12/20/2008
优化很好 - 我忘记了 GCC 不会在不启用优化的情况下进行数据流分析。我必须更彻底地调查我同事的问题,看看为什么它没有在那里发出警告。
0赞 Robert Gamble 12/20/2008
问题和您的示例使用 gcc,但您的介绍谈到了 g++,您可以将其更改为 gcc 吗?
0赞 richq 8/17/2009
对此的警告在实践中效果不太好 - gcc.gnu.org/bugzilla/show_bug.cgi?id=35587 此外,如果您使用带有算法的原始数组,则会出现误报 forum.gbadev.org/viewtopic.php?t=15505
0赞 Lazer 9/29/2010
好点:没有优化,就没有做足够的分析。
0赞 Vineet Menon 9/19/2011
什么是默认优化?通过 GCC?
10赞 Johannes Schaub - litb 12/21/2008 #8

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

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

如果您的程序是多线程的,请使用 -fmudflapth 而不是 -fmudflapp 进行编译和链接。用 -fmudflapir,此外 设置为 -fmudflap 或 -fmudflapth,如果检测应忽略指针读取。这会产生 更少的仪器(并且有 前更快的执行),并且仍然提供一些保护 完全损坏内存写入,但 错误地允许 读取数据以在程序内传播。

这是 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 进程,可以显示程序泄漏的位置(使用 )或检测未初始化的变量读取。用于获取选项列表。由于 mudflap 只输出地址,而不输出源的文件名和行,所以我写了一个小 gawk 脚本:-print-leaksMUDFLAP_OPTIONS=-help ./a.out

/^ / {
    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 字节的函数。在输入函数时初始化防护,然后在函数退出时检查。如果防护检查失败,则会打印一条错误消息,并退出程序。

-fstack-protector-all与 -fstack-protector 类似,只是所有函数都受到保护。

4赞 Charlie Martin 12/21/2008 #9

C 不这样做的原因是 C 没有信息。像这样的声明

int a[10];

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

a : address of a[0]

或用 C 术语

a : &a[0]

仅此而已。事实上,在 C 语言中,你可以与(几乎*)所有情况互换,但根据定义没有效果。所以你的问题相当于问“为什么我可以向这个(地址)值添加任何整数?*(a+i)a[i]

* 流行测验:这不是真的一种情况是什么?

评论

5赞 Matthew Crumley 12/21/2008
但是 C 编译器会跟踪大小,否则 sizeof 将不适用于数组。
0赞 Charlie Martin 12/22/2008
查看 sizeof 对未在同一文件中定义的数组执行的操作。
2赞 Matthew Crumley 12/22/2008
这是一个很好的观点。我并不是说你错了,只是编译器确实记住了编译单元中的大小,并且可以检测到一些溢出。
0赞 pm100 3/20/2018
“C 不这样做的原因是 C 没有信息”错了。它有确切的信息。正如其他人指出的那样,如果你打开正确的标志,它会发出警告。可以肯定的是,在编译器不知道的地方传递数组。但这不是其中一种情况
0赞 Charlie Martin 3/20/2018
废话。例如,考虑一个带有 malloc 缓冲区的缓冲区,在单独的文件中引用。编译器无法进行边界检查;边界在编译时甚至都不知道。char * bufextern char[] buf
4赞 PolyThinker 12/21/2008 #10

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

评论

0赞 pm100 3/20/2018
除非它警告你它认为可能是狡猾的事情。