提问人:Adam Rosenfield 提问时间:12/20/2008 更新时间:12/10/2016 访问量:9723
为什么编译器不警告越界静态数组索引?
Why do compilers not warn about out-of-bounds static array indices?
问:
我的一位同事最近因为在堆栈上越界写入静态数组而受到严重咬伤(他在不增加数组大小的情况下向其中添加了一个元素)。编译器不应该捕获这种错误吗?以下代码使用 gcc 干净地编译,即使使用选项也是如此,但它显然是错误的:-Wall -Wextra
int main(void)
{
int a[10];
a[13] = 3; // oops, overwrote the return address
return 0;
}
我确信这是未定义的行为,尽管我目前找不到 C99 标准的摘录。但是在最简单的情况下,数组的大小被称为编译时,索引在编译时是已知的,编译器不应该至少发出警告吗?
答:
gcc 中对此(从编译器端)有一些扩展 http://www.doc.ic.ac.uk/~awl03/projects/miro/
另一方面,Splint、RAT 和相当多的其他静态代码分析工具将具有 发现。
您还可以在代码上使用 valgrind 并查看输出。http://valgrind.org/
另一个广泛使用的库似乎是 Libefence
这只是一个设计决策。现在这导致了这件事。
问候 弗里德里希
评论
你是对的,行为是未定义的。C99 指针必须指向声明或堆分配的数据结构内部或外部的一个元素。
我一直无法弄清楚人们是如何决定何时发出警告的。我很震惊地发现,它本身不会警告未初始化的变量;至少你需要 ,即便如此,警告有时也会被省略。gcc
-Wall
-O
我推测,由于无界数组在 C 中非常常见,编译器可能在其表达式树中没有一种方法来表示在编译时具有已知大小的数组。因此,尽管声明中存在信息,但我推测在使用时它已经丢失了。
我支持valgrind的建议。如果你是用 C 语言编程的,你应该在每个程序上运行 valgrind,直到你再也无法承受性能的打击。
它不是一个静态数组。
无论行为是否未定义,它都会写入从数组开头 13 个整数的地址。有什么是你的责任。有几种 C 技术出于合理原因故意错误分配数组。这种情况在不完整的编译单元中并不罕见。
根据您的标志设置,此程序的许多功能将被标记,例如从不使用数组的事实。编译器可能很容易地优化它,而不是告诉你 - 一棵树倒在森林里。
这是 C 方式。它是你的数组,你的内存,用它做你想做的事。:)
(有很多 lint 工具可以帮助你找到这种东西;你应该自由地使用它们。不过,它们并不都通过编译器工作;编译和链接通常很乏味。
评论
编译器至少不应该发出警告吗?
不可以;C 编译器通常不进行数组边界检查。正如您提到的,这样做的明显负面影响是具有未定义行为的错误,这可能很难找到。
这样做的积极方面是在某些情况下可能具有较小的性能优势。
评论
我相信某些编译器在某些情况下会这样做。例如,如果我的记忆正确,较新的 Microsoft 编译器有一个“缓冲区安全检查”选项,该选项将检测缓冲区溢出的微不足道的情况。
为什么不是所有的编译器都这样做?要么(如前所述)编译器使用的内部表示不适合这种类型的静态分析,要么它只是在编写器优先级列表中不够高。老实说,无论哪种方式,这都是一种耻辱。
-fbounds-checking 选项可用于 gcc。
值得一读这篇文章 http://www.doc.ic.ac.uk/~phjk/BoundsChecking.html
不过,“le dorfier”已经对你的问题给出了恰当的答案,这是你的程序,它是 C 的行为方式。
GCC确实对此发出了警告。但是你需要做两件事:
- 启用优化。如果没有至少 -O2,GCC 就没有做足够的分析来知道什么是,并且你跑出了边缘。
a
- 更改示例,以便实际使用 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 会再次优化数组。
评论
你试过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-leaks
MUDFLAP_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 类似,只是所有函数都受到保护。
C 不这样做的原因是 C 没有信息。像这样的声明
int a[10];
做两件事:它分配字节空间(可能还有一点死角用于对齐),并在符号表中放置一个条目,从概念上讲,该条目的内容为sizeof(int)*10
a : address of a[0]
或用 C 术语
a : &a[0]
仅此而已。事实上,在 C 语言中,你可以与(几乎*)所有情况互换,但根据定义没有效果。所以你的问题相当于问“为什么我可以向这个(地址)值添加任何整数?*(a+i)
a[i]
* 流行测验:这不是真的一种情况是什么?
评论
char * buf
extern char[] buf
C 的理念是程序员永远是对的。因此,它将默默地允许您访问您在那里提供的任何内存地址,假设您始终知道自己在做什么并且不会用警告来打扰您。
评论