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

时间:2020-07-25 17:34:46

标签: c linux assembly cpu 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;
}

如您所见,我注册了一个信号处理程序,并进一步行了一些代码,我取消了对空指针的引用==>触发了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

但是缺少一些东西,信号如何从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].

在这两个标记为???的地方会发生什么?

2 个答案:

答案 0 :(得分:5)

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

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

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

页面错误处理程序可以找出用户空间尝试访问的地址。例如在x86上,有一个control register (CR2),其中包含引起故障的线性(虚拟)地址。操作系统可以使用mov rax, cr2将其副本放入通用寄存器中。

其他ISA具有其他机制,可让OS告知CPU它的页面错误处理程序在哪里,并让该处理程序查找用户空间试图访问的地址。但是具有虚拟内存的系统具有本质上等效的机制是相当普遍的。


该访问尚未被确认为无效。有几个原因可能导致OS不必费心将进程分配的内存“连接”到硬件页表中。这就是分页的全部内容:让OS纠正这种情况,例如写时复制,延迟分配或从交换空间恢复页面。

页面错误分为三类:(从我的答案on another question复制)。维基百科的page-fault article说类似的话。

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

因此,只有在操作系统查询了自己的数据结构以查看某个进程被赋予 所拥有的虚拟地址之后,才能确保内存访问无效。

确定页面错误是否无效完全取决于软件。就像我在Why page faults are usually handled by the OS, not hardware?上写的那样-如果HW可以解决所有问题,那么就不必陷入操作系统。

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

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


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


向用户空间传递信号

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

当然,一个用户空间进程只是从SIGSEGV处理程序返回而没有解决问题,这会使主线程再次重新运行相同的故障指令。 (操作系统将返回引发页面错误异常的指令。)

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

答案 1 :(得分:3)

通常发生的情况是,当CPU的内存管理单元发现程序尝试访问的虚拟地址不在物理内存的任何映射中时,它将引发中断。操作系统将设置中断服务程序,以防万一。该例程将执行OS内部所需的任何操作,以用SEGV发出信号。作为ISR的回报,违规指令尚未完成。

随后发生的情况取决于是否为SEGV安装了处理程序。该语言的运行时可能已经安装了一个例外。进程几乎总是终止,因为它无法恢复。诸如valgrind之类的东西会对信号产生有用的作用,例如,准确告诉您程序必须到达代码的哪个位置。

有趣的地方是当您查看C运行时库(如glibc)使用的内存分配策略时。 NULL指针取消引用是显而易见的,但是如何访问数组末尾呢?通常,对malloc()new的调用将导致库要求的内存比要求的要多。可以肯定的是,它可以使用该内存来满足对内存的进一步请求,而不会打扰操作系统,这既好又快速。但是,CPU的MMU不知道发生了这种情况。因此,如果您确实要访问数组末尾之外的内容,那么您仍在访问MMU可以看到的已映射到您的进程的内存,但实际上,您正在开始践踏不应该访问的内存。某些防御性很强的OS不会这样做,特别是MMU确实会超出范围。

这导致有趣的结果。我遇到过在Linux上构建并运行良好的软件,该软件针对FreeBSD进行了编译,并开始抛出SEGV。 GNURadio就是这样一个软件(它是一个复杂的流程图)。这很有趣,因为它大量使用了boost / c ++ 11智能指针,专门用于避免内存滥用。我还无法确定错误的原因是提交那个错误报告...

相关问题