假设有一个指针,我们将其初始化为NULL。

int* ptr = NULL;
*ptr = 10;

现在,该程序将崩溃,因为ptr没有指向任何地址,并且我们正在为其分配一个值,这是无效的访问。那么,问题是,操作系统内部发生了什么?是否发生页面错误/分段错误?内核是否还会在页表中搜索?还是在那之前发生了崩溃?

我知道我不会在任何程序中执行此类操作,但这只是为了了解在这种情况下OS或编译器内部发生的情况。这不是重复的问题。

最佳答案

简短答案:它取决于很多因素,包括编译器,处理器体系结构,特定处理器模型和OS等。

长答案(x86和x86-64):让我们进入最低层:CPU。在x86和x86-64上,该代码通常将编译为一条指令或指令序列,如下所示:

movl $10, 0x00000000

表示“将常量整数10存储在虚拟内存地址0”。 Intel® 64 and IA-32 Architectures Software Developer Manuals详细描述了该指令执行后会发生什么,因此我将为您总结一下。

CPU可以在几种不同的模式下运行,其中几种是为了向后兼容许多较旧的CPU。现代操作系统以称为保护模式的模式运行用户级代码,该模式使用paging将虚拟地址转换为物理地址。

对于每个进程,操作系统都会保留一个页表,该页表指示如何映射地址。页表以CPU可以理解的特定格式(并 protected ,以致不能被用户代码修改)存储在内存中。对于发生的每一次内存访问,CPU都会根据页表对其进行转换。如果转换成功,它将对物理内存位置执行相应的读/写操作。

当地址转换失败时,会发生有趣的事情。并非所有地址都有效,并且如果任何内存访问生成了无效地址,则处理器将引发页面错误异常。这会触发从用户模式(即x86 / x86-64上的当前特权级别(CPL)3)到内核模式(即CPL 0)到内核代码中由中断描述符表(IDT)定义的特定位置的转换。 。

内核重新​​获得控制权,并根据异常和进程的页表中的信息确定发生了什么。在这种情况下,它意识到用户级进程访问了无效的内存位置,然后做出了相应的反应。在Windows上,它将调用structured exception handling以允许用户代码处理异常。在POSIX系统上,操作系统将向过程传递SIGSEGV信号。

在其他情况下,操作系统将在内部处理页面错误,并从其当前位置重新开始该过程,就好像什么都没发生一样。例如,guard pages放置在堆栈的底部,以允许堆栈按需增长到最大限制,而不是为堆栈预先分配大量的内存。类似的机制用于实现copy-on-write内存。

在现代操作系统中,通常将页表设置为使地址0成为无效的虚拟地址。但是有时候可以更改它,例如在Linux上,通过将0写入伪文件/proc/sys/vm/mmap_min_addr,然后可以使用mmap(2)映射虚拟地址0。在这种情况下,取消引用空指针不会导致页面错误。

上面的讨论都是关于当原始代码在用户空间中运行时发生的情况。但这也可能在内核内部发生。内核可以(并且肯定比用户代码更有可能)映射虚拟地址0,因此这种内存访问将是正常的。但是,如果未映射,则发生的情况将非常相似:CPU引发页面错误错误,该错误会捕获到内核中的预定义点,内核会检查发生的事情,并做出相应的反应。如果内核无法从异常中恢复,则通常会通过将一些调试信息打印到控制台或串行端口然后暂停来以某种方式崩溃(例如内核崩溃,内核oop或Windows上的BSOD)。

另请参阅Much ado about NULL: Exploiting a kernel NULL dereference ,以了解攻击者如何利用内核内部的空指针解除引用错误来获取Linux机器上的root用户特权的示例。

关于c - 当我们取消引用C中的NULL指针时,在OS中会发生什么?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/12645647/

10-11 23:14
查看更多