繁体   English   中英

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

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

我最近读完了有关虚拟 memory 的内容,我对 malloc 如何在虚拟地址空间和物理地址空间 Memory 中工作有疑问。

例如(从另一个 SO 帖子复制的代码)

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

为什么允许这种情况发生? 或者为什么 p[500] 的地址甚至是可写的?

这是我的猜测。

当 malloc 被调用时,操作系统可能决定给进程一个完整的页面。 我将假设每个页面都值得 4KB 的空间。 整个东西都标记为可写吗? 这就是为什么您可以将 go 最大 500*sizeof(int) 放入页面(假设 32 位系统,其中 int 的大小为 4 个字节)。

我看到当我尝试以更大的值进行编辑时...

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

段错误。

如果是这样,那么这是否意味着有些页面专用于您的代码/指令/文本段并标记为不可写,与您的堆栈/变量所在的页面(事情发生变化的地方)完全分开并标记为可写? 当然,进程认为它们在 32 位系统上的 4gb 地址空间中的每个订单旁边。

“为什么允许这种情况发生?” (写在界外)

C不需要通常需要的额外CPU指令来防止这种超出范围的访问。

这就是C的速度 - 它信任程序员,为编码人员提供执行任务所需的所有绳索 - 包括足够的绳索悬挂自己。

考虑以下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;
}

它打印进程的内存映射和某些不同类型内存的地址。

[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

或者为什么p [500]的地址甚至可写?

堆是从0824d000-0826f000和&p [500]偶然是0x824ea70,因此内存是可写和可读的,但是这个内存区域可能包含将被更改的实际数据! 在示例程序的情况下,它很可能是未使用的,因此写入此内存对于进程的工作无害。

&p [50000000]偶然是0x1410a4a0,它不在内核映射到进程的页面中,因此是不可写和不可读的,因此是seg错误。

如果使用-fsanitize=address编译它,将检查内存访问,并且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

如果是这样,那么这是否意味着有些页面专用于您的代码/指令/文本段并标记为不可写入与您的堆栈/变量所在的页面完全分离(事情确实发生了变化)并标记为可写?

是的,请参阅上面进程'内存映射的输出。 r-xp表示可读和可执行, rw-p表示可读和可写。

为什么允许这种情况发生?

C(和C ++)语言的主要设计目标之一是尽可能提高运行时间。 C(或C ++)的设计者可能已经决定在语言规范中包含一条规则,即“在数组范围之外写入必须导致X发生”(其中X是一些明确定义的行为,例如崩溃或抛出异常)...但是如果他们这样做了,那么每个C编译器都需要为C程序的每个数组访问生成边界检查代码。 根据目标硬件和编译器的聪明程度,强制执行这样的规则可以轻松地使每个C(或C ++)程序比当前速度慢5-10倍。

因此,它们不是要求编译器强制执行数组边界,而是简单地指出在数组边界外写入是未定义的行为 - 也就是说,你不应该这样做,但如果你这样做,那么就没有了关于会发生什么的保证,任何你不喜欢的事情都是你的问题,而不是他们的问题。

然后,实际的实现可以随意执行任何操作 - 例如,在具有内存保护的操作系统上,您可能会看到如您所描述的基于页面的行为,或者在嵌入式设备中(或者在Mac OS 9,MS等旧操作系统上) -DOS,或AmigaDOS)计算机可能只是乐意让你写到内存中的任何地方,因为否则会使计算机太慢。

作为一种低级(通过现代标准)语言,C(C ++)希望程序员遵循这些规则,并且只有在能够这样做的情况下才能机械地强制执行这些规则而不会产生运行时开销。

未定义的行为

就是这样。 可以尝试写出界限,但不能保证工作。 它可能会起作用,也可能不起作用。 发生的事情是完全未定义的。

为什么允许这种情况发生?

因为C和C ++标准允许它。 这些语言设计得很快 必须检查越界访问才需要运行时操作,这会降低程序的速度。

为什么p [500]的地址甚至可写?

恰好是。 未定义的行为。

当我尝试以更大的值编辑时,我看到了...

看到? 它再一次发生在段错误中。

当调用malloc时,操作系统可能会决定为进程提供整个页面。

也许,但C和C ++标准不需要这样的行为。 它们只要求操作系统至少提供可供程序使用的所需内存量。 (如果有可用的内存。)

这是未定义的行为......

  • 如果您尝试访问外部边界,可能会发生任何事情,包括SIGEGV或堆栈中其他地方的损坏导致程序产生错误结果,挂起,崩溃等等。

  • 对于某些编译器/标志/操作系统/星期几等,某些给定运行中的内存可能是可写的而没有明显的故障,因为:

    • malloc()实际上可能会分配一个更大的已分配块,其中[500]可以写入(但在程序的另一次运行中,可能不会),或者
    • [500]可能在分配的块之后,但程序仍可访问存储器
      • 很可能[500] - 一个相对较小的增量 - 仍然会在堆中,这可能超出了malloc调用迄今为止产生的地址,因为一些早期的堆内存保留(例如使用sbrk() )准备预期使用
      • 很可能[500]在“堆的末尾”,你最终写入其他一些内存区域,例如静态数据,特定于线程的数据(包括堆栈)

为什么允许这种情况发生?

这有两个方面:

  • 检查每次访问的索引会膨胀(添加额外的机器代码指令)并减慢程序的执行速度,通常程序员可以对索引进行一些最小的验证(例如,在函数输入时验证一次,然后使用索引多次),或以保证其有效性的方式生成索引(例如,从0循环到数组大小)

  • 非常精确地管理内存,以便某些CPU故障报告越界访问,高度依赖于硬件,并且通常仅在页面边界处可能(例如1k到4k范围内的粒度),以及额外的指令(无论是在一些增强的malloc函数内还是在一些malloc -wrapping代码中)和编排时间。

简单地说,在C语言中,数组的概念是相当基本的。

对p []的赋值在C中与:

*(p+500)=999999;

并且所有编译器都实现了:

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.

在许多体系结构中,这可以在一个或两个指令中实现。

请注意,编译器不仅不知道值500不在数组中,它实际上并不知道要开始的数组大小!

在C99及更高版本中,已经做了一些工作来使数组更安全,但从根本上说C是一种设计用于快速编译和快速运行的语言,而不是安全的。

换一种方式。 在Pascal中,编译器会阻止你射击。 在C ++中,编译器提供了使你的脚更难射击的方法,而在C语言中,编译器甚至不知道你有脚。

1974年C参考手册描述的语言中, int arr[10]; 在文件 scope 中是“保留一个连续存储位置的区域,该区域足够大以容纳 10 个int类型的值,并将名称arr绑定到该区域开头的地址。表达式arr[someInt]的含义将是“乘以someInt乘以int的大小,将该字节数添加到arr的基地址,然后访问恰好存储在结果地址中的任何int 如果someInt在 0..9 范围内,则结果地址将落在声明arr时保留的空间内,但语言不知道该值是否会落在该范围内。 如果在int是两个字节的平台上,程序员碰巧知道某个 object x的地址比arr的起始地址高 200 字节,那么访问arr[100]就是访问x 至于程序员如何碰巧知道xarr开始后的 200 个字节,或者为什么程序员想要使用表达式arr[100]而不是x来访问x ,语言的设计完全不可知这样的事情。

C 标准允许(但不要求)实现无条件地按照上述方式运行,即使在地址落在正在索引的数组 object 范围之外的情况下也是如此。 依赖于这种行为的代码通常是不可移植的,但在某些平台上可能能够比其他方式更有效地完成某些任务。

暂无
暂无

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

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