我的一位同事最近通过写出堆栈上的静态数组的界限而被严重咬了(他在不增加数组大小的情况下添加了一个元素)。编译器不应该捕获这种错误吗?以下代码使用gcc完全编译,即使使用-Wall -Wextra
选项,但它显然是错误的:
int main(void)
{
int a[10];
a[13] = 3; // oops, overwrote the return address
return 0;
}
我很肯定这是未定义的行为,虽然我现在找不到C99标准的摘录。但在最简单的情况下,数组的大小称为编译时,并且索引在编译时是已知的,编译器是否应该至少发出警告?
答案 0 :(得分:27)
GCC 确实对此发出警告。但你需要做两件事:
a
是什么,并且你跑掉了边缘。
$ 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会再次优化数组。
答案 1 :(得分:10)
您是否尝试过使用GCC -fmudflap
?这些是运行时检查,但很有用,因为大多数情况下,您仍然需要处理运行时计算的索引。它不是默默地继续工作,而是会通知你这些错误。
-fmudflap -fmudflapth -fmudflapir
对于支持它的前端(C和C ++),仪器都有风险 指针/数组解除引用 操作,一些标准 库字符串/堆函数,以及其他一些相关的函数 具有范围/有效性测试的构造。 模块如此检测 应该免受缓冲区溢出,无效堆使用等问题 其他类的C / C ++编程 错误。仪器 - tation依赖于一个单独的运行时库(libmudflap) 如果,将被链接到一个程序 -fmudflap在链接处给出 时间。控制仪表程序的运行时行为 通过MUDFLAP_OPTIONS环境 变量。见“环境 MUDFLAP_OPTIONS = -help a.out“为其选项。如果您的程序是多线程的,请使用-fmudflapth而不是-fmudflap进行编译和链接。使用 -fmudflapir,另外 to -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进程,可以显示程序泄漏的位置(使用-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字节的缓冲区的函数。输入功能时会初始化防护装置,然后在功能退出时进行检查。如果防护检查失败,则会打印一条错误消息并退出程序。
-fstack-protector-all
与-fstack-protector类似,但所有功能都受到保护。
答案 2 :(得分:7)
你是对的,行为未定义。 C99指针必须指向或超出声明或堆分配的数据结构之外的一个元素。
我从来没有弄清楚gcc
人如何决定何时发出警告。我很震惊地得知-Wall
本身不会警告未初始化的变量;至少你需要-O
,即使这样,警告有时会被忽略。
我猜想因为无界数组在C中是如此常见,所以编译器可能在其表达式树中没有办法表示一个在编译时已知大小的数组。因此,虽然信息出现在宣言中,但我猜想在使用时它已经丢失了。
我第二个valgrind的建议。如果您使用C语言进行编程,则应该始终在每个程序上运行valgrind ,直到您不能更长时间的性能打击。
答案 3 :(得分:5)
它不是静态数组。
是否未定义行为,它从数组的开头写入地址13个整数。你有什么责任呢?由于合理的原因,有几种C技术故意错误分配数组。在不完整的编译单元中,这种情况并不罕见。
根据您的标志设置,此程序的许多功能都会被标记,例如从不使用该数组的事实。而且编译器可能很容易将其优化出来而不会告诉你 - 一棵树落在森林里。
这是C方式。这是你的阵列,你的记忆,用它做你想做的事。 :)
(有许多lint工具可以帮助你找到这类东西;你应该自由地使用它们。虽然它们并不都是通过编译器完成的;编译和链接通常很乏味。)
答案 4 :(得分:4)
C不这样做的原因是C没有这些信息。像
这样的陈述int a[10];
做了两件事:它分配sizeof(int)*10
个字节的空间(加上,可能还有一些用于对齐的死空间),它在符号表中放入一个条目,从概念上讲,
a : address of a[0]
或以C语言
a : &a[0]
就是这样。实际上,在C语言中,您可以在{几乎*}所有情况下与*(a+i)
交换a[i]
,但不会影响定义。所以你的问题等同于问“为什么我可以为这个(地址)值添加任何整数?”
*流行测验:这个中的一个案例是是真的吗?
答案 5 :(得分:4)
C哲学是程序员总是正确的。因此,它会默默地允许您访问您在那里提供的任何内存地址,假设您总是知道自己在做什么,并且不会在发出警告时打扰您。
答案 6 :(得分:2)
编译器不应该至少发出警告吗?
没有; C编译器通常不会执行数组边界检查。正如你所提到的,这种明显的负面影响是未定义行为的错误,这很难找到。
在某些情况下,积极的一面是可能的小的性能优势。
答案 7 :(得分:2)
我相信某些编译器在某些情况下会这样做。例如,如果我的内存正确地为我服务,较新的Microsoft编译器有一个“缓冲区安全检查”选项,它将检测缓冲区溢出的微不足道的情况。
为什么不是所有编译器都这样做? (如前所述)编译器使用的内部表示不适合这种类型的静态分析,或者它只是编写器优先级列表不够高。说实话,无论如何都是一种耻辱。
答案 8 :(得分:0)
gcc中有一些扩展(来自编译器端) http://www.doc.ic.ac.uk/~awl03/projects/miro/
另一方面,夹板,鼠和其他一些静态代码分析工具都会有 发现了。您还可以在代码上使用valgrind并查看输出。 http://valgrind.org/
另一个广泛使用的库似乎是libefence
这只是一个设计决策。现在这导致了这件事。
此致 弗里德里希
答案 9 :(得分:0)
-fbounds-checking选项适用于gcc。
但是,'le dorfier'给出了你的问题的答案,它是你的程序,它是C的行为方式。