A procedure is a piece of code you can run from many places, like a function:
PRINT_NL PROC
...
RET
PRINT_NL ENDP
CALL PRINT_NL runs it, and RET comes back to the instruction after the CALL.
How CALL and RET work
CALL pushes the return address (the address of the next instruction, the IP after the CALL) and jumps to the procedure. RET pops that address back into IP. So the stack must be exactly as the procedure found it when RET runs: everything the procedure pushed, it must pop first.
; A procedure called twice: PRINT_NL prints a new line.
.MODEL SMALL
.STACK 100H
.CODE
MAIN PROC
MOV DL, 'A'
MOV AH, 2
INT 21H
CALL PRINT_NL ; push the return address, jump to PRINT_NL
MOV DL, 'B'
MOV AH, 2
INT 21H
CALL PRINT_NL
MOV AH, 4CH
INT 21H
MAIN ENDP
PRINT_NL PROC
PUSH AX ; save what the caller had
PUSH DX
MOV AH, 2
MOV DL, 0DH
INT 21H
MOV DL, 0AH
INT 21H
POP DX ; restore in reverse order
POP AX
RET ; pop the return address
PRINT_NL ENDP
END MAINWatch the stack at each CALL: a word appears, marked as the return address, and RET takes it away again.
Passing values
Small programs pass values in registers: "PRINT_DIGIT prints the number in DL". A procedure that uses other registers saves them (PUSH at the start, POP before RET, in reverse) so the caller's values survive.
Larger programs push the arguments before the CALL, and the procedure reads them with BP, which addresses the stack segment:
PUSH AX ; the argument
CALL TWICE
...
TWICE PROC
PUSH BP
MOV BP, SP ; [BP] = old BP, [BP + 2] = return address,
MOV AX, [BP + 4] ; [BP + 4] = the argument
...
POP BP
RET 2 ; return, and remove the 2-byte argument
TWICE ENDP
Recursion
A procedure may call itself; each call has its own return address on the stack. Factorial: n! = n × (n − 1)!, and 1! = 1:
; 5! with a recursive procedure: FACT(n) = n x FACT(n - 1), FACT(1) = 1.
.MODEL SMALL
.STACK 100H
.CODE
MAIN PROC
MOV CX, 5
CALL FACT ; AX = 5! = 120
MOV BX, 10 ; print AX
MOV CX, 0
DIGITS:
MOV DX, 0
DIV BX
PUSH DX
INC CX
CMP AX, 0
JNE DIGITS
SHOWIT:
POP DX
ADD DL, '0'
MOV AH, 2
INT 21H
LOOP SHOWIT
MOV AH, 4CH
INT 21H
MAIN ENDP
FACT PROC ; in: CX = n; out: AX = n!
CMP CX, 1
JA RECURSE
MOV AX, 1 ; the base case: 1! = 1
RET
RECURSE:
PUSH CX ; keep n for after the call
DEC CX
CALL FACT ; AX = (n - 1)!
POP CX
MUL CX ; AX = n x (n - 1)!
RET
FACT ENDP
END MAINStep through it and watch the stack grow by a return address and a saved CX at each call, then shrink as the answers come back.
Common mistakes
- Leaving something pushed inside a procedure: RET then pops it as the return address and jumps to the wrong place. StepVioz catches this.
- Forgetting RET: the CPU runs on into whatever follows the procedure.
- Putting a procedure where the main program runs into it: put procedures after MAIN ENDP.
- Changing a register the caller still needs without saving it.
Quick check
Practice problems for this lesson
My notes
Write down what you want to remember from this lesson. Notes are saved on this device as you type, and appear on your dashboard.