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 boot sector loads the kernel, where the kernel relocates (the High Memory Area at FFFF:0010), where CD/FAT/read/write/subdirectory-cache scratch sectors live, where the DOS arena begins, and where conventional memory ends.
With the kernel image and stack resident in the HMA, low memory holds only the filesystem scratch buffers: the DOS arena starts at 0B00h — kept at the lowest placement real MS-DOS could produce, because era programs (MONKEY2.EXE among them) corrupt themselves when loaded below that — 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 enables the A20 line, copies itself to the HMA at HMA_SEG:HMA_OFF, switches DS/ES/SS to the relocated segment, and puts the stack at KERNEL_STACK_TOP near the top of the HMA. 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, sized to the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O. INT 12h conventional-memory line (the EBDA above it stays with the BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O.). 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 HMA-resident kernel clear of its own stack, keep the image small enough for the boot loader's staging area at LOAD_SEG, keep the low FAT/CD/sector/read/root/write/CD-cache/subdirectory-cache buffers ordered, and ensure the cache 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, and AH=48h dispatches them straight onto the shared allocators: alloc_mem_direct (first fit), alloc_mem_direct_best, and alloc_mem_direct_high (last fit), after mcb_chain_validate has checked every signature.
The default strategy is plain DOS first fit. An earlier build biased tiny requests to the last suitable block, but that deviation handed DOS/4GWDOS/4GWA DOS extender that switches games into protected mode while still using DOS for startup and file services.'s transfer buffer a top-of-memory segment and broke programs that sign-extend real-mode segments (Settlers II's VBE path); real-DOS placement turned out to be the safer behavior.
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_envoflow.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 XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. at XMS_MAX_KB when there is enough RAM left for the default EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. pool. 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, and the private AX=43E0h subfunction reports the pool size in DX so the FREE utility can show a real XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. total.
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 default with an upper frame
Normal builds install INT 67h with 32 handles, four page-frame slots, and a 6 MiB backing pool when the D000h frame probe succeeds.EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. needs a 64 KiB page frame, but putting that frame in conventional memory would make Millennia-style 580 KiB base-memory checks impossible. LainDOS therefore enables and probes PCI shadow RAM for a D000h upper-memory frame before advertising EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame..
The default EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. driver exposes 32 handles, 384 logical pages (6 MiB), and four physical page-frame slots. AH=44h maps one page; EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. 4.0 AH=50h AL=00h maps or unmaps a table of physical-page entries. Mapping saves the old frame page back to high backing storage, copies the requested logical page into the frame, and records the mapping. The backing base is computed after XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. sizing so EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. and XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. do not alias.
Tests that pin this
scripts/test_ems.pyscripts/test_emsmulti.pyscripts/test_emsmap40.pyscripts/test_emspreserve.pyscripts/test_emsmem.pyscripts/test_emslarge.pyscripts/test_emsxms.pyFREE.COM is the user-visible audit
The shell memory report walks the same MCB chain users depend on.The FREE utility first moves its stack into the image and shrinks its own COM block, so its large inherited allocation does not hide all free conventional memory. It then 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