如果取消引用空指针,在 CPU 级别会发生什么?

JCW*_*x86 0 c linux cpu assembly signal-handling

假设我有以下程序:

#include <signal.h>
#include <stddef.h>
#include <stdlib.h>

static void myHandler(int sig){
        abort();
}

int main(void){
        signal(SIGSEGV,myHandler);
        char* ptr=NULL;
        *ptr='a';
        return 0;
}

Run Code Online (Sandbox Code Playgroud)

如您所见,我注册了一个信号处理程序和一些行,我取消引用了一个空指针 ==> 触发了 SIGSEGV。但它是如何触发的?如果我使用strace(输出剥离)运行它:

//Set signal handler (In glibc signal simply wraps a call to sigaction)
rt_sigaction(SIGSEGV, {sa_handler=0x563b125e1060, sa_mask=[SEGV], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7ffbe4fe0d30}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
//SIGSEGV is raised
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=NULL} ---
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [SEGV], 8) = 0
Run Code Online (Sandbox Code Playgroud)

但是缺少一些东西,信号如何从 CPU 传递到程序?我的理解:

[Dereferences null pointer] -> [CPU raises an exception] -> [??? (How does it go from the CPU to the kernel?) ] -> [The kernel is notified, and sends the signal to the process] -> [??? (How does the process know, that a signal is raised?)] -> [The matching signal handler is called].
Run Code Online (Sandbox Code Playgroud)

这两个标有 的地方会发生什么???

Pet*_*des 6

一个NULL在大多数(但不是全部)C实现指针地址0。通常,此地址不在有效(映射)页面中。

对未被 HW 页表映射的虚拟页的任何访问都会导致页错误异常。例如在 x86 上,#PF.

这会调用操作系统的页面错误异常处理程序来解决这种情况。例如,在 x86-64 上,CPU 将异常返回信息推送到内核堆栈上,并从与该异常编号对应的IDT(中断描述符表)条目加载 CS:RIP 。就像用户空间触发的任何其他异常一样,例如整数除以零 ( #DE) 或一般保护错误#GP(尝试在用户空间中运行特权指令,或需要对齐的未对齐 SIMD 指令,或许多其他可能的事情)。

页面错误处理程序可以找出用户空间尝试访问的地址。例如,在 x86 上,有一个控制寄存器 (CR2)保存导致故障的线性(虚拟)地址。操作系统可以使用mov rax, cr2.

其他 ISA 有其他机制让操作系统告诉 CPU 其页面错误处理程序在哪里,并让该处理程序找出用户空间试图访问的地址。但是对于具有虚拟内存的系统来说,具有本质上等效的机制是非常普遍的。


该访问尚不知道是否无效。操作系统可能不会费心将进程分配的内存“连接”到硬件页表中的原因有很多。这就是分页的全部意义:让操作系统纠正这种情况,例如写时复制、延迟分配或从交换空间中取回页面。

页面错误分为三类:(从我对另一个问题的回答中复制)。维基百科的页面错误文章也说了类似的话。

  • 有效(该进程逻辑上已映射内存,但操作系统懒惰或玩弄写时复制之类的技巧):
    • 硬:页面需要从磁盘调入,无论是从交换空间还是从磁盘文件(例如内存映射文件,如可执行或共享库的页面)。通常操作系统会在等待 I/O 时安排另一个任务:这是硬(主要)和软(次要)之间的主要区别。
    • 软:不需要磁盘访问,只是例如分配+归零一个新的物理页面来支持用户空间刚刚尝试写入的虚拟页面。或者对多个进程已映射的可写页面进行写时复制,但其中一个的更改不应该对另一个可见(例如 mmap(MAP_PRIVATE))。这会将共享页面变成私有脏页面。
  • invalid:该页面甚至没有逻辑映射。 像 Linux 这样的 POSIX 操作系统将向违规进程/线程发送 SIGSEGV 信号。

因此,只有在操作系统查阅自己的数据结构以查看进程应该拥有哪些虚拟地址之后,才能确定内存访问无效。

确定缺页错误是否无效完全取决于软件。正如我在为什么页面错误通常由操作系统而不是硬件处理?- 如果硬件可以解决所有问题,它就不需要陷入操作系统。

有趣的事实:在 Linux 上,可以将系统配置0为(或可以)有效的虚拟地址。 设置mmap_min_addr= 0 允许进程到mmap那里。例如,WINE 需要它来模拟 16 位 Windows 内存布局。

由于这不会将NULL指针的内部对象表示更改为以外0,因此这样做将意味着 NULL 取消引用将不再出错。这使得调试更加困难,这就是为什么默认mmap_min_addr值为 64k。


在没有虚拟内存的更简单的系统上,操作系统可能仍然能够配置 MMU 以捕获对某些地址空间区域的内存访问。操作系统的陷阱处理程序不需要检查任何东西,它知道触发它的任何访问都是无效的。(除非它也在为地址空间的某些区域模拟某些东西......)


向用户空间传递信号

这部分是纯软件。传递 SIGSEGV 与传递由另一个进程发送的 SIGALRM 或 SIGTERM 没有什么不同。

当然,一个从 SIGSEGV 处理程序返回而不修复问题的用户空间进程将使主线程再次重新运行相同的错误指令。(操作系统将返回到引发页面错误异常的指令。)

这就是 SIGSEGV 的默认操作是终止的原因,以及将行为设置为“忽略”没有意义的原因。