Last active
October 19, 2025 00:14
-
-
Save PluMGMK/2546e5fdbbb3953871fdef9a03e8dc2c to your computer and use it in GitHub Desktop.
FPU exception hang checker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ; | |
| ; 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ; | |
| ; 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