Arguments pushed before a CALL and local variables a procedure needs both live on the stack. SP moves with every push, so the procedure fixes a reference point: it saves the caller's BP and copies SP into BP. From then on every argument and every local has a constant offset from BP. Addresses through BP use the stack segment SS by default, which is exactly right.
Prologue and epilogue
PUSH BP ; save the caller's frame pointerMOV BP, SP ; BP = this frameSUB SP, 4 ; two local words: [BP-2], [BP-4]; ... body ...MOV SP, BP ; drop the localsPOP BP ; caller's BP backRET ; or RET n
For a near procedure with two word arguments, the frame looks like this:
| Address | Holds |
|---|---|
| [BP+6] | second argument |
| [BP+4] | first argument |
| [BP+2] | return IP, pushed by CALL |
| [BP+0] | the caller's BP |
| [BP-2] | first local word |
| [BP-4] | second local word |
A far procedure has CS on the stack too, so its arguments start at [BP+6]. Registers the procedure must keep, such as SI and DI, are pushed after the SUB SP and popped before the MOV SP, BP.
Who removes the arguments
In the C convention the caller pushes the arguments right to left, so the first one ends up at [BP+4], and removes them itself with ADD SP, n after the call. That allows a variable number of arguments, as printf needs. In the Pascal convention arguments are pushed left to right and the callee removes them with RET n, which is shorter when there are many calls.
MOV AX, 2PUSH AX ; second argument (b): C pushes right to leftMOV AX, 9PUSH AX ; first argument (a)CALL DIFF1 ; AX = a - bADD SP, 4 ; the caller removes the argumentsADD AL, "0" ; digit -> characterMOV DL, ALMOV AH, 02h ; DOS: print the character in DLINT 21hDIFF1 PROC NEARPUSH BPMOV BP, SPSUB SP, 2 ; one local word at [BP-2]MOV AX, [BP+4] ; aMOV [BP-2], AX ; local = aMOV AX, [BP+6] ; bSUB [BP-2], AX ; local = a - bMOV AX, [BP-2] ; the result goes back in AXMOV SP, BPPOP BPRETDIFF1 ENDP
Pitfalls
Mixing conventions leaves the stack 2n bytes off and the next RET goes astray. Every call gets a fresh frame, which is what makes recursion work, but the stack of a .COM program is shared with everything else, so deep recursion can run out of it. In an .EXE where SS ≠ DS, the address of a local is an SS offset and cannot be used through DS.
Later CPUs
The 80186 adds ENTER n, 0 and LEAVE for the prologue and epilogue. The 386 uses EBP and ESP in 32-bit code, where arguments start at [EBP+8].