为什么在访问设置了 16 个最高有效位中的任何一个的内存时,段错误地址为 NULL?

Why is the segfault address NULL when accessing memory that has any of the 16 most significant bits set?

考虑以下汇编程序:

bits 64
global _start
_start:
    mov rax, 0x0000111111111111
    add byte [rax*1+0x0], al
    jmp _start

当你用 nasmld 编译它时(在 Ubuntu,内核 5.4.0-48-generic,Ryzen 3900X 上),你会得到一个段错误:

$ ./segfault-addr
[1]    107116 segmentation fault (core dumped)  ./segfault-addr

When you attach gdb you can see the address that caused this fault:

(gdb) p $_siginfo._sifields._sigfault.si_addr
 = (void *) 0x111111111111

但是,如果您像这样将 16 个最高有效位中的任何一个设置为 1:

bits 64
global _start
_start:
    mov rax, 0x0001111111111111
    add byte [rax*1+0x0], al
    jmp _start

您显然仍然遇到段错误,但现在地址为 NULL:

(gdb) p $_siginfo._sifields._sigfault.si_addr
 = (void *) 0x0

为什么会这样?是gdb、Linux还是CPU本身造成的?

我能做些什么来防止这种行为吗?

这是 canonical and non-canonical addresses 之间的区别,因为 x86-64 没有完整的 64 位虚拟地址 space。你的第二个例子是一个 non-canonical 地址,因为它不是 sign-extended 48 位值(你显然没有在你的机器上的 5 级页面 table 扩展,或者它会为 57 位);这样的地址永远无法解析为物理内存位置。

对规范地址的无效访问会产生页面错误 (#PF),为此 CPU 向内核提供错误地址(在 CR2 寄存器中),内核将其传递给用户space 在 struct siginfosi_addr 字段中,如您所见。但是对 non-canonical 地址的访问总是无效的,并且 CPU 会引发一般保护异常(#GP),或者在极少数情况下会引发堆栈错误(#SS)。 x86 架构的设计者以其无限的智慧选择不向软件提供错误地址以防发生#GP 或#SS 异常,因此内核无法获得它,您也无法获得。

如果您确实需要该地址,您唯一的选择是解码导致异常的指令,并根据需要检查寄存器的内容,以确定它试图做什么。


我认为这个决定是因为内核确实需要地址以防页面错误。访问 not-present 页面可能是内存违规,应该终止进程;或者,例如,它可能只是一个已从物理内存换出的页面。在后一种情况下,内核使用故障地址在磁盘上找到合适的页面并将其加载回物理内存。然后它从异常处理程序更新页面 tables 和 returns 以重新启动错误指令,程序可以继续。

但是,一般保护故障通常是不可恢复的,必须终止该进程,或者至少发出信号以便它可以尝试清理。在这种情况下,对错误地址没有任何可操作的操作,我猜体系结构设计者认为它的潜在调试价值不值得 CPU 保存它。无论如何,#GP 的许多可能原因根本不是由内存访问引起的(例如,尝试从非特权模式读取或写入控制寄存器),在这种情况下没有错误地址。