从程序集中捕获/禁用 SIGFPE 异常

Stu*_*ffe 1 x86 assembly x86-64 integer-division

在使用 .x86 程序集除以 0 时,出现 SIGFPE 异常idiv。我如何从汇编中禁用它?我需要系统调用还是可以直接在 x86 中完成?

再生产:

测试.asm

default rel
global WinMain

section .data 

section .text
WinMain:
    mov rcx, 0
    mov rdx, 0
    idiv rcx
Run Code Online (Sandbox Code Playgroud)

命令:

nasm -f win64 test.asm
gcc test.obj
gdb a
运行

Pet*_*des 5

TL:DR:您无法使硬件不会因#DE异常而发生故障,并且在跳过信号处理程序中的故障指令以“修复”情况后恢复执行也很重要。(您还需要将 RDX 和 RAX 设置为一些特殊值,例如-10或明显的值,例如0xcccccccccccccccc。或者取决于什么可能使程序的其余部分默默地做一些有用的事情(?)而不是以不同的方式再次崩溃,也许+1。)

对于实际的浮点异常,x87 FPU 和 SSE(通过 MXCSR)默认具有异常掩码,因此您在无效操作(如除以零)的目标中得到 NaN,没有硬件异常。POSIX 要求,如果任何算术运算出现错误并且操作系统传递有关该错误的信号,则该信号必须是 SIGFPE。


您能做的最好的事情就是使用信号处理程序捕获 SIGFPE(sigaction(2)在 Linux 等 POSIX 操作系统上)。奇怪的是,您正在谈论带有WinMain入口点的程序的 SIGFPE,除非 Windows SEH(结构化异常处理)也使用相同的 SIGFPE 名称?

根据 POSIX,使用 SIG_IGN 忽略它是未定义的,实际上,如果您忽略该信号或让信号处理程序返回而不执行任何操作,则会导致无限循环。硬件#DE错误有一个指向错误指令的返回地址(与页面错误相同),这样处理程序就可以看到它是哪条指令,因此以某种方式修复情况(就像 #PF 处理程序通常所做的那样)将重新运行希望下次尝试时不会出错的说明。

所以你的处理程序应该检查是否si_code == FPE_INTDIV。如果是这样,它可能会尝试将程序计数器推进到超过错误指令的末尾,就像它根本没有执行一样运行,使 FLAGS 和 RDX:RAX 保持不变。(或者您的信号处理程序可以将它们设置为一些一致的值。)

线程的寄存器可由在其中运行的 Linux 信号处理程序访问和修改。 获取生成 UNIX 信号的故障地址也涵盖了这些ucontext_t内容,其中包含 GPR(包括 RAX 和 RDX)以及 RIP(程序计数器)。

x86-64 指令的长度可变。您必须从 RIP 中的地址解码机器代码,跳过前缀,查找作为idiv或 的操作码的操作码字节div。如果是这样,请将 RIP 前进到该指令之后以跳过它。指令长度解码并非易事,因为具有可变长度寻址模式的内存源操作数是可能的:您必须对 ModRM 和可选的 SIB 进行解码,足以计算出总指令长度。(x86-64 的设计目的是让这不会太痛苦,因此硬件可以有效地完成它:在对前缀后面的部分进行指令长度解码时,您不需要记住 REX 前缀中的位(如果有的话): * rbp not允许作为 SIB 基础吗?


没有标志/控制寄存器/其他硬件设置会在除数 = 0 或商不适合操作数大小时引发但不会引发除法div异常idiv#DE例如 64 位的 RAX idiv rcx,就像INT64_MIN / -1在 x86-64 上也会出现故障。

或者对于(unsigned long)-1 / 1有符号除法,就像您在有符号之前通过零扩展到 RDX:RAX 所做的那样idiv。通常使用cqo/idiv在有符号除法之前将 RAX 符号扩展为 RDX:RAX,或在无符号除法之前使用xor edx,edx/div将零扩展为 RDX。
什么时候以及为什么我们要签署扩展并使用 cdq 与 mul/div?

您可以屏蔽 FP 异常,默认的 x87 和 SSE (MXCSR) 状态是屏蔽 FP 异常,因此通常在 x86-64 系统上唯一可以引发 SIGFPE 的是整数除法。(或者可能是一条into指令,如果设置了 OF 标志则捕获。)


有关的:

  • 顺便说一句,在 Windows 上的 SEH 处理程序中,您会得到异常代码 0xC0000094 又名 EXCEPTION_INT_DIVIDE_BY_ZERO,但将其留给像 gdb 这样的 unix-land 工具来假装整个宇宙都是 unix... (2认同)