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:

OffsetSizeContents
00hbyte"M" (4Dh): another block follows; "Z" (5Ah): the last block
01hwordthe owner: the PSP segment of the program it belongs to; 0 = free
03hwordthe size of the block in paragraphs, its MCB not counted
08h8 bytesDOS 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.

prints: allocate 64 KB: error 8 / shrink to 64 KB: ok / allocate 64 KB: ok / owner = our PSP, size = 1000h paragraphs / free it: ok
MOV DX, OFFSET ASKM1
MOV AH, 09h
INT 21h
MOV AH, 48h ; DOS: allocate BX paragraphs
MOV BX, 1000h ; 1000h x 16 bytes = 64 KB
INT 21h
CALL RESULT ; error 8: we own all the memory
MOV DX, OFFSET SHRINKM1
MOV AH, 09h
INT 21h
MOV AH, 4Ah ; DOS: resize the block at ES, our PSP
MOV BX, 1000h ; keep 64 KB: code, data and stack
INT 21h
CALL RESULT ; ok
MOV DX, OFFSET ASKM1
MOV AH, 09h
INT 21h
MOV AH, 48h ; and ask again
MOV BX, 1000h
INT 21h
CALL RESULT ; ok: AX = the block's segment
JC DONE1
MOV BLOCK1, AX
DEC AX ; its MCB is the paragraph below
MOV ES, AX
XOR DI, DI
MOV DX, OFFSET OWNERM1
MOV AX, CS
CMP ES:[DI+1], AX ; the owner: our PSP?
JE SAY1
MOV DX, OFFSET OTHERM1
SAY1:
MOV AH, 09h
INT 21h
MOV DX, OFFSET SIZEM1
MOV AH, 09h
INT 21h
MOV AX, ES:[DI+3] ; the size, in paragraphs
CALL PHEX
MOV DX, OFFSET PARASM1
MOV AH, 09h
INT 21h
MOV ES, BLOCK1 ; ES = the block
MOV DX, OFFSET FREEM1
MOV AH, 09h
INT 21h
MOV AH, 49h ; DOS: free the block at ES
INT 21h
CALL RESULT ; ok
PUSH CS ; ES = our PSP again
POP ES
DONE1:
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 0
RESULT PROC NEAR ; print "ok", or "error" and AX when CF = 1
PUSHF ; keeps AX and CF for the caller
PUSH AX
MOV DX, OFFSET OKMSG
JNC RESULT1
MOV DX, OFFSET ERRMSG
MOV AH, 09h
INT 21h
POP AX
PUSH AX
CALL PDEC ; the error code
MOV DX, OFFSET CRLF
RESULT1:
MOV AH, 09h
INT 21h
POP AX
POPF
RET
RESULT ENDP
OKMSG DB "ok", 13, 10, "$"
ERRMSG DB "error $"
PDEC PROC NEAR ; print AX in decimal (changes AX BX CX DX)
MOV BX, 10
XOR CX, CX ; digits on the stack
PDEC1:
XOR DX, DX
DIV BX ; AX = AX / 10, DX = the last digit
PUSH DX
INC CX
OR AX, AX
JNZ PDEC1
PDEC2:
POP DX ; the digits come back first to last
ADD DL, "0"
MOV AH, 02h
INT 21h
LOOP PDEC2
RET
PDEC ENDP
PHEX PROC NEAR ; print AX as four hex digits (changes AX BX CX DX)
MOV BX, AX
MOV CX, 4
PHEX1:
PUSH CX
MOV CL, 4
ROL BX, CL ; the top digit to the bottom
POP CX
MOV DL, BL
AND DL, 0Fh
ADD DL, "0"
CMP DL, "9"
JBE PHEX2
ADD DL, 7 ; "A" to "F"
PHEX2:
MOV AH, 02h
INT 21h
LOOP PHEX1
RET
PHEX ENDP
CRLF 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.