The Art of
ASSEMBLY LANGUAGE PROGRAMMING

Chapter Eighteen (Part 4)

Table of Content

Chapter Eighteen (Part 6)

CHAPTER EIGHTEEN:
RESIDENT PROGRAMS (Part 5)
18.8 - A Keyboard Monitor TSR
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

Chapter Eighteen (Part 4)

Table of Content

Chapter Eighteen (Part 6)

Chapter Eighteen: Resident Programs (Part 5)
29 SEP 1996