Chapter 11
|
Writing Memory-Resident Software |
Through its memory-management system, MS-DOS allows a program to remain resident in memory after terminating. The resident program can later regain control of the processor to perform tasks such as background printing or popping up a calculator on the screen. Such a program is commonly called a TSR, from the terminate-and-stay-resident function it uses to return to MS-DOS.
This chapter explains the techniques of writing memory-resident software. The first two sections present introductory material. Following sections describe important MS-DOS and BIOS interrupts and focus on how to write safe, compatible, memory-resident software. Two example programs illustrate the techniques described in the chapter. The MASM 6.1 disks contain complete source code for the two example TSR programs.
MS-DOS maintains a pointer to the beginning of unused memory. Programs load into memory at this position and terminate execution by returning control to MS-DOS. Normally, the pointer remains unchanged, allowing MS-DOS to reuse the same memory when loading other programs.
A terminating program can, however, prevent other programs from loading on top of it. These programs exit to MS-DOS through the terminate-and-stay-resident function, which resets the free-memory pointer to a higher position. This leaves the program resident in a protected block of memory, even though it is no longer running.
|
|
The terminate-and-stay-resident function (Function 31h) is one of the MS-DOS services invoked through Interrupt 21h. The following fragment shows how a TSR program terminates through Function 31h and remains resident in a 1000h-byte block of memory:
mov ah, 31h ; Request DOS Function 31h
mov al, err ; Set return code
mov dx, 100h ; Reserve 100h paragraphs
; (1000h bytes)
int 21h ; Terminate-and-stay-resident
|
Note |
In current versions of MS-DOS, Interrupt 27h also provides a terminate-and-stay-resident service. However, Microsoft cannot guarantee future support for Interrupt 27h and does not recommend its use.
TSRs consist of two distinct parts that execute at different times. The first part is the installation section, which executes only once, when MS-DOS loads the program. The installation code performs any initialization tasks required by the TSR and then exits through the terminate-and-stay-resident function.
The second part of the TSR, called the resident section, consists of code and data left in memory after termination. Though often identified with the TSR itself, the resident section makes up only part of the entire program.
The TSRs resident code must be able to regain control of the processor and execute after the program has terminated. Methods of executing a TSR are classified as either passive or active.
The simplest way to execute a TSR is to transfer control to it explicitly from another program. Because the TSR in this case does not solicit processor control, it is said to be passive. If the calling program can determine the TSRs memory address, it can grant control via a far jump or call. More commonly, a program activates a passive TSR through a software interrupt. The installation section of the TSR writes the address of its resident code to the proper position in the interrupt vector table (see MS-DOS Interrupts in Chapter 7). Any subsequent program can then execute the TSR by calling the interrupt.
Passive TSRs often replace existing software interrupts. For example, a passive TSR might replace Interrupt 10h, the BIOS video service. By intercepting calls that read or write to the screen, the TSR can access the video buffer directly, increasing display speed.
Passive TSRs allow limited access since they can be invoked only from another program. They have the advantage of executing within the context of the calling program, and thus run no risk of interfering with another process. Such a risk does exist with active TSRs.
The second method of executing a TSR involves signaling it through some hardware event, such as a predetermined sequence of keystrokes. This type of TSR is active because it must continually search for its startup signal. The advantage of active TSRs lies in their accessibility. They can take control from any running application, execute, and return, all on demand.
An active TSR, however, must not seize processor control blindly. It must contain additional code that determines the proper moment at which to execute. The extra code consists of one or more routines called interrupt handlers, described in the following section.
The memory-resident portion of an active TSR consists of two parts. One part contains the body of the TSR the code and data that perform the programs main tasks. The other part contains the TSRs interrupt handlers.
An interrupt handler is a routine that takes control when a specific interrupt occurs. Although sometimes called an interrupt service routine, a TSRs handler usually does not service the interrupt. Instead, it passes control to the original interrupt routine, which does the actual interrupt servicing. (See the section Replacing an Interrupt Routine in Chapter 7 for information on how to write an interrupt handler.)
Collectively, interrupt handlers ensure that a TSR operates compatibly with the rest of the system. Individually, each handler fulfills one or more of the following functions:
Auditing hardware events that may signal a request for the TSR
Monitoring system status
Determining whether a request for the TSR should be honored, based on current system status
Active TSRs commonly use a special keystroke sequence or the timer as a request signal. A TSR invoked through one of these channels must be equipped with handlers that audit keyboard or timer events.
A keyboard handler receives control at every keystroke. It examines each key, searching for the proper signal or hot key. Generally, a keyboard handler should not attempt to call the TSR directly when it detects the hot key. If the TSR cannot safely interrupt the current process at that moment, the keyboard handler is forced to exit to allow the process to continue. Since the handler cannot regain control until the next keystroke, the user has to press the hot key repeatedly until the handler can comply with the request.
Instead, the handler should merely set a request flag when it detects a hot-key signal and then exit normally. Examples in the following paragraphs illustrate this technique.
For computers other than MCA (IBM PS/2 and compatible), an active TSR audits keystrokes through a handler for Interrupt 09, the keyboard interrupt:
Keybrd PROC FAR
sti ; Interrupts are okay
push ax ; Save AX register
in al, 60h ; AL = key scan code
call CheckHotKey ; Check for hot key
.IF carry? ; If hot key pressed,
mov cs:TsrRequestFlag, TRUE ; raise flag and
. ; set up for exit
.
.
A TSR running on a PS/2 computer cannot reliably read key scan codes using this method. Instead, the TSR must search for its hot key through a handler for Interrupt 15h (Miscellaneous System Services). The handler determines the current keypress from the AL register when AH equals 4Fh, as shown here:
MiscServ PROC FAR
sti ; Interrupts okay
.IF ah == 4Fh ; If Keyboard Intercept Service:
call CheckHotKey ; Check for hot key
.IF carry? ; If hot key pressed,
mov cs:TsrRequestFlag, TRUE ; raise flag and
. ; set up for exit
.
.
The example program on page 293 shows how a TSR tests for a PS/2 machine and then sets up a handler for either Interrupt 09 or Interrupt 15h to audit keystrokes.
Setting a request flag in the keyboard handler allows other code, such as the timer handler (Interrupt 08), to recognize a request for the TSR. The timer handler gains control at every timer interrupt, which occurs an average of 18.2 times per second.
|
|
The following fragment shows how a timer handler tests the request flag and continually polls until it can safely execute the TSR.
NewTimer PROC FAR
.
.
.
cmp TsrRequestFlag, FALSE ; Has TSR been requested?
.IF !zero? ; If so, can system be
call CheckSystem ; interrupted safely?
.IF carry? ; If so,
call ActivateTsr ; activate TSR
.
.
.
A TSR that uses a hardware device such as the video or disk must not interrupt while the device is active. A TSR monitors a device by handling the devices interrupt. Each interrupt handler simply sets a flag to indicate the device is in use, and then clears the flag when the interrupt finishes.
The following shows a typical monitor handler:
NewHandler PROC FAR
mov cs:ActiveFlag, TRUE ; Set active flag
pushf ; Simulate interrupt by
; pushing flags, then
call OldHandler ; far-calling original routine
mov cs:ActiveFlag, FALSE ; Clear active flag
iret ; Return from interrupt
NewHandler ENDP
Only hardware used by the TSR requires monitoring. For example, a TSR that performs disk input/output (I/O) must monitor disk use through Interrupt 13h. The disk handler sets an active flag that prevents the TSR from executing during a read or write operation. Otherwise, the TSRs own I/O would move the disk head. This would cause the suspended disk operation to continue with the head incorrectly positioned when the TSR returned control to the interrupted program.
In the same way, an active TSR that displays to the screen must monitor calls to Interrupt 10h. The Interrupt 10h BIOS routine does not protect critical sections of code that program the video controller. The TSR must therefore ensure it does not interrupt such nonreentrant operations.
The activities of the operating system also affect the system status. With few exceptions, MS-DOS functions are not reentrant and must not be interrupted. However, monitoring MS-DOS is somewhat more complicated than monitoring hardware. This subject is discussed in Using MS-DOS in Active TSRs, later in this chapter.
Figure 11.1 illustrates the process described so far. It shows a time line for a typical TSR signaled from the keyboard. When the keyboard handler detects the proper hot key, it sets a request flag called TsrRequestFlag. Thereafter, the timer handler continually checks the system status until it can safely call the TSR.
Figure 11.1 Time Line of Interactions Between Interrupt Handlers for a Typical TSR
The following comments describe the chain of events depicted in Figure 11.1. Each comment refers to one of the numbered pointers in the figure.
1. At time = t, the timer handler activates. It finds the flag TsrRequestFlag clear, indicating the user has not requested the TSR. The handler terminates without taking further action. Notice that Interrupt 13h is currently processing a disk I/O operation.
2. Before the next timer interrupt, the keyboard handler detects the hot key, signaling a request for the TSR. The keyboard handler sets TsrRequestFlag and returns.
3. At time = t + 1/18 second, the timer handler again activates and finds TsrRequestFlag set. The handler checks other active flags to determine if the TSR can safely execute. Since Interrupt 13h has not yet completed its disk operation, the timer handler finds DiskActiveFlag set. The handler therefore terminates without activating the TSR.
4. At time = t + 2/18 second, the timer handler again finds TsrRequestFlag set and repeats its scan of the active flags. DiskActiveFlag is now clear, but in the interim, Interrupt 10h has activated as indicated by the flag VideoActiveFlag. The timer handler accordingly terminates without activating the TSR.
5. At time = t + 3/18 second, the timer handler repeats the process. This time it finds all active flags clear, indicating the TSR can safely execute. The timer handler calls the TSR, which sets its own active flag to ensure it will not interrupt itself if requested again.
6. The timer and other interrupts continue to function normally while the TSR executes.
The timer itself can serve as the startup signal if the TSR executes periodically. Screen clocks that continuously show seconds and minutes are examples of TSRs that use the timer this way. ALARM.ASM, a program described in the next section, shows another example of a timer-driven TSR.
Once a handler receives a request signal for the TSR, it checks the various active flags maintained by the handlers that monitor system status. If any of the flags are set, the handler ignores the request and exits. If the flags are clear, the handler invokes the TSR, usually through a near or far call. Figure 11.1 illustrates how a timer handler detects a request and then periodically scans various active flags until all the flags are clear.
A TSR that changes stacks must not interrupt itself. Otherwise, the second execution would overwrite the stack data belonging to the first. A TSR prevents this by setting its own active flag before executing, as shown in Figure 11.1. A handler must check this flag along with the other active flags when determining whether the TSR can safely execute.
This section presents a simple alarm clock TSR that demonstrates some of the material covered so far. The program accepts an argument from the command line that specifies the alarm setting in military form, such as 1635 for 4:35 P.M. For simplicity, the argument must consist of four digits, including leading zeros. To set the alarm at 7:45 A.M., for example, enter the command:
ALARM 0745
The installation section of the program begins with the Install procedure. Install computes the number of five-second intervals that must elapse before the alarm sounds and stores this number in the word CountDown. The procedure then obtains the vector for Interrupt 08 (timer) through MS-DOS Function 35h and stores it in the far pointer OldTimer. Function 25h replaces the vector with the far address of the new timer handler NewTimer. Once installed, the new timer handler executes at every timer interrupt. These interrupts occur 18.2 times per second or 91 times every five seconds.
Each time it executes, NewTimer subtracts one from a secondary counter called Tick91. By counting 91 timer ticks, Tick91 accurately measures a period of five seconds. When Tick91 reaches zero, its reset to 91 and CountDown is decremented by one. When CountDown reaches zero, the alarm sounds.
;* ALARM.ASM - A simple memory-resident program that beeps the speaker
;* at a prearranged time. Can be loaded more than once for multiple
;* alarm settings. During installation, ALARM establishes a handler
;* for the timer interrupt (Interrupt 08). It then terminates through
;* the terminate-and-stay-resident function (Function 31h). After the
;* alarm sounds, the resident portion of the program retires by setting
;* a flag that prevents further processing in the handler.
.MODEL tiny ; Create ALARM.COM
.STACK
.CODE
ORG 5Dh ; Location of time argument in PSP,
CountDown LABEL WORD ; converted to number of 5-second
; intervals to elapse
.STARTUP
jmp Install ; Jump over data and resident code
; Data must be in code segment so it wont be thrown away with Install code.
OldTimer DWORD ? ; Address of original timer routine
tick_91 BYTE 91 ; Counts 91 clock ticks (5 seconds)
TimerActiveFlag BYTE 0 ; Active flag for timer handler
;* NewTimer - Handler routine for timer interrupt (Interrupt 08).
;* Decrements CountDown every 5 seconds. No other action is taken
;* until CountDown reaches 0, at which time the speaker sounds.
NewTimer PROC FAR
.IF cs:TimerActiveFlag != 0 ; If timer busy or retired,
jmp cs:OldTimer ; jump to original timer routine
.ENDIF
inc cs:TimerActiveFlag ; Set active flag
pushf ; Simulate interrupt by pushing flags,
call cs:OldTimer ; then far-calling original routine
sti ; Enable interrupts
push ds ; Preserve DS register
push cs ; Point DS to current segment for
pop ds ; further memory access
dec tick_91 ; Count down for 91 ticks
|
|
.IF zero? ; If 91 ticks have elapsed,
mov tick_91, 91 ; reset secondary counter and
dec CountDown ; subtract one 5-second interval
.IF zero? ; If CountDown drained,
call Sound ; sound speaker
inc TimerActiveFlag ; Alarm has sounded--inc flag
.ENDIF ; again so it remains set
.ENDIF
dec TimerActiveFlag ; Decrement active flag
pop ds ; Recover DS
iret ; Return from interrupt handler
NewTimer ENDP
;* Sound - Sounds speaker with the following tone and duration:
BEEP_TONE EQU 440 ; Beep tone in hertz
BEEP_DURATION EQU 6 ; Number of clocks during beep,
; where 18 clocks = approx 1 second
Sound PROC USES ax bx cx dx es ; Save registers used in this routine
mov al, 0B6h ; Initialize channel 2 of
out 43h, al ; timer chip
mov dx, 12h ; Divide 1,193,180 hertz
mov ax, 34DCh ; (clock frequency) by
mov bx, BEEP_TONE ; desired frequency
div bx ; Result is timer clock count
out 42h, al ; Low byte of count to timer
mov al, ah
out 42h, al ; High byte of count to timer
in al, 61h ; Read value from port 61h
or al, 3 ; Set first two bits
out 61h, al ; Turn speaker on
; Pause for specified number of clock ticks
mov dx, BEEP_DURATION ; Beep duration in clock ticks
sub cx, cx ; CX:DX = tick count for pause
mov es, cx ; Point ES to low memory data
add dx, es:[46Ch] ; Add current tick count to CX:DX
adc cx, es:[46Eh] ; Result is target count in CX:DX
.REPEAT
mov bx, es:[46Ch] ; Now repeatedly poll clock
mov ax, es:[46Eh] ; count until the target
sub bx, dx ; time is reached
sbb ax, cx
.UNTIL !carry?
in al, 61h ; When time elapses, get port value
xor al, 3 ; Kill bits 0-1 to turn
out 61h, al ; speaker off
ret
Sound ENDP
;* Install - Converts ASCII argument to valid binary number, replaces
;* NewTimer as the interrupt handler for the timer, then makes program
;* memory-resident by exiting through Function 31h.
;*
;* This procedure marks the end of the TSR's resident section and the
;* beginning of the installation section. When ALARM terminates through
;* Function 31h, the above code and data remain resident in memory. The
;* memory occupied by the following code is returned to DOS.
Install PROC
; Time argument is in hhmm military format. Converts ASCII digits to
; number of minutes since midnight, then converts current time to number
; of minutes since midnight. Difference is number of minutes to elapse
; until alarm sounds. Converts to seconds-to-elapse, divides by 5 seconds,
; and stores result in word CountDown.
DEFAULT_TIME EQU 3600 ; Default alarm setting = 1 hour
; (in seconds) from present time
mov ax, DEFAULT_TIME
cwd ; DX:AX = default time in seconds
.IF BYTE PTR CountDown != ' ' ; If not blank argument,
xor CountDown[0], '00' ; convert 4 bytes of ASCII
xor CountDown[2], '00' ; argument to binary
mov al, 10 ; Multiply 1st hour digit by 10
mul BYTE PTR CountDown[0] ; and add to 2nd hour digit
add al, BYTE PTR CountDown[1]
mov bh, al ; BH = hour for alarm to go off
mov al, 10 ; Repeat procedure for minutes