INT 21h AH=4Ah changes the size of a block without moving it. Shrinking always works, and the paragraphs given up become a free block. Growing works only if the memory right after the block is free; otherwise the call fails with error 8 and BX = the largest size the block could have.
Its main use is on the program's own block, which starts with the PSP. At start ES holds the PSP segment in a .COM and an .EXE alike, so ES is already right for the call.
How much to keep
A .COM needs its whole 64 KB unless it moves its stack: SP starts at FFFEh, the top of the segment, and shrinking below 1000h paragraphs leaves the stack in free memory, where the next allocation or a child program overwrites it. To keep less, move SP inside the program first, then compute the size at run time, in bytes from the PSP and rounded up to paragraphs:
MOV SP, OFFSET STACKTOP ; a stack inside the programMOV BX, OFFSET STACKTOP + 15MOV CL, 4SHR BX, CL ; bytes from the PSP -> paragraphsMOV AH, 4AhINT 21h...STACK1 DW 128 DUP(?) ; 256 bytes of stackSTACKTOP LABEL WORD
DOSBox-X's own programs on Z: do exactly this: each one starts with MOV SP, 400h and shrinks its block to 40h paragraphs, 1 KB.
MOV DX, OFFSET SHRINKM1MOV AH, 09hINT 21hMOV AH, 4Ah ; DOS: resize the block at ES (our PSP)MOV BX, 1000h ; 64 KBINT 21hCALL RESULT ; okMOV DX, OFFSET OURSM1MOV AH, 09hINT 21hMOV AX, CS ; our MCB: the paragraph below the PSPDEC AXMOV ES, AXXOR DI, DIMOV AX, ES:[DI+3] ; its size fieldCALL PHEX ; 1000MOV DX, OFFSET PARASM1MOV AH, 09hINT 21hPUSH CSPOP ES ; ES = our PSP againMOV DX, OFFSET GROWM1MOV AH, 09hINT 21hMOV AH, 4AhMOV BX, 0FFFFh ; more than there can beINT 21hMOV MAX1, BX ; BX = the most we can haveCALL RESULT ; error 8MOV DX, OFFSET ALLM1MOV AH, 09hINT 21hMOV AH, 4AhMOV BX, MAX1INT 21hCALL RESULT ; okSHRINKM1 DB "shrink to 64 KB: $"OURSM1 DB "our block: $"PARASM1 DB "h paragraphs", 13, 10, "$"GROWM1 DB "grow to FFFFh paragraphs: $"ALLM1 DB "grow to all there is: $"MAX1 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, "$"
Errors
Error 8: not enough memory to grow, BX = the largest size possible. Error 7: ES - 1 is not an MCB (DOSBox-X reports 7 here, where 49h reports 9 for the same mistake), or the chain is broken. DOSBox-X already leaves the block at the largest size when growing fails; DOS versions differ in this, so call again with the BX that came back, as the example does.