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

其他回答

二维或多维数组的考虑超出了其他答案中提到的那些。考虑以下函数:

char arr1[2][8];
char arr2[4];
int test1(int n)
{
  arr1[1][0] = 1;
  for (int i=0; i<n; i++) arr1[0][i] = arr2[i];      
  return arr1[1][0];
}
int test2(int ofs, int n)
{
  arr1[1][0] = 1;
  for (int i=0; i<n; i++) *(arr1[0]+i) = arr2[i];      
  return arr1[1][0];
}

The way gcc will processes the first function will not allow for the possibility that an attempt to write arr[0][i] might affect the value of arr[1][0], and the generated code is incapable of returning anything other than a hardcoded value of 1. Although the Standard defines the meaning of array[index] as precisely equivalent to (*((array)+(index))), gcc seems to interpret the notion of array bounds and pointer decay differently in cases which involve using [] operator on values of array type, versus those which use explicit pointer arithmetic.

Objective-C中的nsarray被分配一个特定的内存块。超过数组的边界意味着您将访问没有分配给数组的内存。这意味着:

This memory can have any value. There's no way of knowing if the data is valid based on your data type. This memory may contain sensitive information such as private keys or other user credentials. The memory address may be invalid or protected. The memory can have a changing value because it's being accessed by another program or thread. Other things use memory address space, such as memory-mapped ports. Writing data to unknown memory address can crash your program, overwrite OS memory space, and generally cause the sun to implode.

从程序的角度来看,您总是想知道代码何时超出了数组的边界。这可能导致返回未知值,导致应用程序崩溃或提供无效数据。

我正在使用一个用于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标准定义”。当然,有时候未定义的行为是绝对没有人能定义的。

在测试代码时,您可能想尝试使用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:不过,正如卡兹的回答所说,它不是万能的,并且并不总是提供最有帮助的输出,特别是当您使用令人兴奋的访问模式时。

你写的:

我读过很多“任何事情都可能发生”,“市场细分可能是。 “最不坏的问题”,“你的硬盘可能会变成粉红色,独角兽也可能 在你的窗下唱歌,这是很好的,但真正的 危险吗?

这么说吧,给枪上膛。瞄准窗外,不要瞄准,然后开火。危险在哪里?

The issue is that you do not know. If your code overwrites something that crashes your program you are fine because it will stop it into a defined state. However if it does not crash then the issues start to arise. Which resources are under control of your program and what might it do to them? I know at least one major issue that was caused by such an overflow. The issue was in a seemingly meaningless statistics function that messed up some unrelated conversion table for a production database. The result was some very expensive cleanup afterwards. Actually it would have been much cheaper and easier to handle if this issue would have formatted the hard disks ... with other words: pink unicorns might be your least problem.

认为操作系统会保护你的想法是乐观的。如果可能,尽量避免越界写作。