|
Table of Content | Chapter Eighteen
(Part 6) |
| CHAPTER EIGHTEEN: RESIDENT PROGRAMS (Part 5) |
| 18.8 - A Keyboard Monitor TSR |
The following program extends the keystroke counter program presented a little earlier in this chapter. This particular program monitors keystrokes and each minute writes out data to a file listing the date time and approximate number of keystrokes in the last minute.
This program can help you discover how much time you spend typing versus thinking at a display screen.
; This is an example of an active TSR that counts keyboard interrupts
; once activated. Every minute it writes the number of keyboard
; interrupts that occurred in the previous minute to an output file.
; This continues until the user removes the program from memory.
;
;
; Usage:
; KEYEVAL filename- Begins logging keystroke data to
; this file.
;
; KEYEVAL REMOVE - Removes the resident program from
; memory.
;
;
; This TSR checks to make sure there isn't a copy already active in
; memory. When doing disk I/O from the interrupts
it checks to make
; sure DOS isn't busy and it preserves application globals (PSP
DTA
; and extended error info). When removing itself from memory
it
; makes sure there are no other interrupts chained into any of its
; interrupts before doing the remove.
;
; The resident segment definitions must come before everything else.
ResidentSeg segment para public 'Resident'
ResidentSeg ends
EndResident segment para public 'EndRes'
EndResident ends
.xlist
.286
include stdlib.a
includelib stdlib.lib
.list
; Resident segment that holds the TSR code:
ResidentSeg segment para public 'Resident'
assume cs:ResidentSeg
ds:nothing
; Int 2Fh ID number for this TSR:
MyTSRID byte 0
; The following variable counts the number of keyboard interrupts
KeyIntCnt word 0
; Counter counts off the number of milliseconds that pass
SecCounter
; counts off the number of seconds (up to 60).
Counter word 0
SecCounter word 0
; FileHandle is the handle for the log file:
FileHandle word 0
; NeedIO determines if we have a pending I/O opearation.
NeedIO word 0
; PSP is the psp address for this program.
PSP word 0
; Variables to tell us if DOS
INT 13h
or INT 16h are busy:
InInt13 byte 0
InInt16 byte 0
InDOSFlag dword ?
; These variables contain the original values in the interrupt vectors
; we've patched.
OldInt9 dword ?
OldInt13 dword ?
OldInt16 dword ?
OldInt1C dword ?
OldInt28 dword ?
OldInt2F dword ?
; DOS data structures:
ExtErr struct
eeAX word ?
eeBX word ?
eeCX word ?
eeDX word ?
eeSI word ?
eeDI word ?
eeDS word ?
eeES word ?
word 3 dup (0)
ExtErr ends
XErr ExtErr {} ;Extended Error Status.
AppPSP word ? ;Application PSP value.
AppDTA dword ? ;Application DTA address.
; The following data is the output record. After storing this data
; to these variables
the TSR writes this data to disk.
month byte 0
day byte 0
year word 0
hour byte 0
minute byte 0
second byte 0
Keystrokes word 0
RecSize = $-month
; MyInt9- The system calls this routine every time a keyboard
; interrupt occus. This routine increments the
; KeyIntCnt variable and then passes control on to the
; original Int9 handler.
MyInt9 proc far
inc ResidentSeg:KeyIntCnt
jmp ResidentSeg:OldInt9
MyInt9 endp
; MyInt1C- Timer interrupt. This guy counts off 60 seconds and then
; attempts to write a record to the output file. Of course
; this call has to jump through all sorts of hoops to keep
; from reentering DOS and other problematic code.
MyInt1C proc far
assume ds:ResidentSeg
push ds
push es
pusha ;Save all the registers.
mov ax
ResidentSeg
mov ds
ax
pushf
call OldInt1C
; First things first
let's bump our interrupt counter so we can count
; off a minute. Since we're getting interrupted about every 54.92549
; milliseconds
let's shoot for a little more accuracy than 18 times
; per second so the timings don't drift too much.
add Counter
549 ;54.9 msec per int 1C.
cmp Counter
10000 ;1 second.
jb NotSecYet
sub Counter
10000
inc SecCounter
NotSecYet:
; If NEEDIO is not zero
then there is an I/O operation in progress.
; Do not disturb the output values if this is the case.
cli ;This is a critical region.
cmp NeedIO
0
jne SkipSetNIO
; Okay
no I/O in progress
see if a minute has passed since the last
; time we logged the keystrokes to the file. If so
it's time to start
; another I/O operation.
cmp SecCounter
60 ;One minute passed yet?
jb Int1CDone
mov NeedIO
1 ;Flag need for I/O.
mov ax
KeyIntCnt ;Copy this to the output
shr ax
1 ; buffer after computing
mov KeyStrokes
ax ; # of keystrokes.
mov KeyIntCnt
0 ;Reset for next minute.
mov SecCounter
0
SkipSetNIO: cmp NeedIO
1 ;Is the I/O already in
jne Int1CDone ; progress? Or done?
call ChkDOSStatus ;See if DOS/BIOS are free.
jnc Int1CDone ;Branch if busy.
call DoIO ;Do I/O if DOS is free.
Int1CDone: popa ;Restore registers and quit.
pop es
pop ds
iret
MyInt1C endp
assume ds:nothing
; MyInt28- Idle interrupt. If DOS is in a busy-wait loop waiting for
; I/O to complete
it executes an int 28h instruction each
; time through the loop. We can ignore the InDOS and CritErr
; flags at that time
and do the I/O if the other interrupts
; are free.
MyInt28 proc far
assume ds:ResidentSeg
push ds
push es
pusha ;Save all the registers.
mov ax
ResidentSeg
mov ds
ax
pushf ;Call the next INT 28h
call OldInt28 ; ISR in the chain.
cmp NeedIO
1 ;Do we have a pending I/O?
jne Int28Done
mov al
InInt13 ;See if BIOS is busy.
or al
InInt16
jne Int28Done
call DoIO ;Go do I/O if BIOS is free.
Int28Done: popa
pop es
pop ds
iret
MyInt28 endp
assume ds:nothing
; MyInt16- This is just a wrapper for the INT 16h (keyboard trap)
; handler.
MyInt16 proc far
inc ResidentSeg:InInt16
; Call original handler:
pushf
call ResidentSeg:OldInt16
; For INT 16h we need to return the flags that come from the previous call.
pushf
dec ResidentSeg:InInt16
popf
retf 2 ;Fake IRET to keep flags.
MyInt16 endp
; MyInt13- This is just a wrapper for the INT 13h (disk I/O trap)
; handler.
MyInt13 proc far
inc ResidentSeg:InInt13
pushf
call ResidentSeg:OldInt13
pushf
dec ResidentSeg:InInt13
popf
retf 2 ;Fake iret to keep flags.
MyInt13 endp
; ChkDOSStatus- Returns with the carry clear if DOS or a BIOS routine
; is busy and we can't interrupt them.
ChkDOSStatus proc near
assume ds:ResidentSeg
les bx
InDOSFlag
mov al
es:[bx] ;Get InDOS flag.
or al
es:[bx-1] ;OR with CritErr flag.
or al
InInt16 ;OR with our wrapper
or al
InInt13 ; values.
je Okay2Call
clc
ret
Okay2Call: clc
ret
ChkDOSStatus endp
assume ds:nothing
; PreserveDOS- Gets a copy's of DOS' current PSP
DTA
and extended
; error information and saves this stuff. Then it sets
; the PSP to our local PSP and the DTA to PSP:80h.
PreserveDOS proc near
assume ds:ResidentSeg
mov ah
51h ;Get app's PSP.
int 21h
mov AppPSP
bx ;Save for later
mov ah
2Fh ;Get app's DTA.
int 21h
mov word ptr AppDTA
bx
mov word ptr AppDTA+2
es
push ds
mov ah
59h ;Get extended err info.
xor bx
bx
int 21h
mov cs:XErr.eeDS
ds
pop ds
mov XErr.eeAX
ax
mov XErr.eeBX
bx
mov XErr.eeCX
cx
mov XErr.eeDX
dx
mov XErr.eeSI
si
mov XErr.eeDI
di
mov XErr.eeES
es
; Okay
point DOS's pointers at us:
mov bx
PSP
mov ah
50h ;Set PSP.
int 21h
push ds ;Set the DTA to
mov ds
PSP ; address PSP:80h
mov dx
80h
mov ah
1Ah ;Set DTA call.
int 21h
pop ds
ret
PreserveDOS endp
assume ds:nothing
; RestoreDOS- Restores DOS' important global data values back to the
; application's values.
RestoreDOS proc near
assume ds:ResidentSeg
mov bx
AppPSP
mov ah
50h ;Set PSP
int 21h
push ds
lds dx
AppDTA
mov ah
1Ah ;Set DTA
int 21h
pop ds
push ds
mov si
offset XErr ;Saved extended error stuff.
mov ax
5D0Ah ;Restore XErr call.
int 21h
pop ds
ret
RestoreDOS endp
assume ds:nothing
; DoIO- This routine processes each of the I/O operations
; required to write data to the file.
DoIO proc near
assume ds:ResidentSeg
mov NeedIO
0FFh ;A busy flag for us.
; The following Get Date DOS call may take a while
so turn the
; interrupts back on (we're clear of the critical section once we
; write 0FFh to NeedIO).
sti
call PreserveDOS ;Save DOS data.
mov ah
2Ah ;Get Date DOS call
int 21h
mov month
dh
mov day
dl
mov year
cx
mov ah
2Ch ;Get Time DOS call
int 21h
mov hour
ch
mov minute
cl
mov second
dh
mov ah
40h ;DOS Write call
mov bx
FileHandle ;Write data to this file.
mov cx
RecSize ;This many bytes.
mov dx
offset month ;Starting at this address.
int 21h ;Ignore return errors (!).
mov ah
68h ;DOS Commit call
mov bx
FileHandle ;Write data to this file.
int 21h ;Ignore return errors (!).
mov NeedIO
0 ;Ready to start over.
call RestoreDOS
PhasesDone: ret
DoIO endp
assume ds:nothing
; MyInt2F- Provides int 2Fh (multiplex interrupt) support for this
; TSR. The multiplex interrupt recognizes the following
; subfunctions (passed in AL):
;
; 00- Verify presence. Returns 0FFh in AL and a pointer
; to an ID string in es:di if the
; TSR ID (in AH) matches this
; particular TSR.
;
; 01- Remove. Removes the TSR from memory.
; Returns 0 in AL if successful
; 1 in AL if failure.
MyInt2F proc far
assume ds:nothing
cmp ah
MyTSRID ;Match our TSR identifier?
je YepItsOurs
jmp OldInt2F
; Okay
we know this is our ID
now check for a verify vs. remove call.
YepItsOurs: cmp al
0 ;Verify Call
jne TryRmv
mov al
0ffh ;Return success.
lesi IDString
iret ;Return back to caller.
IDString byte "Keypress Logger TSR"
0
TryRmv: cmp al
1 ;Remove call.
jne IllegalOp
call TstRmvable ;See if we can remove this guy.
je CanRemove ;Branch if we can.
mov ax
1 ;Return failure for now.
iret
; Okay
they want to remove this guy *and* we can remove it from memory.
; Take care of all that here.
assume ds:ResidentSeg
CanRemove: push ds
push es
pusha
cli ;Turn off the interrupts while
mov ax
0 ; we mess with the interrupt
mov es
ax ; vectors.
mov ax
cs
mov ds
ax
mov ax
word ptr OldInt9
mov es:[9*4]
ax
mov ax
word ptr OldInt9+2
mov es:[9*4 + 2]
ax
mov ax
word ptr OldInt13
mov es:[13h*4]
ax
mov ax
word ptr OldInt13+2
mov es:[13h*4 + 2]
ax
mov ax
word ptr OldInt16
mov es:[16h*4]
ax
mov ax
word ptr OldInt16+2
mov es:[16h*4 + 2]
ax
mov ax
word ptr OldInt1C
mov es:[1Ch*4]
ax
mov ax
word ptr OldInt1C+2
mov es:[1Ch*4 + 2]
ax
mov ax
word ptr OldInt28
mov es:[28h*4]
ax
mov ax
word ptr OldInt28+2
mov es:[28h*4 + 2]
ax
mov ax
word ptr OldInt2F
mov es:[2Fh*4]
ax
mov ax
word ptr OldInt2F+2
mov es:[2Fh*4 + 2]
ax
; Okay
with that out of the way
let's close the file.
; Note: INT 2F shouldn't have to deal with DOS busy because it's
; a passive TSR call.
mov ah
3Eh ;Close file command
mov bx
FileHandle
int 21h
; Okay
one last thing before we quit- Let's give the memory allocated
; to this TSR back to DOS.
mov ds
PSP
mov es
ds:[2Ch] ;Ptr to environment block.
mov ah
49h ;DOS release memory call.
int 21h
mov ax
ds ;Release program code space.
mov es
ax
mov ah
49h
int 21h
popa
pop es
pop ds
mov ax
0 ;Return Success.
iret
; They called us with an illegal subfunction value. Try to do as little
; damage as possible.
IllegalOp: mov ax
0 ;Who knows what they were thinking?
iret
MyInt2F endp
assume ds:nothing
; TstRmvable- Checks to see if we can remove this TSR from memory.
; Returns the zero flag set if we can remove it
clear
; otherwise.
TstRmvable proc near
cli
push ds
mov ax
0
mov ds
ax
cmp word ptr ds:[9*4]
offset MyInt9
jne TRDone
cmp word ptr ds:[9*4 + 2]
seg MyInt9
jne TRDone
cmp word ptr ds:[13h*4]
offset MyInt13
jne TRDone
cmp word ptr ds:[13h*4 + 2]
seg MyInt13
jne TRDone
cmp word ptr ds:[16h*4]
offset MyInt16
jne TRDone
cmp word ptr ds:[16h*4 + 2]
seg MyInt16
jne TRDone
cmp word ptr ds:[1Ch*4]
offset MyInt1C
jne TRDone
cmp word ptr ds:[1Ch*4 + 2]
seg MyInt1C
jne TRDone
cmp word ptr ds:[28h*4]
offset MyInt28
jne TRDone
cmp word ptr ds:[28h*4 + 2]
seg MyInt28
jne TRDone
cmp word ptr ds:[2Fh*4]
offset MyInt2F
jne TRDone
cmp word ptr ds:[2Fh*4 + 2]
seg MyInt2F
TRDone: pop ds
sti
ret
TstRmvable endp
ResidentSeg ends
cseg segment para public 'code'
assume cs:cseg
ds:ResidentSeg
; SeeIfPresent- Checks to see if our TSR is already present in memory.
; Sets the zero flag if it is
clears the zero flag if
; it is not.
SeeIfPresent proc near
push es
push ds
push di
mov cx
0ffh ;Start with ID 0FFh.
IDLoop: mov ah
cl
push cx
mov al
0 ;Verify presence call.
int 2Fh
pop cx
cmp al
0 ;Present in memory?
je TryNext
strcmpl
byte "Keypress Logger TSR"
0
je Success
TryNext: dec cl ;Test USER IDs of 80h..FFh
js IDLoop
cmp cx
0 ;Clear zero flag.
Success: pop di
pop ds
pop es
ret
SeeIfPresent endp
; FindID- Determines the first (well
last actually) TSR ID available
; in the multiplex interrupt chain. Returns this value in
; the CL register.
;
; Returns the zero flag set if it locates an empty slot.
; Returns the zero flag clear if failure.
FindID proc near
push es
push ds
push di
mov cx
0ffh ;Start with ID 0FFh.
IDLoop: mov ah
cl
push cx
mov al
0 ;Verify presence call.
int 2Fh
pop cx
cmp al
0 ;Present in memory?
je Success
dec cl ;Test USER IDs of 80h..FFh
js IDLoop
xor cx
cx
cmp cx
1 ;Clear zero flag
Success: pop di
pop ds
pop es
ret
FindID endp
Main proc
meminit
mov ax
ResidentSeg
mov ds
ax
mov ah
62h ;Get this program's PSP
int 21h ; value.
mov PSP
bx
; Before we do anything else
we need to check the command line
; parameters. We must have either a valid filename or the
; command "remove". If remove appears on the command line
then remove
; the resident copy from memory using the multiplex (2Fh) interrupt.
; If remove is not on the command line
we'd better have a filename and
; there had better not be a copy already loaded into memory.
argc
cmp cx
1 ;Must have exactly 1 parm.
je GoodParmCnt
print
byte "Usage:"
cr
lf
byte " KeyEval filename"
cr
lf
byte "or KeyEval REMOVE"
cr
lf
0
ExitPgm
; Check for the REMOVE command.
GoodParmCnt: mov ax
1
argv
stricmpl
byte "REMOVE"
0
jne TstPresent
call SeeIfPresent
je RemoveIt
print
byte "TSR is not present in memory
cannot remove"
byte cr
lf
0
ExitPgm
RemoveIt: mov MyTSRID
cl
printf
byte "Removing TSR (ID #%d) from memory..."
0
dword MyTSRID
mov ah
cl
mov al
1 ;Remove cmd
ah contains ID
int 2Fh
cmp al
1 ;Succeed?
je RmvFailure
print
byte "removed."
cr
lf
0
ExitPgm
RmvFailure: print
byte cr
lf
byte "Could not remove TSR from memory."
cr
lf
byte "Try removing other TSRs in the reverse order "
byte "you installed them."
cr
lf
0
ExitPgm
; Okay
see if the TSR is already in memory. If so
abort the
; installation process.
TstPresent: call SeeIfPresent
jne GetTSRID
print
byte "TSR is already present in memory."
cr
lf
byte "Aborting installation process"
cr
lf
0
ExitPgm
; Get an ID for our TSR and save it away.
GetTSRID: call FindID
je GetFileName
print
byte "Too many resident TSRs
cannot install"
cr
lf
0
ExitPgm
; Things look cool so far
check the filename and open the file.
GetFileName: mov MyTSRID
cl
printf
byte "Keypress logger TSR program"
cr
lf
byte "TSR ID = %d"
cr
lf
byte "Processing file:"
0
dword MyTSRID
puts
putcr
mov ah
3Ch ;Create file command.
mov cx
0 ;Normal file.
push ds
push es ;Point ds:dx at name
pop ds
mov dx
di
int 21h ;Open the file
jnc GoodOpen
print
byte "DOS error #"
0
puti
print
byte " opening file."
cr
lf
0
ExitPgm
GoodOpen: pop ds
mov FileHandle
ax ;Save file handle.
InstallInts: print
byte "Installing interrupts..."
0
; Patch into the INT 9
13h
16h
1Ch
28h
and 2Fh interrupt vectors.
; Note that the statements above have made ResidentSeg the current data
; segment
so we can store the old values directly into
; the OldIntxx variables.
cli ;Turn off interrupts!
mov ax
0
mov es
ax
mov ax
es:[9*4]
mov word ptr OldInt9
ax
mov ax
es:[9*4 + 2]
mov word ptr OldInt9+2
ax
mov es:[9*4]
offset MyInt9
mov es:[9*4+2]
seg ResidentSeg
mov ax
es:[13h*4]
mov word ptr OldInt13
ax
mov ax
es:[13h*4 + 2]
mov word ptr OldInt13+2
ax
mov es:[13h*4]
offset MyInt13
mov es:[13h*4+2]
seg ResidentSeg
mov ax
es:[16h*4]
mov word ptr OldInt16
ax
mov ax
es:[16h*4 + 2]
mov word ptr OldInt16+2
ax
mov es:[16h*4]
offset MyInt16
mov es:[16h*4+2]
seg ResidentSeg
mov ax
es:[1Ch*4]
mov word ptr OldInt1C
ax
mov ax
es:[1Ch*4 + 2]
mov word ptr OldInt1C+2
ax
mov es:[1Ch*4]
offset MyInt1C
mov es:[1Ch*4+2]
seg ResidentSeg
mov ax
es:[28h*4]
mov word ptr OldInt28
ax
mov ax
es:[28h*4 + 2]
mov word ptr OldInt28+2
ax
mov es:[28h*4]
offset MyInt28
mov es:[28h*4+2]
seg ResidentSeg
mov ax
es:[2Fh*4]
mov word ptr OldInt2F
ax
mov ax
es:[2Fh*4 + 2]
mov word ptr OldInt2F+2
ax
mov es:[2Fh*4]
offset MyInt2F
mov es:[2Fh*4+2]
seg ResidentSeg
sti Okay
ints back on.
; We're hooked up
the only thing that remains is to terminate and
; stay resident.
print
byte "Installed."
cr
lf
0
mov dx
EndResident ;Compute size of program.
sub dx
PSP
mov ax
3100h ;DOS TSR command.
int 21h
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
The following is a short little application that reads the data file produced by the above program and produces a simple report of the date time and keystrokes:
; This program reads the file created by the KEYEVAL.EXE TSR program.
; It displays the log containing dates
times
and number of keystrokes.
.xlist
.286
include stdlib.a
includelib stdlib.lib
.list
dseg segment para public 'data'
FileHandle word ?
month byte 0
day byte 0
year word 0
hour byte 0
minute byte 0
second byte 0
KeyStrokes word 0
RecSize = $-month
dseg ends
cseg segment para public 'code'
assume cs:cseg
ds:dseg
; SeeIfPresent- Checks to see if our TSR is present in memory.
; Sets the zero flag if it is
clears the zero flag if
; it is not.
SeeIfPresent proc near
push es
push ds
pusha
mov cx
0ffh ;Start with ID 0FFh.
IDLoop: mov ah
cl
push cx
mov al
0 ;Verify presence call.
int 2Fh
pop cx
cmp al
0 ;Present in memory?
je TryNext
strcmpl
byte "Keypress Logger TSR"
0
je Success
TryNext: dec cl ;Test USER IDs of 80h..FFh
js IDLoop
cmp cx
0 ;Clear zero flag.
Success: popa
pop ds
pop es
ret
SeeIfPresent endp
Main proc
meminit
mov ax
dseg
mov ds
ax
argc
cmp cx
1 ;Must have exactly 1 parm.
je GoodParmCnt
print
byte "Usage:"
cr
lf
byte " KEYRPT filename"
cr
lf
0
ExitPgm
GoodParmCnt: mov ax
1
argv
print
byte "Keypress logger report program"
cr
lf
byte "Processing file:"
0
puts
putcr
mov ah
3Dh ;Open file command.
mov al
0 ;Open for reading.
push ds
push es ;Point ds:dx at name
pop ds
mov dx
di
int 21h ;Open the file
jnc GoodOpen
print
byte "DOS error #"
0
puti
print
byte " opening file."
cr
lf
0
ExitPgm
GoodOpen: pop ds
mov FileHandle
ax ;Save file handle.
; Okay
read the data and display it:
ReadLoop: mov ah
3Fh ;Read file command
mov bx
FileHandle
mov cx
RecSize ;Number of bytes.
mov dx
offset month ;Place to put data.
int 21h
jc ReadError
test ax
ax ;EOF?
je Quit
mov cx
year
mov dl
day
mov dh
month
dtoam
puts
free
print
byte "
"
0
mov ch
hour
mov cl
minute
mov dh
second
mov dl
0
ttoam
puts
free
printf
byte "
keystrokes = %d\n"
0
dword KeyStrokes
jmp ReadLoop
ReadError: print
byte "Error reading file"
cr
lf
0
Quit: mov bx
FileHandle
mov ah
3Eh ;Close file
int 21h
ExitPgm
Main endp
cseg ends
sseg segment para stack 'stack'
stk db 1024 dup ("stack ")
sseg ends
zzzzzzseg segment para public 'zzzzzz'
LastBytes db 16 dup (?)
zzzzzzseg ends
end Main
|
Table of Content | Chapter Eighteen
(Part 6) |
Chapter Eighteen: Resident Programs
(Part 5)
29 SEP 1996