The QBNews Page 12 Volume 1, Number 4 September 3, 1990 ---------------------------------------------------------------------- P o w e r P r o g r a m m i n g ---------------------------------------------------------------------- Peeking at DOS with P.D.Q. by Ethan Winer When most programmers consider writing a TSR program, they immediately think of having to learn C or assembly language. DOS was never intended to support multiple applications operating simultaneously, and creating a TSR involves some extremely tricky programming. To answer this need my company, Crescent Software, has developed an add-on library for use with QuickBASIC and BASIC 7 that lets BASIC programmers write several types of TSR programs which take as little as 3K of memory when resident. P.D.Q. can also be used to write non-TSR programs as well, with .EXE file sizes starting at 350 bytes. Our purpose here, however, is to examine an interesting and useful utility that we provide as a TSR programming example with the product. A ready to run version of this program is also provided in the file DOSWATCH.EXE. P.D.Q. supports three different ways to create a TSR program using BASIC. The first uses a "simplified" method, whereby the programmer specifies a hot key, and a location in the program that is to be executed each time that key is pressed. Once the program receives control, it may freely open and access disk files, regardless of what the underlying application is doing at the time. Because DOS is not reentrant, this is normally a very difficult programming feat to accomplish. Indeed, entire books have been written about the various complexities of writing TSR programs. Therefore, much of the appeal of P.D.Q. is that it does all of the hard parts for you through the supplied assembly language routines. An example of this type of TSR is in the accompanying article for TSRTerm. The second type of TSR lets you manually trap system interrupts, and with the ease of BASIC accomplish virtually anything that could be done in assembly language. Of course, the programmer must understand how interrupt services are accessed, and I often recommend Peter Norton's excellent "Programmer's Guide to the IBM PC" published by Microsoft Press. The third type of TSR lets you combine the flexibility of manual interrupt handling with the safety of the simplified hot key methods. In the DOSWATCH program I will describe here we will manually intercept DOS Interrupt 21h (Hex) to provide a "window" into its inner workings. P.D.Q. TSR INTERRUPT HANDLING SERVICES There are many routines provided with P.D.Q. to support interrupt handling, and several of these will be described. Also, for each interrupt that a program will be handling, an 18-element TYPE variable must be defined. This TYPE variable is used to hold the CPU's registers, as well as the address and segment to use if the original interrupt will be accessed. The Registers TYPE variable is very much The QBNews Page 13 Volume 1, Number 4 September 3, 1990 like the TYPE variable BASIC requires when using CALL INTERRUPT, in that the various CPU registers may be assigned and examined. Several additional TYPE elements are also defined, which are specific to P.D.Q. For example, the segment and address of the BASIC interrupt handling code are stored there. We'll begin by briefly examining the P.D.Q. TSR routines used in DOSWATCH. The first is PointIntHere, and it indicates where in the BASIC program to begin executing when the specified interrupt occurs. Because BASIC does not provide a direct way to obtain the address of a particular statement or line, a call to PointIntHere must be immediately followed by a GOTO. The very next program statement will then receive control each time the specified interrupt occurs. The next two routines are IntEntry1 and IntEntry2, and these must be the very first two statements in the body of the BASIC interrupt handler code. These routines copy the current CPU register values into the TYPE variable, so they may be examined and set by your program. (Internally, IntEntry1 saves the current value of the AX register and establishes "DGROUP addressability", so variables within the TSR program will be accessed correctly. IntEntry2 then copies the remaining register values into the TYPE variable, and jumps to the correct location in the TSR program.) IntEntry2 also expects an "Action" parameter, which tells it what to do if another interrupt occurs before you have finished processing the first one. There are two options -- pass control on to the original interrupt handler, or ignore the interrupt entirely and simply return to the caller. The only situation in which a second interrupt could occur like this is when trapping hardware interrupts such as the timer tick or the keyboard or communications interrupts. While a program is processing the interrupt, it may call one of the three P.D.Q. interrupt service routines. The first of these is GotoOldInt, which passes control to the original interrupt handler. GotoOldInt is used when a program is intercepting only certain services, and the current service is not one of those. In that case you would want to pass control on to the original (or subsequent) handler. For example, if you are intercepting DOS interrupt 21h and care only about, say, service 4Eh, then you would call GotoOldInt for all of the other services so DOS could handle them. The second routine is CallOldInt, and this routine lets you call the original interrupt as a subroutine, and then receive control again when it has finished. This would be useful when intercepting the printer interrupt, for instance, perhaps to test the success of the last print attempt. The final interrupt handling routine is ReturnFromInt, which returns control to the underlying application. ReturnFromInt would be used when your program has processed the interrupt entirely by itself, and no further action is needed by the original handler. Neither CallOldInt nor ReturnFromInt are used by DOSWATCH, and these are The QBNews Page 14 Volume 1, Number 4 September 3, 1990 described solely for your interest. INSIDE DOSWATCH The DOSWATCH program shows how to intercept DOS Interrupt 21h, and it also serves as a good example of writing a general purpose interrupt handler using P.D.Q. DOSWATCH is a TSR program that monitors every call made to DOS Interrupt 21h, and then displays information about the current service that is being requested. The service number is printed in the upper left corner of the screen, and in many cases additional information is also shown. Watching DOS as it works can be very enlightening, and DOSWATCH lets you view not only the activity of your application programs, but also DOS itself. The DOSWATCH.BAS source listing is shown in Figure 1. DOSWATCH begins by including PDQDECL.BAS, which declares all of the available P.D.Q. extensions and defines the Registers TYPE variable. Next, a unique ID string is defined, which in this case is also the sign-on message. P.D.Q. uses the ID string internally to let you check for multiple installations. However, that feature is not exploited here for the sake of simplicity. Because DOSWATCH may receive control at any time, it is essential that the regular BASIC PRINT statement is not used. Unlike the simplified TSR method which allows nearly any BASIC statement to be used freely without regard to the current state of DOS or the BIOS, DOSWATCH must use the PDQPrint routine that writes directly to video memory. PDQPrint is a "quick print" routine, and it expects the row and column as passed parameters. Therefore, we must use CSRLIN and POS(0) to know where that is. (P.D.Q. does in fact allow TSR programs to perform nearly any service within a manual interrupt handler, but an additional call is needed and DOSWATCH doesn't truly need that feature.) The next two statements define the strings that will receive the information message, and the file or directory name when appropriate. The statements that follow establish several variables that will be used by the program. Using variables as parameters is always faster than constants, because BC.EXE generates extra code to store constants in memory each time they are used. Likewise, assigning Zero$ once eliminates repeated references to CHR$() each time INSTR is used later on in the program. This is true for both P.D.Q. and regular QuickBASIC. Although it may not be obvious, using INSTR(DOSName$, Zero$) is faster and creates less code than INSTR(DOSName$, CHR$(0)), because CHR$() is actually a called routine. A few microseconds either way is unlikely to matter in most programs, but in a TSR that steals interrupt processing time, speed is at least as important as program size. (Defining and assigning Zero$ adds a dozen or so bytes.) The last preparatory step is to assign Registers.IntNum to the The QBNews Page 15 Volume 1, Number 4 September 3, 1990 value &H21, to specify which interrupt is to be trapped. Finally, PointIntHere is called, followed by a GOTO to the very end of the source listing where the program is installed as a TSR by calling the EndTSR routine. The remaining program statements will now be executed every time an Interrupt 21h occurs. The first two steps are calls to IntEntry1 and IntEntry2, and this is mandatory in all TSR programs that do not use the simplified method. These routines simply save the current processor registers values, so they can be restored when DOSWATCH passes control to DOS later on. The next two steps clear the Ticks variable, and derive the current service number from the AX register. For DOS services that process a file or directory name, DOSWATCH pauses for a half-second allowing time to read the name. Ticks specifies the number of timer ticks in 18ths of a second. For other services only the service number is displayed, and there is no added delay. Fifteen different DOS services are recognized, and these are filtered through a SELECT/CASE block. All of the supported services assign Message$ to the appropriate text, and those services that process a file or directory name also use the GetDOSName subroutine. GetDOSName copies the current name into the DOSName$ variable, and also sets Ticks to 8 to allow time to read it. In some cases, additional information is assigned to Message$, for example when reporting a drive letter or file handle number. This information is taken from the appropriate registers as necessary. If a particular service is not supported, then Message$ is simply cleared to blanks in the CASE ELSE code. Because DOSWATCH is not actually processing any of the DOS services, the final step is to call GotoOldInt, which in turn jumps to the internal DOS function dispatcher. Thus, DOSWATCH displays what is about to happen, before DOS actually receives control. The remaining statements comprise the GetDOSName subroutine, which copies the current file or directory name into the DOSName$ variable. In this case, the P.D.Q. BlockCopy routine copies the first 50 characters pointed to by DS:DX into DOSName$, and then INSTR is used to locate the CHR$(0) which marks the end of the name. Only those characters that precede the zero byte are retained, and the LEFT$ assignment clears any characters that follow the name. Running DOSWATCH once will install it as a TSR, and all subsequent DOS operations will then be displayed on the top line of the screen. Observing DOSWATCH as it works can provide much insight into DOS' internal operation. For example, you will no doubt find it enlightening to start QuickBASIC and load a program such as DOSWATCH itself. This shows DOS at work as it loads and executes QB.EXE, and then you can watch QuickBASIC as it loads the main program and the PDQDECL.BAS Include file. Equally interesting is all the unnecessary DOS activity QuickBASIC performs to obtain a list of file names when you select Alt-F-L from the pull-down menu. The QBNews Page 16 Volume 1, Number 4 September 3, 1990 You could also modify DOSWATCH to pause briefly for all of the DOS services rather than just some of them. Even though the operation of your PC will be slowed dramatically, many varied and interesting facets of DOS will become apparent. For example, each time you use the DIR command, DOS (actually COMMAND.COM) always changes to the current directory, opens the directory "file", and then writes the directory information to Handle 1. The added delay will also let you observe DOS as it closes all available handles each time a program terminates. As you can see, besides providing an excellent way to learn more about TSR programming, DOSWATCH is also a very useful utility program in its own right. One of our primary goals in developing P.D.Q. was to allow BASIC programmers to create programs as small and fast as those written in C. With a minimum .EXE file size less than one-fourth that of QuickC and Turbo C, I believe we have met that goal. [Editor's note: P.D.Q. costs $129 plus $6 for 2nd day shipping, and it is available from Crescent Software at the address shown below.] ********************************************************************** Ethan Winer is President of Crescent Software, and is the author of their QuickPak Professional and P.D.Q. products. Ethan may be reached at 32 Seventy Acres, West Redding, CT 06896, 203-438-5300; on CompuServe at 72657,3070; and on MCI Mail as EWINER. **********************************************************************