THE FOLLOWING IS A TRANSCRIPT OF AN "APPLICATION NOTE" FROM DIGITAL RESEARCH, THOUGH THE ORIGINAL BEARS NO NOTICE OF SOURCE OR COPYRIGHT: CHAINING PROGRAMS UNDER CPM 2.2 by DOUG HUSKEY I have often been asked how to write menu driven applications which will run under CP/M. If the applications are being developed using PL/I-80, this can be accomplished by writing the programs as a set of overlays. Often, however, some of the programs may be written in assembly language, or require too much memory to make the use of the overlay feature of PL/I-80 appropriate. Without using overlays, there are only two effective ways of chaining under CP/M 2.2. First, you can use the CP/M submit facility. The trick is to have the main menu program create a submit file with the programs to be chained listed in it. The file must be written to drive A, and have the name "$$$.SUB". The submit file consists of command lines exactly as would be typed at the console following the system prompt. The commands are placed in reverse order so that the last command in the file is the first to be executed. Each command is placed in a 128 byte record with the following format: |---|----|----|-----|----|---|-----| | n | c1 | c2 | ... | cn | 0 | ... | |---|----|----|-----|----|---|-----| The first byte of the record contains the number of characters in the command (n), followed by the characters (c1-cn), and terminated with a zero. The number of characters in the command is written as a binary number and each character is represented in its normal ASCII format. It does not matter what follows the terminating zero in the record. For example, if the command was "STAT *.*", the first byte would be a binary 8, followed by the letters "STAT *.*" and terminated with a zero. The second approach to program chaining is simpler. In this approach, you simply include a procedure in the menu program which will load the next program and chain to it. Each program that might chain to another program must include a copy of the procedure. The trick here is that the procedure must first move itself out of the way so that it is not overwritten by the program it is loading. The assembly language program listed at the end of this article accomplishes this. It was written to be linked with PL/I-80 modules as an external procedure. Of course, it could also be used in an assembly language menu program. If you wish to link it to a PL/I-80 program the following entry declaration must be included in the PL/I-80 program doing the chaining: dcl chain entry (char(12)); The character 12 variable consists of the standard CP/M file control block (FCB) format. This can be created in the PL/I-80 program as a structure. A char(12) variable can then be based at the same address as the structure for the purpose of interfacing to the chain procedure. The PL/I-80 program below illustrates this. Note that the drive is not an ASCII character but a binary number between 0 and 16, where 0 is the current default drive and 1 through 16 represent the CP/M drives A through P, respectively. chainl: proc options(main); /* chain subroutine tester */ dcl 1 fcb static, 2 drive fixed(7) init(0), 2 name char(8) init('CHAIN2'), 2 type char(3) init('COM'), dummy char(12) based(dp), dp pointer, chain entry(char(12)); put skip list ('Chain Test program 1'); dp = addr(fcb); call chain(dummy); put skip(2) list('Shouldn''t be here!!'); end chainl; This program will print the message "Chain Test program 1", and chain to the program CHAIN2.COM on the default drive. CHAIN2 is a program identical to CHAIN1 except that it prints "Chain Test program 2" and chains to CHAIN1.COM. Thus chain1 and chain2 continue to chain back and forth to each other, not real useful but an interesting demonstration. Note that any statements following the call to the chain procedure will never be executed as the chain procedure never returns, it chains. The chain procedure consists of two routines, an initialization routine and the loader routine. The initialization routine initializes the FCB for the program to be loaded and then relocates the loader and FCB to the very top of the transient program area (TPA), immediately below the BDOS, so that it won't be overwritten by the loaded program. The loader begins at the label "code:" and ends at the end of the FCB at the statement "codelen equ $-code". The initialization portion first copies the drive, file name and type into the FCB using the "move" routine. It then fills the rest of the FCB with zeros using the "fill" routine. Now comes the tricky part. It picks up the BDOS base address from the jump at location 5 in low memory. Next the routine subtracts from this the length of the loader and fcb and fills in the jump back to "code:" in the loader. It calculates the address of the FCB after it is moved and fills in the lxi instruction at the label "fcbr:". Then another call to the move routine moves the code and fcb into the proper location below the BDOS. It opens the file to be loaded, tests the A register to see it the open was successful, and signals an error ("Bad Chain Attempt") if the file was not found. Finally, it pops the address of the start of the loader routine, sets the stack to grow down from below the loader and pushes the address back onto the stack in preparation for a return. Only one thing left to do, initialize the HL register to the beginning of the TPA at 100H, where the program will be loaded. The return fires off the loader. The loader routine simply sets the DMA address, reads a sector, checks for an end of file, increments the DMA address by 128 bytes, and repeats the process. When the end of file is detected, it jumps to the chained program. This routine provides an effective and relatively simple method of chaining programs under CP/M, MP/M II and CP-NET. In addition to being compatible with all of these systems, it is also faster than the submit file method described at the beginning of this article. public chain ; (char(12)) extrn ?signal ; /* loads another COM file and executes it */ bdos equ 5 openf equ 15 readf equ 20 dmaf equ 26 cseg chain: move e,m ! inx h ! mov d,m ! xchg ;get first arg address lxi d,fcb ! mvi c,12 ! call move ;move string to fcb lxi d,fcb+12 ! mvi a,0 ! mvi c,21 ! call fill ;zero rest of fcb lhld bdos+1 ! lxi b,-code$len ! dad b ;make space at top of TPA shld jmpr+1 ;jump address push h ;save code address for RET xchg ! lxi h,fcb-code ! dad d ;make address of FCB shld fcbr+1 ;and fix LXI push h ;save FCB destination add. lxi h,code ! mvi c,code$len ! call move ;destination in DE pop d ;recover FCB address mvi c,openf ! call bdos ;open file inr a ! jz sig ;signal if error pop h ! sphl ! push h ;point stack to top of ;TPA and save address lxi h,100h ;point to start of TPA ret code: push h ! xchg ! mvi c,dmaf ! call bdos ;set DMA address fcbr: lxi d,$-$ ! mvi c,readf ! call bdos ;read next record ora a ! jnz 100h ;EOF -> start TPA pop h ! lxi d,128 ! dad d ;recover and bump DMA ;address jmpr: jmp $-$ ;jump to code fcb: ds 1 ;drive code ds 8 ;file name ds 3 ;file type ds 4 ;control info ds 16 ;disk map ds 1 ;rrec codelen equ $-code move: ; c = # bytes, hl = source, de = destination mov a,m ! stax d inx h ! inx d ! dcr c jnz move ret fill: ; a =byte to fill, c = # bytes, de = start address stax d ! inx d dcr c ! jnz fill ret sig: lxi h,siglist ! call ?signal ! jmp 0 ;signal error siglist: dw sigcode,sigsub,sigfil,message ;(fixed(6),bit(8),ptr,p sigcode db 6 ;undefined file error sigsub db 2 ;arbitrary subcode sigfil dw fpb ;ptr to file parameter message dw quack ;auxiliary oper. msg. fpb: ;PL/I file parameter blk fcbptr dw fcb-1 ;.fcb-1 fpblst dw 0 ;(unused)ptr column dw 0 ;current col fixed (15) curline dw 0 ;current line " curpage dw 0 ;current page " currec dw 0 ;(unused) lookchr db 0 ;lookahead char (1) ioend dw 0 ;i/o end address iostk dw 0 ;user stack upon sio entry spacer ds 4 ;spacer linesz dw 0 ;line size fixed (15) pagesz dw 0 ;page size " fixedsz dw 0 ;fixed size " blocksz dw 0 ;block size " filedes dw 0 ;file descriptor dtitle db 0,'' ;default title ; char(14) varying quack db 17,'Bad Chain Attempt',0 ;error message