Skip to content
FreeDOSDeep Dive Published Updated 8 min readViews unavailable

The DOS Program Segment Prefix: Process State, Startup Data, and Safe Boundaries

Read the DOS PSP as a process ABI: map parent, environment, handle, FCB, and command-tail fields while avoiding unsafe assumptions about private bytes.

The Program Segment Prefix (PSP) is the small, addressable record DOS prepares for a process before transferring control to it. It is not just a command-line buffer: it connects the program to its parent, environment block, inherited handle table, termination vectors, default FCBs, and the memory allocation that the process owns. Programs that inspect it for diagnostics or compatibility must distinguish documented process conventions from fields that happen to exist in one kernel’s implementation.

This guide describes the classic 256-byte PSP used by DOS-compatible real-mode programs and checks its layout against the FreeDOS kernel’s process definitions. The PSP is an ABI boundary, not a license to edit kernel-owned bytes. Offsets and semantics can differ across DOS generations, Windows DOS boxes, network redirectors, and extenders; use documented INT 21h services for mutable process state whenever one exists.

Where the PSP sits in a process

DOS reserves a segment for the PSP and places the 256-byte structure at offset 0000h within it. A .COM image conventionally starts at offset 0100h, after those 256 bytes. An .EXE receives a PSP too, but its entry point and initial segment registers follow the executable loader contract rather than the .COM fixed-offset rule. The address of the PSP is therefore a segment value, not a flat pointer that can be used without regard to the current segment registers.

The PSP participates in lifecycle operations. DOS sets up the child PSP during EXEC, records the parent PSP, installs termination and error-return addresses, and associates the process’s memory blocks with an owner. On normal process exit, DOS uses that context while restoring the parent. A child program should not treat its PSP as a global structure shared with its parent: the parent is suspended for the usual EXEC call, and the child has its own process context.

A field map for inspection

The following offsets are useful when reading a memory dump. They are a practical map, not a promise that every field is portable or writable.

Offset Classic meaning Safe interpretation
00h INT 20h termination entry Historical compatibility entry; modern code normally exits with INT 21h/AH=4Ch.
02h Segment immediately beyond the process allocation A boundary value used by DOS conventions; it is not a replacement for querying the MCB or resizing through DOS.
05h Compatibility far-call sequence Kept for CP/M-style program conventions; do not overwrite it as scratch space.
0Ah, 0Eh, 12h Saved INT 22h, INT 23h, and INT 24h vectors Termination, Ctrl-Break, and critical-error return state managed by DOS.
16h Parent PSP segment Useful for ancestry diagnostics, but validate it before following memory.
18h First 20 bytes of the legacy handle table Entries refer to DOS handles; FFh is an unused slot in the FreeDOS layout.
2Ch Environment segment Points to a separate DOS memory block, not inline strings in the PSP.
2Eh Saved user stack pointer in the FreeDOS definition Kernel/process state; do not infer a portable stack frame from it.
32h and 34h Maximum handle count and handle-table pointer in extended layouts Later DOS behavior can place a larger table elsewhere; the original 20-byte array is not always the whole table.
38h Previous PSP pointer in FreeDOS’s structure An implementation field; do not assume it is an application-owned linked list.
5Ch and 6Ch Default FCB areas Compatibility inputs created from the command tail; may overlap other startup conventions.
80h Command-tail length and bytes; default DTA begins here This storage is reused by directory search unless the program selects another DTA.

The table illustrates why an offset is meaningful only together with a DOS version and a documented interface. For example, the first 20 handle bytes predate the larger handle-table convention. Modern code should use INT 21h handle functions and the documented maximum-handle operation instead of changing table pointers directly. Likewise, the MCB immediately before a memory block - not the PSP’s 02h word - is the allocator’s authoritative boundary record.

Startup bytes are shared with compatibility features

At offset 80h, DOS stores the command-tail length followed by the tail bytes, ending with a carriage return in the conventional representation. The same address is the process’s default Disk Transfer Area. A call to INT 21h/AH=4Eh or 4Fh can therefore overwrite arguments if the program starts a file search before copying the command tail it still needs.

The robust pattern is to copy startup input into application-owned storage early, then choose a dedicated DTA with INT 21h/AH=1Ah before enumeration. Do not retain a pointer into the default DTA after a search, and do not assume that a compiler’s runtime has already copied the PSP tail into an immutable buffer. The FCBs at 5Ch and 6Ch are also legacy command-tail products; they are not a C argv array and do not replace parsing the raw tail according to the application’s own conventions.

Environment and handles are indirect state

The environment pointer at 2Ch names a separate block. Under EXEC, a zero environment segment requests the DOS-defined inheritance behavior; a caller can instead pass a valid custom environment block. The child receives process environment storage that is distinct from the parent’s active block, so a child’s SET-like changes do not mutate the suspended parent’s variables. The bytes are strings terminated by a double zero, with DOS-version-specific trailing metadata possible; scan only according to the documented format and do not treat arbitrary bytes after the terminator as additional variables.

Handle state is similarly more complex than the 20 visible bytes. The table entries are indices/references into DOS open-file state, and inherited handles may share file-position effects with a parent after the child returns. DOS 3.x and later can keep an extended table whose pointer and size are represented in fields beyond the original 20-byte array. The safe application-level operations are the normal open, duplicate, force-duplicate, close, and EXEC rules - not a direct PSP byte patch.

Process identity and return paths

The parent PSP value supports DOS’s parent/child relationship, but software should obtain the current PSP through a documented call when it needs the segment. The INT 21h functions for setting/getting PSP state exist for specialized process managers; ordinary applications should not change the current PSP as a shortcut for manipulating another process. A wrong PSP can cause DOS to attribute allocations, handles, or termination to the wrong owner.

The saved vectors help DOS restore the parent context when a child terminates or is interrupted. This is one reason a program should use the proper termination API instead of returning from an interrupt handler or jumping through a guessed vector. AH=4Ch supplies a return code and terminates the current process; the parent retrieves termination information with AH=4Dh after a successful child execution. The PSP’s old INT 20h entry remains relevant to compatibility, but it is not a universal substitute for the documented exit service.

A read-only inspection pattern

For a diagnostic program, first ask DOS for the current PSP segment and inspect only fields the target environment documents. The following assembly outline reports a process identifier without changing its context:

        mov     ah, 62h         ; Get current PSP
        int     21h             ; BX = PSP segment on supported DOS versions
        mov     [current_psp], bx

The data symbol must be in the program’s writable data segment. This is an API sketch: a complete routine must preserve registers required by its calling convention and account for the minimum DOS version it supports. If a program needs the parent segment, it can read the documented field from the PSP it just obtained, but should validate that segment before treating it as readable memory. Never walk the MCB chain or PSP chain indefinitely based on untrusted values.

Review and test checklist

When debugging startup or process-cleanup problems, capture the DOS/kernel version, executable format, PSP segment, relevant MCB owner, and the exact service that failed. Test .COM and .EXE startup separately. Exercise a command tail longer than the expected default, a search after tail parsing, a child with inherited handles, a custom environment, normal exit, and Ctrl-Break or critical-error termination in a disposable VM image.

Keep all mutation within DOS APIs. Do not rewrite the parent PSP, handle-table pointer, interrupt vectors, environment pointer, or allocation-size word to “repair” a process. A memory editor may make the symptom disappear while corrupting a later exit path. If a PSP field looks inconsistent, determine whether an extender, shell, or DOS box owns that convention before drawing conclusions from a raw dump.

The PSP is most useful when treated as a compact index into a process, not as the complete process itself. Its stable compatibility fields explain how old programs start and return; DOS services and the kernel’s allocation/handle machinery remain responsible for the state behind those fields.

Related:

Sources:

Comments