INT 21h AH=49h returns a block to DOS. ES is the segment INT 21h AH=48h returned, the block itself; DOS finds the MCB in the paragraph below and marks the block free (owner 0). Free neighbours are merged into one block on a later call.
Memory a program forgets to free is not lost: when it exits with INT 21h AH=4Ch, DOS frees every block owned by its PSP. Freeing matters to a program that keeps running: one that allocates and frees buffers as it goes, one that runs other programs, a resident program trimming itself.
DOS checks very little. It only looks for "M" or "Z" at ES - 1:0. Freeing a block twice, or a block another program owns, succeeds without complaint, and the trouble shows up later. An ES that does not point just past an MCB gives error 9:
MOV AH, 4Ah ; first make room (ES = our PSP)MOV BX, 1000hINT 21hMOV AH, 48h ; DOS: allocate 1 KBMOV BX, 64 ; 64 paragraphsINT 21hJC DONE1MOV ES, AX ; ES = the blockMOV DX, OFFSET FREEM1MOV AH, 09hINT 21hMOV AH, 49h ; DOS: free the block at ESINT 21hCALL RESULT ; okMOV DX, OFFSET BOGUSM1MOV AH, 09hINT 21hMOV AX, CSINC AXMOV ES, AX ; "MCB" at CS: that is our PSP, CDh 20h...MOV AH, 49hINT 21hCALL RESULT ; error 9PUSH CSPOP ESDONE1:FREEM1 DB "free the block: $"BOGUSM1 DB "free CS+1, not a block: $"RESULT 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 ENDPCRLF DB 13, 10, "$"
Freeing the environment
Each program gets its own copy of the environment, in a block whose segment is at PSP:2Ch. A program that stays resident frees it before INT 21h AH=31h to save memory, and often writes 0 to PSP:2Ch afterwards so that nothing follows a stale pointer:
MOV ES, DS:[2Ch] ; DS = our PSP: the environment's segmentMOV AH, 49h ; DOS: free itINT 21h