Procedures: CALL and RET

Intermediate

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 MAIN
Open in compiler

Watch 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 MAIN
Open in compiler

Step 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

1. What does CALL put on the stack?
2. A procedure does PUSH AX, then RET without POP AX. What happens?
3. After PUSH BP and MOV BP, SP inside a procedure, where is the first pushed argument?
4. In a recursive factorial, what does each call keep on the stack?

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.