x86除法异常-返回地址

Gub*_*ber 2 x86 assembly virtualbox interrupt-handling x86-16

当尝试在 x86 程序集中为引导加载程序编写一些例程时,我遇到了一个错误,当发生除法错误时,程序将陷入无限循环。通过调查,我发现调用 int 0 会正常通过异常处理程序,然后继续执行程序的其余部分。为 x86 编写自己的异常处理程序,发生除法错误异常时的返回地址是指令的地址,这意味着它将永远循环执行除法。这是正常行为还是 Virtualbox/我的 cpu 特有的错误?

org 0x7c00      ;put all label addresses at offset 0x7c00

xor ax, ax      ;set up all segment registers
mov ds, ax
mov ax, 0x9000
mov ss, ax
mov sp, 0x1000
mov ax, 0xB800  ;video text memory starts at this address
mov es, ax

mov ah, 0x00
mov al, 0x02
int 0x10        ;go into 80x25 monochrome text

mov [0x0000], word DivideException
mov [0x0002], word 0x0000

xor di, di
xor bx, bx

;int 0   ;this and the divide CX below will cause a division error exception

mov ax, 0
mov cx, 0 ;when exception is handled it prints out
div cx    ;"a divide by zero error happened 0000:7C2D 0000:7C2F
          ;the first address is the division instruction and the second one is 2 bytes after
          ;when int 0 is uncommented out then it will have the two same addresses
jmp $

ToHex:
push bp
mov bp, sp
push bx

mov ax, word [bp+6]
mov bx, word [bp+4]
add bx, 3
mov cx, 16

.Loop:
xor dx, dx
div cx
add dx, 48
cmp dx, 58
jb .Skip
add dx, 7
.Skip:
mov byte [bx], dl
dec bx
cmp ax, 0
jne .Loop

.Ret:
pop bx
mov sp, bp
pop bp
ret

PrintStr:
push bp
mov bp, sp
push bx

mov bx, word [bp+6]
mov ah, byte [bx]
mov bx, word [bp+4]

.PrintLoop:
mov al, byte [bx]

mov word [es:di], ax
inc di
inc di
inc bx
cmp byte [bx], 0x00
jne .PrintLoop

pop bx
mov sp, bp
pop bp
ret

DivideException:
push bp
mov bp, sp
push bx

push word ColorAttributes1
push word String3
call PrintStr
add sp, 4

push word [bp+4]
push word String1
call ToHex
add sp, 4

push word [bp+2]
push word String2
call ToHex
add sp, 4

push word ColorAttributes1
push word String1
call PrintStr

push ds
mov ds, word [bp+4]
mov bx, word [bp+2]

cmp byte [ds:bx], 0xF7  ;checks if theres a 0xF7 byte at the return address
jne .DontAdd            ;for some reason the return address when calling int 0
add word [bp+2], 2      ;directly is the address after the instruction while
.DontAdd:               ;causing a divide error exception through divsion will
pop ds                  ;put the return address at the division leading to an
                        ;infinite loop
push word [bp+4]
push word String1
call ToHex
add sp, 4

push word [bp+2]
push word String2
call ToHex
add sp, 4

push word ColorAttributes1
push word String1
call PrintStr

add sp, 4

pop bx
mov sp, bp
pop bp
iret



String1: db "0000:";, 0x00
String2: db "0000 ", 0x00
String3: db "a divide by zero error happened ", 0x00
ColorAttributes1: db 0x0F ; first nibble is backround color
                         ;second nibble is foreground


times 2048-2- ($-$$) db 0  ;fills the rest with 0's until 510 bytes
dw 0xAA55               ;magic boot sector number
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 6

原始8086/8088确实会推送以下指令的地址以进行#DE异常处理。
但所有其他 x86 CPU 都会推送故障div/idiv指令的起始地址。 (至少从 386 开始;但 286 很可能与 386 相同。)

一般来说,这对于 x86 来说是正常的:出错指令会推送出错指令的地址。x86 机器代码无法可靠/明确地向后解码,因此设计意图是异常处理程序可以检查情况并可能修复它,并重新运行错误指令。

请参阅Intel x86 - 中断服务例程责任,它详细说明了故障、陷阱和中止之间的差异,甚至特别提到了int 0和 故障之间的差异div

这对于 #PF 页面错误很有用,尽管对于 FP 和整数算术异常等情况不太现实。但如果不修复,那么至少报告出现故障的实际指令。例如idiv dword [fs: rdi + 0xf1f7f1f7]向后拆卸会产生歧义。disp32 中的字节f7 f1是 的编码div ecx。您也不知道跳转是否直接跳转到idivFS 前缀之后的操作码。因此,获取错误指令开始的实际地址(而不是结束地址)对于调试和可能的其他目的绝对有用。

int 0(如果 IDT 允许,如果您不在实模式下)当然会推送以下指令的 CS:[ER]IP,因为在修复情况后它无法重新运行而不会出现故障。 int一般来说,其目的类似于call返回指令。


8086 的行为似乎是一个有意的决定,旨在以更糟糕的行为为代价来简化硬件。它对最大指令长度没有限制,并且完全避免记住 CPU 内任何位置的指令开始(Ken Shirriff 在关于8086 中的中断、指令指针和指令队列的回答中引用了英特尔专利)。

如果cs rep movsb被外部中断中断,则中断返回地址位于最终前缀之前,而不是实际的指令开始处。(即它会像rep movsb没有cs前缀一样恢复,如果您按该顺序放置前缀,这将是一场灾难。这是最大的“更糟糕的行为”;您可以通过放入rep cs movsb循环来解决它。)由于 8086 没有任何类型的页面错误或可配置的段限制,它不能在执行rep cs movsb或其他代表字符串指令期间采取同步异常,只能采取异步外部中断。

请参阅为什么调用和跳转指令使用相对于下一条指令而不是当前指令的位移?有关 8086 设计决策的更多猜测。