Lesson 26 of 48 intermediate

Procedures, CALL, RET, and Modular Thinking

Break programs into reusable procedures — the assembly version of functions

Open interactive version (quiz + challenge)

Real-world analogy

A procedure is like a restaurant kitchen. The waiter (CALL) sends an order slip and remembers table 7 (return address pushed to stack). The kitchen does its work (procedure body). When the food is ready, the waiter picks it up and returns to table 7 (RET pops the return address). Nested calls are like the kitchen calling the dessert station — everyone remembers where to return.

What is it?

Procedures (subroutines) are reusable blocks of 8086 code defined with PROC/ENDP and invoked with CALL, which pushes the return address onto the stack. RET pops the return address to resume the caller. Parameters can be passed via registers or the stack. BP-based stack frames provide structured access to parameters and local variables, enabling modular, maintainable assembly programs.

Real-world relevance

Every operating system, BIOS routine, and DOS service is implemented as a procedure. When you call INT 21h, the interrupt handler itself calls internal procedures. Device drivers organize hardware access into callable routines. Even your boot loader calls procedures to read disk sectors and set up memory.

Key points

Code example

; Program: Compute factorial of N using a procedure
.MODEL SMALL
.STACK 100h
.DATA
  N  DW 5
  res DW ?

.CODE
MAIN PROC
  MOV AX, @DATA
  MOV DS, AX

  MOV AX, [N]      ; AX = 5
  CALL factorial    ; AX = 5! = 120
  MOV [res], AX

  MOV AH, 4Ch
  INT 21h
MAIN ENDP

; Procedure: factorial
; Input:  AX = n (unsigned, n >= 0)
; Output: AX = n!
; Uses recursive calls via stack
factorial PROC NEAR
  CMP AX, 1
  JLE base_case     ; if n <= 1, return 1

  PUSH AX           ; save current n
  DEC AX            ; AX = n - 1
  CALL factorial    ; AX = (n-1)!
  POP BX            ; BX = saved n
  MUL BX            ; AX = n * (n-1)!
  RET

base_case:
  MOV AX, 1         ; 0! = 1! = 1
  RET
factorial ENDP
END MAIN

Line-by-line walkthrough

  1. 1. MOV AX, [N] — load the value 5 into AX as the input to our factorial procedure
  2. 2. CALL factorial — push the address of the next instruction (MOV [res], AX) onto the stack, then jump to the factorial label
  3. 3. CMP AX, 1 / JLE base_case — check if we have reached the base case (n <= 1). If so, return 1
  4. 4. PUSH AX — save the current value of n on the stack before we modify AX for the recursive call
  5. 5. DEC AX — AX becomes n-1, preparing for the recursive call to compute (n-1)!
  6. 6. CALL factorial — recursive call. This pushes another return address and re-enters the procedure with a smaller n
  7. 7. POP BX — after the recursive call returns, BX gets the saved n, and AX already contains (n-1)!
  8. 8. MUL BX — AX = AX * BX = (n-1)! * n = n!. The result is in AX for the caller
  9. 9. RET — pop the return address from the stack and resume at the caller. Each nested call returns to its own caller

Spot the bug

my_proc PROC NEAR
  PUSH AX
  PUSH BX
  MOV AX, [BP+4]
  ADD AX, [BP+6]
  POP AX
  POP BX
  RET
my_proc ENDP
Need a hint?
Look at the procedure prologue — is BP set up correctly? Also check the POP order.
Show answer
Two bugs: (1) BP is never set up — missing PUSH BP / MOV BP, SP, so [BP+4] and [BP+6] reference the wrong stack locations. (2) POP AX / POP BX is in the wrong order — it should be POP BX / POP AX to reverse the PUSH order. Also, POP AX destroys the computed result.

Explain like I'm 5

Imagine you are doing homework (main program). Your mom calls you downstairs for dinner (CALL). You put a bookmark in your textbook (push return address) and go eat (execute procedure). When dinner is done (RET), you go back to your room and find your bookmark — you continue exactly where you left off. If during dinner, dad asks you to grab something from the garage (nested CALL), you remember to come back to dinner first, then back to homework.

Fun fact

The x86 CALL instruction has barely changed since the 8086. Modern 64-bit processors still push a return address and jump, though now they also use a hidden Return Address Stack (RAS) predictor to guess where RET will go before actually reading the stack — achieving near-zero-cycle return overhead.

Hands-on challenge

Write two procedures: (1) max_of_two that takes two values in AX and BX and returns the larger in AX, and (2) max_of_three that calls max_of_two twice to find the maximum of three values passed in AX, BX, CX. Test with values 15, 42, 27.

More resources

Open interactive version (quiz + challenge) ← Back to course: Microprocessor A–Z