为什么在访问设置了 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
当你用 nasm
和 ld
编译它时(在 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 siginfo
的 si_addr
字段中,如您所见。但是对 non-canonical 地址的访问总是无效的,并且 CPU 会引发一般保护异常(#GP),或者在极少数情况下会引发堆栈错误(#SS)。 x86 架构的设计者以其无限的智慧选择不向软件提供错误地址以防发生#GP 或#SS 异常,因此内核无法获得它,您也无法获得。
如果您确实需要该地址,您唯一的选择是解码导致异常的指令,并根据需要检查寄存器的内容,以确定它试图做什么。
我认为这个决定是因为内核确实需要地址以防页面错误。访问 not-present 页面可能是内存违规,应该终止进程;或者,例如,它可能只是一个已从物理内存换出的页面。在后一种情况下,内核使用故障地址在磁盘上找到合适的页面并将其加载回物理内存。然后它从异常处理程序更新页面 tables 和 returns 以重新启动错误指令,程序可以继续。
但是,一般保护故障通常是不可恢复的,必须终止该进程,或者至少发出信号以便它可以尝试清理。在这种情况下,对错误地址没有任何可操作的操作,我猜体系结构设计者认为它的潜在调试价值不值得 CPU 保存它。无论如何,#GP 的许多可能原因根本不是由内存访问引起的(例如,尝试从非特权模式读取或写入控制寄存器),在这种情况下没有错误地址。
考虑以下汇编程序:
bits 64
global _start
_start:
mov rax, 0x0000111111111111
add byte [rax*1+0x0], al
jmp _start
当你用 nasm
和 ld
编译它时(在 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 siginfo
的 si_addr
字段中,如您所见。但是对 non-canonical 地址的访问总是无效的,并且 CPU 会引发一般保护异常(#GP),或者在极少数情况下会引发堆栈错误(#SS)。 x86 架构的设计者以其无限的智慧选择不向软件提供错误地址以防发生#GP 或#SS 异常,因此内核无法获得它,您也无法获得。
如果您确实需要该地址,您唯一的选择是解码导致异常的指令,并根据需要检查寄存器的内容,以确定它试图做什么。
我认为这个决定是因为内核确实需要地址以防页面错误。访问 not-present 页面可能是内存违规,应该终止进程;或者,例如,它可能只是一个已从物理内存换出的页面。在后一种情况下,内核使用故障地址在磁盘上找到合适的页面并将其加载回物理内存。然后它从异常处理程序更新页面 tables 和 returns 以重新启动错误指令,程序可以继续。
但是,一般保护故障通常是不可恢复的,必须终止该进程,或者至少发出信号以便它可以尝试清理。在这种情况下,对错误地址没有任何可操作的操作,我猜体系结构设计者认为它的潜在调试价值不值得 CPU 保存它。无论如何,#GP 的许多可能原因根本不是由内存访问引起的(例如,尝试从非特权模式读取或写入控制寄存器),在这种情况下没有错误地址。