Memory
How LainDOS fits a kernel, filesystem buffers, a DOS MCB arena, optional EMS frame, and XMS shims into a real-mode machine that still has to run games below 640K.
The memory path
LainDOS is small enough to explain as a map. The kernel and its fixed buffers live below the program arena; DOS allocations are MCB headers linked by paragraph counts; child exit is just owner cleanup plus coalescing. The hard part is not the algorithm, it is keeping every fixed segment from colliding as the kernel grows.
0102030405Fixed segments define the machine
The low-memory layout is a contract, not a suggestion.src/memory.inc is the first file to read before moving buffers. These equates decide where the kernel relocates, where scratch sectors live, where the DOS arena begins, and where conventional memory ends.
The split is intentionally conservative: filesystem scratch buffers sit below the MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. arena, programs start at 1000h, and VGAVGAThe PC video standard LainDOS and the demos use for text and graphics output. graphics memory begins at A000h. MEM_TOP must stay 256-byte aligned because several bounds checks compare segment values directly.
Tests that pin this
scripts/test_boot.pyscripts/test_highmcb.pyscripts/test_free.pyBoot installs the initial arena
Relocation, stack placement, XMS sizing, and the first MCB happen before loading a child.The kernel copies itself to RELOC_SEG, switches DS/ES/SS to the relocated segment, and puts the stack at KERNEL_STACK_TOP. Only after serial/VGAVGAThe PC video standard LainDOS and the demos use for text and graphics output. bring-up and memory reporting does it initialize optional XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. sizing and the DOS arena.
The first arena is a single last-block MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. at MCB_START: signature Z, owner zero, size MEM_TOP - MCB_START - 1 paragraphs. Every later allocation is just a split or owner change inside that chain.
Tests that pin this
scripts/test_boot.pyscripts/test_memfail.pyscripts/test_highmcb.pyCompile-time guards catch overlap
Kernel size and buffer placement are checked before an image can boot.The dangerous edits are not in allocation code; they are usually new kernel code, larger buffers, or an EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. frame moved into the wrong segment. The final assertions in src/kernel.asm stop those mistakes at NASMNASMNetwide Assembler, the assembler used for LainDOS boot, kernel, shell, and focused test programs. time.
These guards keep the loaded kernel below the loader gap, prevent it from reaching SEC_BUF, keep sector/read/root buffers ordered, preserve a root-stack guard, and ensure the stack plus buffers stay below MCB_START.
Tests that pin this
scripts/test_boot.pyscripts/test_free.pyscripts/test_ems.pyMCBs split, merge, and walk by size
Each block has a one-paragraph header before the segment DOS returns.An MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. header starts one paragraph before the usable block. Byte 0 is M or Z, word 1 is the owner PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., and word 3 is the block size in paragraphs. The next header is current segment plus size plus one.
alloc_mem_direct is the compact helper used by loader-owned internal allocations. It walks from mcb_first, chooses the first free block large enough, splits if the remainder can hold another MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it., stamps the owner with cur_psp, and returns the usable segment.
Tests that pin this
scripts/test_highmcb.pyscripts/test_envmcb.pyscripts/test_memrelease.pyDOS allocation strategy is visible
AH=58h selects first, best, or last fit; AH=48h applies it.Programs can query and set the DOS allocation strategy through INT 21h AH=58h. LainDOS stores only values 0 through 2, matching first-fit, best-fit, and last-fit behavior used by the allocator.
The default first-fit path has one compatibility twist: requests from 2 through SMALL_ALLOC_HIGH_MAX paragraphs are biased to the last suitable block. That keeps tiny runtime allocations from fragmenting the low end of the arena before larger program loads.
Tests that pin this
scripts/test_stratapi.pyscripts/test_memfail.pyscripts/test_highmcb.pyFree and resize repair the chain
AH=49h and AH=4Ah validate headers, split remainders, and merge adjacent free blocks.Freeing a block checks the header immediately before ES, clears its owner, and merges forward if the next block is also free. Resizing uses the same header contract: grow by absorbing the next free block, or shrink by carving a new free MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. after the requested size.
Failure paths return DOS error codes and preserve the original allocation where possible. When allocation fails, BX is filled with the largest free block so callers can retry with a smaller request.
Tests that pin this
scripts/test_memfail.pyscripts/test_memrelease.pyscripts/test_tsr.pyOwners make cleanup deterministic
The current PSP owns program blocks, environment blocks, and child allocations.Environment blocks start with a temporary owner while EXEC is still building the child. 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 changes the MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. owner to the child PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., putting it on the same cleanup path as ordinary allocations.
Normal termination clears transient XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders./EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. state, closes handles, walks the MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. chain, releases every block whose owner matches cur_psp, then coalesces free neighbors before returning to the parent PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. saved at PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer.:16h.
Tests that pin this
scripts/test_envmcb.pyscripts/test_execenv.pyscripts/test_memrelease.pyExit releases process memory
A child can leak only if its owner tag is wrong.Termination is not a wholesale arena reset. It is owner-based: each MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. is checked against the current PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer., matching blocks are marked free, and unrelated parent or resident blocks remain intact.
After the walk, mcb_coalesce_all_free merges adjacent free blocks. That is why the shell can run a child repeatedly and still report a stable largest executable block.
Tests that pin this
scripts/test_memrelease.pyscripts/test_free.pyscripts/test_shell.pyXMS is a single-handle shim
INT 2Fh advertises an XMS entry point backed by BIOS INT 15h moves.On boot, LainDOS asks BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. INT 15h AH=88h for extended memoryXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. and caps it at XMS_MAX_KB. INT 2Fh AX=4300h/4310h then advertises one XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. entry point for callers that probe HIMEM-style services.
The implementation intentionally supports a single allocated handle: allocation succeeds only if no handle is active, handle 1 represents the whole block, and moves validate both real-mode endpoints and XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. offsets before chunking through BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. INT 15h AH=87h.
Tests that pin this
scripts/test_xms.pyscripts/test_free.pyscripts/test_shell.pyEMS is optional and off by default
Normal builds report no EMS; EMS test builds install INT 67h with one handle and four page-frame slots.EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. needs a 64 KiB page frame in conventional memory, so the default build leaves ENABLE_EMS at zero. In that mode, INT 67h returns AH=80h, which lets callers treat EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. as absent without consuming arena space.
When built with ENABLE_EMS=1, LainDOS exposes one handle, up to 64 logical pages, and four physical page-frame slots. Mapping saves the old frame page back to high backing storage, copies the requested logical page into the frame, and records the mapping.
Tests that pin this
scripts/test_ems.pyscripts/test_free.pyscripts/test_shell.pyFREE.COM is the user-visible audit
The shell memory report walks the same MCB chain users depend on.The FREE utility is intentionally simple: it starts at MCB_START, validates each header, totals free paragraphs, records the largest free block, probes XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. via INT 2Fh, probes EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. via INT 67h, and prints the table the tests inspect.
This gives contributors a quick manual sanity check after memory-sensitive changes: if MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. headers are corrupt, largest executable size is wrong, or XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders./EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. totals become inconsistent, make test and the shell MEM/FREE path should catch it.
Tests that pin this
scripts/test_free.pyscripts/test_shell.pyscripts/test_xms.pyscripts/test_ems.py