简答:这取决于很多因素,包括编译器、处理器架构、特定处理器型号和操作系统等。
长答案(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 理解的特定格式存储在内存中(并受到保护,以便用户代码无法修改它们)。对于发生的每一次内存访问,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 引发一个页面错误错误,该错误会陷入内核的预定义点,内核检查发生了什么,并做出相应的反应。如果内核无法从异常中恢复,它通常会通过打印输出以某种方式出现恐慌(kernel panic、kernel oops 或 Windows 上的 BSOD)一些调试信息到控制台或串行端口,然后停止。
另请参阅Much ado about NULL: Exploiting a kernel NULL dereference ,了解攻击者如何利用内核内部的空指针取消引用错误来获得 Linux 计算机上的 root 权限的示例。