访问超出其边界的数组(在C语言中)有多危险?有时会发生这样的情况:我从数组外部读取数据(我现在知道我访问了程序的其他部分使用的内存,甚至超出了这个范围),或者我试图将一个值设置为数组外部的索引。程序有时会崩溃,但有时只是运行,只会给出意想不到的结果。

现在我想知道的是,这到底有多危险?如果它损坏了我的程序,那也不算太坏。另一方面,如果它破坏了我程序之外的东西,因为我设法访问了一些完全不相关的内存,那么我想这是非常糟糕的。 我读过很多“任何事情都可能发生”,“细分市场可能是最不坏的问题”,“你的硬盘可能变成粉红色,独角兽可能在你的窗户下唱歌”,这些都很好,但真正的危险是什么?

我的问题:

Can reading values from way outside the array damage anything apart from my program? I would imagine just looking at things does not change anything, or would it for instance change the 'last time opened' attribute of a file I happened to reach? Can setting values way out outside of the array damage anything apart from my program? From this Stack Overflow question I gather that it is possible to access any memory location, that there is no safety guarantee. I now run my small programs from within XCode. Does that provide some extra protection around my program where it cannot reach outside its own memory? Can it harm XCode? Any recommendations on how to run my inherently buggy code safely?

我使用OSX 10.7, Xcode 4.6。


当前回答

一般来说,现在的操作系统(流行的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入存在于已分配给进程的区域之外的REAL空间中的位置(就其本身而言)并不容易。

直接回答:

Reading will almost never directly damage another process, however it can indirectly damage a process if you happen to read a KEY value used to encrypt, decrypt, or validate a program / process. Reading out of bounds can have somewhat adverse / unexpected affects on your code if you are making decisions based on the data you are reading The only way your could really DAMAGE something by writing to a loaction accessible by a memory address is if that memory address that you are writing to is actually a hardware register (a location that actually is not for data storage but for controlling some piece of hardware) not a RAM location. In all fact, you still wont normally damage something unless you are writing some one time programmable location that is not re-writable (or something of that nature). Generally running from within the debugger runs the code in debug mode. Running in debug mode does TEND to (but not always) stop your code faster when you have done something considered out of practice or downright illegal. Never use macros, use data structures that already have array index bounds checking built in, etc....

ADDITIONAL I should add that the above information is really only for systems using an operating system with memory protection windows. If writing code for an embedded system or even a system utilizing an operating system (real-time or other) that does not have memory protection windows (or virtual addressed windows) that one should practice a lot more caution in reading and writing to memory. Also in these cases SAFE and SECURE coding practices should always be employed to avoid security issues.

其他回答

我正在使用一个用于DSP芯片的编译器,该编译器故意生成代码,从C代码中访问一个数组的末尾,而C代码没有!

这是因为循环是结构化的,因此迭代结束时将为下一次迭代预取一些数据。因此,在最后一次迭代结束时预取的数据实际上从未被使用。

编写这样的C代码会调用未定义的行为,但这只是一个标准文档的形式,它关注的是最大的可移植性。

更常见的情况是,访问越界的程序没有被巧妙地优化。它只是有bug。代码获取一些垃圾值,并且与前面提到的编译器的优化循环不同,代码随后在后续计算中使用该值,从而破坏了它们。

捕获这样的错误是值得的,因此即使仅仅为了这个原因,也值得使行为未定义:这样运行时就可以产生类似“main.c第42行数组溢出”这样的诊断消息。

在具有虚拟内存的系统上,分配数组时,后面的地址可能位于虚拟内存的未映射区域。访问将轰炸程序。

说句题外话,请注意,在C语言中,我们允许创建一个指针,它位于数组的末尾之后。这个指针必须比任何指向数组内部的指针都要大。 这意味着C实现不能将数组放在内存的末尾,在那里,1 +地址会被环绕,看起来比数组中的其他地址更小。

Nevertheless, access to uninitialized or out of bounds values are sometimes a valid optimization technique, even if not maximally portable. This is for instance why the Valgrind tool does not report accesses to uninitialized data when those accesses happen, but only when the value is later used in some way that could affect the outcome of the program. You get a diagnostic like "conditional branch in xxx:nnn depends on uninitialized value" and it can be sometimes hard to track down where it originates. If all such accesses were trapped immediately, there would be a lot of false positives arising from compiler optimized code as well as correctly hand-optimized code.

Speaking of which, I was working with some codec from a vendor which was giving off these errors when ported to Linux and run under Valgrind. But the vendor convinced me that only several bits of the value being used actually came from uninitialized memory, and those bits were carefully avoided by the logic.. Only the good bits of the value were being used and Valgrind doesn't have the ability to track down to the individual bit. The uninitialized material came from reading a word past the end of a bit stream of encoded data, but the code knows how many bits are in the stream and will not use more bits than there actually are. Since the access beyond the end of the bit stream array does not cause any harm on the DSP architecture (there is no virtual memory after the array, no memory-mapped ports, and the address does not wrap) it is a valid optimization technique.

“未定义的行为”并没有多大意义,因为根据ISO C,简单地包含一个C标准中没有定义的头文件,或者调用一个程序本身或C标准中没有定义的函数,都是未定义行为的例子。未定义的行为并不意味着“没有被地球上的任何人定义”,而是“没有被ISO C标准定义”。当然,有时候未定义的行为是绝对没有人能定义的。

一般来说,现在的操作系统(流行的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入存在于已分配给进程的区域之外的REAL空间中的位置(就其本身而言)并不容易。

直接回答:

Reading will almost never directly damage another process, however it can indirectly damage a process if you happen to read a KEY value used to encrypt, decrypt, or validate a program / process. Reading out of bounds can have somewhat adverse / unexpected affects on your code if you are making decisions based on the data you are reading The only way your could really DAMAGE something by writing to a loaction accessible by a memory address is if that memory address that you are writing to is actually a hardware register (a location that actually is not for data storage but for controlling some piece of hardware) not a RAM location. In all fact, you still wont normally damage something unless you are writing some one time programmable location that is not re-writable (or something of that nature). Generally running from within the debugger runs the code in debug mode. Running in debug mode does TEND to (but not always) stop your code faster when you have done something considered out of practice or downright illegal. Never use macros, use data structures that already have array index bounds checking built in, etc....

ADDITIONAL I should add that the above information is really only for systems using an operating system with memory protection windows. If writing code for an embedded system or even a system utilizing an operating system (real-time or other) that does not have memory protection windows (or virtual addressed windows) that one should practice a lot more caution in reading and writing to memory. Also in these cases SAFE and SECURE coding practices should always be employed to avoid security issues.

如果您曾经做过系统级编程或嵌入式系统编程,如果您随机写入内存位置,可能会发生非常糟糕的事情。旧系统和许多微控制器使用内存映射IO,因此写入映射到外设寄存器的内存位置可能会造成严重破坏,特别是如果它是异步完成的。

一个例子是编程闪存。内存芯片上的编程模式是通过将特定的值序列写入芯片地址范围内的特定位置来实现的。如果在此期间另一个进程写入芯片中的任何其他位置,则会导致编程周期失败。

在某些情况下,硬件会将地址环绕起来(地址中最重要的位/字节会被忽略),因此写入超出物理地址空间末端的地址实际上会导致数据在中间写入。

最后,像MC68000这样的老cpu可能会锁定到只有硬件重置才能让它们重新工作的地步。我已经几十年没有使用它们了,但我相信当它在试图处理异常时遇到总线错误(不存在内存)时,它会简单地停止,直到断言硬件重置。

我最大的建议是为一个产品做一个明显的宣传,但我个人对此没有兴趣,我也不以任何方式与他们联系——但基于几十年的C编程和嵌入式系统,可靠性是至关重要的,Gimpel的PC Lint不仅可以检测这类错误,还可以通过不断地唠叨你的坏习惯,让你成为更好的C/ c++程序员。

我还建议你阅读MISRA C编码标准,如果你能从别人那里得到一份的话。我没有看到最近的任何一个,但在过去的日子里,他们给了一个很好的解释,为什么你应该/不应该做他们覆盖的事情。

我不知道你的情况,但当我第二次或第三次从任何应用程序中得到一个coredump或挂起时,我对任何公司的看法都会下降一半。第四次或第五次,不管是什么包装都变成了架子,我用一根木桩穿过包装/光盘的中心,只是为了确保它永远不会回来缠着我。

不以根用户或其他特权用户身份运行程序不会对任何系统造成损害,因此通常这可能是一个好主意。

通过将数据写入某个随机的内存位置,你不会直接“破坏”计算机上运行的任何其他程序,因为每个进程都运行在自己的内存空间中。

如果你试图访问任何没有分配给你的进程的内存,操作系统将停止你的程序执行分割错误。

因此,直接(无需以根用户身份运行并直接访问/dev/mem之类的文件),您的程序不会干扰在您的操作系统上运行的任何其他程序。

尽管如此——这可能是你听说过的危险——盲目地将随机数据写入随机的内存位置,你肯定会损坏任何你能损坏的东西。

例如,您的程序可能希望删除存储在程序某处的文件名所给出的特定文件。如果你不小心覆盖了文件名所在的位置,你可能会删除一个完全不同的文件。

我只是想为这个问题添加一些实际的例子——想象一下下面的代码:

#include <stdio.h>

int main(void) {
    int n[5];
    n[5] = 1;

    printf("answer %d\n", n[5]);

    return (0);
}

它具有未定义的行为。例如,如果你启用了clang优化(-Ofast),结果会是这样的:

answer 748418584

(如果你没有编译,可能会输出答案1的正确结果)

这是因为在第一种情况下,赋值给1的函数实际上从未在最终代码中组装(您也可以查看godbolt asm代码)。

(然而,必须注意的是,根据这种逻辑,main甚至不应该调用printf,所以最好的建议是不要依赖于优化器来解决你的UB -而是要知道有时它可能会以这种方式工作)

这里的结论是,现代C优化编译器将假设未定义的行为(UB)永远不会发生(这意味着上面的代码将类似于(但不相同):

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n[5];

    if (0)
        n[5] = 1;

    printf("answer %d\n", (exit(-1), n[5]));

    return (0);
} 

相反,这是完全定义的)。

这是因为第一个条件语句从来没有达到它的真状态(0总是假的)。

在printf的第二个参数上,我们有一个序列点,在此之后,我们调用exit,程序在调用第二个逗号操作符中的UB之前终止(因此它的定义很好)。

所以第二个结论是,只要没有求值,UB就不是UB。

此外,我没有看到这里提到有相当现代的未定义行为消毒器(至少在clang)(选项-fsanitize= Undefined)将在第一个例子(但不是第二个)上给出以下输出:

/app/example.c:5:5: runtime error: index 5 out of bounds for type 'int[5]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.c:5:5 in 
/app/example.c:7:27: runtime error: index 5 out of bounds for type 'int[5]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.c:7:27 in 

以下是godbolt的所有样品:

https://godbolt.org/z/eY9ja4fdh(第一个示例,没有标记)

https://godbolt.org/z/cGcY7Ta9M(第一个示例和-Ofast clang)

https://godbolt.org/z/cGcY7Ta9M(第二个例子和UB消毒剂打开)

https://godbolt.org/z/vE531EKo4(第一个例子和UB消毒剂打开)