DOSCALL From BASIC (PC Age April 1984 by Dan Rollins) DOS 2.0 is a delight to the assembly language programmer and this article gives an in-depth look at the EXEC service that allows one program to load and execute another program. We will focus on the ability to invoke DOS commands from within a BASIC(A) program. Consider some of the possibilities: - Use the DIR command to determine the size and time-stamp of files - Change the default disk at will - Execute a DOS batch file and return to the BASIC program without disturbing BASIC variables - Issue the DOS PRINT command to begin print-spooling - Issue FORMAT and DISKCOPY or BACKUP as selections from a BASIC program menu - Become the "parent" of a BASCOM, Pascal or machine language program - Execute DEBUG in the rarefied atmosphere above BASIC - Use the machine language speed of the SORT and FIND filters without needing to exit to DOS This article presents and explains a program named DOSCALL that gives all the advantages of the DOS command mode without exiting from BASIC. DOSCALL.COM is an assembly language program that is executed via the BASIC CALL statement. It is a low-memory resident program that becomes part of your DOS system. Using techniques described in the DOS 2.0 manual, it nestles into the lowest free memory and increases the size of your operating system by about 400 bytes. When resident, any BASIC program may issue a CALL to DOSCALL by passing a string argument. When DOSCALL takes control, it invokes a secondary copy of the DOS 2.0 high-level command processor. That is, the program COMMAND.COM is loaded into memory and executed. Using a special parameter, COMMAND is told to execute the statement that was passed from BASIC. After that command has been executed, the copy of COMMAND.COM is removed from memory and control passes back to DOSCALL. DOSCALL returns to BASIC, right where it left off, returning a value that indicates whether an error occurred during any of this process-shuffling. If you add up all of the RAM that is occupied after the second copy of COMMAND has been loaded (24Kb as the minimum used by DOS, 1Kb for DOSCALL, 4Kb for the resident part of the second copy of COMMAND, 25Kb for the resident part of BASIC and 64Kb for the BASIC program and data segment), you will see that DOSCALL can only be used in machines with at least 128Kb. The full value of DOSCALL can only be realized when your machine has at least 192Kb above and beyond what you're using for an electronic disk. DOSCALL hogs memory, but if you're running with less that 256Kb, then you're not taking advantage of the true power of the 8088. I will assume that you are a serious applications programmer with some experience in 8088 assembly language coding. Get out your DOS 2.0 manual and note page 10-9 (Invoking a Secondary Command Processor), page D-5 (DOS EXEC service), page F-1 (Executing Commands from Within an Application), and page D-32 (the DOS KEEP service). The manual says that you may invoke a secondary copy of the command processor. Therefore, any command from the DOS prompt that may be invoked from the secondary copy could also have been invoked from the primary command processor. By redirecting the input and output of the secondary command processor, you can give yourself a tight grip on batch-processing. For instance: A>COMMAND will invisibly run a series of commands taking all of their input from a file named CMDS.TXT. This example makes CMDS.TXT the standard input for all subsequent processes and routes all the standard output to the bit-bucket. That means you could, for example, invoke BASIC in a batch mode that takes its input from the same file that has the DOS commands. All normal screen output would be invisible to the operator because it would be sent to the standard output device which, in this case, simply ignores it. The last line in CMDS.TXT should be the special command EXIT which is a valid DOS command that is mentioned exactly once in the DOS manual. EXIT terminates the secondary copy of COMMAND.COM and passes control to the parent -- it passes control back to the primary copy of the command processor. The COMMAND command has two other options. The first is the /C text parameter. This allows you to invoke a copy of COMMAND that will execute a single DOS command before it terminates and returns to the parent. For instance, the results of the two commands: A>chkdsk a: A>COMMAND /c chkdsk a: are functionally equivalent, but they're obtained by different actions. The first one simply loads and executes CHKDSK.COM. The second one loads the program COMMAND.COM and passes it the string parameter "/c chkdsk a:". Then this second copy of COMMAND loads and executes the program CHKDSK.COM. After CHKDSK is finished, it passes control back to COMMAND, which then exits to its parent process, i.e., the primary command processor. You can verify that the second copy of COMMAND is in memory by invoking the CHKDSK program both ways. The "available memory" figure is 4Kb smaller when the second method is used. Another option of the COMMAND command seems meaningless, and it is in the current single-user DOS environment. If you use the /P parameter when you invoke COMMAND, the program will remain resident, even if you use the EXIT command. The only conceivable use for this option would be in a multi-tasking environment. Eventually (perhaps in DOS 3.0) we will be able to have two or more command processors idling in the background. It may be possible, with a little undocumented trickery, to do so now. The point is that DOS provides a way to invoke a secondary copy of COMMAND.COM that works just like the primary version. It will accept commands from the standard input, load and execute programs, and exit back to the caller. Page F-1 of the DOS manual explains a method to invoke DOS commands from an assembly language program. You create a parameter string in the described format and use the EXEC service (function 4BH of INT 21H) to load and execute the COMMAND.COM program. When you do this, your program becomes a "parent" and COMMAND.COM is its child. If the command that you pass to COMMAND involves executing an external DOS command or program, then that program becomes one of your grandchildren. Provided you have enough memory, it's possible to have COMMAND invoke DEBUG which can load and execute a third generation program (which could well be COMMAND.COM, or some other program). As each program is finished, control passes back to its direct parent, which should in turn pass control to its parent, eventually returning to your assembly language program. When your program is finished, it passes control to its parent, presumably the primary copy of COMMAND. The key to all of this is EXEC, the new DOS service that gives you all of the power of the DOS 2.0 command processor. All we need to do is follow the simple steps on page D-44 of the DOS manual. The gist of that discussion is that you need to come up with the information that COMMAND usually passes to its children. EXEC needs to know the characters that are normally typed on a command line following the name of the command to be invoked. For instance, it needs the "A:/8/S" that are the last characters of the command: A>FORMAT A:/8/S Programs that examine their command line always look to address 80H in their Program Segment Prefix (PSP) to find these characters. We need to make sure that these programs have something to look at. To complicate matters slightly, we must also supply a leading byte that gives the size of the string and add a terminating carriage return (0DH) at the end of the string. This string of characters is called the "unformatted parameter area" of PSP. Programs that process files often require (or allow for) up to two filenames to be entered on the DOS command line. COMMAND has always done the extra work of massaging these strings into acceptable filenames so that a program can quickly open the files without going through a lot of fuss. For instance, when DEBUG takes control after you enter the command: A>DEBUG a:myfile.exe it will expect to find a "formatted parameter area" starting at PSP:5CH composed of the 12 characters: dMYFILE...EXE where d is a binary number indicating the drive, with 0 being the default drive, 1 being drive A, 2 being drive B, etc. The details of what the formatted parameter area looks like are contained in the discussion of DOS function 29H on page D-30. They're not really significant here because we're only interested in using EXEC to load and execute COMMAND.COM, and it doesn't examine this formatted parameter area. We also need to give EXEC the paragraph address of something called an environment string. This turns out to be quite simple, because we can simply pass it the address that our program received when it started which is found at offset 2CH in our program's PSP. If, for some reason, you need to pass a different envirnoment to a process, you can create one in the format described on page D-46. Just make sure that it starts on a paragraph boundary and ends with two bytes of 0. These four items of information (a 16-bit pointer to the environment, a 32-bit pointer to a command parameter string and two 32- bit pointers to FCB information) are all placed into a data packet and passed, by reference, to the EXEC service. Before invoking the EXE function, the ES register must point to the segment and BX must point to the offset within that segment where the packet resides. Also, DS and DX must be set to the segment and offset of an ASCIIZ string that names the drive, path, and filename of the program you want to execute. (An ASCIIZ string is a series of ASCII characters terminated by a byte of 00H.) Finally, after the data packet is set up and before invoking the EXEC service, you must make sure that there is enough free memory to handle the request to load and execute the program. When your expectant-parent program takes control, DOS has allocated all available memory to it. There won't be room for the child process unless you free some memory before invoking EXEC. This involves a call to the DOS SETBLOCK service (function 4AH). Set ES to point to the PSP and put the minimum number of paragraphs needed by your parent process into BX before making the call. (In the assembly language program, this is omitted because BASIC has already freed up the memory that it doesn't use.) Write a short assembly language program that uses EXEC to invoke a "hard-coded" program with a "hard-coded" parameter line. That makes a good staging point. Now to move on to the more complex details that make DOSCALL flexible enough to execute any DOS command. If you're shaky at this point, you will do well to try some experiments on your own until you're certain of your work. The assembly language is a typical COM-format file. Notice that there is no stack segment and the entire program is enclosed in the code segment. The first opcode is preceded by an ORG 100H so that the EXE file can be processed by EXE2BIN to convert it into a COM-format program. Also notice that the END pseudo-op names the beginning as the starting point. COM-format files are more concise and load more quickly than their EXE counterparts. Because everything (code, data and stack) is contained in a single segment, you don't need to worry about losing track of where your data is -- you can always use the CS: override to make a data access or just copy CS to DS. DOSCALL is a special type of COM program because it is not meant to be executed and then discarded. After it's finished, it will remain fixed in memory. Any programs that are loaded afterwards will be loaded above it, at higher addresses. The program is divided into three different sections: the data, the initialization procedure and the DOSCALL procedure, which will be invoked from BASIC. When first invoked from DOS, the program jumps past the data and then initializes certain program variables and sets up a special pointer in low memory to point to the DOSCALL procedure. It then exits back to DOS using the DOS KEEP service (function 31H). This tells DOS that the program is to remain resident in memory and it specifies how large the program is. Examine lines 111-117 to see how this is accomplished. The data in the first lines of the code segment contain temporary and permanent program variables: addresses to store register values that are lost when the EXEC service is used, a string that is used for a search pattern, a place to store the address of the matching string, the partially completed data packet that is passed during the EXEC call, a buffer to hold the command that is passed from BASIC, and a message that is displayed when an error is encountered during initialization. The first order of business (lines 69-76) is to begin setting up the data packet that will be transferred when DOSCALL invokes the EXEC service. The first line stores the current environment address into the packet. The following lines fill in the segment values of the three 32-bit address that must be passed (the offsets are already known). Next the program must locate the path name of the program that will be executed by the EXEC function. The program will always be COMMAND.COM, but the drive and subdirectory in which it resides is unknown. There's a possibility that the filespec: A:COMMAND.COM would be acceptable, since it seems likely that there will be a bootable DOS disk in the root directory of drive A. This section of the program (lines 80-95) is actually a speed optimization of a previous version of DOSCALL. It looks through the environment for the string "COMSPEC=". When it finds that, it has found an ASCIIZ string with the full drive, path and filename of the command processor. This address is saved in the variable COMSPEC_ADDR which is later referenced on every call to DOSCALL. Every time DOSCALL is invoked from BASIC, COMMAND.COM must be loaded into memory and executed. Sometimes it will need to be loaded twice -- once because the transient part of DOS has been overwritten by a previous command and the EXEC service is unavailable, and a second time when the offspring copy of COMMAND.COM is loaded and executed by the call to EXEC. That's 17Kb (or 34Kb) of memory being read from a disk drive for each call to DOSCALL. Thus, the program provides a means to select a different path to look for COMMAND.COM. If drive C is your electronic disk, you will need to enter three DOS commands in this order: A>COPY a:command.com c: (puts COMMAND.COM on the RAM disk) A>SET comspec=c:command.com (tells DOS [and DOSCALL] where it is) A>DOSCALL DOSCALL is installed (fixes DOSCALL.COM in memory) It's important that the SET command takes place before installing DOSCALL because the environment that DOSCALL inherits remains static after it has been installed. Note that you don't need to copy COMMAND.COM to your RAM disk; you don't even need to have a RAM disk. The initialization of DOSCALL will go smoothly without the preceding commands. But when you start writing BASIC programs that make full use of DOSCALL, you will likely be placing SORT, FIND, FORMAT, DISKCOPY and other utilities on your RAM disk to really speed them up. Besides, when the DOS filter "pipes" are created on a RAM disk, you will see vastly improved throughput. The next part of the program (lines 98-101) set up a pointer so that the BASIC program that wants to invoke DOSCALL will be able to know where to find it. Unlike other programs called from BASIC, DOSCALL is not POKEd or BLOADed into memory; it is already resident. The reason for this is that it simplifies some of the memory-management details that accompany any use of the EXEC service. Compare these lines with lines 10-40 of DCTEST.BAS. These perform complementary actions and both are absolutely essential to the success of DOSCALL. The assembly listing stores a double-word pointer at 0000: 04F0 and the BASIC listing fetches that segment and offset so that it may be used as the address to CALL. The address 0000:04F0 was not selected at random. The Technical Reference manual lists this as being part of the "interprocess communication area"; that is, the ideal place to store such information. Finally the initialization procedure prints a message indicating that all is well, and then it takes the KEEP (fix-in-memory) exit back to DOS. Notice how a byte offset label is converted into a paragraph value in lines 111-115. The next part of the program is executed every time your BASIC program issues a CALL to DOSCALL. DOSCALL expects to be passed two variables: a string containing a DOS command and an integer in which it returns an error flag. You should be familiar with the way that BASIC, BASCOM, Pascal, FORTRAN and C pass parameters to external routines (Appendix C of the BASIC manual). The variables are passed by pushing their addresses onto the stack before making the far CALL. In the case of a BASIC string variable, the called routine receives a pointer to a string descriptor block that contains the length of the string and a pointer to the address of the string. Program lines 135-144 copy the characters of the string and its length into the buffer pointed to by the EXEC data packet (lines 55-62 define this packet). Once this parameter string is in place, the SS and SP registers are saved, and the ES:BX and DS:DX pointers are initialized for the EXEC function. The AH register selects the EXEC function and AL selects subfunction 0 (load, then execute). Finally, the program invokes interrupt 21H. If all goes well, the program COMMAND.COM will be loaded and executed and it, in turn, will execute the single command that was passed to it from BASIC. When it is finished, control returns to line 163 where the critical registers are restored. Then the program closes up shop and returns control to BASIC and the BASIC program that it's executing. The EXEC function alters two bytes in the "reserved area" of the PSP area of the process that invokes it. This is part of the "memory control block" that is mentioned indirectly in the DOS manual. When BASIC takes control, it manipulates two different blocks of memory. The first block has a true PSP (like all COM and EXE programs) at the lowest available address. It contains the code that handles all non- cassette BASIC commands and corrects errors. The other memory block is usually called the BASIC segment. It is the 64Kb of memory that begins at the paragraph addressed by the normal DEFault SEGment. This block has no PSP, but because BASIC makes an ALLOCATE call (DOS function 48H) DOS treats the start of this segment as if it were a PSP. When the EXEC function takes control, it needs to allocate a block of storage for the COMMAND.COM program. So it goes to the previous "memory control block" and updates it -- apparently to provide a forward-chain for future memory allocations. The bottom line is that although DOS believes it is modifying a reserved area in a PSP, it is, in fact, modifying a critical BASIC.COM variable. The relevant addresses are in the BASIC segment at offsets 30H and 31H. These constitute the pointer that BASIC refers to as the beginning of BASIC text. The final version of DOSCALL solves this problem by saving the word at 30H before invoking EXEC and then restoring it afterwards. Also note that BASIC and BASICA are two programs that may not be invoked via DOSCALL. These programs make some hidden changes to interrupt vectors. Among other things, they make non-standard alterations to the keyboard interrupt driver. BASIC is not a re-entrant program. The second copy of BASIC changes some vectors that will not be restored when it returns to the caller. If you try to execute BASIC will DOSCALL, your keyboard will enter a "freeze-up mode" and you will have to turn off and restart the computer. Entering and RUNning MAKEDC.BAS and DCTEST.BAS will give you DOSCALL and provide you a sample driver to demonstrate how to use DOSCALL from your own application. MAKEDC.BAS reads DATA lines and outputs the DOSCALL.COM file. One of the reasons for the COM format for DOSCALL is that COM files are just a binary image of all of the bytes (both data and opcodes) that make it up. Thus, it is relatively easy to convert these to ASCII hex digits, and thereby make it easier for you to use DOSCALL. The MAKEDC.BAS program will take about 30 seconds to create the DOSCALL.COM file. The program does a checksum of the DATA it reads to make sure that you haven't made any typing errors. When the program displays the "DONE" message, you are ready to install DOSCALL. Use SYSTEM to exit to DOS and enter the command: A>DOSCALL You should see a message indicating that DOSCALL has been installed. Next, get back into BASIC(A) and enter and RUN DCTEST.BAS. DCTEST.BAS asks for a DOS command. Pretend you are at the DOS prompt and enter any valid DOS command. Don't worry about passing a blank line or an invalid command. The command processor handles these just as it normally does. If, for some reason, the EXEC service returns with an error, the ERR.FLAG% variable is returned with a value of -1. If everything went all right, then the return value is 0. The DCTEST.BAS program displays the value of this flag after every call to DOSCALL. The most likely reasons that you might receive an error would be if EXEC can't find COMMAND.COM, or if there is a shortage of memory in your PC. DCTEST.BAS has two built-in error traps. If it senses that DOSCALL.COM has not been loaded beforehand, then it prints a message and aborts. Also, it will not let you try to execute a second copy of BASIC or BASICA. When you use a DOS command that creates a lot of output, pressing Ctrl-Break will not immediately halt the process. COMMAND returns control only after it completes the specifid action. After returning to BASIC, the Break is noticed and the BASIC program is halted. However, Ctrl-NumLock and Ctrl-S will temporarily halt the scrolling, and Ctrl- PrtSc will force the output to be echoed to your printer. The value of DOSCALL is limited only by your imagination. Picture a "shell" around DOS that presents the user with a mouse-controlled menu. Or think about being able to omit telling your secretary how to back up the fixed disk (you can make your own self-explanatory prompts). Consider how quickly and easily you'll be able to sort your mailing list if you use the DOS SORT command and imagine being able to print-spool that mailing list without needing to exit from BASIC. The possiblities are endless. Following is the assembly language program (included here to provide clarity to the text above): 1 ; 2 ;------------------------------------------------------------- 3 ; DOSCALL.COM Dan Rollins 9/8/83 4 ; 5 ; This COM-format program interfaces with interpretive BASIC 6 ; to provide a means to execute DOS 2.0 commands from BASIC. 7 ; The program resides in low memory and must be invoked before 8 ; BASIC. 9 ; Address of this program is saved at 0000:04F0 (IP, followed 10 ; by CS). 11 ; When called from BASIC, expects two arguments: 12 ; CALL DOSCALL (CMD$,ERR.FLAG%) 13 ; 14 ; where CMD$ = any valid DOS command like: 15 ; "DIR A:" 16 ; "FORMAT B: /V" 17 ; "COPY *.* C:" 18 ; "sort b:sorted.txt" 19 ; 20 ; ERR.FLAG% = an integer variable. Upon return, 21 ; = 0 for no problem 22 ; = -1 if an error occurred 23 ; 24 ; COMMAND.COM must be in the environment as: 25 ; COMSPEC=d:[path]COMMAND.COM 26 ; As a COM-format program, this must be assembled & linked 27 ; (ignore the "No Stack" error), then converted with 28 ; EXE2BIN into DOSCALL.COM 29 30 fn_print_string equ 9 ;used when COMSPEC not found 31 fn_keep equ 31H ;used by initialization 32 fn_exec equ 4BH ;load and execute a program 33 34 code_seg segment para public 'code' 35 assume cs:code_seg 36 org 100H ;Must have ORG for COM-format files 37 begin: 38 jmp near ptr init ;need override for forward ref 39 40 ;--- data used by program --- 41 save_ds dw ? 42 save_ss dw ? 43 save_sp dw ? 44 save_30 dw ? 45 46 comspec db 'COMSPEC=' 47 comspec_len equ 8 48 comspec_addr dw ? ;when comspec found place addr here 49 default_fcb db 0 ;really just a dummy address 50 51 cmd_parm db 0 ;changed to length of string passed 52 db 'C' ;always used 53 user_cmd db 255 dup (' ') ;cmd chars (+CR) go into here 54 55 parm_block label word ;ES:BX here (for DOS function 4BH) 56 env_ptr dw 0 ;for env. para. from PSP:002CH 57 cmd_ptr dw cmd_parm ;ptr to cmd passed from BASIC 58 dw 0 ;<-- segment filled in with CS 59 fcb1_ptr dw default_fcb ;pointers to FCBs (not used) 60 dw 0 61 fcb2_ptr dw default_fcb 62 dw 0 63 64 ok_msg db "DOSCALL is installed.",0DH,0AH,'$' 65 err_msg db "ERROR: "COMSPEC=" not in env.',0DH,0AH,'$' 66 67 ;--the INIT proc is only executed once when invoked from DOS 68 69 init proc far 70 mov ax,dx:[2cH] 71 mov es,ax ;point to environment 72 mov cs:env_ptr,ax ;save it in parm block 73 74 mov cs:cmd_ptr+2,cs ;fix up segment refs of 75 mov cs:fcb1_ptr+2,cs ;other fields 76 mov cs:fcb2_ptr+2,cs 77 78 ;---look thru env. for "COMSPEC=" for program path to EXEC 79 80 mov di,0 ;ES:DI =>environment 81 push cs 82 pop ds ;copy CS to DS so that ... 83 again: 84 mov si,offset comspec ;DI:SI =>"COMSPEC=" string 85 mov cx,comspec_len 86 rep cmpsb 87 je found 88 cmp word ptr [di],0 ;end of environment? 89 jne again ;no, continue 90 mov dx,offset err_msg ;yes, handle the error 91 mov ah,fn_print_string 92 int 21H 93 int 20H ;<--init error exit 94 found: 95 mov cs:comspec_addr,di ;save addr of ASCIIZ 96 ;comspec 97 ;---set up pointer at 0000:04F0 to point to DOSCALL process 98 mov ax,0 99 mov ds,ax 100 mov ds:[4F0H],offset doscall ;so that caller can 101 mov ds:[4F2H],cs ;find where to call 102 103 push cs 104 pop ds 105 mov dx,offset ok_msg 106 mov ah,fn_print_string 107 int 21H ;display reassuring msg 108 109 ;---set up for DOS 2.00 KEEP process service 110 111 mov dx,offset last_byte+15 112 shr dx,1 113 shr dx,1 114 shr dx,1 115 shr dx,1 ;DX = paragraphs to KEEP 116 mov ah,fn_keep 117 int 21H ;<--initialization NO ERROR exit 118 init endp 119 120 ;-- following code is executed when called from BASIC 121 122 doscall proc far 123 mov cs:save_ds,ds 124 mov ax,ds:[30H] ;this addr is chngd by EXEC 125 mov cs:save_30,ax ;therfor it must be saved & 126 ;restored 127 push bp 128 mov bp,sp ;[BP+6] pts to ptr to ERR.FLAG% 129 ;[BP+8] pts to CMD$ strg dscriptr 130 mov bx,[bp+8] ;point to CMD$ pointer 131 mov cl,[bx] ;fetch length from descriptor blk 132 add cl,3 ;add the length of "/C" 133 mov cs:cmd_parm,cl ;store the length 134 135 mov cl[bx] ;get length again 136 mov ch,0 ;CX=length of BASIC string to copy 137 138 mov si,[bx+1] ;DS:SI => source (in BASIC seg) 139 push cs 140 pop es 141 mov di,offset user_cmd ;ES:DI=>dest in CODE_SEG 142 rep movsb ;copy CMD$ into parm blk 143 mov al,0DH 144 stosb ;add the CR 145 146 ;---prepare for EXEC function 147 148 mov ds,cs:env_ptr 149 mov dx,cs:comspec_addr ;DS:DX=>path of CMD.COM 150 151 push cs 152 pop es 153 mov bx,offset parm_block ;ES:BX=>EXEC parm blk 154 155 push bp 156 mov cs:save_ss,ss ;must save 157 mov cs:save_sp,sp 158 159 mov ah,fn_exec 160 mov al,0 ;specify LOAD and EXECUTE 161 int 21H 162 163 cli 164 mov ss,cs:save_ss ;restore segment registers 165 mov sp,cs:save_sp 166 sti 167 pop bp 168 mov ds,cs:save_ds 169 170 mov di,cs:save_30 ;restore value chngd by EXEC 171 mov ds:[30H],di 172 173 jc err_exit ;something happened if CF=CY 174 175 mov bx,[bp+6] ;else, 176 mov word ptr [bx],0 ;indicate no error 177 exit: 178 push ds 179 pop es ;and set ES how BASIC likes it 180 181 pop bp 182 ret 4 ;Exit to BASIC, discarding 2 arguments 183 err_exit: 184 mov bx[bp+6] 185 mov word ptr [bx],-1 ;indicate error occurred 186 jmp exit 187 188 doscall endp 189 last_byte label byte 190 code_seg ends 191 end begin ;need label for EXE2BIN conversion