Real-mode memory track

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.

01
Layout
The kernel relocates to the HMA at FFFF:0010, keeps scratch buffers below 0B00h, and reserves A000h for VGAVGAThe PC video standard LainDOS and the demos use for text and graphics output..
02
Arena
One free MCBMCBA 16-byte DOS memory header that describes the allocated or free block immediately after it. starts at 0B00h and grows upward until MEM_TOP, with owner 0 meaning free.
03
Allocate
INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH. AH=48h supports first, best, and last-fit strategy, plus a high-biased small allocation path.
04
Own
Program and environment blocks are stamped with the current PSPPSPThe DOS data block placed before each program, holding terminate vectors, the job file table, command tail, and environment pointer. so exit cleanup can reclaim them.
05
Extend
XMSXMSExtended Memory Specification services for memory above 1 MiB, used by many later DOS games and extenders. is a single-handle BIOSBIOSFirmware services available before DOS exists; it loads the boot sector and provides interrupts such as INT 13h disk I/O.-move shim; default EMSEMSExpanded Memory Specification: bank-switched memory exposed through an EMS page frame. uses a validated upper-memory frame.

Fixed 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.

src/memory.incNASM · 16-bit
3
4
5
6
7
8
9
10
11
12
13
15
16
17
20
21
22
23
26
27
28
36
37
38
39
40
41
42
44
45
LOAD_SEG equ 0x1000
HMA_SEG equ 0xFFFF
HMA_OFF equ 0x0010
ENTRY_SEG equ (LOAD_SEG - 1)
SECTOR_BUF_PARAS equ 0x20
READ_CACHE_SECTORS equ 4
READ_CACHE_PARAS equ (SECTOR_BUF_PARAS * READ_CACHE_SECTORS)
CD_BUF_PARAS equ 0x80
CD_CACHE_SLOTS equ 4
%if (CD_CACHE_SLOTS & (CD_CACHE_SLOTS - 1)) != 0
%error "CD_CACHE_SLOTS must be a power of two"
CD_CACHE_SLOT_PARAS equ CD_BUF_PARAS
SUBDIR_CACHE_SLOTS equ 16
%if (SUBDIR_CACHE_SLOTS & (SUBDIR_CACHE_SLOTS - 1)) != 0
SUBDIR_CACHE_SLOT_PARAS equ SECTOR_BUF_PARAS
CD_BUF equ 0x0180
SEC_BUF equ 0x0200
READ_CACHE_BUF equ (SEC_BUF + SECTOR_BUF_PARAS)
WRITE_CACHE_BUF equ 0x06A0
CD_CACHE_BUF equ (WRITE_CACHE_BUF + SECTOR_BUF_PARAS)
SUBDIR_CACHE_BUF equ (CD_CACHE_BUF + (CD_CACHE_SLOTS * CD_CACHE_SLOT_PARAS))
MCB_START equ 0x0B00
MEM_TOP equ 0xA000
ENV_PARAS equ 16
ENV_SIZE_BYTES equ (ENV_PARAS * 16)
ENV_OWNER_TEMP equ 0xFFFF
%if (MEM_TOP & 0xFF) != 0
%error "MEM_TOP must be 256-byte aligned"
MCB_SIG_M equ 'M'
MCB_SIG_Z equ 'Z'

Tests that pin this

scripts/test_boot.pyscripts/test_highmcb.pyscripts/test_free.py

Boot 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.

src/kernel.asmNASM · 16-bit
132
148
156
163
164
168
169
180
181
189
190
206
207
208
209
212
214
219
220
220
221
222
kernel_entry:
call enable_a20
mov ax, HMA_SEG
jmp HMA_SEG:.relocated
.relocated:
mov ss, ax
mov sp, KERNEL_STACK_TOP
int 0x12
mov [mem_kib], ax
%if ENABLE_XMS
call init_xms_size
mov ax, MCB_START
mov es, ax
mov byte [es:0], MCB_SIG_Z
mov word [es:1], 0
mov ax, [mem_kib]
shl ax, cl
sub ax, MCB_START + 1
mov word [es:3], ax
mov word [es:3], ax
mov word [mcb_first], MCB_START
mov word [cur_psp], 0

Tests that pin this

scripts/test_boot.pyscripts/test_memfail.pyscripts/test_highmcb.py

Compile-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.

src/kernel.asmNASM · 16-bit
5072
5073
5075
5076
5078
5079
5081
5082
5084
5085
5087
5088
5093
5094
5096
5097
5099
5100
5102
5103
5105
5106
5108
5109
5111
5112
5114
5115
5126
5128
%if (HMA_OFF + (kernel_end - kernel_entry)) > (KERNEL_STACK_TOP - KERNEL_STACK_GUARD_BYTES)
%error "kernel leaves too little HMA stack guard"
%if (kernel_end - kernel_entry) > ((MEM_TOP - LOAD_SEG) * 16)
%error "kernel exceeds boot load area"
%if (FAT_SEG + 0x120) > CD_BUF
%error "FAT buffer overlaps CD_BUF"
%if (CD_BUF + CD_BUF_PARAS) > SEC_BUF
%error "CD_BUF overlaps SEC_BUF"
%if (SEC_BUF + SECTOR_BUF_PARAS) > READ_CACHE_BUF
%error "SEC_BUF overlaps READ_CACHE_BUF"
%if (READ_CACHE_BUF + READ_CACHE_PARAS) > ROOT_SEG
%error "READ_CACHE_BUF overlaps ROOT_SEG"
%if (ROOT_SEG + ROOT_BUF_PARAS) > WRITE_CACHE_BUF
%error "ROOT_SEG overlaps WRITE_CACHE_BUF"
%if (WRITE_CACHE_BUF + SECTOR_BUF_PARAS) > MCB_START
%error "WRITE_CACHE_BUF overlaps MCB arena"
%if (WRITE_CACHE_BUF + SECTOR_BUF_PARAS) > CD_CACHE_BUF
%error "WRITE_CACHE_BUF overlaps CD cache"
%if (CD_CACHE_BUF + (CD_CACHE_SLOTS * CD_CACHE_SLOT_PARAS)) > MCB_START
%error "CD cache overlaps MCB arena"
%if (CD_CACHE_BUF + (CD_CACHE_SLOTS * CD_CACHE_SLOT_PARAS)) > SUBDIR_CACHE_BUF
%error "CD cache overlaps subdirectory cache"
%if (SUBDIR_CACHE_BUF + (SUBDIR_CACHE_SLOTS * SUBDIR_CACHE_SLOT_PARAS)) > MCB_START
%error "subdirectory cache overlaps MCB arena"
%if MCB_START >= MEM_TOP
%error "MCB arena is empty"
%if ENABLE_EMS && EMS_FRAME_SEG < 0xD000
%error "EMS frame must use an upper-memory window at D000h or above"
%if ENABLE_XMS && XMS_MAX_KB > 15360
%error "XMS BIOS move backing must remain below 16 MiB"

Tests that pin this

scripts/test_boot.pyscripts/test_free.pyscripts/test_ems.py

MCBs 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.

src/kernel/memory_mcb.incNASM · 16-bit
1
2
5
7
9
22
54
83
102
86
107
109
112
29
36
41
66
43
115
116
118
mcb_walk_next:
mov ax, [ds:3]
add ax, si
inc ax
cmp ax, MEM_TOP
mcb_split_low:
mcb_split_high:
mcb_chain_validate:
alloc_mem_direct:
mov si, [cs:mcb_first]
MCB_WALK_EACH .amd_walk, .amd_check, .amd_next, .amd_nomem, .amd_nomem, .amd_nomem
cmp word [ds:1], 0
cmp ax, [cs:am_req]
cmp ax, 2
mov [es:0], al
mov [es:3], cx
mov byte [ds:0], MCB_SIG_M
mov [ds:3], bx
mov ax, [cs:cur_psp]
mov word [ds:1], ax
inc ax

Tests that pin this

scripts/test_highmcb.pyscripts/test_envmcb.pyscripts/test_memrelease.py

DOS 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.

src/kernel/int21.incNASM · 16-bit
1477
1147
1491
1149
1493
831
1550
1500
1502
1539
1548
1553
1555
1556
1559
1562
1598
1599
1600
.alloc_strategy:
cmp al, 0
je .as_get
cmp al, 1
je .as_set
xor ax, ax
mov al, [cs:alloc_strat]
cmp bl, 2
mov [cs:alloc_strat], bl
.alloc_mem:
call mcb_chain_validate
je .am_strat_best
je .am_strat_high
.am_strat_first:
.am_strat_best:
.am_strat_high:
.am_nomem:
call find_largest_free_block
mov ax, 8

Tests that pin this

scripts/test_stratapi.pyscripts/test_memfail.pyscripts/test_highmcb.py

Free 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.

src/kernel/int21.incNASM · 16-bit
1626
1632
1633
1635
1635
1640
1641
1648
1655
1632
1664
1666
1674
1679
1683
1665
1696
1696
1641
1692
1696
1641
1716
1725
1797
1727
.free_mem:
mov si, es
dec si
MCB_IS_VALID
MCB_IS_VALID
mov word [ds:1], 0
call mcb_merge_free_forward
.resize_mem:
mov [cs:rm_req], bx
mov si, es
mov ax, [ds:3]
jae .rm_shrink
cmp byte [es:0], MCB_SIG_M
cmp word [es:1], 0
add ax, cx
cmp ax, bx
call mcb_split_low
call mcb_split_low
call mcb_merge_free_forward
mov ax, [ds:3]
call mcb_split_low
call mcb_merge_free_forward
mov [es:0x02], ax
.rm_cant_grow:
mov bx, ax
mov ax, 8

Tests that pin this

scripts/test_memfail.pyscripts/test_memrelease.pyscripts/test_tsr.py

Owners 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.

src/kernel/exec.incNASM · 16-bit
733
737
561
743
745
748
749
751
764
774
780
793
797
802
807
809
alloc_exec_environment:
mov word [cs:exec_env_seg], 0
mov bx, ENV_PARAS
call alloc_mem_direct
mov [cs:exec_env_seg], ax
dec ax
mov ds, ax
mov word [ds:1], ENV_OWNER_TEMP
free_exec_environment:
dec ax
mov word [ds:1], 0
assign_exec_environment_owner:
mov bx, [cs:exec_env_seg]
dec bx
mov ax, [cs:cur_psp]
mov [ds:1], ax

Tests that pin this

scripts/test_envmcb.pyscripts/test_execenv.pyscripts/test_envoflow.pyscripts/test_memrelease.py

Exit 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.

src/kernel.asmNASM · 16-bit
3099
3110
5048
3113
3114
3115
3117
3118
3056
3121
3123
3126
3131
3133
do_terminate:
%if ENABLE_EMS
mov word [cs:ems_alloc_pages], 0
mov word [cs:xms_alloc_kb], 0
call release_inherited_handles
call close_owned_handles
mov si, [cs:mcb_first]
MCB_WALK_EACH .dt_mcb_walk, .dt_mcb_check, .dt_mcb_next, .dt_mcb_done, .dt_mcb_done, .dt_mcb_done
mov ax, [cs:cur_psp]
cmp word [ds:1], ax
mov word [ds:1], 0
call mcb_coalesce_all_free
mov ax, [0x16]
mov [cs:cur_psp], ax

Tests that pin this

scripts/test_memrelease.pyscripts/test_free.pyscripts/test_shell.py

XMS 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.

src/kernel.asmNASM · 16-bit
45
46
124
125
2044
2046
2047
2048
2053
4779
4784
4789
4800
4796
2081
2095
2097
2103
2106
2119
2136
2138
2140
2142
2179
2185
2189
2191
2167
2156
2205
2209
2218
2226
2234
2280
2286
%ifndef XMS_MAX_KB
%define XMS_MAX_KB 15360
%ifndef ENABLE_XMS
%define ENABLE_XMS 1
init_xms_size:
mov word [cs:xms_total_kb], 0
mov ah, 0x88
int 0x15
call init_xms_store
init_xms_store:
cmp ax, (EMS_TOTAL_PAGES * 16)
mov ax, XMS_MAX_KB - 64
mov [cs:ems_backing_hi], bx
mov [cs:xms_total_kb], ax
int2f_handler:
cmp ax, 0x4300
cmp ax, 0x4310
mov al, 0x80
mov bx, xms_entry
xms_entry:
cmp ah, 0x08
cmp ah, 0x09
cmp ah, 0x0A
cmp ah, 0x0B
mov ax, [cs:xms_total_kb]
cmp word [cs:xms_alloc_kb], 0
cmp dx, [cs:xms_total_kb]
mov [cs:xms_alloc_kb], dx
mov ax, 1
mov dx, 1
mov word [cs:xms_alloc_kb], 0
.move:
test ax, 1
call xms_prepare_endpoint
call xms_prepare_endpoint
mov ax, 0x8700
int 0x15

Tests that pin this

scripts/test_xms.pyscripts/test_free.pyscripts/test_shell.py

EMS 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.

src/kernel.asmNASM · 16-bit
48
49
50
51
53
56
62
128
129
1849
1850
1852
1853
2438
2439
2440
2444
2445
2447
2480
2481
2491
2492
2497
2500
2501
2502
2504
2506
2508
2509
2510
2520
2521
2523
2524
2529
2532
2533
2535
2536
2544
2546
2548
2551
2631
2632
2634
2636
2648
2650
2674
2692
2704
2730
2755
2761
2789
2790
2798
2800
2804
2810
2813
2830
2841
2858
2863
2884
2891
5048
5049
5052
5055
2596
2598
XMS_BASE_HI equ 0x0011
EMS_TOTAL_PAGES equ 384
%ifndef EMS_FRAME_SEG
%define EMS_FRAME_SEG 0xD000
EMS_FRAME_PARAS equ 0x1000
EMS_BOUNCE_PHYS_LO equ ((EMS_BOUNCE_BUF << 4) & 0xFFFF)
EMS_COPY_GDT_BUF equ SEC_BUF
%ifndef ENABLE_EMS
%define ENABLE_EMS 1
%if ENABLE_EMS
mov [es:0x67*4], word int67_handler
%else
mov [es:0x67*4], word int67_absent_handler
%if !ENABLE_EMS
int67_absent_handler:
mov ah, 0x80
%if ENABLE_EMS
EMS_MAX_HANDLES equ 32
int67_handler:
cmp ah, 0x50
je .map_multi
.pages:
mov bx, [cs:ems_total_pages]
.alloc:
mov ax, [cs:ems_total_pages]
sub ax, [cs:ems_alloc_pages]
cmp bx, ax
call ems_find_free_handle
call ems_find_page_run
mov [cs:si+ems_handle_pages], bx
mov [cs:si+ems_handle_base], ax
add [cs:ems_alloc_pages], bx
.map:
call ems_map_request
.map_multi:
call ems_map_multi_request
.free:
call ems_clear_handle_maps
mov ax, [cs:si+ems_handle_base]
call ems_clear_owner_pages
sub [cs:ems_alloc_pages], bx
.handles:
mov bx, EMS_MAX_HANDLES
.info:
mov bx, [cs:si+ems_handle_pages]
ems_map_request:
call ems_handle_offset
cmp al, 3
cmp bx, [cs:si+ems_handle_pages]
cmp bx, [cs:si+ems_map_pages]
je .map_already
call ems_copy_16k
mov [cs:si+ems_map_pages], bx
.map_already:
ems_unmap_phys_page:
call ems_copy_16k
mov word [cs:si+ems_map_pages], 0xFFFF
ems_map_multi_request:
cmp al, 0
mov bx, [ds:si]
cmp bx, 0xFFFF
call ems_map_request
call ems_unmap_phys_page
add word [cs:ems_multi_ptr], 4
ems_copy_16k:
cmp word [cs:xms_dst_phys+2], EMS_FRAME_PHYS_HI
mov dx, EMS_BOUNCE_BUF
call ems_flat_copy_chunk
call ems_flat_copy_chunk
call ems_copy_real_chunk
mov word [cs:ems_alloc_pages], 0
mov di, ems_handle_pages
mov di, ems_handle_base
mov di, ems_page_owner
ems_clear_map:
mov word [cs:ems_map_pages], 0xFFFF

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.py

FREE.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.

programs/free.asmNASM · 16-bit
4
6
15
19
20
21
22
26
27
28
35
40
44
48
50
59
61
65
68
69
73
77
85
90
91
101
102
116
120
121
124
125
%include "src/memory.inc"
CONV_TOTAL_KB equ (MEM_TOP / 64)
mov sp, free_stack_top
mov bx, free_resident_paras
mov ah, 0x4A
int 0x21
jc resize_failed
call collect_mcb
call query_xms
call query_ems
collect_mcb:
mov si, MCB_START
cmp si, MEM_TOP
cmp al, MCB_SIG_M
cmp al, MCB_SIG_Z
mov ax, [1]
mov ax, [3]
cmp word [block_owner], 0
add [free_paras], ax
cmp ax, [largest_free]
cmp byte [block_sig], MCB_SIG_Z
add ax, [block_size]
query_xms:
mov ax, 0x4300
int 0x2F
mov ah, 0x08
call far [xms_entry]
query_ems:
mov ah, 0x40
int 0x67
mov ah, 0x42
int 0x67

Tests that pin this

scripts/test_free.pyscripts/test_shell.pyscripts/test_xms.pyscripts/test_ems.py