Industrial Training




The First TSR



Suppose we want write a program which changes all capital letters on the screen to small case letters and all small case letters to capitals. To achieve this we must check the ASCII values of the characters present on the screen. These ASCII values are available in VDU memory which begins at address B8000000h if the VDU is CGA, EGA, VGA or SVGA and at address B0000000h if the VDU is MA. Note that for every character on the screen there exists a pair of bytes in VDU memory. The first byte contains the ASCII value of the character whereas the next contains its colour. Naturally, in this program we will have to access every alternate byte in VDU memory to bring about the change on the screen. Here is the program:

main( )

{

our( ) ;

}

our( )

{

char far *scr = ( char far * ) 0xB8000000L ;

int i ;

while ( !kbhit( ) )

{

for( i = 0 ; i <<= 3999 ; i += 2 )

{

if ( * ( scr + i ) >>= 'A' && * ( scr + i ) <<= 'Z' )

* ( scr + i ) += 32 ;

else

if ( * ( scr + i ) >>= 'a' && * ( scr + i ) <<= 'z' )

* ( scr + i ) -= 32 ;

}

}

}

When our( ) gets called from main( ), first time through the for loop all capitals on the screen are changed to small case and vice versa. The indefinite while loop ensures that this effect is immediately reversed thereby making characters small case one moment and capitals the next. This change takes place so fast that it gives an illusion of the characters dancing on the screen (to your tune). This activity is similar to that of a commonly found virus called 'Dancing Dolls'.

How long would this effect persist? Till you don't press a key from the keyboard. Moreover, once we quit out of this program and try something else like say a DOS command the effect is no longer evident. What if we are to ensure that all characters on the screen are changed once for every key pressed, irrespective of the software that we are working with? We will have to hook up this activity with something which we commonly do while working with any software, like say hitting keys. To do this we will have to carry out two tasks:

(a)Change the contents of the IVT such that location number 36, 37, 38, 39 contain the address of function our( ), instead of the normal ROM-BIOS routine.

(b) Make the program resident in memory even when its execution is terminated. How to achieve these two tasks is shown in the following program:

#include "dos.h"

void interrupt our( ) ;

void interrupt ( *prev ) ( ) ;

char far *scr = ( char far * ) 0xB8000000L ;

main( )

{

unsigned long int far *p ;

p = ( char far * ) 36 ;

prev = *p ; /* save the existing address */

*p = our ; /* set up new address */

keep ( 0, 500 ) ; /* make the program resident in memory */

}

void interrupt our( )

{

int i ;

for ( i = 0 ; i <<= 3999 ; i += 2 )

{

if ( * ( scr + i ) >>= 'A' && * ( scr + i ) <<= 'Z' )

* ( scr + i ) += 32 ;

else

if ( * ( scr + i ) >>= 'a' && * ( scr + i ) <<= 'z' )

* ( scr + i ) -= 32 ;

}

( *prev )( ) ;

}

Let us begin with main( ) . Using the variable p firstly the contents of location numbers 36, 37, 38 and 39 in IVT are saved in the variable prev. Then these locations are set up with the address of the function our( ) . Note that p has been declared as an unsigned long int pointer. This is because the address in IVT is 4 bytes long. Why are we using locations 36, 37, 38 and 39 ? This is because when the keyboard interrupt ( interrupt number 9 ) occurs, the interrupt number 9 is multiplied by 4 and then the address present in bytes 36 to 39 is picked up.The program is made resident by calling a standard library function keep( ) . The keep( ) function in the TSR tells DOS, "I am through for now, but leave me around I have more work to do later on". The first parameter used in keep( ) indicates the exit status. This status is usually 0. The second parameter tells DOS how much memory to reserve for our program. While allocating memory DOS allocates one paragraph (16 bytes) of memory at a time. This means in our program we are requesting DOS to set aside 500 paragraphs (500 * 16 bytes) for storing our program. Once allocated for our program this part of memory will never be given to any other program by DOS. In effect, on execution, our program would become resident in memory and would remain there even when the execution of the program is terminated.

Remember to compile the program in Turbo C using F9, quit out to DOS prompt using Alt-X and then execute the EXE file of our program.

Once executed, let us see what would happen when we hit a key from the keyboard. Firstly, interrupt number 9 would be generated, then it would be multiplied by 4 resulting into 36. Next the address present in locations 36 to 39 would be picked and control would be passed to this address. We have already changed this address such that it points to our( ) in place of the normal ROM-BIOS routine. Once the control reaches our( ), the ifs within the for loop would check all characters on the screen. If any of the characters are in upper case then 32 is added to them. If any are in lower case then 32 is deducted from them. Why 32? Because all capital characters fall in the range 65 to 90. If we add 32 to any of them we get the corresponding lower case characters. The opposite happens when 32 is deducted from the ASCII value of a lowercase character.

At the end, the control would be handed over to the normal ROM-BIOS routine through the statement,

( *prev )( ) ;

It is necessary that the ROM-BIOS routine be called since it is only this routine which knows what is to be done when a key is hit from the keyboard. If not called, the key hit would not be processed at all resulting in the computer getting hung.

The interrupt Function Modifier

Examine how prev has been declared:

void interrupt ( *prev )( ) ;

The * indicates that prev contains an address, hence is a pointer. The empty parentheses at the end imply that prev is a pointer to a function. Since this function doesn't return anything, the void has been used. The keyword interrupt indicates that prev contains address of that function which is to get called whenever an interrupt occurs. In short, prev is a pointer to an ISR. Remember that the parentheses surrounding prev are necessary. Without these parentheses the declaration would mean that prev is a function which returns a pointer of the type void interrupt. Since there doesn't exist a pointer of the type void interrupt * this would result into an error.

When we declare a function to be of type interrupt it tells the compiler to preserve all registers values and to terminate the function with an IRET instruction. An interrupt function is passed all registers as arguments. However, if we don't need these arguments we can simply ignore them by not collecting them while defining the ISR function. This is what we have done while developing our( ).

Variables in TSRs

You must be wondering why we have declared all variables in our TSR as global. For this we will have to understand how the different components of a program are laid out in memory, when the program is executed. Figure 2 shows this map for a small-model Turbo C program. For medium and large memory models the layout is different. Since we are going to compile all programs in small memory model the reader is referred to the Turbo C/Borland C manual for finding the variations in compact and large memory models.




From the figure you can observe that at the beginning there exists a 256-byte entity called Program Segment Prefix (PSP). It contains information necessary to initiate, run and terminate a particular process (process is a program running in memory). Following the PSP you find all the instructions of your program. This is the so called `Code Segment'. At the next highest paragraph after the code segment, you find the data of your program. This is called the `data segment', and its address is stored in the DS register. In small memory model the data segment consists of the following:

(a) Initialised external and static variables

(b) Uninitialised external and static variables

(c) Near Heap

(d) Stack

Let us just note that all local variables used in our program are created in the stack whereas all dynamic memory allocations (done by functions like malloc( ) , calloc( ) etc.) take place from the near heap. The near heap is so called because it can be reached by an offset from the segment address in DS.

When memory allocation is requested from an area beyond the data segment it is allocated from the far heap. When our TSR is active it uses the stack of the foreground program. Hence all local variables in our TSR would be created in the foreground program's stack. We cannot imagine how big a stack would the foreground program have. Hence if our TSR uses the stack extensively (by having many local variables and a lot of function calls), in all probability the foreground program's stack would overflow, resulting in a program crash. Hence, it is necessary that we declare all variables as global, since these variables are not created on the stack.

Ideally a TSR should build its own stack when it becomes active. If it does so it would become all the more necessary to declare variables as global since the local variables would be created in the foreground program's stack, and a stack switch would make them inaccessible.

That brings us to the end of our first TSR. As we examine more TSRs you will realize that writing a TSR is just a matter of stealing (capturing) interrupts, reassigning the address in the IVT to point to the function we want executed and keeping the program resident in memory. The interrupt vector table changes made by the TSR are the only means it has of getting control of the PC again. Without that step in its initialization, no TSR can ever do anything useful for you.



Hi I am Pluto.