17

我注意到我的 C 编译器 (gcc) 可以让我执行以下操作:

#include <stdio.h>
main(){
    short m[32768];
    short y = -1;
    short z = -1;
    printf("%u\n", y);
    m[y] = 12;
    printf("%d\n%d\n", y, m[z]);
}

当我运行它时,它会吐出:

4294967295
12
12

这对我来说似乎有点莫名其妙。

首先,运行这样的程序对我来说安全吗?我是否有可能意外覆盖操作系统(如果相关,我正在运行 OS X)?

此外,我曾预料到至少会出现像我过去遇到的某种段错误,但悄悄地忽略这样的错误真的让我害怕。为什么这个程序不对我造成段错误?

最后,出于好奇(这可能是最愚蠢的问题),有没有办法解决这种疯狂?我可以期望所有 ANSI C 编译器都以这种方式工作吗?不同平台上的 gcc 怎么样?内存布局是否明确定义为可利用(也许如果您要编写跨平台的混淆代码)?

4

6 回答 6

15

C 语言将某些程序的行为定义为“未定义”。他们可以做任何事情。我们称这样的程序是错误的。

其中之一是访问数组的声明/分配边界之外的程序,您的程序会非常小心地执行此操作。

你的程序是错误的;你的错误程序发生的事情就是你看到的:-}它可能“覆盖操作系统”;实际上,大多数现代操作系统都会阻止您这样做,但是您可以覆盖进程空间中的关键值,并且您的进程可能会崩溃、死亡或挂起。

简单的回答是“不要编写错误的程序”。然后你看到的行为将使“C”有意义。

在这种特殊情况下,使用您的特定编译器,数组索引“有点”起作用:您在数组外部进行索引,它会获取一些值。分配给 m 的空间在栈帧中;m[0] 位于堆栈帧中的某个位置,“m[-1]”基于结合数组地址和索引的机器算术,因此不会发生段错误并且访问内存位置。这使编译后的程序可以读取和写入该内存位置……作为错误程序。基本上,编译的 C 程序不会检查您的数组访问是否超出范围。

我们的CheckPointer工具在应用于此程序时会告诉您数组索引在执行时是非法的。因此,您可以自己观察程序以查看您是否犯了错误,或者让 CheckPointer 在您犯错时告诉您。我强烈建议你无论如何都要目测。

于 2012-05-28T02:01:44.620 回答
3

您可能对 INRIA 的CompCert C感兴趣,它是 C 语言的形式化、数学上可验证和验证的实现。它与著名的Coq 证明助手是同一作者。还有另一个变体 Verifiable C

我对此知之甚少,但我知道法国的飞机工程师使用它来对即将推出的飞机嵌入式计算机进行编程,因此至少在法国,它是一种官方接受的关键系统编程语言。

最后,请注意,形式上可验证的语言不同于安全语言。

例如,据说 MISRA C 是一种安全的 C 语言(虽然这是有争议的),还有Safe-C、微软的Checked-CCyclone,以及Safe C Librarylibsrt等无需更改编译器的安全库,或仅使用标准编译器和库,但使用诸如frama-c 之类的源代码分析器。

但是,尽管安全语言可以修复缓冲区溢出等一些问题,但不能保证关键系统所需的一致逻辑流。例如,CompCert C 应该始终为相同的 C 指令生成相同的汇编器指令集。CompCert C 和 Ada 等形式上可验证的语言提供了这种形式上的保证。

您可能还对这些文章感兴趣:

于 2019-10-26T01:12:23.830 回答
2

首先,运行这样的程序对我来说安全吗?

你的例子:不。绝对不是。你为什么还要尝试?你期待它做什么?使用负索引的更通用示例 - 只要它们取消引用合法内存就可以了。

此外,我曾预料到至少会出现像我过去遇到的某种段错误,但悄悄地忽略这样的错误真的让我害怕。为什么这个程序不对我造成段错误?

盲目的运气。(实际上并不完全——正如 Ira Baxter 所解释的那样)

最后,出于好奇(这可能是最愚蠢的问题),有没有办法解决这种疯狂?

如果您设置指向数组内部内容的指针,那么负索引可能会起作用,但对于其他人来说,理解和维护它们将是一场噩梦!- 我已经看到它在嵌入式系统中完成。

我可以期望所有 ANSI C 编译器都以这种方式工作吗?

是的。

不同平台上的 gcc 怎么样?

是的

内存布局是否明确定义为可利用(也许如果您要编写跨平台的混淆代码)?

是的 - 但我不确定你是否真的想依赖它。

于 2012-05-28T02:09:26.327 回答
1
  • “安全”,从可能损坏操作系统的角度来看——是的,大多数情况下,大多数“现代”操作系统实施某种形式的“存储保护”,因此任性或恶意程序不能对操作系统或其他程序做坏事。(但“大部分”表示没有超过 50 行的软件是完美的,而且你总是有奇怪的机会在盔甲中发现裂缝。)
  • 从您的程序“行为”正确的角度来看,“安全”?Wellllllllll,不,主要是。这里的“大部分”与知识渊博(我不会说“聪明”)的程序员偶尔会做“奇怪”的事情有关,例如使用负数组索引,当他们知道编译器和运行时如何操作并且需要有效地完成一些超出常规的事情。但是他们(希望)这样做是知道他们使用的“技巧”非常依赖于系统/编译器。

但正如@jb 指出的那样,“安全的 C 编程”是矛盾的。

于 2012-05-28T02:31:15.450 回答
0

除了上述内容之外,我建议针对 valgrind 运行的程序比仅基于编译器和操作系统信任而被释放的程序更有可能被检测为错误。Valgrind 不会捕获所有内容,但在检测对未初始化或越界内存的访问方面做得很合理。

于 2012-05-28T03:38:03.853 回答
-1

如果程序请求内存中的合法地址(m[-1]可能会这样做,因为它的行为是未定义的),你不会得到一个段错误......特别是如果你要求一个短的,因为它适合大多数单词长度。这是一个非常糟糕的主意,您可以轻松地覆盖磁盘上的某些内容,尽管内核受到保护,除非您在启动时立即运行您的代码。

我不确定你为什么打印出 -1 的地址。

于 2012-05-28T02:21:33.570 回答