Loader track

Programs

How LainDOS moves from a shell command to a child process and back: EXEC, PSP layout, environments, COM and MZ EXE handoff, overlays, return codes, and termination cleanup.

The loader path

LainDOS is single-tasking, but DOS still has process state. A child gets its own PSP, MCB owner, JFT view, DTA, environment block, command tail, and terminate vectors; the parent is suspended by a saved real-mode stack frame until the child exits.

01
Shell
SHELL.COM parses a command and calls INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH. AH=4Bh with DS:DX = child path and ES:BX = EXEC parameter block.
02
Loader
LainDOS resolves the path, reads the first sector, detects COM vs MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs., allocates an MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it.-backed block, and loads clusters.
03
PSP
The child gets a PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. with terminate vectors, parent PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., environment segment, JFT, command tail, and default FCBs.
04
Run
COM starts at PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:0100; EXE relocations are applied, then CS:IP and SS:SP come from the MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs. header.
05
Return
INT 20h, AH=4Ch, or AH=31h returns through cleanup, restores the parent frame, and records the return code.

EXEC captures the parent frame

AH=4Bh is the bridge from a shell command to a child process.

The parent supplies the path in DS:DX and an EXEC parameter block in ES:BX. LainDOS saves the parent's registers, PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., DTADTAThe DOS buffer where FindFirst/FindNext and some FCB calls return directory search results., and stack frame before resolving the child. That saved frame is what makes returning from a child look like a normal DOS call to the parent.

Only AL=00h load-and-run and AL=03h overlay load are implemented. Other EXEC variants fail explicitly instead of becoming silent compatibility stubs.

src/kernel/int21.incNASM · 16-bit
1908
1909
1910
1911
1912
1934
1935
1936
1937
1938
.exec:
cmp al, 0
je .exec_program
cmp al, 3
je .exec_overlay
mov [cs:exec_param_off], bx
mov [cs:exec_param_seg], es
mov [cs:exec_path_off], dx
mov [cs:exec_path_seg], ds
call load_exec_program

Tests that pin this

scripts/test_shell.pyscripts/test_execparam.pyscripts/test_spawn.py

Resolve, size, and classify the image

The first sector decides whether the child is COM or MZ EXE.

load_exec_program turns the path into a directory entry, records the first cluster and size, reads sector zero into SEC_BUF, and then checks for the MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs. signature. COM programs get a flat allocation plus slack; EXE programs compute a paragraph requirement from the header, minalloc, maxalloc, and file image size.

After allocation the image is loaded immediately above the PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. at prog_seg + 10h. COM images get their PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. and command tail before control transfers; EXE images wait until relocation setup has validated the header.

src/kernel/exec.incNASM · 16-bit
1
4
5
6
19
21
27
29
30
31
148
159
170
load_exec_program:
mov ds, [cs:exec_path_seg]
mov si, [cs:exec_path_off]
call resolve_path
call exec_read_first_sector
call update_exec_environment_path
mov ax, SEC_BUF
cmp word [0], 0x5A4D
jne .com_size
mov byte [cs:exec_is_exe], 1
.alloc:
mov [cs:prog_seg], ax
call load_file_direct

Tests that pin this

scripts/test_badreloc.pyscripts/test_memrelease.pyscripts/test_shell.py

Environment blocks and executable path tail

Each child receives an environment block owned by its PSP.

If the EXEC parameter block names a custom environment, LainDOS copies it. Otherwise it writes the default variables, including COMSPEC, PATH, PROMPT, and BLASTER. It then appends the DOS convention tail: a word count followed by the fully normalized executable path.

The environment MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. starts with a temporary owner while loading. Once the PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. is committed, assign_exec_environment_owner stamps that MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. with the child PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. so termination cleanup can release it with the rest of the process.

src/kernel/exec.incNASM · 16-bit
264
265
270
273
275
285
296
298
299
306
375
mov word [cs:exec_env_src_seg], 0
mov ax, [cs:exec_param_seg]
mov ax, [bx]
mov [cs:exec_env_src_seg], ax
call alloc_exec_environment
.copy_env:
call write_environment_vars
mov ax, 1
stosw
mov ds, [cs:exec_path_seg]
stosb

Tests that pin this

scripts/test_execenv.pyscripts/test_envmcb.pyscripts/test_envpath.py

The Program Segment Prefix

The PSP is the child process contract DOS programs expect at DS=ES.

LainDOS clears the 256-byte PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., writes the CD 20 terminate instruction, records the top-of-memory word, copies INT 22h/23h/24h vectors, builds the Job File Table, links the parent PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., and stores the environment pointer at PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:2Ch.

The command tail lives at PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:80h. Default FCBs from the EXEC parameter block are copied to PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:5Ch and PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:6Ch so older startup code and C runtimes see the DOS layout they expect.

src/kernel/exec.incNASM · 16-bit
844
852
853
866
882
884
943
944
945
946
950
951
build_psp:
mov byte [es:0x00], 0xCD
mov byte [es:0x01], 0x20
mov ax, [0x22*4]
mov word [es:0x32], MAX_HANDLES
mov [es:0x36], ax
mov bx, [cs:cur_psp]
mov [es:0x16], bx
mov bx, [cs:exec_env_seg]
mov [es:0x2C], bx
call assign_exec_environment_owner
call exec_copy_default_fcbs

Tests that pin this

scripts/test_shell.pyscripts/test_execparam.pyscripts/test_jft.py

COM and EXE handoff

COM is flat; MZ EXE is relocated before CS:IP and SS:SP are loaded.

COM handoff is simple: DS, ES, and SS all point at the PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., SP is placed near the top of the allocated block, and a far return lands at offset 0100h. EXE handoff is stricter: relocation table bounds are validated, relocation entries are applied against exe_load_seg, the image is slid down past the MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs. header, and then the header's CS:IP and SS:SP are used.

Both paths reset the keyboard buffer and FPU before entering the child so old startup code sees a predictable machine state.

src/kernel/exec.incNASM · 16-bit
1103
1106
1125
1126
1142
1143
1152
1153
1154
1186
1225
1255
1259
1260
mov ax, [cs:exe_reloc_count]
cmp word [cs:exe_reloc_off], 0x1C
mov ax, [cs:prog_seg]
call build_psp
mov di, [bx]
mov ax, [bx+2]
mov ax, [es:di]
add ax, [cs:exe_load_seg]
mov [es:di], ax
call exec_exe_dyn
exec_com_dyn:
mov ss, ax
push word 0x0100
retf

Tests that pin this

scripts/test_badreloc.pyscripts/test_overlay.pyscripts/test_regpres.py

Overlays load without becoming processes

EXEC AL=03h copies code into a caller-supplied segment.

Overlay loads use the same path resolver but no PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. switch. The caller supplies a load segment and relocation segment in the overlay parameter block. MZMZThe DOS EXE header signature. LainDOS uses it to distinguish EXE files from flat COM programs. overlays skip the header, copy the image to the requested segment, and apply relocation entries using the supplied relocation base.

Because no child process starts, success returns directly to the caller with carry clear; failures return DOS-style errors without changing process context.

src/kernel/int21.incNASM · 16-bit
1961
1969
1974
1975
1976
1977
1980
1988
1991
1992
.exec_overlay:
mov [cs:ov_param_off], bx
mov ax, [es:bx]
mov [cs:ov_load_seg], ax
mov ax, [es:bx+2]
mov [cs:ov_reloc_seg], ax
call resolve_path
call load_overlay_direct
xor ax, ax
jmp iret_nc

Tests that pin this

scripts/test_overlay.pytests/programs/ovltest.asmtests/programs/overlay.asm

Termination returns to the parent

Cleanup releases child-owned state and restores the saved parent stack.

Normal termination clears transient hardware state, releases inherited handles, closes child-owned handles, frees child-owned MCBs, coalesces the arena, restores the parent PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. from PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:16h, and jumps back through the saved EXEC frame.

TSR termination is different: it keeps the requested part of the PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. block resident, frees the rest of the child's allocations, records return type 3, and then restores the parent just like a normal return.

src/kernel.asmNASM · 16-bit
2132
2136
2146
2147
2149
2161
2169
2174
2175
2184
2187
2189
do_terminate:
call restore_irq1_null_mask
call release_inherited_handles
call close_owned_handles
mov si, [cs:mcb_first]
mov word [ds:1], 0
call mcb_coalesce_all_free
mov ax, [0x16]
mov [cs:cur_psp], ax
mov ax, [cs:saved_ss]
mov sp, [cs:saved_sp]
jmp exec_com.back

Tests that pin this

scripts/test_retcode.pyscripts/test_termflush.pyscripts/test_tsr.py