20

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

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

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

4

10 回答 10

28

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 再次优化了数组。

于 2008-12-20T08:28:36.673 回答
10

你试过-fmudflapGCC 吗?这些是运行时检查,但很有用,因为无论如何您通常都必须处理运行时计算的索引。它不会默默地继续工作,而是会通知您这些错误。

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

如果您的程序是多线程的,请使用 -fmudflapth 而不是 -fmudflap 来编译和链接。如果检测应忽略指针读取,则使用 -fmudflapir 和 -fmudflap 或 -fmudflapth。这产生了更少的检测(因此更快的执行),并且仍然提供了一些保护以防止直接破坏内存的写入,但允许错误读取的数据在程序中传播。

以下是泥瓣给我的例子:

[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 类似,只是所有函数都受到保护。

于 2008-12-20T18:27:54.423 回答
7

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

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

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

赞同 valgrind的推荐。 如果你用 C 编程,你应该在每个程序上运行 valgrind,一直到你不能再承受性能损失为止。

于 2008-12-20T06:21:17.010 回答
5

它不是静态数组。

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

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

这是C路。这是你的阵列,你的记忆,用它做你想做的事。:)

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

于 2008-12-20T06:21:59.643 回答
4

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

int a[10];

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

a : address of a[0]

或用 C 术语

a : &a[0]

就这样。事实上,在 C 中,您可以在(几乎*)所有情况下*(a+i)a[i]按定义无效的情况互换。所以你的问题相当于问“为什么我可以向这个(地址)值添加任何整数?”

*流行测验:这不是真的吗?

于 2008-12-21T00:41:52.263 回答
4

C 哲学是程序员永远是对的。所以它会默默地允许你访问你在那里提供的任何内存地址,假设你总是知道你在做什么并且不会用警告来打扰你。

于 2008-12-21T04:37:41.197 回答
2

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

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

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

于 2008-12-20T06:22:41.360 回答
2

我相信某些编译器在某些情况下会这样做。例如,如果我没记错的话,较新的 Microsoft 编译器有一个“缓冲区安全检查”选项,可以检测缓冲区溢出的琐碎情况。

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

于 2008-12-20T06:34:36.703 回答
0

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

另一方面,夹板、老鼠和相当多的其他静态代码分析工具会发现这一点。

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

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

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

问候弗里德里希

于 2008-12-20T06:15:08.147 回答
0

-fbounds-checking 选项可用于 gcc。

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

'le dorfier' 已经对你的问题给出了恰当的回答,它是你的程序,它是 C 的行为方式。

于 2008-12-20T08:06:17.267 回答