; as86 irq2fix.asm -0 -o irq2fix.obj && ld86 -M -d -T0x100 -o irq2fix.com irq2fix.obj ; ; IRQ2Fix: A loader to fix IRQ2 redirection on PC/AT compatibles. ; This problem usually occurs with programs written for an original Roland MPU-401. ; ; Tested with the following problematic programs: ; - Cakewalk 3.0 DOS ; - Gateway (Legend Entertainment) MAX_CMDLINE_LENGTH equ 127 entry start start: ; Save PSP seg passed to program mov psp_segment, ds ; Relocate stack cli mov bx, program_end add bx, #0x100 sub bx, #2 ; Make room for return address mov bp, sp mov ax, word ptr [bp] mov word ptr [bx], ax mov sp, bx sti ; Resize loader to minimum size lea ax, program_end ; program length including 0x100 bytes PSP add ax, #0x100 ; include relocated stack cwd mov bx, #16 ; in 16 byte paragraphs div bx cmp dx, #0 je noremainder add ax, #1 ; round up noremainder: mov bx, ax mov es, psp_segment mov ah, #0x4a int #0x21 jnc continue ; Error message, exit push #0 push #crlf push #couldntresize call printstrings mov ax, #0x4c01 int #0x21 continue: ; Print banner push #0 push #crlf push #banner call printstrings ; Detect 286+; only Tandy 1000TL/TX have 80286 without PIC2 pushf pop ax not ax and ax, #0x8000 jz errorbail ; Detect AT-class by trying to use a XT-only keyboard controller port ; XXX: This could be flakey. ; pushf ; mov cx, #6 ; Try six times ; cli ; in al, #0x61 ; PPI Port B on XT ; call iodelay ; mov bl, al ; save value ;retryloop: ; mov al, bl ; xor al, #0x80 ; Flip top bit; KBC acknowledge (RW) on XT, RAM parity error (RO) on AT ; out #0x61, al ; call iodelay ; in al, #0x61 ; cmp al, bl ; Did the flipped bit stick? ; jne XTend ; If so, it's a XT ; cmp cx, #0 ; Did it fail to stick six times? ; jmp ATend ; If so, it's an AT ; dec cx ; jmp retryloop ; ;XTend: ; popf ; jmp errorbail ; ;ATend: ; popf ; Test manipulation of PIC2 cli mov dx, #0xA1 ; PIC2 mask xor ah, ah in al, dx mov oldpic2mask, al ; Save for later restoration xor al, #2 ; Toggle IRQ9 mask bit mov bl, al ; Save toggled mask out dx, al call iodelay in al, dx mov cl, al mov al, bl ; Restore toggled mask xor al, #2 ; Toggle bit back out dx, al call iodelay sti xor bl, cl ; Bit toggle remained, and no random bitflips? jz continue2 ; If so, assume working PIC2. errorbail: ; Error message, exit push #0 push #crlf push #notpcat call printstrings mov ax, #0x4c01 int #0x21 continue2: ; Save old IRQ2 handler and install test handler. cli mov ah, #0x35 mov al, #0x0A ; IRQ2 int #0x21 mov [oldirq2+2], es ;segment mov [oldirq2], bx ;offset lea dx, testirq2 push ds push cs pop ds ; New handler is in our code segment mov ah, #0x25 mov al, #0x0A ; IRQ2 int #0x21 pop ds sti ; Test IRQ9 invokes IRQ2 call testirq9 jnc pass ; Test failed, display notice. push #0 push #crlf push #irq9bad call printstrings ; Save old IRQ9 and install a IRQ9 handler that sends EOI to PIC at 0xA0 before then calling IRQ2. cli mov ah, #0x35 mov al, #0x71 ; IRQ9 int #0x21 mov [oldirq9+2], es ;segment mov [oldirq9], bx ;offset lea dx, newirq9 push ds push cs pop ds ; New handler is in our code segment mov ah, #0x25 mov al, #0x71 ; IRQ9 int #0x21 pop ds sti ; Display notice of new handler. push #0 push #crlf push #irq9installed call printstrings ; Re-test that IRQ9 invokes IRQ2. call testirq9 jnc pass ; Test failed, display notice. push #0 push #crlf push #irq9stillbad call printstrings jmp afterfix pass: push #0 push #crlf push #irq9ok call printstrings afterfix: ; Restore old IRQ2 handler cli mov dx, [oldirq2] mov ax, [oldirq2+2] push ds mov ds, ax mov ah, #0x25 mov al, #0x0A ; IRQ2 int #0x21 pop ds sti ; Note to user whether IRQ9 is masked. push #0 push #crlf mov dx, #0xA1 in al, dx test al, #0x02 jz unmasked push #ismasked jmp endtest unmasked: push #isunmasked endtest: push #irq9atslave call printstrings foobar: ; Save old int21 handler cli mov ah, #0x35 mov al, #0x21 int #0x21 mov [oldint21+2], es ;segment mov [oldint21], bx ;offset ; Install our int21 handler lea dx, newint21 push ds push cs pop ds ; New handler is in our code segment mov ah, #0x25 mov al, #0x21 int #0x21 pop ds sti ; Create child process with remaining command line ; Copy program name and command tail from PSP xor ch, ch mov cl, byte ptr [0x80] ; Command tail length in PSP cmp cl, #MAX_CMDLINE_LENGTH ; Don't overrun childname buffer jle bigenough mov cl, #MAX_CMDLINE_LENGTH bigenough: cmp cl, #0 je iszero dec cl ; We will skip the first space iszero: cmp cx, #0 jne initcopy cmp byte ptr [0x81], #0x0D ; Nothing but CR in command tail? jne initcopy ; Error message if program argument missing push #0 push #crlf push #programmissing2 push #crlf push #programmissing1 call printstrings lea ax, bailout push ax ret ; long jump initcopy: mov dx, #childname ; Copy to childname first mov si, #1 ; Skip first space between loader and program argument xor di, di loopcopy: cmp cx, #0 je nextstate mov bx, #0x81 ; Command tail in PSP mov al, byte ptr [bx+si] cmp dx, #cmdline je spacesallowed cmp al, #0x20 ; Tokenize on spaces je nextstate spacesallowed: cmp al, #0x0D ; And on CR je nextstate cmp al, #0 ; Just in case we ran off the end je nextstate mov bx, dx ; Destination for this state mov byte ptr [bx+di], al inc si inc di dec cx cmp dx, #cmdline jne notcmdline inc byte ptr [cmdline_length] notcmdline: jmp loopcopy nextstate: cmp dx, #childname jne endcopy mov bx, #childname mov byte ptr [bx+di], #0 ; ASCIIZ: null terminate mov dx, #cmdline xor di, di jmp loopcopy endcopy: mov bx, #0x81 mov al, byte ptr [bx+si] ; Should be trailing CR on cmdline mov bx, #cmdline mov byte ptr [bx+di], al inc di mov byte ptr [bx+di], #0 ; Null terminator needed? ; Load our copy of rest-of-command-line into exec param block, starting with length lea ax, cmdline_length mov word ptr [cmdline_ptr], ax ; ax=dseg from here mov ax, ds mov word ptr [cmdline_ptr+2], ax ; FCB's point to our PSP mov word ptr [fcb1], #0x5c ; FCB1 mov word ptr [fcb1+2], ax mov word ptr [fcb2], #0x6c ; FCB2 mov word ptr [fcb2+2], ax mov es, ax ; end ax=dseg push #0 push #crlf cmp byte ptr [cmdline_length], #0 je nocmdline push #endbracket push #cmdline push #withargs nocmdline: push #endbracket push #childname push #starting call printstrings ; To debug our ISRs if necessary ;mov trace_ints, #1 ; Attempt to exec lea dx, childname lea bx, param_block mov ax, #0x4b00 int #0x21 jnc execsuccess ; Failure... ; TODO: display DOS error code push #0 push #crlf push #failstart call printstrings execsuccess: ; Save child's exit code mov ah, #0x4d int #0x21 mov child_exitcode, al bailout: ; TODO: Cleanup from INT23 (Break) and INT24 (Critical Error) ; https://courses.engr.illinois.edu/ece390/books/artofasm/CH19/CH19-2.html ; http://webpages.charter.net/danrollins/techhelp/0253.HTM ; http://stanislavs.org/helppc/int_23.html ; Restore old INT21 handler cli mov dx, [oldint21] mov ax, [oldint21+2] push ds mov ds, ax mov ah, #0x25 mov al, #0x21 int #0x21 pop ds ; Restore old IRQ9 handler; do this after restoring old int21 handler! cmp [oldirq9], #0 je skipirq9restore mov dx, [oldirq9] mov ax, [oldirq9+2] push ds mov ds, ax mov ah, #0x25 mov al, #0x71 ; IRQ9 int #0x21 pop ds skipirq9restore: ; Always enable IRQ2 when done, since it should never be masked ; but the program may have masked it. in al, #0x20 ; PIC1 call iodelay not al or al, #4 ; IRQ2 not al out #0x20, al call iodelay sti push #0 push #crlf push #endbanner push #crlf call printstrings ; Propagate child's exit code mov ah, #0x4c mov al, child_exitcode int #0x21 ; The end. iodelay: out #0x80, al ret testirq9: ; Note: assumes our IRQ2 handler is installed mov testirq2_flag, #0x0 ; Mask PIC2 completely to avoid handling someone else's IRQ during test. push bx push ax in al, #0xA1 mov ah, al ; save mask or al, #0xFF out #0xA1, al call iodelay ; Test that IRQ9 handler invokes IRQ2 handler. int #0x71 mov bx, testirq2_flag ; Restore PIC2 mask when done mov al, ah out #0xA1, al call iodelay ; Fail = set carry flag test bx, bx jz failtest clc jmp passtest failtest: stc passtest: pop ax pop bx ret printstrings: push bp push si mov bp, sp mov si, #6 ; stack: SI, BP, RA, string2, string1, 0 printstringsloop: cmp word ptr [bp+si], #0 ; null list terminator je printstringsend push bx mov bx, word ptr [bp+si] call outputstring pop bx add si, #2 ; next string jmp printstringsloop printstringsend: ; Fix up stack, consuming inputs cli add sp, si add sp, #2 push word ptr [bp+4] mov si, word ptr [bp] mov bp, word ptr [bp+2] sti ret outputstring: ; string ptr in BX push si xor si, si nextchar: mov dl, byte ptr [bx+si] cmp dl, #0 ; null terminator je endstring cmp dl, #0x24 ; $ terminator je endstring cmp bx, #crlf ; Only print CR or LF if we requested a CRLF je iscrlf cmp dl, #0x0D je skipchar cmp dl, #0x0A je skipchar iscrlf: mov ah, #0x06 int #0x21 skipchar: inc si jmp nextchar endstring: pop si ret INT_TF_restore: ; Restore TF if the INT caller had it, so we can single-step push bp push ds push cs pop ds pushf mov bp, sp test word ptr[bp+10], #0x100 jz no_INT_TF or word ptr[bp+0], #0x100 no_INT_TF: popf pop ds pop bp ret testirq2: cseg mov testirq2_flag, #1 iret newirq9: pushf cseg test trace_ints, #1 jz notrace_irq9 cseg call INT_TF_restore notrace_irq9: popf ; Send EOI to PIC2 push ax mov al, #0x20 out #0xA0, al cseg call iodelay pop ax ; Chain IRQ2 int #0x0A iret newint21: pushf cseg test trace_ints, #1 jz notrace_int21 cseg call INT_TF_restore notrace_int21: popf pushf ; Save caller's flags to chain INT21 correctly ; Is program exiting? cmp ah, #0x4c jne not_exiting ; Exiting, so restore old PIC2 mask push ax push dx cseg mov al, oldpic2mask mov dx, #0xA1 out dx, al cseg call iodelay pop dx pop ax ; Then call INT21 jmp chain not_exiting: ; Is program installing IRQ2 handler? cmp ax, #0x250A jne not_installing_irq2 ; Call INT21 to install program's handler cseg call far [oldint21] pushf ; Save INT21 flags push ax push bx in al, #0xA1 ; Unmask IRQ9, we will restore the old state on exit mov bl, #0x02 not bl and al, bl out #0xA1, al cseg call iodelay pop bx pop ax popf retf #2 not_installing_irq2: chain: cseg ; Access saved vector via our CS, otherwise leave caller segments intact. call far [oldint21] ; Chain old INT21 handler done_chain: pushf ; Save INT21 returned flags ; Note: Any other post-INT21 processing can go here if needed ; Finally, restore caller's interrupt flag push ax push bp mov bp, sp mov ax, #0x0200 ; IF and ax, word ptr [bp+10] ; Get old IF or ax, word ptr [bp+4] ; Combine old IF with newly returned INT21 flags mov word ptr [bp+10], ax ; Replace flags on caller's stack with ours pop bp pop ax popf ; Now we should have a stack frame safe to IRET iret childname: .space MAX_CMDLINE_LENGTH .byte 0 cmdline_length: .byte 0 cmdline: .space MAX_CMDLINE_LENGTH-1 .byte 0 ; Unknown if null terminator needed? ; Parameter block for child process creation. param_block: .word 0 cmdline_ptr: .long 0 fcb1: .long 0 fcb2: .long 0 trace_ints: .byte 0 child_exitcode: .byte 0 testirq2_flag: .byte 0 oldint21: .byte 0,0,0,0 oldirq2: .byte 0,0,0,0 oldirq9: .byte 0,0,0,0 oldpic1mask: .byte 0 oldpic2mask: .byte 0 psp_segment: .byte 0,0 crlf: db 0x0d, 0x0a .ascii "$" couldntresize: .ascii "Couldn't free unused memory.$" banner: .ascii "IRQ2Fix, by Ryan C. Underwood $" irq9ok: .ascii "IRQ9->IRQ2 revectoring is working correctly.$" irq9bad: .ascii "IRQ9->IRQ2 BIOS revectoring is not working.$" irq9stillbad: .ascii "IRQ9->IRQ2 still not working after fixup.$" irq9installed: .ascii "IRQ9 fixup installed.$" irq9atslave: .ascii "IRQ9 at slave PIC $" ismasked: .ascii "is masked, will be unmasked as necessary.$" isunmasked: .ascii "is already unmasked by BIOS.$" programmissing1: .ascii "Program arguments required following IRQ2FIX.COM.$" programmissing2: .ascii "Example: IRQ2FIX GATE.EXE MT32.$" starting: .ascii "Starting program [$" withargs: .ascii " with arguments: [$" endbracket: .ascii "]$" failstart: .ascii "Failed to start program! error#$" notpcat: .ascii "Error: IRQ2Fix requires a PC/AT or compatible.$" endbanner: .ascii "IRQ2Fix unloaded.$" program_end: