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.

AL=00h load-and-run, AL=01h load-only (the child's entry SS:SP and CS:IP are returned through the parameter block), and AL=03h overlay load are implemented. Other EXEC variants fail explicitly instead of becoming silent compatibility stubs.

src/kernel/int21.incNASM · 16-bit
1950
1951
1952
1953
1954
1955
1956
2038
2039
2040
2041
2042
.exec:
cmp al, 0
je .exec_program
cmp al, 1
je near .exec_load
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
165
169
170
177
208
211
217
219
220
221
249
260
274
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. If the parameter block asks to inherit with env segment 0, it copies the parent PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. environment; only a missing parent environment falls back to generated defaults. 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
528
471
534
541
543
552
559
583
594
609
615
616
617
628
mov word [cs:exec_env_src_seg], 0
mov ax, [cs:exec_param_seg]
mov ax, [bx]
mov [cs:exec_env_src_seg], ax
.inherit_parent_env:
mov ax, [0x2C]
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.pyscripts/test_envoflow.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
1480
1488
1489
1502
1518
1520
1579
1580
1582
1583
1587
1589
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
1708
139
1750
1715
1732
1733
1742
1739
1744
1778
1830
1810
1858
1871
mov ax, [cs:exe_reloc_count]
cmp dx, 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
2104
2112
2117
2118
2119
2120
2248
2156
831
550
.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 first loads the child MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it.'s EXEC resume state, then 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
3099
3103
3105
3114
3115
3117
3123
3126
3131
3133
3142
3145
3146
3150
do_terminate:
call load_exec_resume_state
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:term_resume_ss]
mov sp, [cs:term_resume_sp]
mov ax, [cs:term_resume_kbase]
jmp exec_resume_parent

Tests that pin this

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