简体   繁体   English

为什么我们可以在 C 中写越界?

[英]Why is it that we can write outside of bounds in C?

I recently finished reading about virtual memory and I have a question about how malloc works within the Virtual address space and Physical Memory.我最近读完了有关虚拟 memory 的内容,我对 malloc 如何在虚拟地址空间和物理地址空间 Memory 中工作有疑问。

For example (code copied from another SO post)例如(从另一个 SO 帖子复制的代码)

void main(){
int *p;
p=malloc(sizeof(int));
p[500]=999999;
printf("p[0]=%d\n",p[500]); //works just fine. 
}

Why is this allowed to happen?为什么允许这种情况发生? Or like why is that address at p[500] even writable?或者为什么 p[500] 的地址甚至是可写的?

Here is my guess.这是我的猜测。

When malloc is called, perhaps the OS decides to give the process an entire page.当 malloc 被调用时,操作系统可能决定给进程一个完整的页面。 I will just assume that each page is worth 4KB of space.我将假设每个页面都值得 4KB 的空间。 Is that entire thing marked as writable?整个东西都标记为可写吗? That's why you can go as far as 500*sizeof(int) into the page (assuming 32bit system where int is size of 4 bytes).这就是为什么您可以将 go 最大 500*sizeof(int) 放入页面(假设 32 位系统,其中 int 的大小为 4 个字节)。

I see that when I try to edit at a larger value...我看到当我尝试以更大的值进行编辑时...

   p[500000]=999999; // EXC_BAD_ACCESS according to XCode

Seg fault.段错误。

If so, then does that mean that there are pages that are dedicated to your code/instructions/text segments and marked as unwrite-able completely separate from your pages where your stack/variables are in (where things do change) and marked as writable?如果是这样,那么这是否意味着有些页面专用于您的代码/指令/文本段并标记为不可写,与您的堆栈/变量所在的页面(事情发生变化的地方)完全分开并标记为可写? Of course, the process thinks they're next to each order in the 4gb address space on a 32-bit system.当然,进程认为它们在 32 位系统上的 4gb 地址空间中的每个订单旁边。

"Why is this allowed to happen?" “为什么允许这种情况发生?” (write outside of bounds) (写在界外)

C does not require the additional CPU instructions that would typically be needed to prevent this out-of-range access. C不需要通常需要的额外CPU指令来防止这种超出范围的访问。

That is the speed of C - it trusts the programmer, giving the coder all the rope needed to perform the task - including enough rope to hang oneself. 这就是C的速度 - 它信任程序员,为编码人员提供执行任务所需的所有绳索 - 包括足够的绳索悬挂自己。

Consider the following code for Linux: 考虑以下Linux代码:

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

int staticvar;
const int constvar = 0;

int main(void)
{
        int stackvar;
        char buf[200];
        int *p;

        p = malloc(sizeof(int));
        sprintf(buf, "cat /proc/%d/maps", getpid());
        system(buf);

        printf("&staticvar=%p\n", &staticvar);
        printf("&constvar=%p\n", &constvar);
        printf("&stackvar=%p\n", &stackvar);
        printf("p=%p\n", p);
        printf("undefined behaviour: &p[500]=%p\n", &p[500]);
        printf("undefined behaviour: &p[50000000]=%p\n", &p[50000000]);

        p[500] = 999999; //undefined behaviour
        printf("undefined behaviour: p[500]=%d\n", p[500]);
        return 0;
}

It prints the memory map of the process and the addresses of some different type of memory. 它打印进程的内存映射和某些不同类型内存的地址。

[osboxes@osboxes ~]$ gcc tmp.c -g -static -Wall -Wextra -m32
[osboxes@osboxes ~]$ ./a.out
08048000-080ef000 r-xp 00000000 fd:00 919429                /home/osboxes/a.out
080ef000-080f2000 rw-p 000a6000 fd:00 919429                /home/osboxes/a.out
080f2000-080f3000 rw-p 00000000 00:00 0
0824d000-0826f000 rw-p 00000000 00:00 0                     [heap]
f779c000-f779e000 r--p 00000000 00:00 0                     [vvar]
f779e000-f779f000 r-xp 00000000 00:00 0                     [vdso]
ffe4a000-ffe6b000 rw-p 00000000 00:00 0                     [stack]
&staticvar=0x80f23a0
&constvar=0x80c2fcc
&stackvar=0xffe69b88
p=0x824e2a0
undefined behaviour: &p[500]=0x824ea70
undefined behaviour: &p[50000000]=0x1410a4a0
undefined behaviour: p[500]=999999

Or like why is that address at p[500] even writable? 或者为什么p [500]的地址甚至可写?

Heap is from 0824d000-0826f000 and &p[500] is 0x824ea70 by chance, so the memory is writeable and readable, but this memory region may contain real data which will be altered! 堆是从0824d000-0826f000和&p [500]偶然是0x824ea70,因此内存是可写和可读的,但是这个内存区域可能包含将被更改的实际数据! In the case of the sample program it is most likely that it is unused so the write to this memory is not harmful for the process to work. 在示例程序的情况下,它很可能是未使用的,因此写入此内存对于进程的工作无害。

&p[50000000] is 0x1410a4a0 by chance, which is not in a page the kernel mapped to the process and therefore is unwriteable and unreadable, hence the seg fault. &p [50000000]偶然是0x1410a4a0,它不在内核映射到进程的页面中,因此是不可写和不可读的,因此是seg错误。

If you compile it with -fsanitize=address memory accesses will be checked and many but not all illegal memory accesses will be reported by AddressSanitizer . 如果使用-fsanitize=address编译它,将检查内存访问,并且AddressSanitizer将报告许多但不是所有非法内存访问。 Slowdown is about two times slower than without AddressSanitizer. 减速比没有AddressSanitizer慢大约两倍。

[osboxes@osboxes ~]$ gcc tmp.c -g -Wall -Wextra -m32 -fsanitize=address
[osboxes@osboxes ~]$ ./a.out
[...]
undefined behaviour: &p[500]=0xf5c00fc0
undefined behaviour: &p[50000000]=0x1abc9f0
=================================================================
==2845==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5c00fc0 at pc 0x8048972 bp 0xfff44568 sp 0xfff44558
WRITE of size 4 at 0xf5c00fc0 thread T0
    #0 0x8048971 in main /home/osboxes/tmp.c:24
    #1 0xf70a4e7d in __libc_start_main (/lib/libc.so.6+0x17e7d)
    #2 0x80486f0 (/home/osboxes/a.out+0x80486f0)

AddressSanitizer can not describe address in more detail (wild memory access suspected).
SUMMARY: AddressSanitizer: heap-buffer-overflow /home/osboxes/tmp.c:24 main
[...]
==2845==ABORTING

If so, then does that mean that there are pages that are dedicated to your code/instructions/text segments and marked as unwrite-able completely separate from your pages where your stack/variables are in (where things do change) and marked as writable? 如果是这样,那么这是否意味着有些页面专用于您的代码/指令/文本段并标记为不可写入与您的堆栈/变量所在的页面完全分离(事情确实发生了变化)并标记为可写?

Yes, see the output of the process' memory map above. 是的,请参阅上面进程'内存映射的输出。 r-xp means readable and executable, rw-p means readable and writeable. r-xp表示可读和可执行, rw-p表示可读和可写。

Why is this allowed to happen? 为什么允许这种情况发生?

One of the primary design goals of the C (and C++) languages is to be as run-time efficient as possible. C(和C ++)语言的主要设计目标之一是尽可能提高运行时间。 The designers of C (or C++) could have decided to include a rule in the language specification that said "writing outside the bounds of an array must cause X to happen" (where X is some well-defined behavior, such as a crash or thrown exception)... but had they done so, every C compiler would have been required to generate bounds-checking code for every array access the C program does. C(或C ++)的设计者可能已经决定在语言规范中包含一条规则,即“在数组范围之外写入必须导致X发生”(其中X是一些明确定义的行为,例如崩溃或抛出异常)...但是如果他们这样做了,那么每个C编译器都需要为C程序的每个数组访问生成边界检查代码。 Depending on the target hardware and cleverness of the compiler, enforcing a rule like that could easily make every C (or C++) program 5-10 times slower than it currently can be. 根据目标硬件和编译器的聪明程度,强制执行这样的规则可以轻松地使每个C(或C ++)程序比当前速度慢5-10倍。

So instead of requiring the compiler to enforce array bounds, they simply indicated that writing outside the bounds of the array is undefined behavior -- which is to say, you shouldn't do it, but if you do do it, then there are no guarantees about what will happen, and anything that happens that you don't like is your problem, not theirs. 因此,它们不是要求编译器强制执行数组边界,而是简单地指出在数组边界外写入是未定义的行为 - 也就是说,你不应该这样做,但如果你这样做,那么就没有了关于会发生什么的保证,任何你不喜欢的事情都是你的问题,而不是他们的问题。

Real-world implementations are then free to do whatever they want -- for example, on an OS with memory protection you will likely see page-based behavior like you described, or in an embedded device (or on older OS's like MacOS 9, MS-DOS, or AmigaDOS) the computer may just happily let you write to anywhere in memory, because to do otherwise would make the computer too slow. 然后,实际的实现可以随意执行任何操作 - 例如,在具有内存保护的操作系统上,您可能会看到如您所描述的基于页面的行为,或者在嵌入式设备中(或者在Mac OS 9,MS等旧操作系统上) -DOS,或AmigaDOS)计算机可能只是乐意让你写到内存中的任何地方,因为否则会使计算机太慢。

As a low-level (by modern standards) language, C (C++) expects the programmer to follow the rules, and will only mechanically enforce those rules if/when it can do so without incurring runtime overhead. 作为一种低级(通过现代标准)语言,C(C ++)希望程序员遵循这些规则,并且只有在能够这样做的情况下才能机械地强制执行这些规则而不会产生运行时开销。

Undefined behavior . 未定义的行为

That's what it is. 就是这样。 You can try to write out of bounds but it's not guaranteed to work. 可以尝试写出界限,但不能保证工作。 It might work, it might not. 它可能会起作用,也可能不起作用。 What happens is completely undefined. 发生的事情是完全未定义的。

Why is this allowed to happen? 为什么允许这种情况发生?

Because the C and C++ standards allow it. 因为C和C ++标准允许它。 The languages are designed to be fast . 这些语言设计得很快 Having to check for out of bounds accesses would require a run-time operation, which would slow the program down. 必须检查越界访问才需要运行时操作,这会降低程序的速度。

why is that address at p[500] even writable? 为什么p [500]的地址甚至可写?

It just happened to be. 恰好是。 Undefined behavior. 未定义的行为。

I see that when I try to edit at a larger value... 当我尝试以更大的值编辑时,我看到了...

See? 看到? Again, it just happened to segfault. 它再一次发生在段错误中。

When malloc is called, perhaps the OS decides to give the process an entire page. 当调用malloc时,操作系统可能会决定为进程提供整个页面。

Maybe, but the C and C++ standards don't require such behavior. 也许,但C和C ++标准不需要这样的行为。 They only require that the OS make at least the requested amount of memory available for use by the program. 它们只要求操作系统至少提供可供程序使用的所需内存量。 (If there's memory available.) (如果有可用的内存。)

It's undefined behaviour... 这是未定义的行为......

  • if you try to access outside bounds anything may happen, including SIGEGV or corruption elsewhere in the stack that causes your program to produce wrong results, hang, crash later etc.. 如果您尝试访问外部边界,可能会发生任何事情,包括SIGEGV或堆栈中其他地方的损坏导致程序产生错误结果,挂起,崩溃等等。

  • the memory may be writable without obvious failure on some given run for some compiler/flags/OS/day-of-the-week etc. because: 对于某些编译器/标志/操作系统/星期几等,某些给定运行中的内存可能是可写的而没有明显的故障,因为:

    • malloc() might actually allocate a larger sized allocated block wherein [500] can be written to (but on another run of the program, might not), or malloc()实际上可能会分配一个更大的已分配块,其中[500]可以写入(但在程序的另一次运行中,可能不会),或者
    • [500] might be after the allocated block, but still memory accessible to the program [500]可能在分配的块之后,但程序仍可访问存储器
      • it's likely that [500] - being a relatively small increment - would still be in the heap, which might extend beyond further than the addresses that malloc calls have so-far yielded due to some earlier reservation of heap memory (eg using sbrk() ) in preparation for anticipated use 很可能[500] - 一个相对较小的增量 - 仍然会在堆中,这可能超出了malloc调用迄今为止产生的地址,因为一些早期的堆内存保留(例如使用sbrk() )准备预期使用
      • it's vaguely possible that [500] is "off the end of" the heap, and you end up writing to some other memory area, where eg over static data, thread-specific data (including the stack) 很可能[500]在“堆的末尾”,你最终写入其他一些内存区域,例如静态数据,特定于线程的数据(包括堆栈)

Why it this allowed to happen? 为什么允许这种情况发生?

There's two aspects to this: 这有两个方面:

  • checking indices on every access would bloat (add extra machine code instructions) and slow down execution of the program, and generally the programmer can do some minimal validation of indices (eg validating once when a function's entered, then using the index however-many times), or generate the indices in a way that guarantees their validity (eg looping from 0 to the array size) 检查每次访问的索引会膨胀(添加额外的机器代码指令)并减慢程序的执行速度,通常程序员可以对索引进行一些最小的验证(例如,在函数输入时验证一次,然后使用索引多次),或以保证其有效性的方式生成索引(例如,从0循环到数组大小)

  • managing the memory extremely precisely, such that out-of-bounds access is reported by some CPU fault, is highly dependent on hardware and in general only possible at page boundaries (eg granularity in the 1k to 4k range), as well as taking extra instruction (whether within some enhanced malloc function or in some malloc -wrapping code) and time to orchestrate. 非常精确地管理内存,以便某些CPU故障报告越界访问,高度依赖于硬件,并且通常仅在页面边界处可能(例如1k到4k范围内的粒度),以及额外的指令(无论是在一些增强的malloc函数内还是在一些malloc -wrapping代码中)和编排时间。

It's simply that in C the concept of an array is rather basic. 简单地说,在C语言中,数组的概念是相当基本的。

The assignment to p[] is in C the same as: 对p []的赋值在C中与:

*(p+500)=999999;

and all the compiler does to implement that is: 并且所有编译器都实现了:

fetch p;
calculate offset : multiply '500' by the sizeof(*p) -- e.g. 4 for int;
add p and the offset to get the memory address
write to that address.

In many architectures this is implementable in one or two instructions. 在许多体系结构中,这可以在一个或两个指令中实现。

Note that not only does the compiler not know that the value 500 is not within the array, it doesn't actually know the array size to begin with! 请注意,编译器不仅不知道值500不在数组中,它实际上并不知道要开始的数组大小!

In C99 and later, some work has been done to make arrays safer, but fundamentally C is a language designed to be fast to compile and fast to run, not safe. 在C99及更高版本中,已经做了一些工作来使数组更安全,但从根本上说C是一种设计用于快速编译和快速运行的语言,而不是安全的。

Put another way. 换一种方式。 In Pascal, the compiler will prevent you from shooting your foot. 在Pascal中,编译器会阻止你射击。 In C++ the compiler provides ways to make it more difficult to shoot your foot, while in C the compiler doesn't even know you have a foot. 在C ++中,编译器提供了使你的脚更难射击的方法,而在C语言中,编译器甚至不知道你有脚。

In the language described by the 1974 C Reference Manual, the meaning of int arr[10]; 1974年C参考手册描述的语言中, int arr[10]; at file scope was "reserve a region of consecutive storage locations large enough to hold 10 values of type int , and bind the name arr to the address at the start of that region. The meaning of expression arr[someInt] would then be "multiply someInt by the size of an int , add that number of bytes to the base address of arr , and access whatever int happens to be stored at the resulting address.在文件 scope 中是“保留一个连续存储位置的区域,该区域足够大以容纳 10 个int类型的值,并将名称arr绑定到该区域开头的地址。表达式arr[someInt]的含义将是“乘以someInt乘以int的大小,将该字节数添加到arr的基地址,然后访问恰好存储在结果地址中的任何int If someInt is in the range 0..9, the resulting address will fall within the space that was reserved when arr was declared, but the language was agnostic to whether the value would fall within that range.如果someInt在 0..9 范围内,则结果地址将落在声明arr时保留的空间内,但语言不知道该值是否会落在该范围内。 If on a platform where int was two bytes, a programmer happened to know that the address of some object x was 200 bytes past the starting address of arr , then an access to arr[100] would be an access to x .如果在int是两个字节的平台上,程序员碰巧知道某个 object x的地址比arr的起始地址高 200 字节,那么访问arr[100]就是访问x As to how a programmer would happen to know that x was 200 bytes past the start of arr , or why the programmer would want to use the expression arr[100] rather than x to access x , the design of the language was completely agnostic to such things.至于程序员如何碰巧知道xarr开始后的 200 个字节,或者为什么程序员想要使用表达式arr[100]而不是x来访问x ,语言的设计完全不可知这样的事情。

The C Standard allows, but does not require, implementations to behave as described above unconditionally, even in cases where the address would fall outside the bounds of the array object being indexed. C 标准允许(但不要求)实现无条件地按照上述方式运行,即使在地址落在正在索引的数组 object 范围之外的情况下也是如此。 Code which relies upon such behavior will often be non-portable, but on some platforms may be able to accomplish some tasks more efficiently than would otherwise be possible.依赖于这种行为的代码通常是不可移植的,但在某些平台上可能能够比其他方式更有效地完成某些任务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM