EXEC is how one program runs another; the shell starts every program this way. DOS reads the file into free memory (a .COM as it is, an .EXE relocated as its header says), builds a PSP for it, and jumps to it. The INT 21h returns to the parent when the child exits, and INT 21h AH=4Dh then tells how it ended.
The name is a complete file name with its extension. EXEC does not look through PATH, does not try .COM and .EXE, and does not run batch files or built-in commands such as DIR or ECHO: those belong to COMMAND.COM. To run a command line, run the shell with /C and the command, which is what the example does.
The parameter block (AL = 0)
| Offset | Size | Contents |
|---|---|---|
| 00h | word | the environment segment for the child; 0 = a copy of the parent's |
| 02h | dword | the command tail: offset, then segment |
| 06h | dword | an FCB, copied to the child's PSP:5Ch |
| 0Ah | dword | an FCB, copied to PSP:6Ch |
The command tail is what the child finds at PSP:80h: a count byte, the text, and a carriage return (0Dh) that is not counted. The shell passes it with the space after the program's name, so COMMAND /C ECHO hi gets 11, " /C ECHO hi", 13; the text can be up to 126 characters. The FCBs matter only to old programs that take file names from them; pointing both at the parent's own PSP:5Ch and 6Ch is the usual choice.
Memory and registers
The child is loaded into free memory, so the parent must first give back what it does not need with INT 21h AH=4Ah. A .COM that has not shrunk gets error 8.
DOS 2 returned from EXEC with every register lost except CS and IP, so programs keep SS:SP in a variable they can reach through CS and restore it first. Later versions keep the stack, but the habit is cheap. Restore DS and ES too, with MOV, which leaves CF alone for the JC that checks for failure.
MOV AH, 4Ah ; DOS: shrink our block (ES = our PSP)MOV BX, 1000h ; to 64 KB: the .COM and its stackINT 21hMOV TAILP1+2, CS ; the segments of the block's pointersMOV FCB1P+2, CSMOV FCB2P+2, CSMOV SAVESP1, SP ; DOS 2 lost SS:SP over an EXECMOV AX, 4B00h ; DOS: EXEC, AL = 0: load and runMOV DX, OFFSET PROG1 ; DS:DX = the programMOV BX, OFFSET ENV1 ; ES:BX = the parameter blockINT 21hMOV BX, CS ; MOV leaves CF alone: segments back firstMOV SS, BXMOV SP, CS:SAVESP1MOV DS, BXMOV ES, BXPUSHF ; keep CF while printingMOV DX, OFFSET BACKM1MOV AH, 09hINT 21hPOPFCALL RESULT ; ok, or the error codePROG1 DB "Z:\COMMAND.COM", 0TAIL1 DB 27, " /C ECHO hello from a child", 13 ; 27 characters, CR not countedENV1 DW 0 ; parameter block: environment, 0 = a copy of oursTAILP1 DW OFFSET TAIL1, 0 ; the command tail, offset and segmentFCB1P DW 5Ch, 0 ; FCB 1: the one in our PSPFCB2P DW 6Ch, 0 ; FCB 2SAVESP1 DW 0BACKM1 DB "back in the parent: $"RESULT PROC NEAR ; print "ok", or "error" and AX when CF = 1PUSHF ; keeps AX and CF for the callerPUSH AXMOV DX, OFFSET OKMSGJNC RESULT1MOV DX, OFFSET ERRMSGMOV AH, 09hINT 21hPOP AXPUSH AXCALL PDEC ; the error codeMOV DX, OFFSET CRLFRESULT1:MOV AH, 09hINT 21hPOP AXPOPFRETRESULT ENDPOKMSG DB "ok", 13, 10, "$"ERRMSG DB "error $"PDEC PROC NEAR ; print AX in decimal (changes AX BX CX DX)MOV BX, 10XOR CX, CX ; digits on the stackPDEC1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXOR AX, AXJNZ PDEC1PDEC2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP PDEC2RETPDEC ENDPCRLF DB 13, 10, "$"
COMSPEC
The shell is not always C:\COMMAND.COM. Its full name is in the environment variable COMSPEC; in DOSBox-X, whose shell lives on the virtual drive Z:, that is Z:\COMMAND.COM. A program that needs a shell looks it up in its environment, at the segment stored at PSP:2Ch: a list of NAME=value strings, each ending with a 0 byte, and an empty string after the last:
MOV BX, 2Ch ; PSP:2Ch = the environment's segmentMOV ES, [BX]XOR DI, DI ; ES:DI = the first NAME=valueCLDFIND1:CMP BYTE PTR ES:[DI], 0 ; an empty string ends the listJE NONE1MOV SI, OFFSET KEY1 ; "COMSPEC="MOV CX, 8REPE CMPSB ; DS:SI against ES:DIJE FOUND1DEC DI ; back to the byte that differedMOV AL, 0 ; and on past the end of this stringMOV CX, 0FFFFhREPNE SCASBJMP FIND1FOUND1:MOV DL, ES:[DI] ; print the value, up to its 0OR DL, DLJZ DONE1MOV AH, 02hINT 21hINC DIJMP FOUND1NONE1:MOV DX, OFFSET NOKEY1MOV AH, 09hINT 21hDONE1:MOV DX, OFFSET CRLFMOV AH, 09hINT 21hPUSH CSPOP ESKEY1 DB "COMSPEC="NOKEY1 DB "no COMSPEC$"CRLF DB 13, 10, "$"
AL = 1 loads a program without running it and returns its entry point and stack in the parameter block, which is how a debugger starts; AL = 3 loads an overlay into memory the parent already allocated, without a PSP.