访问超出其边界的数组(在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。


当前回答

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

#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消毒剂打开)

其他回答

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

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

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

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

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

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

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

除了你自己的程序,我不认为你会破坏任何东西,在最坏的情况下,你会尝试从一个内存地址读取或写入一个页面,内核没有分配给你的进程,产生适当的异常并被杀死(我的意思是,你的进程)。

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

#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消毒剂打开)

在测试代码时,您可能想尝试使用Valgrind中的memcheck工具——它不会捕获堆栈框架内的单个数组边界违规,但它应该捕获许多其他类型的内存问题,包括那些会导致单个函数范围之外的微妙、更广泛的问题的问题。

摘自手册:

Memcheck is a memory error detector. It can detect the following problems that are common in C and C++ programs. Accessing memory you shouldn't, e.g. overrunning and underrunning heap blocks, overrunning the top of the stack, and accessing memory after it has been freed. Using undefined values, i.e. values that have not been initialised, or that have been derived from other undefined values. Incorrect freeing of heap memory, such as double-freeing heap blocks, or mismatched use of malloc/new/new[] versus free/delete/delete[] Overlapping src and dst pointers in memcpy and related functions. Memory leaks.

ETA:不过,正如卡兹的回答所说,它不是万能的,并且并不总是提供最有帮助的输出,特别是当您使用令人兴奋的访问模式时。

一般来说,现在的操作系统(流行的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入存在于已分配给进程的区域之外的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.