DOS Memory Control Blocks: INT 21h Allocation, Resize, and Ownership
Use DOS INT 21h functions 48h, 49h, and 4Ah safely by sizing paragraphs, interpreting MCB chains, handling fragmentation, and validating ownership.
DOS memory allocation is a contiguous real-mode arena, not a protected virtual-memory heap. A program asks the kernel for a number of 16-byte paragraphs, receives a segment at which its data block begins, and must preserve that block’s ownership and size until it is released. The Memory Control Block (MCB) immediately before the data records the allocation’s extent and owner. Understanding this boundary is essential when a utility allocates buffers, shrinks before launching a child, or diagnoses an allocation failure that occurs despite apparently abundant free memory.
This article focuses on the DOS INT 21h allocator and FreeDOS’s MCB implementation. It is separate from XMS, EMS, DPMI, and UMB provisioning: those services can expose or manage additional memory, but they do not change the basic semantics of the three DOS allocation functions. The kernel’s own allocator implementation is useful evidence, not permission for applications to edit MCB bytes directly.
Paragraphs, segments, and the allocator boundary
The unit passed to DOS is one paragraph, or 16 bytes. A request for a byte count must be rounded up without overflowing the arithmetic: paragraphs = (bytes + 15) / 16. A 4,097-byte object therefore needs 257 paragraphs, not 256. Check the addition and conversion before narrowing the result into the 16-bit BX register. The block is contiguous, so a successful allocation of 257 paragraphs represents one 4,112-byte region; it is not a collection of smaller pieces.
In the FreeDOS kernel, an MCB is one paragraph long. Its M type marks an ordinary link in the chain and Z marks the final block. The owner field normally contains the owning process’s PSP segment, or zero for a free block; the size field counts data paragraphs after the header. Thus the next MCB begins at the current MCB segment plus its data size plus one paragraph for the current header. These are facts about the kernel’s representation. Programs should use DOS services rather than scanning or rewriting that chain, because allocation strategy, UMB linkage, and implementation details can vary.
The three DOS allocation operations
| Function | Inputs | Successful result | Operational meaning |
|---|---|---|---|
| INT 21h, AH=48h | BX = requested paragraphs | Carry clear; AX = data-block segment | Allocate a new block. |
| INT 21h, AH=49h | ES = data-block segment | Carry clear | Free the exact block previously allocated or otherwise owned by the caller. |
| INT 21h, AH=4Ah | ES = data-block segment; BX = desired paragraph count | Carry clear | Resize an existing block in place; it does not relocate the contents for the caller. |
An allocation failure is not the same as total exhaustion. FreeDOS reports a DOS error through carry and AX; its kernel also computes the largest available block in BX on an AH=48h failure. A caller should preserve the error registers immediately, distinguish a too-large request from other failures, and decide whether to retry with a smaller, genuinely acceptable size. Silently accepting a short buffer when the application requires the full amount can turn a clean allocation failure into memory corruption later.
For example, an assembly caller may request 4 KiB like this:
mov bx, 0100h ; 256 paragraphs = 4096 bytes
mov ah, 48h
int 21h
jc allocation_failed
mov [block_segment], ax
The symbol block_segment must reside in the caller’s writable data segment. The code intentionally does not treat BX as a byte count. It also does not assume a particular segment value: DOS chooses a suitable block from the currently available chain.
To release that allocation, load its returned data segment into ES and call AH=49h. Do not pass the MCB segment, an offset, an interior pointer, or a segment remembered from a different process. A double-free or a free using the wrong segment can damage the arena. A routine that returns to a caller must also ensure no live pointer or DMA operation still refers to the block.
AH=4Ah changes the size of a block in place. Shrinking creates a free remainder after the retained prefix; existing contents within the retained range stay at their addresses, but pointers beyond the new end become invalid. Growing normally requires enough contiguous free space immediately following the current block. Free memory elsewhere in the arena cannot be joined to it without moving the block, which this API does not do. A failed growth should be treated as a failed resize, not as evidence that the block was relocated or enlarged.
Fragmentation and allocation strategy
The arena is a chain of adjacent blocks, not a single free-byte counter. Suppose the free portions are 96 KiB, 48 KiB, and 72 KiB with allocated blocks between them. Their total is 216 KiB, but a 120 KiB contiguous request still fails. The largest available block is the useful first diagnostic for an allocation of one contiguous region. Reducing unrelated resident use or changing load order may help; increasing an XMS figure does not make a conventional-memory block contiguous.
DOS exposes allocation-strategy controls through INT 21h, AH=58h; the supported strategy and whether upper-memory blocks participate depend on the DOS-compatible kernel and configuration. FreeDOS’s memmgr.c contains first-fit, best-fit, last-fit, and UMB-aware variants. An application should not depend on which segment a successful request returns or assume that a different strategy is a universal performance improvement. Strategy affects where a block may be selected, not the application’s right to assume a fixed address.
When UMBs are linked into the DOS allocation chain, allocations can come from more than one physical segment range. A segment comparison that assumes every DOS allocation lies below 640 KiB can therefore be wrong. Conversely, XMS handles and DPMI selectors are not ordinary MCB data segments. Use the allocator corresponding to the kind of memory needed, and never pass an XMS handle or protected-mode pointer to AH=49h.
Ownership, process exit, and child execution
DOS associates memory blocks with a PSP owner. FreeDOS’s process cleanup walks MCBs and releases blocks belonging to the terminating PSP. That automatic cleanup applies at process termination; it does not excuse a long-running program from releasing temporary allocations or a TSR from retaining only the memory it needs. Terminate-and-stay-resident code uses a resize operation to keep a small resident prefix, while every pointer to the discarded tail must be dead before the call.
A parent that launches a child with EXEC may need to shrink its own block first, because the child must fit in the memory DOS can allocate while the parent remains resident. Keep the stack, code, data, EXEC parameter block, child-return state, and any post-child buffers inside the retained extent. This is an interaction between process loading and allocation, not a reason to guess how many paragraphs a compiler’s program uses. Check the linker map and calculate the retained range from actual segment layout.
Environment blocks are separate DOS blocks in FreeDOS’s implementation. This is one reason custom TSR or process code must not blindly free memory based only on the PSP’s main block size. Follow the relevant DOS environment and process conventions, and prefer releasing memory through supported APIs with verified ownership.
A safe implementation and test workflow
For each allocation, record the requested byte count, rounded paragraph count, returned segment, intended owner, and lifetime. On failure, record carry, AX, and the available-size value if the target kernel supplies it. On resize, distinguish shrink from grow and retain a rollback path before relying on the new size. Check that every code path, including errors and cancellation, frees temporary blocks exactly once.
Test in a VM snapshot or disposable disk image, not first on a machine whose startup depends on a fragile memory configuration. Exercise a small successful allocation, a deliberately impossible request, a shrink followed by use of the retained prefix, and a grow that succeeds only when adjacent space is available. Run a child-program launch from the same test harness if the production workflow uses EXEC. Reboot after a controlled fault test and verify the known-good boot menu remains available.
The MEM utility can help record overall conventional, upper, and extended-memory state, but its display is not a substitute for checking the actual return from the API call. Compare measurements before and after each configuration change, and use the help text for the installed MEM build because utility options can differ. Do not run an MCB-editing tool against a valuable installation merely to obtain a more attractive free-memory number.
The core invariants are straightforward: count paragraphs correctly; treat an allocation as one contiguous block; pass the data segment, not the preceding MCB, to free and resize; and never use bytes beyond the current extent. FreeDOS’s source shows how its allocator maintains and merges the chain, while the public DOS calls give applications a safer abstraction. Staying on that side of the interface keeps one local allocation error from becoming system-wide arena corruption.
Related:
- Upper Memory Blocks and Loading TSRs High
- DOS INT 21h EXEC: Launch a Child and Collect Its Real Exit Result
Sources: