最近,我的一位同事通过写出堆栈上的静态数组(他在不增加数组大小的情况下添加了一个元素)来严重受伤.编译器不应该捕获这种错误吗?以下代码使用gcc完全编译,即使使用-Wall -Wextra
选项,但它显然是错误的:
int main(void) { int a[10]; a[13] = 3; // oops, overwrote the return address return 0; }
我很肯定这是不确定的行为,虽然我现在找不到C99标准的摘录.但是在最简单的情况下,数组的大小称为编译时,并且索引在编译时是已知的,编译器是否应该至少发出警告?
GCC 并警告这一点.但是你需要做两件事:
启用优化.没有至少-O2,GCC没有做足够的分析来知道什么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
顺便说一句:如果你在测试程序中返回[13],那也不会有效,因为GCC再次优化了数组.
你有没有尝试-fmudflap
过GCC?这些是运行时检查,但很有用,因为大多数情况下,您仍然需要处理运行时计算的索引.它将不会默默地继续工作,而是会通知您有关这些错误的信息.
-fmudflap -fmudflapth -fmudflapir
对于支持它的前端(C和C++),检测所有有风险的指针/数组解除引用操作,一些标准库字符串/堆函数,以及一些其他具有范围/有效性测试的相关构造.如此检测的模块应该不受缓冲区溢出,无效堆使用以及一些其他类C/C++编程错误的影响.该指令依赖于一个单独的运行时库(libmudflap),如果在链接时给出-fmudflap,它将链接到一个程序中.已检测程序的运行时行为由MUDFLAP_OPTIONS环境变量控制.有关其选项,请参阅"env MUDFLAP_OPTIONS = -help a.out".如果您的程序是多线程的,请使用-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字节的缓冲区的函数.输入功能时会初始化防护装置,然后在功能退出时进行检查.如果防护检查失败,则会打印错误消息并退出程序.
-fstack-protector-all
与-fstack-protector类似,但所有功能都受到保护.
你是对的,行为是不确定的.C99指针必须指向或超出声明或堆分配的数据结构之外的一个元素.
我从来没有弄清楚gcc
人们如何决定何时发出警告.我很震惊地得知,-Wall
它本身不会警告未初始化的变量; 至少你需要-O
,即使这样,有时也会省略警告.
我猜想因为无界数组在C中是如此常见,所以编译器可能在其表达式树中没有办法表示在编译时具有已知大小的数组.因此,虽然声明中有信息,但我猜想在使用时它已经丢失了.
我是valgrind的推荐. 如果您使用C编程,则应始终在每个程序上运行valgrind,直到您无法再获得性能.
它不是静态数组.
是否未定义行为,它从数组的开头写入地址13个整数.你有什么责任呢?由于合理的原因,有几种C技术故意错误分配数组.在不完整的编译单元中,这种情况并不罕见.
根据您的标志设置,此程序的许多功能将被标记,例如从不使用该数组的事实.并且编译器可能很容易优化它而不是告诉你 - 一棵树落在森林里.
这是C方式.这是你的阵列,你的记忆,用它做你想做的事.:)
(有许多lint工具可以帮助你找到这类东西;你应该自由地使用它们.虽然它们并不都是通过编译器完成的;编译和链接通常很乏味.)