[phpBB Debug] PHP Warning: in file [ROOT]/phpbb/db/driver/mysqli.php on line 264: mysqli_fetch_assoc(): Couldn't fetch mysqli_result
[phpBB Debug] PHP Warning: in file [ROOT]/phpbb/db/driver/mysqli.php on line 326: mysqli_free_result(): Couldn't fetch mysqli_result
Pete's QBASIC Site Discuss QBasic, Freebasic, QB64 and more 2021-09-01T14:44:22-05:00 http://petesqbsite.com/phpBB3/app.php/feed/topic/14877 2021-09-01T14:44:22-05:00 2021-09-01T14:44:22-05:00 http://petesqbsite.com/phpBB3/viewtopic.php?p=39177#p39177 <![CDATA[Need second opinion on ASM snippet (MULTIKEY issues)]]>
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:

Code:

; pushing registers to stack so they can be restored to their initial value9C          PUSHF50          PUSH AX53          PUSH BX51          PUSH CX52          PUSH DX1E          PUSH DS56          PUSH SI06          PUSH ES57          PUSH DI; port readingEA 60       IN   AL, 60       ; Read port 0x60, store in ALB4 01       MOV  AH, 01       ; Assume the key is pressedA8 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 released24 7F       AND  AL, 7F       ; Strip bit-7 from AL (only keep scancode); getting offset from begining of the arrayD0 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 ALB7 00       MOV  BH, 00       ; Set BX (high byte) to 0B0 00       MOV  AL, 00       ; Set AL to 0; going to the array memory address and write key status2E          CS:               ; Change code segment, set BX (offset)03 1E 12 00 ADD  BX, [0012]   ; 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, [0010]   ; 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
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:

; pushing registers like aboveEA 60       IN   AL, 60       ; Read port 0x60, store in ALB4 01       MOV  AH, 01       ; Assume the key is pressedA8 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 = ALB7 00       MOV  BH, 0        ; Set BX to scancode: BH = 02E 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
And here's the full code in QuickBASIC (it will exit on its own after 5 seconds, so all keys can be tested:)

Code:

'$INCLUDE: 'QB.BI'DECLARE SUB memFree (segAdr AS INTEGER)DECLARE FUNCTION memAlloc% (numBytes AS LONG)DECLARE FUNCTION keyInit% ()DIM keySegm AS INTEGER, tmr AS DOUBLECLStmr = TIMER + 5keySegm = keyInit%DODEF SEG = keySegmLOCATE 1, 1FOR i% = 0 TO 128PRINT PEEK(i%);NEXT i%LOOP UNTIL (tmr < TIMER)keySegm = keyInit%FUNCTION keyInit%STATIC oldISRSeg AS INTEGER, oldISROfs AS INTEGER, newISRSeg AS INTEGERDIM regs AS RegTypeXIF (newISRSeg = 0) THEN' Reserve memory for buffer & codenewISRSeg = memAlloc%(182) ' key status buffer (129) + code (53)' Clear key strokes (starting at offset 0 of segment [newISRSeg])DEF SEG = newISRSegFOR i% = 0 TO 128POKE i%, 0NEXT i%' Write code (starting at offset 129 of segment [newISRSeg])FOR i% = 0 TO 52POKE i% + 129, VAL("&H" + MID$("FB9C505351521E560657E460B401A8807404B400247F88C3B7002E8827E4610C80E661247FE661B020E6205F075E1F5A595B589DCF", 1 + i% * 2, 2))NEXT i%' Preserve vector interrupt 9 (BIOS keyboard ISR)regs.ax = &H3509CALL INTERRUPTX(&H21, regs, regs)oldISRSeg = regs.esoldISROfs = regs.bx' Clear keyboard bufferDEF SEG = 0POKE (&H41A), PEEK(&H41C)DEF SEG' Hook custom keyboard handlerregs.ax = &H2509regs.ds = newISRSeg ' interrupt code (and buffer) memory segmentregs.dx = 129       ' interrupt code offsetCALL INTERRUPTX(&H21, regs, regs)ELSE' Restore BIOS keyboard ISRregs.ax = &H2509regs.ds = oldISRSegregs.dx = oldISROfsCALL INTERRUPTX(&H21, regs, regs)' Deallocate memory reserved for buffer & codememFree newISRSegEND IFkeyInit% = newISRSeg ' offset to key status bufferEND 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 bytesmemReq = (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 memoryregs.ax = &H4800regs.bx = memReqCALL INTERRUPTX(&H21, regs, regs)' If CF is not clear, something went wrongIF (regs.flags AND &H1) THENjunk = SETMEM(650000)ELSEmemAlloc% = regs.axEND IFEND 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, abortIF (segAdr = 0) THEN EXIT SUB' Free allocated memoryregs.ax = &HA900regs.es = segAdrCALL INTERRUPTX(&H21, regs, regs)' Clear segment and offsetsegAdr = 0' Give back memory to QuickBASICjunk = SETMEM(650000)END SUB
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.

Statistics: Posted by MikeHawk — Wed Sep 01, 2021 2:44 pm


]]>