Skip to content
FreeDOSDeep Dive Published Updated 7 min readViews unavailable

DOS INT 21h EXEC: Launch a Child and Collect Its Real Exit Result

Build a reliable DOS child-process call around EXEC, its parameter block, inherited environment and handles, memory requirements, and one-time return status.

DOS INT 21h function AH=4Bh, commonly called EXEC, is the interface a program uses to load and run another executable while retaining the ability to resume afterward. It creates a child execution context, gives it a Program Segment Prefix (PSP), provides an environment and command tail, and later returns control to the suspended parent. The call is not equivalent to typing a command at COMMAND.COM: path search, batch-file interpretation, and shell I/O redirection are command-processor responsibilities.

A dependable launcher treats EXEC as a multi-stage protocol: release enough memory, build valid parameters, check the carry flag and AX after the call, then retrieve the child’s termination type and return code exactly once with AH=4Dh. The environment and handles are inherited according to DOS rules, so a child can affect parent-visible file positions even though the parent is suspended.

EXEC is a loader API, not a shell parser

To launch a program, set AH=4Bh and AL=00h, pass a null-terminated pathname in DS:DX, and pass a parameter block in ES:BX. The path must identify one file and cannot contain wildcards. If no path is given, the current directory is searched on the default drive; EXEC does not perform the same PATH search as COMMAND.COM.

Batch files are interpreted by COMMAND.COM; they are not directly executed by the EXEC subfunction. Similarly, parsing command syntax, finding a program through PATH, and applying shell redirection are not implicit parts of this API. If a utility needs exactly the same behavior as a command typed at the DOS prompt, it may need to start a secondary command processor using COMSPEC and pass a /C command tail, with careful quoting and redirection design.

Prepare memory before calling

DOS programs often begin with more memory than they need, which can prevent EXEC from loading a child. Before invoking it, a parent can resize its own memory block with INT 21h, AH=4Ah, retaining the paragraphs needed for its code, data, stack, parameter block, and post-child work. This is especially important for a .COM parent, whose initial allocation and stack placement have historical constraints. Never shrink a block past a stack or buffer that will still be used after the child returns.

An EXEC failure does not mean the child started and returned a nonzero code. It can mean the image was not found or memory could not be allocated. Keep launch failure separate from child exit status in logs and user messages.

Understand the parameter block

For subfunction AL=00h, the parameter block contains an environment segment, a far pointer to the command tail, and pointers to two default FCBs. A zero environment segment asks DOS to copy the parent’s environment. If the parent supplies a segment, that block must be valid, paragraph-aligned, null-terminated in the DOS format, and large enough for the strings and final terminator.

The DOS environment is a sequence of NAME=value strings terminated by a double zero byte. It is not a Unix envp vector with an explicit pointer array. The child receives its own environment block, so changes made by the child do not rewrite the parent’s environment. In DOS 3.0 and later, the fully qualified program path may be appended as environment metadata; code that scans the block should respect its documented terminators and any trailing count/path fields rather than assume arbitrary bytes after the double null belong to another ordinary variable.

The command tail and FCB pointers are also part of the child’s startup contract. The command tail uses the traditional PSP representation, not a C argument vector. The two default FCBs remain for compatibility; modern handle-based applications usually use DOS file handles instead. An invalid pointer can cause a child to behave unpredictably even when EXEC itself reports success, so validate segment:offset pairs and lifetimes before crossing the interrupt boundary.

Handles are inherited, and file positions are shared

The child inherits the standard device handles and eligible handles opened by the parent. That is how COMMAND.COM implements redirection: it prepares the standard handles before loading the program. Other parent programs that call EXEC directly must arrange desired redirection themselves.

An inherited handle refers to shared DOS open-file state. Reads, writes, or seeks performed by the child can advance the file position observed by the parent after it resumes. Do not expect the child’s operations to be isolated merely because it has a separate PSP. Close handles that should not be inherited using the DOS-supported rules for the target version, and test the behavior on the actual DOS or FreeDOS kernel in use.

Check whether the child started

The basic calling sequence is:

; DS:DX = ASCIIZ pathname, ES:BX = prepared EXEC parameter block
mov     ax, 4B00h               ; Load and execute a child program
int     21h
jc      exec_failed            ; AX contains the DOS error code

; The child ran and returned. Read its termination class and code now.
mov     ah, 4Dh
int     21h
; AH = termination method, AL = child's return code
mov     [child_termination], ah
mov     [child_return_code], al

The code assumes the pathname and parameter block are already initialized in the segments indicated. It also stores the return values immediately because subsequent DOS calls or register use can overwrite them. On an EXEC error, report AX as a launch failure and do not call AH=4Dh as if a child had completed.

AH=4Dh returns two distinct facts. AH describes how the child terminated: normal exit, Control-C termination, critical-error abort, or terminate-and-stay-resident. AL is the child’s return code when supplied through the documented termination calls. A TSR outcome is not the same thing as an ordinary short-lived child, and Control-C or a critical error should not be collapsed into a generic nonzero status.

The return-code function is consumptive: its result is guaranteed only once after a successful child run. Read it immediately, save both bytes, and then let application logic decide how to propagate or map the outcome. If there was no preceding successful EXEC, the returned values are undefined. The carry flag is not an error indicator for AH=4Dh.

Keep shell conventions out of the low-level API

If the requirement is only to run one known .COM or .EXE file, direct EXEC avoids a command interpreter and makes the process boundary explicit. If the requirement includes batch files, command search, built-in commands, or command-line redirection, the program must intentionally involve COMMAND.COM; passing a raw string to EXEC does not grant those shell semantics.

This distinction also matters for quoting. The command tail is a byte string that the child interprets according to its own conventions. DOS does not parse it into arguments on the parent’s behalf. A launcher should know the target program’s command-tail rules and construct the bytes it expects rather than assuming a modern shell’s quoting model applies.

Verify failure boundaries separately

Test at least four paths: the executable is missing, memory is insufficient, the child exits normally with zero, and the child exits with a nonzero code or termination condition. For inherited handles, test that a child read or seek has the parent-visible effect expected by the design. For environment behavior, verify that the child’s copy is separate and that any custom environment block has the proper terminators.

Use AH=59h only when the DOS version supports it and an extended launch failure is useful; call it immediately after the failing DOS service. Do not treat modern Windows process behavior as a specification for a DOS or FreeDOS kernel. The parent is suspended while the child owns the machine, and the exact compatibility target matters.

EXEC is simple only when its boundaries are respected: the filename is a direct image path, the parameter block is valid, memory is available, inherited descriptors are understood, and the one-time termination tuple is captured before anything can overwrite it.

Related:

Sources:

Comments