Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Compiler-Specific C Extensions


AUG92: COMPILER-SPECIFIC C EXTENSIONS

This article contains the following executables: TSRPLUS.ARC

Al is a contributing editor to DDJ and can be contacted at 411 Borel Ave., San Mateo, CA 94402.


Extensions to the C and C++ languages take many forms, depending on what the extenders have in mind. The ANSI C committee includes the Numerical C Extensions Group, whose task it is to define numerical extensions to the language beyond the ones already built in. Other language extensions support particular development platforms. For example, Borland's Object Windows Library uses an extension to C++ class definition that supports the declaration of a message-response member function.

The Borland C++ compiler includes a number of other extensions to the C language that support DOS systems programming, that branch of programming that includes device drivers, memory-resident programs, and other low-level activities. To illustrate the use of the extensions, I'll describe TSRPLUS, a public-domain swapping terminate-and-stay-resident (TSR) driver that compiles with all versions of Borland and Turbo C. It is the resident part of a TSR application that swaps the memory of the interrupted program for the TSR application's image, executes the TSR application, and swaps the original program back in. The source code includes the TSRPLUS driver and a brief application program to serve as an example. Because of its length, the source to TSRPLUS is not printed in this issue, but is available electronically under the filename TSRPLUS.ARC.

Note that this article is not a treatise on portable code. The code that you write with these techniques is completely nonportable to other computer architectures and other operating systems. It is mostly nonportable to other compiler products. And in some rare cases, it is potentially nonportable to past and future versions of Borland C (BC). When you do systems programming this close to the hardware and operating system, portability is the least of your concerns. Similarly, this discussion does not address how TSRs work. There are a number of good works on this subject. An understanding of what allows a TSR to pop up and what rules it must obey is helpful here but not necessary. Where I discuss those issues, it is only to illustrate how I have used the language extensions of Borland C++ to solve their problems. You can learn about TSRs in several of my books and in Andrew Schulman's more recent and comprehensive Undocumented DOS (Addison-Wesley, 1990).

Most of the BC systems-programming extensions have been in the compiler since the first version of Turbo C. They provide the programmer with close access to the hardware, BIOS, and operating system. The extensions include register pseudovariables, inline functions, the interrupt function type, and inline assembly code. With them, a programmer can avoid most of the assembly language functions that systems programs normally must call when standard C can neither reach the hardware nor meet the timing performance requirements of the problem at hand.

BC and other compilers include functions that support access to BIOS and DOS in their runtime libraries. These include such functions as int86, bioskey, getvect, and so on. There is no standard for the types, names, and parameters of these functions, but the BC library includes versions compatible with their earlier compilers as well as versions compatible with Microsoft C. Why use language extensions instead of the library functions? In most cases you can achieve the same results by using the language extensions, and you will have smaller, faster code. Sometimes you want to do something for which there is no library function. Some library functions reference other functions or global variables that force other object modules to be linked as a side effect, if not in the latest version of the compiler, then perhaps in a future version. This can cause the executable code to be larger than it needs to be. Using the language extensions can bypass such side effects. Some library functions depend on features provided by the start-up code, such as stack, heap, and environment variable pointers. Later, we'll replace the startup code to get the smallest possible program, and we will not use those things. Calling some library functions from this program would result in unresolved references when you link.

Register Pseudovariables

BC's register pseudovariables are fixed variables that directly address the microprocessor's registers. Their names include _AX, _BX, _ES, _FLAGS, and so on. You can assign an integral value--including address segments and offsets--to one of these pseudovariables, which puts the value into the corresponding hardware register. You can use a pseudovariable in an expression, and its contents are treated as if they came from an unsigned int.

When would you want to use register pseudovariables? A common use is to send parameters to interrupts and to read the results that interrupts return in registers. We will do some of that later. But you must be careful. The compiler assumes not only that you know what you are doing but that you know what it is doing as well. The compiler itself uses registers in many different ways. Your use of a register and the compiler's use of the same register must not conflict. In some cases, the compiler is smart enough to see that you are using registers and it will avoid their use. For example, the compiler can use hardware registers for automatic variables. If you use the corresponding register pseudovariables, the compiler will decide not to use them and will put the automatic variables on the stack frame or in registers that you do not use.

In other cases, the compiler is not so smart. Sometimes it seems to get less so with successive versions of the compiler. For example, the code fragment in Example 1(a) compiles correctly with Turbo C 2.0, but not with Borland C++ 3.0. To see why, we can use the -S command-line option to look at the compiled assembly language code from the two compilers. Turbo C 2.0 generates the code shown in Example 1(b), and Borland C++ 3.0 generates that shown in Example 1(c).

Example 1: (a) This code fragment compiles correctly with Turbo C 2.0, but not with Borland C++ 3.0; (b) code generated by Turbo C 2.0 using the -S command-line option; (c) code generated by Borland C++ 3.0 using the -S command line option; (d) code to resolve the problem of the compiler assigning registers when there is no corresponding mov instruction.

  (a)
  _AX = 123;
  _ES = _DS;

  (b)
  mov ax,123
  push ds
  pop es

  (c)

  mov ax,123
  mov ax,ds
  mov es,ax

  (d)
  _ES = _DS;
  _AX = 123;

In the TC example, the compiler uses a push and pop to assign DS to ES. In the BC example, the compiler moves DS to ES through AX, the same register to which you just assigned a value. Your value is overwritten before you have a chance to use it. The older compiler is no smarter than the newer one with respect to which pseudovariables you used. It just uses a different technique to assign registers where the machine language has no corresponding mov instruction. In this case, the newer technique generates a conflict between your use of the _AX pseudoregister and the compiler's use of the AX register. Example 1(d) shows how you can fix the code. Now, both compilers generate correct code. That does not mean, however, that future versions of the compiler will not find another way to trip up your use of register pseudovariables. At all times, use register pseudovariables only when you know exactly what their use will lead to.

Inline Functions

BC has several macros that generate inline code. With them you can directly access memory, interrupts, and hardware devices. The compiler generates inline code for the machine instructions that perform the tasks of the macros.

The geninterrupt macro takes an interrupt number as an argument and executes the corresponding int machine instruction. You normally use register pseudovariables in conjunction with this macro. For instance, you can allocate a block of DOS memory with the code in Example 2. This is an example of how the BC language extensions generate code as good as you can write in assembly language. You might suspect that the FLAGS & 1 expression will use the AX register, thus interfering with the AX assignment that follows, but not so. The compiler codes a simple JC (jump if carry) opcode for the expression.

Example 2: Allocating a block of DOS memory.

  _AH = 0x48;             // Allocate memory function
  _BX = 10;               // # of paragraphs to allocate
  geninterrupt (0x21);    // call DOS
  if ((_FLAGS & 1) == 0)  // test carry bit
      segment = _AX;      // segment of the allocated block
  else
      // ... error ...

The inport, inportb, outport, and outportb macros read and write hardware I/O ports. Their most common use is to access the interrupt controller and read the keyboard port. If you are using BC to write a device driver for a custom hardware device, you could make extensive use of these macros.

The enable and disable macros generate the sti and cli machine instructions to enable and disable interrupts. You will use them whenever you need to suspend and resume interrupts for any reason. Interrupts are normally enabled when a program is running. Sometimes you will need to disable interrupts so that you may do something that cannot be interrupted. For example, anytime you change the stack segment and pointer registers, you should disable interrupts so that an interrupt does not occur while the stack integrity is compromised. In Example 3, the interrupt-enabled condition is controlled by a bit in the FLAGS register. When an interrupt occurs, it pushes the FLAGS and the CS:IP registers on the stack, disables interrupts, and writes the contents of the interrupt vector into the CS:IP registers. That starts the interrupt service routine (ISR) running with interrupts disabled. The iret instruction from the ISR pops the flags and the registers, which enables interrupts and returns to the interrupted location. The result is that most ISRs execute with interrupts disabled. If you are writing an ISR that involves extensive processing, you will need to enable interrupts from within the ISR. Pop-up TSR programs are examples of programs that execute as the result of an interrupt. If you did not enable interrupts before running the TSR program, it would not be able to use any of the system services.

Example 3: The interrupt-enabled condition is controlled by a bit in the FLAGS register.

  disable();
  _SS = oldss;   // interrupts must not occur now
  _SP = oldsp;   // otherwise SS and SP will be wrong
  enable();

There are macros that allow you to retrieve and write the contents of memory by direct access to the memory address. They are the peek, poke, peekb, and pokeb macros. With them you specify the segment and offset of the address. The peek and peekb macros return the contents of the memory word or byte. The poke and pokeb macros accept a word- or byte-value parameter that they write into the memory location. You will use these macros primarily to read and write video memory and the BIOS data areas. There are other far locations that you will need to read and write, such as DOS memory blocks, and you will usually use far pointers or the movedata function to access them.

The getvect and setvect functions read and write the contents of the specified interrupt vectors. You will use these to hook your ISR to an interrupt and to chain your ISR to the previous holder of the interrupt vector. Example 4(a) shows the initialization code for your program. Your ISR, which now executes when the interrupt occurs, will chain to the old interrupt the way shown in Example 4(b). It might do the chain first before it does its own processing of the interrupt; it might do it last; or it might do it only if certain conditions are satisfied. Some ISRs will not chain the interrupt at all.

Example 4: (a) Initialization code; (b) chaining to the old interrupt; (c) returning the value of the original interrupt vector.

  (a)
  void interrupt (*oldISR)(void);
  oldISR = getvect(VECTOR);
  setvect(VECTOR, newISR);

  (b)
  (*oldISR)();

  (c)
  setvect(VECTOR, oldISR);

Before your program terminates, it must return the original value to the interrupt vector using the call shown in Example 4(c).

The Interrupt-function Type

The interrupt-function type tells the compiler to compile the function as an ISR. An interrupt function assumes that it is being called as the result of an interrupt, perhaps from an unrelated process. It may assume nothing about the values of registers. Therefore, upon entry, the interrupt function pushes all the registers on the stack, initializes the DS register to point to the program's DGROUP segment, and sets up the stack frame in the BP register. Upon exit, the interrupt function pops all registers from the stack and executes the iret machine instruction to return to the interrupted location.

Often an ISR needs to read the values of registers to get its parameters and needs to return its results in registers. Up to a point, you can read the parameters with the register pseudovariables. This works only as long as the compiler does not use the same registers itself. You cannot return values from the ISR by writing to the register pseudovariables because the interrupt function pops the original values into the registers just before it returns.

When the interrupt function pushes all the registers, it leaves them on the stack just as if the function had been called with those registers as parameters. You can declare the function with the IREGS data type, see Example 5(a), in its parameter list. The interrupt function declaration looks like Example 5(b). You can read the value of the registers upon entry by reading the corresponding members of the structure, as in Example 5(c). You can change the value of registers that will be returned to the caller by changing the contents of the structure members, as in Example 5(d).

Example 5: (a) Declaring the function with the IREGS data type in its parameter list; (b) the interrupt-function declaration; (c) reading the value of the registers upon entry by reading the corresponding members of the structure; (d) changing the value of registers that will be returned to the caller by changing the contents of the structure members.

  (a)
  typedef struct {
      int bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl;
  } IREGS;

  (b)
  void interrupt newISR(IREGS ir)
  {
     // ...
  }

  (c)
  if (ir.ax == 5)
      // ....

  (d)
  ir.ax = 3;    // return 3 in ax
  ir.fl |= 1;   // return the carry bit on

When the return sequence pops the values back into the registers, these new values will go from the structure on the stack frame into the machine registers.

If you are chaining to an old ISR, you must make sure that the registers on entry are preserved and that the registers on exit are put into the copy of the structure on the stack frame. If you do any processing before the chain, you must reset the real registers from the stack frame. Example 6(a) illustrates how you do that. If the old ISR returns some values in registers that the caller needs to receive, you must make similar provisions upon the old ISR's exit, as in Example 6(b).

Example 6: (a) Resetting the real registers from the stack frame; (b) making similar provisions upon the old ISR's exit; (c) handling the case when oldISR uses the DS register; preserving the register.

  (a)
  void interrupt newISR(IREGS ir)
  {
     // do some processing that might change registers
     _AX = ir.ax; // restore the registers that the old ISR needs
     _BX = ir.bx;
    (*oldISR)();  // chain to the old isr
  }

  (b)
  void interrupt newISR(IREGS ir)
  {
     (*oldISR)();          // chain to the old isr
     ir.ax = _AX;          // caller will get ax
     ir.fl = _FLAGS;       // and the flags
  }

  (c)
  void interrupt newISR(IREGS ir)
  {
     void interrupt (*tmpISR)();
     tmpISR = oldISR; // use function pointer on the stack
     _DS = ir.ds;     // reset DS
     (*tmpISR)();      // chain to the old isr through tmp ptr
  }

  (d)
  void interrupt newISR(IREGS ir)
  {
     void interrupt (*tmpISR)();
     int oldds = _DS; // save ISR's DS
     tmpISR = oldISR; // use function pointer on the stack
     _DS = ir.ds;     // reset DS
     (*tmp ISR)();    // chain to the old isr through tmp ptr
     _DS = oldds;     // reset DS
     // ... do further processing
  }

If the old ISR uses the DS register for input, you need an additional step. The call to oldISR is through a pointer in the data segment of the ISR. If you change the DS register, the call will fail, and the program will probably crash. Example 6(c) shows you what you then must do. If your ISR does some processing after the chained interrupt service returns, you must take steps to preserve the DS register, as in Example 6(d).

There is one more tricky aspect to all this. Some ISRs, most notably the VGA BIOS, receive parameters and return values in the BP and DS registers. You must intercept and chain these ISRs with a separately compiled assembly language program that maintains its function pointer in the code segment rather than in the data segment or the stack frame. You should similarly intercept any chained interrupts where there is no comprehensive standard for their use. The 0x2f interrupt is an example of such an interrupt.

An interrupt function does not have to be executed as the result of an interrupt. You can call an interrupt function from within your program. The compiler generates a pushf instruction followed by the far call to the function, emulating what happens when an interrupt occurs. You can use this behavior to advantage. The swapping TSR driver stores the address of an interrupt function in the TSR's application module. The interrupt function is the application's pop-up entry point. After swapping the application module into memory, the TSR driver will execute it by calling the entry code through the interrupt-function pointer. The interrupt function's entry logic prepares the function for execution by setting up the registers.

Inline Assembly

BC recognizes the asm keyword, which tells it that the statement is an assembly language instruction. The compiler inserts the instruction into the object code. You can use the names of C variables in these instructions, and the compiler will generate the proper references. You may not use the asm keyword to declare a variable or access things like DGROUP. As with register pseudovariables, you must know what you are doing when you use inline assembly code.

A Swapping TSR Driver

Now we will put these techniques to use. The accompanying program is an example that uses the principles we have been discussing. It is a swapping TSR driver program called TSRPLUS. I originally developed its concept in 1989 and published it in a book called Extending Turbo C Professional. Since then, I have modified the program several times and used it in many programs. There are several versions of TSRPLUS, each of which handles the swapping problem differently. This version installs a 5K TSR driver program that swaps in a much bigger pop-up application when the user presses the hot key.

A swapping TSR consists of two pieces: the permanently resident TSR driver and the transient pop-up application image. The resident part watches the keyboard interrupt for the hot key and manages memory swapping. The transient part does the job of the pop-up application. The swap file can be any mass storage medium. If you can use EMS, XMS, or a RAM disk, the swapping operation is faster. If you use a disk file, the swapping operation will take longer, depending on the sizes of the pop-up application image and interrupted program.

The pop-up application image and the swapped-out interrupted program include copies of the interrupt vectors. This is because an interrupt vector might have been hooked by and point into the program that you are going to temporarily replace. If you do not restore the interrupt vectors to a condition compatible with the pop-up program and the hooked interrupt occurs, the system will crash.

The Pop-up Application

The pop-up application program is a normal DOS program that does not use any of the DOS functions from 0 to 12 and does not spawn other programs by calling DOS. It links with a module that allows it to register itself with the TSR driver program as a pop-up program. The module supplies the program's main function. The program must provide three functions: one for any initialization code that the program needs and that calls the register function; one to be executed upon pop-up from the hot key; and one to be executed if the user runs the pop-up program from the command line without the TSR driver being resident.

Other than those differences, the pop-up application program looks like any other DOS program. It is an .EXE file that will execute as a command-line program or that can be a pop-up.

You load the swapping TSR program into memory by running the TSR driver program. It sets itself up to be a TSR and then calls DOS to execute the pop-up program. DOS loads the pop-up program into memory just above the TSR driver. The pop-up program does its initialization and then calls into the TSR driver to register itself. The registration includes a far pointer to the application's pop-up entry address. The TSR driver writes the pop-up program's image and the current interrupt vectors to the swap file. The driver returns to the pop-up program, which exits, returning to the TSR driver. The TSR driver terminates, declaring itself resident.

When the user presses the hot key, the TSR driver handles all the tests and context switching necessary to pop up a TSR. Then it swaps the interrupted program from memory to the swap device, reads the pop-up application image into memory, and calls the pop-up application to execute. When the pop-up application returns to the TSR driver, the driver reads the swapped image of the interrupted program back into memory and returns to the interrupted location.

Swapping

There are two ways to swap memory. One way swaps just as much memory as the pop-up program needs. This method leaves the DOS memory control block chain in disarray as long as the pop-up program is running. The pop-up program cannot allocate any DOS memory when you use this technique. The other way swaps the entire memory-control block chain-out. If you are at the DOS command line when you press the hot key, very little memory swaps out. If you are running a large program, a lot of memory swaps out. The pop-up program loads up to and including its terminating MCB. It can, therefore, allocate and deallocate DOS memory while it is resident. The example program uses this strategy.

When the pop-up program first runs, it tests to see if the TSR driver is resident. If so, the pop-up program registers itself in the manner just described. If not, it can run as a DOS command line program. To the user, the only difference is the way that the program is executed. This approach allows the same .EXE file to work in the TSR environment, from the command line, or in a window of a multitasking environment such as Desqview or Windows.

Memory Organization

The memory organization of a C program and the memory requirements of a TSR are incompatible. The typical C program consists of all the code, followed by all the data, followed by the heap and stack. A TSR contains initialization code and resident code. Ideally , you would release the memory occupied by the initialization code to DOS when the TSR issues the terminate-and-stay-resident function call. TSRs written in assembly language can do that with little trouble. To get the same effect in C requires some manipulation of the startup code and the order in which things link.

Borland C's startup code does a lot of things for you. It sets up the heap, the stack, the global variables, the divide-by-zero handler, the pointer to the environment variables, the arguments to main, the file-handle table, and the external uninitialized data space. After everything is set up, the startup code calls your main function. When the main function returns, the startup code cleans every thing up, tests for null-pointer assignments, and returns to DOS. In the process of doing all this, the startup code declares some public variables and procedures and refers to some others from the runtime library.

You get the source code to the startup code with the compiler. It is in a file named C0.ASM. Most of what it does is not necessary for the TSR-driver program. The TSR driver does not use a heap, parses its own command-line arguments, and finds its own environment variables. Therefore, the TSR driver uses a highly modified version of the startup code, C0T.ASM. The modified startup code declares the program's starting address, sets up the segment registers and the global _psp variable, sets the external uninitialized data space to 0s, and calls the main function. That's all it does, because that is all the TSR driver needs in the way of startup code.

The TSR executes its initialization code and retains only the resident part when it becomes a resident program. This is not so easy because the compiler and linker organize all the code ahead of the data. To separate the initialization code from the resident code and retain the data segment for the resident program, you must first change the order in which the linker builds the segments. You need the stack and data to come ahead of the code. That way you can truncate the initialization code without losing any of the data space.

The linker determines the order of segments based on the order of their declaration in the first object file it encounters. Remember that we changed the startup code. That code will not be the first module seen by the linker because the startup code is toward the end of the code segment so that its memory is returned to DOS.

There are two other assembly language modules in the TSR-driver program. One of them, INT2F.ASM, contains the assembly code for the video and 0x2f ISRs. This module must be the first one that the linker sees. The code in Example 7 at the front of the module will cause the linker to arrange the segments the way we want them.

Example 7: The code at the front of the module will cause the linker to arrange the segments.

  _data    segment para public 'data'
  _data    ends
  _bss     segment word public 'bss'
  _bss     ends
  _bssend  segment byte public 'bss'
  _bssend  ends
  _stack   segment stack 'stack'
  _stack   ends
  _text    segment byte public 'code'
  _text    ends

The next problem we run into is that the linker will link all our code followed by the functions from the runtime library into the code segment. We need to separate the initialization code from the resident code and get the runtime-library code loaded between the two parts. We will handle that operation in the makefile, as shown in Example 8.

Example 8: Using the makefile to separate the initialization code from the resident code and get the runtime-library code loaded between the two parts.

  tsr.exe : tsr.obj emm.obj xms.obj int2f.obj tsrinit.lib
    tlink /m /s int2f emm xms tsr,tsr.exe,tsr,$(CLIB) tsrinit

  tsrinit.lib : c0t.obj tsrinit.obj emminit.obj xmsinit.obj init.obj
    tlib tsrinit +tsrinit.obj +c0t.obj +emminit.obj +xmsinit.obj +init.obj

The makefile says that the TSR.EXE program depends on the object files that constitute the resident part of the TSR driver program and the tsrinit.lib library file. The TSRINIT.LIB file contains the object files for the TSR driver's initialization code. The TSRINIT.OBJ file must be first, and the INIT.OBJ file must be last in this library. These files, besides anything else they might contain, have the addresses of the beginning and end of the initialization code.

The tlink command links the resident code first, then searches the C runtime library, and finally searches the TSRINIT library. This sequence arranges the source modules in the code segment the way we want them.

Operation

You run the TSR driver program from the command line and it loads the swapping pop-up application. The TSR driver has several command line switches that modify its behavior. These are: -x, don't use XMS for the swap file; -e, don't use EMS for the swap file; and +p<path>, the DOS path for the disk swap file.

If you use -x and -e and do not specify a path, the TSR driver will write the swap file to the subdirectory specified by the TEMP environment variable.

Because of the way the TSR driver program and the pop-up application interact with memory and the DOS memory-control block, you cannot load the program into high memory. If you did, the system would probably crash. Rather than allow that to happen, the TSR driver program tests to see if it is loaded high, as in Example 9(a). The TSR driver program hooks and chains several interrupts with the table and macro statements in Example 9(b). This example shows how the C preprocessor can emulate the C++ inline function to a limited extent. The program calls the newvectors macro and passes the address of the table using newvectors(vectors);.

Example 9: (a) Testing to see if the program is loaded high; (b) hooking and chaining several interrupts with the table and macro statements.

  (a)
  if (_CS > 0xa000) {
      dispstr("\a\r\nCannot loadhigh");
      return;
  }

  (b)
  EXTERN struct vectors {
      int vno;
      void (interrupt **oldvect) (void);
      void (interrupt *newvect) (void);
  } vectors[] =    {
          {TIMER,      &oldtimer,  (void interrupt (*)())newtimer},
          {INT28,      &old28,     (void interrupt (*)())new28},
          {KYBRD,      &oldkb,     (void interrupt (*)())newkb},
          {DISK,       &olddisk,   (void interrupt (*)())newdisk},
          {VIDEO       &oldvideo,  (void interrupt (*)())newvideo},
          {TSRPLUSINT, &old2f,     (void interrupt (*)())new2f},
          {0,          NULL,       NULL}};

  #define newvectors(vecs)                   \
  {                                          \
      register struct vectors *vc = vecs;    \
      while (vc->vno)    {                   \
          *(vc->oldvect) = getvect(vc->vno); \
          setvect(vc->vno, vc->newvect);     \
          vc++;                              \
      }                                      \
  }

There is an equivalent oldvectors macro that restores interrupt vectors from the table. The program uses another table to hook and restore the critical interrupt, break, and ctrl+c interrupt vectors.

Registering the Application

The TSR driver program executes the pop-up application program so that the pop-up can register itself as a swapped TSR. First the TSR driver program reduces its own size by calling the DOS 0x4a function to change the size of the PSP's memory-control block, as shown in Example 10. This step is necessary because DOS assigns to a program's PSP all available memory, leaving no room to run the pop-up application. In a normal C program, the startup code takes care of this reduction, using the sizes of the stack and heap to determine the size of the program. The TSR driver's startup code does not, however, so the driver does it here.

Example 10: The TSR driver program reduces its own size by calling the DOS 0x4a function to changes the size of the PSP's memory-control block.

  /* ------ compute program size ------- */
  highmemory = _CS + ((unsigned)&codeend / 16);
  sizeprogram = highmemory - _psp;

  /* ------ adjust MCB for TSRPLUS ------- */
  _ES = _psp;
  _BX = sizeprogram;
  _AX = 0x4a00;
  geninterrupt(DOS);

The program computes its new size by adding the paragraph address of the codeend label--the last entry in the code segment--to the value in the code-segment register. That value is the segment address of the top of the program. By subtracting the address of the PSP from the high address, the program computes the minimum paragraph size in which it can execute. By telling DOS to reduce itself to that size, the program assures that the next program run will load as close to it as possible.

Next, the TSR-driver program uses the DOS 0x4b function to execute the pop-up application program. The pop-up application program was linked with the tsrbuild.c code, which calls the TSR-driver program with a 0x2f interrupt to register itself. It passes the address of a structure that contains its entry point and PSP address. The TSR-driver program records this information and makes a copy of the pop-up program's interrupt vectors and program image on the swap file. It returns to the pop-up application which terminates, returning control to the TSR-driver program.

Terminating and Staying Resident

Now the TSR-driver program computes a new size for itself a second time. This size will truncate the initialization code when the TSR driver becomes resident. The new size is computed as the distance from the PSP address to the main function, as shown in Example 11(a). The main function is the first function in the initialization code and is, therefore, at the address of the top of the resident code. With the size of the resident portion computed, the TSR can terminate and declare itself resident, as shown in Example 11(b).

Example 11: (a) The TSR driver program computes a new size for itself as the distance from the PSP address to the main function; (b) with the size of the resident portion computed, the TSR can terminate and declare itself resident.

  (a)
  sizeprogram = _CS+((unsigned) main >> 4)-_psp;
  if ((unsigned) main % 4)
      sizeprogram++;

  (b)
  _DX = sizeprogram;
  _AX = 0x3100;
  geninterrupt(DOS);

Now that the TSR driver resides in memory and an executable image of the pop-up application program is stored in the swap file, the system can return to the DOS command line and run other programs. When the user presses the hot key, the program can pop up. After the TSR driver is resident, its keyboard ISR watches the keyboard-input port and the word in the BIOS data space that stores the current shift key's state, like Example 12 . This interrupt function uses several of the Borland C extensions. It reads the value of the keyboard-input port by calling the inportb macro. It reads the BIOS data area's shift-state mask by calling the peekb macro. It resets the keyboard hardware and the interrupt controller by calling the outportb macro. And it chains to the old keyboard ISR by calling it through an interrupt-function pointer. This function does not pop the application program up. It simply sets a flag that says the user pressed the hot key. The hooked timer and 0x28 ISRs pop up the program when the hot-key indicator is set and DOS is in a safe and stable condition for a pop-up.

Example 12: After the TSR driver is resident, its keyboard ISR watches the keyboard-input port and the word in the BIOS data space that stores the current shift key's state.

  /* ----- keyboard ISR ------ */
  static void interrupt newkb(void)
  {
      unsigned char kbval = inportb(0x60);
      if (!hotkeyhit && !running)    {
          if (Keymask && (peekb(0, 0x417) & 0xf) == Keymask)
              if (Scancode == 0 || Scancode == kbval)
                  hotkeyhit = TRUE;
          if (hotkeyhit)    {
              /* --- reset the keyboard ---- */
              kbval = inportb(0x61);
              outportb(0x61, kbval | 0x80);
              outportb(0x61, kbval);
              outportb(0x20, 0x20);
              return;
          }
      }
      (*oldkb)();
  }

After it is safe to pop up, the TSR-driver program switches the DOS context from the interrupted program to itself. First it changes the stack. The interrupted program might not have a deep enough stack, and the swap will probably write over that space anyway. Changing the stack is simple. The initialization code saved the stack's segment and pointer register values before the TSR-driver program became resident. The code in Example 13 saves the interrupted stack location and changes it to the TSR driver's stack. It is not necessary to disable interrupts to protect the stack's integrity. Interrupts are currently disabled because the program is operating from within an ISR. The TSR driver saves and resets the DTA, PSP, and Ctrl-Break setting. Then it starts the swap sequence.

Example 13: Save the interrupted stack location and change it to the TSR driver's stack.

  intsp = _SP;
  intss = _SS;
  _SP = tsrsp;
  _SS = tsrss;

To swap the interrupted program and the pop-up application program, the TSR driver uses this sequence: It writes the interrupted program's memory to the swap file. Then it writes the system-interrupt vector table to the swap file. Next it reads the pop-up application program's interrupt vector table. Finally it reads the pop-up application program's program memory. This sequence is critical. The swapping input/output operations could enable interrupts. If the new code is in memory along with the old interrupt vectors, an unexpected interrupt could jump to the wrong place.

After the pop-up application program is swapped into memory, the TSR-driver program executes it by calling through the far interrupt pointer that holds the address of the application's entry location. That code is in the source file tsrbuild.c that the application links with. It does a similar context switch between the stacks, DTAs, and PSPs of itself and the TSR-driver program, and then it executes its application. When the application returns, the program switches the context back and returns to the TSR-driver program.

When the pop-up application returns to the TSR-driver program, the driver swaps the interrupted program back in, swapping the code first and then the interrupt vectors. Once again, this sequence is critical. The code for the interrupted program's ISRs must be in place before any interrupt vectors point to it. Next, the TSR driver switches the context of the PSP, DTA, and the stack, and returns to the interrupted program.

Unloading the TSR

The pop-up application program decides when to unload itself. It calls the TSR driver through the 0x2f interrupt with a function code that says the driver should unload itself. The driver sets a flag. When the pop-up application returns to pop down and after the driver has swapped the interrupted program back in, the driver tests to see if it can unload itself. That test makes sure that all the interrupt vectors are the same as they were when the TSR driver declared itself resident, and that there is no program loaded above the TSR driver in memory. In other words, the TSR-driver program can unload only when the TSR was popped up from the DOS command line and only when no other TSR programs are loaded above it. The test uses the peek and peekb macros to walk the DOS memory-control block chain and uses getvect to compare the contents of the interrupt vectors with their earlier values.

When Assembly is Needed

Sometimes you cannot get by without some assembly language. We've already discussed the startup code. There are two other assembly language modules that the TSR driver uses. You saw how the INT2F module controls the placement of segments. It also provides the ISRs for the 0x10 and 0x2f interrupts. These interrupts use registers in ways incompatible with the interrupt function. The assembly language ISRs chain to the old ISRs without disturbing register integrity.

The INIT module provides the _code-end variable, and it provides a function that copies the chained 0x10 and 0x2f interrupt vector contents into interrupt-function pointers in the code segment's address space. This allows the TSR driver's assembly language ISRs to chain without needing data-segment references to the pointers.

Conclusion

You debug the pop-up application program as a DOS command-line program by using the source-level debugger. I debugged the TSR driver by using Turbo Debugger with the load and execute of the overlaid pop-up application stubbed out. I debugged those parts in the old way--by displaying messages on the screen at critical places in the program. It was slow, but sometimes that's the best or only way.

Finally, the TSRPLUS source code includes an example pop-up application program. It is a simple D-Flat application, which means that it uses the user-interface library that I have been publishing in Dr. Dobb's Journal for the past year. Its purpose is to show you how to use the TSRPLUS driver. It doesn't do anything other than pop up and down and let you remove it from memory with a menu selection. Link it with the D-Flat library, version 12 or greater.


Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.