当我尝试在x86汇编中为 Boot 加载程序编写一些例程时,我遇到了一个bug,当除法错误发生时,程序会陷入无限循环。通过调查,我发现调用int 0会正常地通过异常处理程序,然后继续执行程序的其余部分。为x86编写自己的异常处理程序时,除法错误异常发生时的返回地址是指令的地址,这意味着它只会一遍又一遍地循环执行除法。2这是正常行为还是Virtualbox/my cpu特有的bug?
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
1条答案
按热度按时间raogr8fs1#
对于
#DE
异常,Original 8086/8088 does压入 * 下一条指令 * 的地址。但所有其它x86 CPU都会压入出错的
div
/idiv
指令的起始地址。(至少从386开始;但286很可能与386相同。)这对于x86来说是正常的:出错指令会将出错指令的地址推入。x86机器码无法可靠地/明确地反向译码,因此设计意图是异常处理程序可检查情况并可能修复它,以及重新执行出错指令。
请参阅Intel x86 - Interrupt Service Routine responsibility,它详细说明了Faults、Traps和Abort之间的区别,甚至还特别提到了
int 0
和出错div
之间的区别。这对于#PF页面错误很有用,尽管not as realistic对于FP和整数运算异常很有用。则至少报告出错的实际指令。例如,
idiv dword [fs: rdi + 0xf1f7f1f7]
对于向后反汇编将是不明确的。disp 32中的f7 f1
字节是div ecx
的编码。您还将我不知道跳转是否直接跳转到FS前缀后的idiv
操作码,因此,对于调试和其他可能的目的来说,获得出错指令的实际起始地址(而不是结束地址)肯定是有用的。当然,
int 0
(如果IDT允许,且您不在真实的模式下)压入以下指令的CS:[ER]IP,因为它不是在修复情况后可以无故障地重新运行的指令。一般来说,int
的工作方式与call
类似,都是在之后返回到指令。8086的行为似乎是一个故意的决定,以更坏的行为为代价来简化硬件。它没有最大指令长度的限制,并且完全避免记住指令的开始,无论是在CPU内部的任何地方(Ken Shirriff在回答 * 8086中的中断、指令指针和指令队列 * 时引用了英特尔的一项专利)。
如果
cs rep movsb
被外部中断中断,中断返回地址在 final 前缀之前,而不是实际的指令开始。(也就是说,它将恢复为没有cs
前缀的rep movsb
,如果你按照这个顺序放置前缀,这将是一场灾难。这是最大的“更糟糕的行为”;您可以通过将rep cs movsb
放入循环中来解决它。)由于8086没有任何类型的页面错误或可配置的段限制,因此它不能在rep cs movsb
或其他rep字符串指令期间发生同步异常,只能发生异步外部中断。有关8086设计决策的更多猜测,请参见Why do call and jump instruction use a displacement relative to the next instruction, not current?。