I used to use Milo Sedlacek's MULTIKEY assembly code to replace the keyboard interrupt service routine because up until recently, it never failed to deliver. I understood the basics of what the code was doing, but never looked into HOW it did it. To get it to work, you have to reserve an array in QuickBASIC, get its pointer (both segment and offset) and then use that to reprogram four bytes into the assembly code, so the code knows where it should store the status of each key (the rest is standard stuff like reading port 0x60, checking if the status is press or release, acknowledge interrupt, etc.)
I recently came across an annoying bug that rendered MULTIKEY unusable. I assumed it was due to the fact I was forcing QuickBASIC to consolidate the memory reserved for variables (and thus moving variables -including the array where key status would be stored- in memory) by reserving memory via a DOS interrupt (QuickBASIC always reserves all the available space for your code and variables; if you want a static memory address, you have to put it away from its control by first telling it to free some memory and then reserving it via DOS.) The obvious side-effect was that the array meant to contain the key status would move and MULTIKEY would write to the old address, where something else is stored. At that point, I decided to also reserve memory for the key status buffer via the DOS interrupt to make sure it wouldn't move. But that didn't exactly fix the issue as it seemed that my own PEEK and POKE calls would not land where I expected them to.
So I read Milo's code going instruction by instruction and realized he was modifying the code segment address to access the key status buffer. Since the code is designed to be an interrupt, it means that it can be executed at any time during the main program's execution - for instance, in the middle of a loop that reads/writes stuff to memory via PEEK and POKE - which means the interrupt could impact DEF SEG silently, and there would be no way of knowing. Here's Milo's interrupt code:
So, as I noob, I assume CS is not restored at the end of the execution of the interrupt and it somehow disrupts the flow of the main QuickBASIC program. I never noticed that bug before so I'm not sure what's up. I rewrote some code so that the buffer (starting at byte 0) and the interrupt code (starting at byte 129) would be stored in the same memory segment, would be out of QuickBASIC's reach, and it would be possible to easily access the key status buffer (129 bytes rather than 129 words) by simply PEEKING memory. It seems to work but I'd like a second opinion:
Code: Select all
; pushing registers to stack so they can be restored to their initial value 9C PUSHF 50 PUSH AX 53 PUSH BX 51 PUSH CX 52 PUSH DX 1E PUSH DS 56 PUSH SI 06 PUSH ES 57 PUSH DI ; port reading EA 60 IN AL, 60 ; Read port 0x60, store in AL B4 01 MOV AH, 01 ; Assume the key is pressed A8 80 TEST AL, 80 ; Test bit-7 in AL, modifies Sign, Zero and Parity flag register. 74 04 JZ 4 ; If Zero flag is SET, skip 4 bytes (2 instructions) B4 00 MOV AH, 00 ; The key was in fact released 24 7F AND AL, 7F ; Strip bit-7 from AL (only keep scancode) ; getting offset from begining of the array D0 E0 SHL AL, 1 ; Multiply AL by 2 (target is an INTEGER array of 129 elements) 88 C3 MOV BL, AL ; Set BX (lower byte) to AL B7 00 MOV BH, 00 ; Set BX (high byte) to 0 B0 00 MOV AL, 00 ; Set AL to 0 ; going to the array memory address and write key status 2E CS: ; Change code segment, set BX (offset) 03 1E 12 00 ADD BX,  ; Add BX to the value stored at 0x12 (array memory offset) 2E CS: ; Change code segment, set DS (segment) 8E 1E 10 00 MOV DS,  ; Set DS to the value stored at 0x10 (array memory segment) 86 E0 XCHG AH, AL ; Swap AH and AL (AH contains the key status) 89 07 MOV [BX], AX ; Write AX (2 bytes) to [BX] (array memory offset) ; the rest is the standard: ; acknowledge interrupt ; restore registers with POP and POPF ; terminate interrupt execution with IRET
And here's the full code in QuickBASIC (it will exit on its own after 5 seconds, so all keys can be tested:)
Code: Select all
; pushing registers like above EA 60 IN AL, 60 ; Read port 0x60, store in AL B4 01 MOV AH, 01 ; Assume the key is pressed A8 80 TEST AL, 80 ; Test bit-7 in AL, modifies Sign, Zero and Parity flag register. 74 04 JZ 4 ; If Zero flag is SET, skip 4 bytes (2 instructions) B4 00 MOV AH, 00 ; Our bad, key is actually released. 24 7F AND AL, 7F ; Only preserve bits 6-0 in AL, discard bit 7. 88 C3 MOV BL, AL ; Set BX to scancode: BL = AL B7 00 MOV BH, 0 ; Set BX to scancode: BH = 0 2E 88 27 MOV CS:[BX], AH ; Copy key status to specified address ; the rest is the standard: ; acknowledge interrupt ; restore registers with POP and POPF ; terminate interrupt execution with IRET
Does it work as expected? Is it safe (or at the very least safer?) Milo also took the safe path by preserving every register, but I think only FLAG, AX and BX need to be preserved in my code (I don't know what instruction could mess with the other registers.) Is that right? I'm probably going to stick to my own version from now on unless someone notices something really terrible going on.
Code: Select all
'$INCLUDE: 'QB.BI' DECLARE SUB memFree (segAdr AS INTEGER) DECLARE FUNCTION memAlloc% (numBytes AS LONG) DECLARE FUNCTION keyInit% () DIM keySegm AS INTEGER, tmr AS DOUBLE CLS tmr = TIMER + 5 keySegm = keyInit% DO DEF SEG = keySegm LOCATE 1, 1 FOR i% = 0 TO 128 PRINT PEEK(i%); NEXT i% LOOP UNTIL (tmr < TIMER) keySegm = keyInit% FUNCTION keyInit% STATIC oldISRSeg AS INTEGER, oldISROfs AS INTEGER, newISRSeg AS INTEGER DIM regs AS RegTypeX IF (newISRSeg = 0) THEN ' Reserve memory for buffer & code newISRSeg = memAlloc%(182) ' key status buffer (129) + code (53) ' Clear key strokes (starting at offset 0 of segment [newISRSeg]) DEF SEG = newISRSeg FOR i% = 0 TO 128 POKE i%, 0 NEXT i% ' Write code (starting at offset 129 of segment [newISRSeg]) FOR i% = 0 TO 52 POKE i% + 129, VAL("&H" + MID$("FB9C505351521E560657E460B401A8807404B400247F88C3B7002E8827E4610C80E661247FE661B020E6205F075E1F5A595B589DCF", 1 + i% * 2, 2)) NEXT i% ' Preserve vector interrupt 9 (BIOS keyboard ISR) regs.ax = &H3509 CALL INTERRUPTX(&H21, regs, regs) oldISRSeg = regs.es oldISROfs = regs.bx ' Clear keyboard buffer DEF SEG = 0 POKE (&H41A), PEEK(&H41C) DEF SEG ' Hook custom keyboard handler regs.ax = &H2509 regs.ds = newISRSeg ' interrupt code (and buffer) memory segment regs.dx = 129 ' interrupt code offset CALL INTERRUPTX(&H21, regs, regs) ELSE ' Restore BIOS keyboard ISR regs.ax = &H2509 regs.ds = oldISRSeg regs.dx = oldISROfs CALL INTERRUPTX(&H21, regs, regs) ' Deallocate memory reserved for buffer & code memFree newISRSeg END IF keyInit% = newISRSeg ' offset to key status buffer END FUNCTION '' '' QuickBASIC always reserves the largest block of memory available for '' the far heap. If we need to allocate memory for our purpose, we must '' first tell QuickBASIC to free part of that memory. '' FUNCTION memAlloc% (numBytes AS LONG) DIM memReq AS INTEGER, junk AS LONG, regs AS RegTypeX ' Paragraphs are groups of 16 bytes memReq = (numBytes \ 16) - ((numBytes AND 15) > 0) ' Tell QuickBASIC to free some memory (not sure why a margin is needed) junk = SETMEM(-CLNG(memReq + 1) * 16) ' Use DOS Interrupt 0x48 to request <memReq> paragraphs of memory regs.ax = &H4800 regs.bx = memReq CALL INTERRUPTX(&H21, regs, regs) ' If CF is not clear, something went wrong IF (regs.flags AND &H1) THEN junk = SETMEM(650000) ELSE memAlloc% = regs.ax END IF END FUNCTION '' '' Free memory reserved via DOS Interrupt 0x21, function 0x48 '' SUB memFree (segAdr AS INTEGER) DIM junk AS LONG, regs AS RegTypeX ' No segment specified, abort IF (segAdr = 0) THEN EXIT SUB ' Free allocated memory regs.ax = &HA900 regs.es = segAdr CALL INTERRUPTX(&H21, regs, regs) ' Clear segment and offset segAdr = 0 ' Give back memory to QuickBASIC junk = SETMEM(650000) END SUB