Skip to content

Instantly share code, notes, and snippets.

@PluMGMK
Last active October 19, 2025 00:14
Show Gist options
  • Select an option

  • Save PluMGMK/2546e5fdbbb3953871fdef9a03e8dc2c to your computer and use it in GitHub Desktop.

Select an option

Save PluMGMK/2546e5fdbbb3953871fdef9a03e8dc2c to your computer and use it in GitHub Desktop.
FPU exception hang checker
;
; FPUCHK10.ASM
;
; A simple COM program to run in Real Mode and check for bad firmware behaviour
; on FPU exception. Completely self-contained, no debugger required. There is
; absolutely no way excecution can be interrupted, except by an NMI or SMI.
;
; Assemble as per usual:
; jwasmr -bin fpuchk10.asm
; move fpuchk10.bin fpuchk10.com
;
; On QEMU and VirtualBox, this runs to completion, taking about half a minute.
; On MSI Z97 GAMING 3 (MS-7918), with a Core i7-4790K, the system completely
; hangs after a random amount of time, less than a second.
;
.model tiny
.8086
SCREEN_WIDTH equ 80
BYTES_PER_CHAR equ 2 ; every char on screen has char and attr bytes
ROWS_USED equ 4 ; we use first four rows (pre-waitcount, enter, waitcount, cleared)
MAX_SWP_WAITCNT equ 2000; so a successful sweep doesn't take ages
SINGLE_WAITCNT equ 60000
.code
org 100h
start: ; Too lazy to code up a proper 486 check, so just use the PCI BIOS
; as a proxy for it...
mov ax,0B101h
xor dx,dx
int 1Ah
cmp dx,4350h ; lower half of ' ICP'
je @F
lea dx,nopcibios
mov ah,9 ; output to stdout
int 21h
mov ax,4C01h ; exit with failure
int 21h
@@: .486 ; we know we have a 486, but may be in VM86 mode
smsw ax
bt ax,0 ; CR0.PE
jnc short @F
lea dx,vm86mode
mov ah,9 ; output to stdout
int 21h
mov ax,4C01h ; exit with failure
int 21h
@@: .486p ; OK, we can use privileged instructions!
; check if user set /s to sweep instead of using fixed wait count
mov di,81h
movzx cx,byte ptr ds:[80h]
@@: mov al,'/'
repne scasb
jne short @F
jcxz short @F
; force uppercase
and byte ptr [di],not 20h
mov al,'W'
scasb
loopne @B
; set the waitcount to zero, which will cause the loop to run up to
; MAX_SWP_WAITCNT
mov [waitcount],0
@@: mov ax,3510h ; get current VBIOS service vector
int 21h
assume es:nothing
mov word ptr int10_backup[0],bx
mov word ptr int10_backup[2],es
; clear the screen...
mov ax,3
int 10h
; and move the cursor down three rows so it doesn't get in the way
; of what we print
lea dx,skiplines
mov ah,9 ; output to stdout
int 21h
; turn off interrupts before replacing the VBIOS service handler, so
; that no ISRs come along and call it, accidentally short-circuiting
; our exception handler!
cli
lea dx,exc_handler
mov ax,2510h ; set int 10h vector
int 21h
; now our handler is installed, we can enable native mode exceptions!
mov eax,cr0
mov [cr0_backup],eax
bts eax,5 ; CR0.NE
mov cr0,eax
@@sweep_loop:
; re-clear the first three rows of the screen (since we're looping)
mov ax,0B800h
mov es,ax
mov ax,720h ; SPACE with grey on black
mov cx,SCREEN_WIDTH * ROWS_USED
xor di,di
cld
rep stosw
; setup the FPU and cause an exception to trap into our handler
fninit
fstcw [control_word]
; unmask exceptions
and [control_word],NOT 7Fh
fldcw [control_word]
; print wait count on first row
; use lower byte of waitcount as the attr, so we have nice changing
; colours to indicate we're doing something (bonus: when waitcount
; is zero, the text is invisible :3)
mov ah,byte ptr [waitcount]
lea si,sweeping_bfr
cmp [waitcount],MAX_SWP_WAITCNT
jna short @F
lea si,waitcount_bfr
@@: xor di,di
call display_message
sub di,BYTES_PER_CHAR ; point DI at the last '0'
; the actual exception:
fld [zero]
fdiv [zero] ; 0/0 error
; wait X amount of time...
mov cx,[waitcount]
jcxz short @@do_fwait
@@: ; use 61h input to kill time
in al,61h
call incr_digit
loop @B
@@do_fwait:
fwait ; exception should hit here
; Survived? Good! Increase the wait count and see if we can do it again!
inc [waitcount]
cmp [waitcount],MAX_SWP_WAITCNT
jbe short @@sweep_loop
; All done? Great! Undo the damage we did, and exit back to DOS!
; reset the FPU...
fninit
; disable native exceptions...
mov eax,[cr0_backup]
mov cr0,eax
; restore VBIOS service vector...
lds dx,[int10_backup]
assume ds:nothing
mov ax,2510h
int 21h
; re-enable interrupts
sti
; And we're done!
mov ax,4C00h ; exit with success
int 21h
exc_handler:
; We install this on int 10h. On entry interrupts are disabled, so
; only NMIs and SMM can intervene while we're doing stuff.
assume es:nothing,ds:nothing
pushad
push es
push ds
; display entry notice on second row
mov ah,4Eh ; bright yellow on red
lea si,entered
mov di,1*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
; print wait count on third row
; use lower byte of waitcount as the attr, so we have nice changing
; colours to indicate we're doing something (bonus: when waitcount
; is zero, the text is invisible :3)
mov ah,byte ptr [waitcount]
neg ah ; so colour is different from "before FWAIT"
lea si,sweeping_hdlr
cmp [waitcount],MAX_SWP_WAITCNT
jna short @F
lea si,waitcount_hdlr
@@: mov di,2*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
sub di,BYTES_PER_CHAR ; point DI at the last '0'
; wait X amount of time...
mov cx,[waitcount]
jcxz short @@clear_fault
@@: ; use 61h input to kill time
in al,61h
call incr_digit
loop @B
@@clear_fault:
fnclex
; print "fault cleared" message on fourth row
mov ah,1Eh ; bright yellow on blue
lea si,cleared
mov di,3*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
pop ds
pop es
popad
iret
display_message proc near
; ENTRY:
; AH = attr byte
; CS:SI --> dollar-terminated string to print
; B800:DI --> where to put it
; EXIT:
; AL = '$'
; CS:SI --> past end of string
; ES:DI --> next char cell after the printed string
push 0B800h
pop es
cld
@@: lodsb
cmp al,'$'
je short @F
stosw
jmp short @B
@@: ret
display_message endp
incr_digit proc near
; ENTRY:
; ES:DI --> digit char byte on screen to increment
; EXIT:
; DI preserved
; full decimal number has been incremented as needed
push di
inc byte ptr es:[di]
cmp byte ptr es:[di],'9' ; need to carry the one?
jna short @F
mov byte ptr es:[di],'0' ; reset current decimal place
sub di,BYTES_PER_CHAR ; increment char byte to the left
call incr_digit
@@: pop di
ret
incr_digit endp
.data
nopcibios db "No PCI BIOS present, assuming machine is too old for these checks",10,13,'$'
vm86mode db "Need to be in Real Mode, try rebooting without EMM386",10,13,'$'
skiplines db ROWS_USED dup (10),'$'; linefeeds to move cursor down
entered db "Entered exception handler...",'$'
sweeping_bfr db "[SWEEPING] "; no $ cause we want it to run on
waitcount_bfr db "Wait count (before FWAIT): 00000",'$'; 00000 will be incremented onscreen
sweeping_hdlr db "[SWEEPING] "; no $ cause we want it to run on
waitcount_hdlr db "Wait count (in handler): 00000",'$'; 00000 will be incremented onscreen
cleared db "Fault cleared!",'$'
; number of times to wait before clearing exception (in sweep mode, we start at
; zero and increase it until something breaks...)
waitcount dw SINGLE_WAITCNT
; dummy float we can use to cause divide errors
zero dd 0
.data?
cr0_backup dd ?
int10_backup dd ?
control_word dw ?
end start
;
; FPUCHK75.ASM
;
; A simple COM program to run in Real Mode and check for bad firmware behaviour
; or wiring for FPU IRQ handling. Completely self-contained, no debugger required.
;
; Assemble as per usual:
; jwasmr -bin fpuchk75.asm
; move fpuchk75.bin fpuchk75.com
;
; On VirtualBox, this runs to completion. On QEMU/KVM it crashes because IRQ13
; isn't emulated / virtualized properly. On MSI Z97 GAMING 3 (MS-7918), with a
; Core i7-4790K and USB legacy support disabled, it hangs once it reaches the
; FWAIT instruction, seemingly because IRQ13 isn't wired at all. However, this
; is not universal for that generation, because Michal Necasek was able to get
; IRQ13 from his Haswell machine...
;
.model tiny
.8086
SCREEN_WIDTH equ 80
BYTES_PER_CHAR equ 2 ; every char on screen has char and attr bytes
ROWS_USED equ 4 ; we use first four rows (pre-waitcount, enter, waitcount, cleared)
MAX_SWP_WAITCNT equ 2000; so a successful sweep doesn't take ages
SINGLE_WAITCNT equ 60000
.code
org 100h
start: ; Too lazy to code up a proper 486 check, so just use the PCI BIOS
; as a proxy for it...
mov ax,0B101h
xor dx,dx
int 1Ah
cmp dx,4350h ; lower half of ' ICP'
je @F
lea dx,nopcibios
mov ah,9 ; output to stdout
int 21h
mov ax,4C01h ; exit with failure
int 21h
@@: .486 ; we know we have a 486, but may be in VM86 mode
smsw ax
bt ax,0 ; CR0.PE
jnc short @F
lea dx,vm86mode
mov ah,9 ; output to stdout
int 21h
mov ax,4C01h ; exit with failure
int 21h
@@: .486p ; OK, we can use privileged instructions!
; fill out the emergency stop message
lea di,returns
cld
mov al,13 ; carriage return
mov cx,size returns
rep stosb
mov al,7 ; bell
stosb
; check if user set /s to sweep instead of using fixed wait count
mov di,81h
movzx cx,byte ptr ds:[80h]
@@: mov al,'/'
repne scasb
jne short @F
jcxz short @F
; force uppercase
and byte ptr [di],not 20h
mov al,'S'
scasb
loopne @B
; set the waitcount to zero, which will cause the loop to run up to
; MAX_SWP_WAITCNT
mov [waitcount],0
@@: mov ax,3575h ; get current IRQ13 vector
int 21h
assume es:nothing
mov word ptr int75_backup[0],bx
mov word ptr int75_backup[2],es
; clear the screen...
mov ax,3
int 10h
; and move the cursor down three rows so it doesn't get in the way
; of what we print
lea dx,skiplines
mov ah,9 ; output to stdout
int 21h
; turn off interrupts while messing with IVT and PIC
cli
lea dx,exc_handler
mov ax,2575h ; set int 75h (IRQ13) vector
int 21h
; now our handler is installed, we can disable native mode exceptions!
mov eax,cr0
mov [cr0_backup],eax
btr eax,5 ; CR0.NE
mov cr0,eax
; and ensure everything except IRQ13 is masked on the PICs
in al,21h
mov byte ptr imr_backup[0],al
mov al,NOT (1 SHL 2); IRQ2 = cascade
out 21h,al
in al,0A1h
mov byte ptr imr_backup[1],al
mov al,NOT (1 SHL (13 - 8))
out 0A1h,al
; now enable interrupts so IRQ13 can be delivered
sti
@@sweep_loop:
; re-clear the first three rows of the screen (since we're looping)
mov ax,0B800h
mov es,ax
mov ax,720h ; SPACE with grey on black
mov cx,SCREEN_WIDTH * ROWS_USED
xor di,di
cld
rep stosw
; setup the FPU and cause an exception to trap into our handler
fninit
fstcw [control_word]
; unmask exceptions
and [control_word],NOT 7Fh
fldcw [control_word]
; print wait count on first row
; use lower byte of waitcount as the attr, so we have nice changing
; colours to indicate we're doing something (bonus: when waitcount
; is zero, the text is invisible :3)
mov ah,byte ptr [waitcount]
lea si,sweeping_bfr
cmp [waitcount],MAX_SWP_WAITCNT
jna short @F
lea si,waitcount_bfr
@@: xor di,di
call display_message
sub di,BYTES_PER_CHAR ; point DI at the last '0'
; the actual exception:
fld [zero]
fdiv [zero] ; 0/0 error
; wait X amount of time...
mov cx,[waitcount]
jcxz short @@do_fwait
@@: ; use 61h input to kill time
in al,61h
call incr_digit
loop @B
@@do_fwait:
; load registers so if an int 10h is triggered it'll print a message
mov ax,1300h
mov bx,4Eh ; bright yellow on red
lea cx,bell[1]
mov dx,100h
push cs
pop es
lea bp,int10_trigged
sub cx,bp
fwait ; exception should hit here
; Survived? Good! Increase the wait count and see if we can do it again!
inc [waitcount]
cmp [waitcount],MAX_SWP_WAITCNT
jbe short @@sweep_loop
; All done? Great! Undo the damage we did, and exit back to DOS!
; reset the FPU...
fninit
; fix the PIC...
cli
mov al,byte ptr imr_backup[0]
out 21h,al
mov al,byte ptr imr_backup[1]
out 0A1h,al
; restore native exceptions...
mov eax,[cr0_backup]
mov cr0,eax
; restore IRQ13 service vector...
lds dx,[int75_backup]
assume ds:nothing
mov ax,2575h
int 21h
; re-enable interrupts
sti
; And we're done!
mov ax,4C00h ; exit with success
int 21h
exc_handler:
; We install this on IRQ13. On entry interrupts are disabled, so
; only NMIs and SMM can intervene while we're doing stuff.
assume es:nothing,ds:nothing
pushad
push es
push ds
; display entry notice on second row
mov ah,4Eh ; bright yellow on red
lea si,entered
mov di,1*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
; print wait count on third row
; use lower byte of waitcount as the attr, so we have nice changing
; colours to indicate we're doing something (bonus: when waitcount
; is zero, the text is invisible :3)
mov ah,byte ptr [waitcount]
neg ah ; so colour is different from "before FWAIT"
lea si,sweeping_hdlr
cmp [waitcount],MAX_SWP_WAITCNT
jna short @F
lea si,waitcount_hdlr
@@: mov di,2*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
sub di,BYTES_PER_CHAR ; point DI at the last '0'
; wait X amount of time...
mov cx,[waitcount]
jcxz short @@clear_fault
@@: ; use 61h input to kill time
in al,61h
call incr_digit
loop @B
@@clear_fault:
fnclex
; do the legacy nonsense...
xor al,al
out 0F0h,al
mov al,20h ; EOI
out 0A0h,al
out 20h,al
; print "fault cleared" message on fourth row
mov ah,1Eh ; bright yellow on blue
lea si,cleared
mov di,3*SCREEN_WIDTH*BYTES_PER_CHAR
call display_message
pop ds
pop es
popad
iret
display_message proc near
; ENTRY:
; AH = attr byte
; CS:SI --> dollar-terminated string to print
; B800:DI --> where to put it
; EXIT:
; AL = '$'
; CS:SI --> past end of string
; ES:DI --> next char cell after the printed string
push 0B800h
pop es
cld
@@: lodsb
cmp al,'$'
je short @F
stosw
jmp short @B
@@: ret
display_message endp
incr_digit proc near
; ENTRY:
; ES:DI --> digit char byte on screen to increment
; EXIT:
; DI preserved
; full decimal number has been incremented as needed
push di
inc byte ptr es:[di]
cmp byte ptr es:[di],'9' ; need to carry the one?
jna short @F
mov byte ptr es:[di],'0' ; reset current decimal place
sub di,BYTES_PER_CHAR ; increment char byte to the left
call incr_digit
@@: pop di
ret
incr_digit endp
.data
nopcibios db "No PCI BIOS present, assuming machine is too old for these checks",10,13,'$'
vm86mode db "Need to be in Real Mode, try rebooting without EMM386",10,13,'$'
skiplines db ROWS_USED dup (10),'$'; linefeeds to move cursor down
entered db "Entered exception handler...",'$'
sweeping_bfr db "[SWEEPING] "; no $ cause we want it to run on
waitcount_bfr db "Wait count (before FWAIT): 00000",'$'; 00000 will be incremented onscreen
sweeping_hdlr db "[SWEEPING] "; no $ cause we want it to run on
waitcount_hdlr db "Wait count (in handler): 00000",'$'; 00000 will be incremented onscreen
cleared db "Fault cleared!",'$'
; number of times to wait before clearing exception (in sweep mode, we start at
; zero and increase it until something breaks...)
waitcount dw SINGLE_WAITCNT
; dummy float we can use to cause divide errors
zero dd 0
cr0_backup dd ?
int75_backup dd ?
imr_backup dw ?
control_word dw ?
; Need to place this string here so it runs into the stream of CRs below
int10_trigged db "Your environment is not sane.",13,10
db "FPU exceptions are triggering int 10h despite CR0.NE being clear.",13,10
db "Please reset your (presumably virtual) machine before the beep,",13,10
db "so that things don't get any worse!"
.data?
; fill in with 25000 Carriage Returns to keep the VBIOS busy for as long as possible!
returns db 25000 dup (?)
; and finish with a beep so user knows we're screwed >:3
bell db ?
end start
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment