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 pointer
MOV BP, SP ; BP = this frame
SUB SP, 4 ; two local words: [BP-2], [BP-4]
; ... body ...
MOV SP, BP ; drop the locals
POP BP ; caller's BP back
RET ; or RET n

For a near procedure with two word arguments, the frame looks like this:

AddressHolds
[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.

Prints 7. For Pascal style, push a first, read a at [BP+6] and b at [BP+4], end with RET 4 and drop the ADD SP, 4.
MOV AX, 2
PUSH AX ; second argument (b): C pushes right to left
MOV AX, 9
PUSH AX ; first argument (a)
CALL DIFF1 ; AX = a - b
ADD SP, 4 ; the caller removes the arguments
ADD AL, "0" ; digit -> character
MOV DL, AL
MOV AH, 02h ; DOS: print the character in DL
INT 21h
DIFF1 PROC NEAR
PUSH BP
MOV BP, SP
SUB SP, 2 ; one local word at [BP-2]
MOV AX, [BP+4] ; a
MOV [BP-2], AX ; local = a
MOV AX, [BP+6] ; b
SUB [BP-2], AX ; local = a - b
MOV AX, [BP-2] ; the result goes back in AX
MOV SP, BP
POP BP
RET
DIFF1 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].