DOS hands out memory in paragraphs of 16 bytes, and a block always starts at offset 0 of a segment, AX:0000. BX = 1000h asks for 64 KB, BX = 1 for 16 bytes. The memory is not cleared.
Memory control blocks
Conventional memory is a chain of blocks, each preceded by a 16-byte memory control block (MCB) in the paragraph just below it, so the MCB of the block at segment AX is at AX - 1:
| Offset | Size | Contents |
|---|---|---|
| 00h | byte | "M" (4Dh): another block follows; "Z" (5Ah): the last block |
| 01h | word | the owner: the PSP segment of the program it belongs to; 0 = free |
| 03h | word | the size of the block in paragraphs, its MCB not counted |
| 08h | 8 bytes | DOS 4 and later: the owner's name, such as COMMAND |
The next MCB follows right after the block, at the MCB segment + 1 + the size. 48h walks the chain for a free block that is large enough (the first one, by default; INT 21h AH=58h changes the strategy), splits it, and marks the part it hands out as owned by the current program. When a program exits, DOS frees every block it owns.
Writing past the end of a block overwrites the next MCB. The next memory call then finds the chain broken and fails with error 7 (memory control blocks destroyed), and MS-DOS may stop with "Memory allocation error, cannot load COMMAND, system halted".
Why a .COM shrinks first
DOS loads a .COM into the largest free block and gives it all of that block, usually the whole rest of conventional memory, though the program can only address 64 KB of it. Right after start there is nothing left, and 48h fails with error 8. Shrink the program's block with INT 21h AH=4Ah first. An .EXE gets what its header asks for, and the linker's default is also everything.
MOV DX, OFFSET ASKM1MOV AH, 09hINT 21hMOV AH, 48h ; DOS: allocate BX paragraphsMOV BX, 1000h ; 1000h x 16 bytes = 64 KBINT 21hCALL RESULT ; error 8: we own all the memoryMOV DX, OFFSET SHRINKM1MOV AH, 09hINT 21hMOV AH, 4Ah ; DOS: resize the block at ES, our PSPMOV BX, 1000h ; keep 64 KB: code, data and stackINT 21hCALL RESULT ; okMOV DX, OFFSET ASKM1MOV AH, 09hINT 21hMOV AH, 48h ; and ask againMOV BX, 1000hINT 21hCALL RESULT ; ok: AX = the block's segmentJC DONE1MOV BLOCK1, AXDEC AX ; its MCB is the paragraph belowMOV ES, AXXOR DI, DIMOV DX, OFFSET OWNERM1MOV AX, CSCMP ES:[DI+1], AX ; the owner: our PSP?JE SAY1MOV DX, OFFSET OTHERM1SAY1:MOV AH, 09hINT 21hMOV DX, OFFSET SIZEM1MOV AH, 09hINT 21hMOV AX, ES:[DI+3] ; the size, in paragraphsCALL PHEXMOV DX, OFFSET PARASM1MOV AH, 09hINT 21hMOV ES, BLOCK1 ; ES = the blockMOV DX, OFFSET FREEM1MOV AH, 09hINT 21hMOV AH, 49h ; DOS: free the block at ESINT 21hCALL RESULT ; okPUSH CS ; ES = our PSP againPOP ESDONE1:ASKM1 DB "allocate 64 KB: $"SHRINKM1 DB "shrink to 64 KB: $"OWNERM1 DB "owner = our PSP$"OTHERM1 DB "owner = someone else$"SIZEM1 DB ", size = $"PARASM1 DB "h paragraphs", 13, 10, "$"FREEM1 DB "free it: $"BLOCK1 DW 0RESULT PROC NEAR ; print "ok", or "error" and AX when CF = 1PUSHF ; keeps AX and CF for the callerPUSH AXMOV DX, OFFSET OKMSGJNC RESULT1MOV DX, OFFSET ERRMSGMOV AH, 09hINT 21hPOP AXPUSH AXCALL PDEC ; the error codeMOV DX, OFFSET CRLFRESULT1:MOV AH, 09hINT 21hPOP AXPOPFRETRESULT ENDPOKMSG DB "ok", 13, 10, "$"ERRMSG DB "error $"PDEC PROC NEAR ; print AX in decimal (changes AX BX CX DX)MOV BX, 10XOR CX, CX ; digits on the stackPDEC1:XOR DX, DXDIV BX ; AX = AX / 10, DX = the last digitPUSH DXINC CXOR AX, AXJNZ PDEC1PDEC2:POP DX ; the digits come back first to lastADD DL, "0"MOV AH, 02hINT 21hLOOP PDEC2RETPDEC ENDPPHEX PROC NEAR ; print AX as four hex digits (changes AX BX CX DX)MOV BX, AXMOV CX, 4PHEX1:PUSH CXMOV CL, 4ROL BX, CL ; the top digit to the bottomPOP CXMOV DL, BLAND DL, 0FhADD DL, "0"CMP DL, "9"JBE PHEX2ADD DL, 7 ; "A" to "F"PHEX2:MOV AH, 02hINT 21hLOOP PHEX1RETPHEX ENDPCRLF DB 13, 10, "$"
To take as much as there is, ask for BX = FFFFh paragraphs: that always fails, and returns the size of the largest free block in BX. Then call again with that BX.