Chapter Twenty (Part 5)
|Table of Content||
Chapter Twenty (Part 7)
THE PC KEYBOARD (Part 6)
20.7.1 - Stuffing Characters in the Type Ahead Buffer
20.7.2 - Using the 80x86 Trace Flag to Simulate IN AL 60H Instructions
|20.7 Simulating Keystrokes|
At one point or another you may want to write a program
that passes keystrokes on to another application. For example
you might want to write a
keyboard macro TSR that lets you capture certain keys on the keyboard and send a sequence
of keys through to some underlying application. Perhaps you'll want to program an entire
string of characters on a normally unused keyboard sequence (e.g.
ctrl-up or ctrl-down).
In any case
your program will use some technique to pass characters to a foreground
application. There are three well-known techniques for doing this: store the scan/ASCII
code directly in the keyboard buffer
use the 80x86 trace flag
or program the on-board 8042
microcontroller to transmit the scan code for you. The next three sections describe these
techniques in detail.
20.7.1 Stuffing Characters in the Type Ahead Buffer
Perhaps the easiest way to insert keystrokes into an application is to insert them directly into the system's type ahead buffer. Most modern BIOSes provide an int 16h function to do this (see "The Keyboard BIOS Interface"). Even if your system does not provide this function it is easy to write your own code to insert data in the system type ahead buffer; or you can copy the code from the int 16h handler provided earlier in this chapter.
The nice thing about this approach is that you can deal directly with ASCII characters (at least for those key sequences that are ASCII). You do not have to worry about sending shift up and down codes around the scan code for tn "A" so you can get an upper case "A" you need only insert 1E41h into the buffer. In fact most programs ignore the scan code so you can simply insert 0041h into the buffer and almost any application will accept the funny scan code of zero.
The major drawback to the buffer insertion technique is that many (popular) applications bypass DOS and BIOS when reading the keyboard. Such programs go directly to the keyboard's port (60h) to read their data. As such shoving scan/ASCII codes into the type ahead buffer will have no effect. Ideally you would like to stuff a scan code directly into the keyboard controller chip and have it return that scan code as though someone actually pressed that key. Unfortunately there is no universally compatible way to do this. However there are some close approximations keep reading...
20.7.2 Using the 80x86 Trace Flag to Simulate IN AL 60H Instructions
One way to deal with applications that access the keyboard
hardware directly is to simulate the 80x86 instruction set. For example
suppose we were
able to take control of the int 9 interrupt service routine and execute each instruction
under our control. We could choose to let all instructions except the
instruction execute normally. Upon encountering an
in instruction (that the
keyboard ISR uses to read the keyboard data)
we check to see if it is accessing port 60h.
we simply load the
al register with the desired scan code rather than
actually execute the
in instruction. It is also important to check for the
since the keyboard ISR will want to send and EOI signal to the 8259A PIC
after reading the keyboard data
we can simply ignore
out instructions that
write to port 20h.
The only difficult part is telling the 80x86 to pass
control to our routine when encountering certain instructions (like
and to execute other instructions normally. While this is not directly possible in real
there is a close approximation we can make. The 80x86 CPUs provide a trace flag that
generates an exception after the execution of each instruction. Normally
the trace flag to single step through a program. However
by writing our own exception
handler for the trace exception
we can gain control of the machine between the execution
of every instruction. Then
we can look at the opcode of the next instruction to execute.
If it is not an
we can simply return and
execute the instruction normally. If it is an
we can determine the I/O address and decide whether to simulate or execute
In addition to the
we will need to simulate any
int instructions we find as well.
The reason is because the
int instruction pushes the flags on the stack and
then clears the trace bit in the flags register. This means that the interrupt service
routine associated with that
int instruction would execute normally and we
would miss any
out instructions appearing therein.
it is easy to simulate the
leaving the trace flag
so we will add
int to our list of instructions to interpret.
The only problem with this approach is that it is slow. Although the trace trap routine will only execute a few instructions on each call it does so for every instruction in the int 9 interrupt service routine. As a result during simulation the interrupt service routine will run 10 to 20 times slower than the real code would. This generally isn't a problem because most keyboard interrupt service routines are very short. However you might encounter an application that has a large internal int 9 ISR and this method would noticeably slow the program. However for most applications this technique works just fine and no one will notice any performance loss while they are typing away (slowly) at the keyboard.
The following assembly code provides a short example of a trace exception handler that simulates keystrokes in this fashion:
.xlist include stdlib.a includelib stdlib.lib .list cseg segment para public 'code' assume ds:nothing byp textequ <byte ptr> ; ScanCode must be in the Code segment. ScanCode byte 0 ;**************************************************************************** ; ; KbdSim- Passes the scan code in AL through the keyboard controller ; using the trace flag. The way this works is to turn on the ; trace bit in the flags register. Each instruction then causes a trace ; trap. The (installed) trace handler then looks at each instruction to ; handle IN OUT INT and other special instructions. Upon encountering ; an IN AL 60 (or equivalent) this code simulates the instruction and ; returns the specified scan code rather than actually executing the IN ; instruction. Other instructions need special treatment as well. See ; the code for details. This code is pretty good at simulating the hardware ; but it runs fairly slow and has a few compatibility problems. KbdSim proc near pushf push es push ax push bx xor bx bx ;Point es at int vector tbl mov es bx ; (to simulate INT 9). cli ;No interrupts for now. mov cs:ScanCode al ;Save output scan code. push es:[1*4] ;Save current INT 1 vector push es:2[1*4] ; so we can restore it later. ; Point the INT 1 vector at our INT 1 handler: mov word ptr es:[1*4] offset MyInt1 mov word ptr es:[1*4 + 2] cs ; Turn on the trace trap (bit 8 of flags register): pushf pop ax or ah 1 push ax popf ; Simulate an INT 9 instruction. Note: cannot actually execute INT 9 here ; since INT instructions turn off the trace operation. pushf call dword ptr es:[9*4] ; Turn off the trace operation: pushf pop ax and ah 0feh ;Clear trace bit. push ax popf ; Disable trace operation. pop es:[1*4 + 2] ;Restore previous INT 1 pop es:[1*4] ; handler. ; Okay we're done. Restore registers and return. VMDone: pop bx pop ax pop es popf ret KbdSim endp ;---------------------------------------------------------------------------- ; ; MyInt1- Handles the trace trap (INT 1). This code looks at the next ; opcode to determine if it is one of the special opcodes we have to ; handle ourselves. MyInt1 proc far push bp mov bp sp ;Gain access to return adrs via BP. push bx push ds ; If we get down here it's because this trace trap is directly due to ; our having punched the trace bit. Let's process the trace trap to ; simulate the 80x86 instruction set. ; ; Get the return address into DS:BX NextInstr: lds bx 2[bp] ; The following is a special case to quickly eliminate most opcodes and ; speed up this code by a tiny amount. cmp byp [bx] 0cdh ;Most opcodes are less than jnb NotSimple ; 0cdh hence we quickly pop ds ; return back to the real pop bx ; program. pop bp iret NotSimple: je IsIntInstr ;If it's an INT instruction. mov bx [bx] ;Get current instruction's opcode. cmp bl 0e8h ;CALL opcode je ExecInstr jb TryInOut0 cmp bl 0ech ;IN al dx instr. je MayBeIn60 cmp bl 0eeh ;OUT dx al instr. je MayBeOut20 pop ds ;A normal instruction if we get pop bx ; down here. pop bp iret TryInOut0: cmp bx 60e4h ;IN al 60h instr. je IsINAL60 cmp bx 20e6h ;out 20 al instr. je IsOut20 ; If it wasn't one of our magic instructions execute it and continue. ExecInstr: pop ds pop bx pop bp iret ; If this instruction is IN AL DX we have to look at the value in DX to ; determine if it's really an IN AL 60h instruction. MayBeIn60: cmp dx 60h jne ExecInstr inc word ptr 2[bp] ;Skip over this 1 byte instr. mov al cs:ScanCode jmp NextInstr ; If this is an IN AL 60h instruction simulate it by loading the current ; scan code into AL. IsInAL60: mov al cs:ScanCode add word ptr 2[bp] 2 ;Skip over this 2-byte instr. jmp NextInstr ; If this instruction is OUT DX AL we have to look at DX to see if we're ; outputting to location 20h (8259). MayBeOut20: cmp dx 20h jne ExecInstr inc word ptr 2[bp] ;Skip this 1 byte instruction. jmp NextInstr ; If this is an OUT 20h al instruction simply skip over it. IsOut20: add word ptr 2[bp] 2 ;Skip instruction. jmp NextInstr ; IsIntInstr- Execute this code if it's an INT instruction. ; ; The problem with the INT instructions is that they reset the trace bit ; upon execution. For certain guys (see above) we can't have that. ; ; Note: at this point the stack looks like the following: ; ; flags ; ; rtn cs -+ ; | ; rtn ip +-- Points at next instr the CPU will execute. ; bp ; bx ; ds ; ; We need to simulate the appropriate INT instruction by: ; ; (1) adding two to the return address on the stack (so it returns ; beyond the INT instruction. ; (2) pushing the flags onto the stack. ; (3) pushing a phony return address onto the stack which simulates ; the INT 1 interrupt return address but which "returns" us to ; the specified interrupt vector handler. ; ; All this results in a stack which looks like the following: ; ; flags ; ; rtn cs -+ ; | ; rtn ip +-- Points at next instr beyond the INT instruction. ; ; flags --- Bogus flags to simulate those pushed by INT instr. ; ; rtn cs -+ ; | ; rtn ip +-- "Return address" which points at the ISR for this INT. ; bp ; bx ; ds IsINTInstr: add word ptr 2[bp] 2 ;Bump rtn adrs beyond INT instr. mov bl 1[bx] mov bh 0 shl bx 1 ;Multiply by 4 to get vector shl bx 1 ; address. push [bp-0] ;Get and save BP push [bp-2] ;Get and save BX. push [bp-4] ;Get and save DS. push cx xor cx cx ;Point DS at interrupt mov ds cx ; vector table. mov cx [bp+6] ;Get original flags. mov [bp-0] cx ;Save as pushed flags. mov cx ds:2[bx] ;Get vector and use it as mov [bp-2] cx ; the return address. mov cx ds:[bx] mov [bp-4] cx pop cx pop ds pop bx pop bp iret ; MyInt1 endp ; Main program - Simulates some keystrokes to demo the above code. Main proc mov ax cseg mov ds ax print byte "Simulating keystrokes via Trace Flag" cr lf byte "This program places 'DIR' in the keyboard buffer" byte cr lf 0 mov al 20h ;"D" down scan code call KbdSim mov al 0a0h ;"D" up scan code call KbdSim mov al 17h ;"I" down scan code call KbdSim mov al 97h ;"I" up scan code call KbdSim mov al 13h ;"R" down scan code call KbdSim mov al 93h ;"R" up scan code call KbdSim mov al 1Ch ;Enter down scan code call KbdSim mov al 9Ch ;Enter up scan code call KbdSim ExitPgm Main endp cseg ends sseg segment para stack 'stack' stk byte 1024 dup ("stack ") sseg ends zzzzzzseg segment para public 'zzzzzz' LastBytes db 16 dup (?) zzzzzzseg ends end Main
Chapter Twenty: The PC Keyboard (Part
29 SEP 1996