Memory Resident Programming


Introduction

Classical programs are loaded into memory and executed by the operating system. After execution, the program is removed from memory and typically overwritten by the next program that is run. Sometimes, it may be necessary to keep a program in memory after it has completed its execution. Such a program is called a Memory Resident Program. The more proper term is Terminate-and-Stay-Resident program or TSR.

TSRs really have no purpose if they just use up memory. However, if a TSR adds to the functionality of the operating system then its memory space can be justified. The standard way of adding to the functionality of the OS is by modifying or adding to the functions of one or more interrupt procedures.

The interrupt procedures, which are formally called Interrupt Service Routines (or ISRs), are a set of procedures that the computer uses to perform useful maintenance and I/O tasks. This set of procedures can be expanded by the programmer, who can write additional or replacement ISRs.

Obviously, the critical ISRs (eg. 21h, 10h, 16h) cannot be rewritten since all the functionality will have to be duplicated. If the new ISR does not conform to the specifications properly, the computer will just cease to work properly.

An alternative is to add to the ISR a bit of processing either before or after the standard procedural code. This way, the ISR seems to work as before, except for the changes made.

Writing an ISR

Writing an ISR is not a trivial task. The first distinction that must be made is whether the interrupt being replaced is a hardware or a software interrupt. If it is a hardware interrupt, it must be remembered that the interrupt may be called at any time at all. Software interrupts do not have such stringent requirements, since the programmer can decide when the interrupt gets called.

In a typical interrupt table one would find entries for the standard hardware interrupts (0-0Fh). This would be followed by software interrupts usually ranging from 10h-30h. Thereafter there are a few notable interrupts, like 33h for the mouse, but there are also lots of unused interrupt numbers. Unfortunately, the programmer cannot just use any interrupt number because some of these are used by application programs, which might run on top of the TSR. A good choice for blank interrupt numbers is the range 60h-66h.

The easiest ISR to write would be to link a standard procedure to an unused interrupt and then keep the program in memory so that subsequent programs may use the ISR. To accomplish this, the procedure must be linked using the Set Interrupt Vector function in DOS. The program must be kept in memory using the Terminate-Stay Resident function also in DOS.

The proper way to write an ISR demands that all registers used in the ISR are saved at the beginning and restored at the end. This is especially important when an existing ISR is being modified.

The distinction between a normal procedure and an ISR is that ISRs use an IRET instead of a RET to return to the point of call. The logical difference in these instructions is that IRET restores the flags off the top of the stack after returning. This obviously means that the flags must be stored before calling an interrupt. The INT command automatically pushes the flags into the stack before jumping to the routine in question. It is definitely possible to simulate an interrupt by pushing the flags onto the stack and CALLing the address of the procedure.

Timer-based ISRs

The simplest ISR to replace is that of the timer-tick. This is because the standard timer-tick routine does nothing. The timer-tick routine is called by the CPU approximately 18.2 times per second. At boot-up time, this interrupt vector points to an IRET instruction, thereby doing absolutely nothing. Its quite safe to to redirect this interrupt (1Ch) to point to a procedure that the programmer wants executed very frequently.

In the simple case the programmer can write a routine and link it to the timer-tick interrupt. However, if another TSR, which uses this timer-tick interrupt, is loaded the first TSR no longer has control of the interrupt and becomes a waste of memory. To prevent this from occurring, the second program has to save the address of the first procedure and execute it as well. This is called chaining and has many variations as illustrated in the following sample programs.

1Ch is a software interrupt so it ought not to be called automatically by the hardware. In fact, it is not called automatically by the hardware but by another interrupt. The hardware timer device in the computer causes interrupt 8 to be called 18.2 times per second. This interrupt 8 then calls interrupt 1Ch each time in addition to its other tasks. The advantage of this is that the programmer need not meddle with the working of interrupt 8, which is critical to the computer, in order to use the timer.

Sample Program #12

; simple pinger TSR - 1C hook
DOSSEG
.MODEL SMALL
.STACK 4096

.DATA

.CODE

int1C PROC ; start of ISR for 1Ch

push ax ; save all registers used
push cx
push bp
push es

mov ax,3000 ; set up parameters and call sound routine
push ax
call sound

pop es ; restore registers used
pop bp
pop cx
pop ax

iret ; return from interrupt

int1C ENDP

sound PROC ; procedure to produce sound
push bp ; save stack position
mov bp,sp

mov al,10110110b ; mode register bitset
out 43h,al ; set mode register
jmp $+2 ; waste time
mov cx,[bp+4] ; get frequency divider
mov al,cl
out 42h,al ; send frequency divider (low) to hardware
jmp $+2 ; waste time
mov al,ch
out 42h,al ; send frequency divider (high) to hardware
jmp $+2 ; waste time

in al,61h ; switch speaker on
or al,03h
out 61h,al

mov ax,0f000h ; delay a while
soundstart:
cmp ax,0
je soundstop
dec ax
jmp soundstart

soundstop:
in al,61h ; switch speaker off
and al,0fch
out 61h,al

mov sp,bp ; restore stack
pop bp
ret 2 ; pop parameters off stack and return
sound ENDP

EndofCode: ; end of resident part of code

ProgramStart:
mov ax,cs ; set data segment = code segment
mov ds,ax

lea dx,int1C ; set interrupt vector 1Ch
mov al,1Ch
mov ah,25h
int 21h

lea dx,EndOfCode ; get length of code to remain resident
mov cl,4 ; divide by 4 to calculate paragraphs
shr dx,cl
add dx,11h ; add 1 for remainder and 10 for PSP
mov ah,31h ; terminate and stay resident
int 21h

END ProgramStart

The DOS Re-Entrancy Problem

DOS is a great boon to programmers but is has some restrictions that make life difficult especially for the TSR programmer. The worst of these is called the DOS Re-Entrancy Problem.

When a DOS interrupt (usually 21h) is called, DOS allocates memory for a stack somewhere within the DOS memory space. When the interrupt gets called again, the same stack is used. This works fine as long as the interrupt is not called from within itself. This may sound preposterous, but occurs very often in memory-resident programming. For example, the 1Ch timer-tick interrupt can get called while DOS is in the middle of outputting a string, since 1Ch is controlled ultimately by hardware. If the ISR tries to output a string using the standard DOS technique, the computer will just crash because the previous stack will be overwritten. Obviously this problem will occur with any hardware or hardware-controlled interrupt.

There are many solutions to this problem.

Sample Program #13

; simple pinger TSR - 28 hook
DOSSEG
.MODEL SMALL
.STACK 4096

.DATA

.CODE

int28 PROC ; start of ISR for 28h

push ax ; save all registers used
push cx
push bp
push es

mov ax,3000 ; set up parameters and call sound routine
push ax
call sound

pop es ; restore registers used
pop bp
pop cx
pop ax

iret ; return from interrupt

int28 ENDP

sound PROC ; procedure to produce sound
push bp ; save stack position
mov bp,sp

mov al,10110110b ; mode register bitset
out 43h,al ; set mode register
jmp $+2 ; waste time
mov cx,[bp+4] ; get frequency divider
mov al,cl
out 42h,al ; send frequency divider (low) to hardware
jmp $+2 ; waste time
mov al,ch
out 42h,al ; send frequency divider (high) to hardware
jmp $+2 ; waste time

in al,61h ; switch speaker on
or al,03h
out 61h,al

mov ax,0f000h ; delay a while
soundstart:
cmp ax,0
je soundstop
dec ax
jmp soundstart

soundstop:
in al,61h ; switch speaker off
and al,0fch
out 61h,al

mov sp,bp ; restore stack
pop bp
ret 2 ; pop parameters off stack and return
sound ENDP

EndofCode: ; end of resident part of code

ProgramStart:
mov ax,cs ; set data segment = code segment
mov ds,ax

lea dx,int28 ; set interrupt vector 28h
mov al,28h
mov ah,25h
int 21h

lea dx,EndOfCode ; get length of code to remain resident
mov cl,4 ; divide by 4 to calculate paragraphs
shr dx,cl
add dx,11h ; add 1 for remainder and 10 for PSP
mov ah,31h ; terminate and stay resident
int 21h

END ProgramStart

Sample Program #14

; pinger TSR with chaining - 8 hook
DOSSEG
.MODEL SMALL
.STACK 4096

.DATA

.CODE

Old1C label FAR

int8 PROC ; start of ISR for 8h

push ax ; save all registers used
push cx
push bp
push es

mov ax,3000 ; set up parameters and call sound routine
push ax
call sound

pop es ; restore registers used
pop bp
pop cx
pop ax

EndOf8:
jmp Old1C ; return from interrupt

int8 ENDP

sound PROC ; procedure to produce sound
push bp ; save stack position
mov bp,sp

mov al,10110110b ; mode register bitset
out 43h,al ; set mode register
jmp $+2 ; waste time
mov cx,[bp+4] ; get frequency divider
mov al,cl
out 42h,al ; send frequency divider (low) to hardware
jmp $+2 ; waste time
mov al,ch
out 42h,al ; send frequency divider (high) to hardware
jmp $+2 ; waste time

in al,61h ; switch speaker on
or al,03h
out 61h,al

mov ax,0f000h ; delay a while
soundstart:
cmp ax,0
je soundstop
dec ax
jmp soundstart

soundstop:
in al,61h ; switch speaker off
and al,0fch
out 61h,al

mov sp,bp ; restore stack
pop bp
ret 2 ; pop parameters off stack and return
sound ENDP

EndofCode: ; end of resident part of code

ProgramStart:
mov ax,cs ; set data segment = code segment
mov ds,ax

mov ax,3508h ; get original interrupt vector 8h
int 21h
mov word ptr EndOf8+1,bx ; patch old procedure address into code
mov word ptr EndOf8+3,es

lea dx,int8 ; set interrupt vector 8h
mov ax,2508h
int 21h

lea dx,EndOfCode ; get length of code to remain resident
mov cl,4 ; divide by 4 to calculate paragraphs
shr dx,cl
add dx,11h ; add 1 for remainder and 10 for PSP
mov ah,31h ; terminate and stay resident
int 21h

END ProgramStart

Sample Program #15

; pinger TSR with rear chaining - 8 hook
DOSSEG
.MODEL SMALL
.STACK 4096

.DATA

.CODE

Old1C label FAR

int8 PROC ; start of ISR for 8h

pushf
StartOf8:
call Old1C

push ax ; save all registers used
push cx
push bp
push es

mov ax,3000 ; set up parameters and call sound routine
push ax
call sound

pop es ; restore registers used
pop bp
pop cx
pop ax

iret ; return from interrupt

int8 ENDP

sound PROC ; procedure to produce sound
push bp ; save stack position
mov bp,sp

mov al,10110110b ; mode register bitset
out 43h,al ; set mode register
jmp $+2 ; waste time
mov cx,[bp+4] ; get frequency divider
mov al,cl
out 42h,al ; send frequency divider (low) to hardware
jmp $+2 ; waste time
mov al,ch
out 42h,al ; send frequency divider (high) to hardware
jmp $+2 ; waste time

in al,61h ; switch speaker on
or al,03h
out 61h,al

mov ax,0f000h ; delay a while
soundstart:
cmp ax,0
je soundstop
dec ax
jmp soundstart

soundstop:
in al,61h ; switch speaker off
and al,0fch
out 61h,al

mov sp,bp ; restore stack
pop bp
ret 2 ; pop parameters off stack and return
sound ENDP

EndofCode: ; end of resident part of code

ProgramStart:
mov ax,cs ; set data segment = code segment
mov ds,ax

mov ax,3508h ; get original interrupt vector 8h
int 21h
mov word ptr Startof8+1,bx ; patch old procedure address into code
mov word ptr Startof8+3,es

lea dx,int8 ; set interrupt vector 8h
mov ax,2508h
int 21h

lea dx,EndOfCode ; get length of code to remain resident
mov cl,4 ; divide by 4 to calculate paragraphs
shr dx,cl
add dx,11h ; add 1 for remainder and 10 for PSP
mov ah,31h ; terminate and stay resident
int 21h

END ProgramStart

The Keyboard Interrupts

Linking a program to the keyboard has the unique distinction that the program can be called when a key is struck. There are two keyboard interrupts that can be linked to your TSR programs. Interrupt 9 gets called by the hardware whenever a key is pressed or released. Interrupt 16h is called by the software whenever a keystroke is required. Each has its peculiarities.

Since 9h is a hardware interrupt the programmer has to be very careful when using this in an ISR. Firstly, all registers must be saved at the beginning and restored at the end. Then, the original procedure must be called properly. No calls can be made to the DOS interrupt 21h because of the DOS reentrancy problem so all output must be made directly to the screen. Programmers that use int9 can rewrite the functionality of the original code to gain speed improvements, but it is not advisable.

Int 16h is a software interrupt so it is slightly relaxed when it comes to the DOS reentrancy problem. However, this interrupt returns flag values to the calling code so the flags must be saved after the original routine is called. Since int16 has many subfunctions, these have to be checked for and unaffected code must be allowed to run as normal or the computer may hang.

The net result of int9 is that the new keystroke is stored in the keyboard buffer within the BDA. Int16 checks this same memory area for a keystroke when required. Thus, instead of trying to interface directly with the hardware in the ISRs, it is normally only necessary to check or change the contents of the keyboard in memory.

Sample Program #16

; simple keyboard remapper to lock small caps
DOSSEG
.MODEL SMALL
.STACK 4096

.DATA

.CODE

Old16 label FAR

int16 PROC ; start of ISR for 16h

push bp ; preserve stack
mov bp,sp

push ax ; save initial value of AX

pushf ; call old int16 routine
StartOf16:
call Old16

push ax ; save new AX value
pushf ; get flags
pop ax
mov [bp+6],ax ; ... and store on stack

mov ax,[bp-2] ; get initial AX from stack
cmp ah,0 ; check for "get keystroke" subfunctions
je gotkey
cmp ah,10h
je gotkey ; if true,
jmp nokey ; otherwise ...

gotkey: ; modify return value
pop ax ; get new AX value from stack
cmp al,'A' ; check if character is before 'A'
jb skipkey
cmp al,'Z' ; check if character is after 'Z'
ja skipkey
add al,32 ; if true, make into small letter
skipkey:
push ax ; otherwise, just save value back on stack

nokey:
pop ax ; restore new AX from stack
mov sp,bp ; restore stack
pop bp
iret ; return from interrupt

int16 ENDP

EndofCode: ; end of resident part of code

ProgramStart:
mov ax,cs ; set data segment = code segment
mov ds,ax

mov ax,3516h ; get original interrupt vector 16h
int 21h
mov word ptr Startof16+1,bx ; patch old procedure address into code
mov word ptr Startof16+3,es

lea dx,int16 ; set interrupt vector 16h
mov ax,2516h
int 21h

lea dx,EndOfCode ; get length of code to remain resident
mov cl,4 ; divide by 4 to calculate paragraphs
shr dx,cl
add dx,11h ; add 1 for remainder and 10 for PSP
mov ah,31h ; terminate and stay resident
int 21h

END ProgramStart

Other Typical ISRs

The timer and keyboard are but two examples of useful interrupts that can be used modified or overwritten to be of use in a TSR. In theory, any ISR can be changed to modify the way in which the OS works. Some typical examples are:
  • rewriting int17 so that all data sent to the printer goes into a file instead
  • forward-chaining to int13 so that the hard drive is write-protected
  • rewriting int5 so that Print-Screen commands print out the contents of the screen as well as send a formfeed to the printer so that it realigns with the top of the next page.