Procedures, CALL, RET, and Modular Thinking
Break programs into reusable procedures — the assembly version of functions
Open interactive version (quiz + challenge)Real-world analogy
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
- Why Procedures Matter — Without procedures, you would copy-paste the same code everywhere. Procedures let you write a block of code once and CALL it from many places. This reduces program size, makes debugging easier, and mirrors how high-level languages use functions.
- PROC and ENDP Directives — PROC and ENDP are assembler directives that mark the beginning and end of a procedure. They help the assembler and the programmer identify procedure boundaries. PROC can specify NEAR (same segment) or FAR (cross-segment).
- CALL Instruction — Near — CALL pushes the IP of the next instruction onto the stack, then loads IP with the procedure's address. This saves the return address so RET can come back. For a near CALL, only IP (2 bytes) is pushed.
- CALL Instruction — Far — A far CALL pushes both CS and IP onto the stack (4 bytes total), then loads both CS:IP with the target address. Used when calling a procedure in a different code segment. Requires a matching far RET (RETF).
- RET Instruction — RET pops the saved IP (for near) or CS:IP (for far) from the stack back into the instruction pointer, resuming execution at the instruction after the CALL. RET n pops n additional bytes to clean up parameters pushed before the call.
- Parameter Passing via Registers — The simplest way to pass data to a procedure: load values into agreed-upon registers before CALL, and read the result from a specific register after RET. Fast but limited by the number of registers available.
- Parameter Passing via Stack — For more parameters, push values onto the stack before CALL. Inside the procedure, use BP to access them. The caller or procedure must clean the stack afterward. This is how C and Pascal calling conventions work.
- Stack Frame and BP — A stack frame is the region of the stack owned by a procedure. BP (Base Pointer) anchors the frame. The standard prologue is PUSH BP / MOV BP, SP. Parameters are at positive offsets from BP, local variables at negative offsets.
- Preserving Registers — A well-written procedure saves any registers it modifies (except those used for return values) and restores them before RET. This prevents the caller's register values from being destroyed unexpectedly.
- Nested Procedure Calls — Procedures can call other procedures. Each CALL pushes a return address, and each RET pops one. The stack grows with each nesting level and shrinks as procedures return. This naturally supports recursion too.
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 MAINLine-by-line walkthrough
- 1. MOV AX, [N] — load the value 5 into AX as the input to our factorial procedure
- 2. CALL factorial — push the address of the next instruction (MOV [res], AX) onto the stack, then jump to the factorial label
- 3. CMP AX, 1 / JLE base_case — check if we have reached the base case (n <= 1). If so, return 1
- 4. PUSH AX — save the current value of n on the stack before we modify AX for the recursive call
- 5. DEC AX — AX becomes n-1, preparing for the recursive call to compute (n-1)!
- 6. CALL factorial — recursive call. This pushes another return address and re-enters the procedure with a smaller n
- 7. POP BX — after the recursive call returns, BX gets the saved n, and AX already contains (n-1)!
- 8. MUL BX — AX = AX * BX = (n-1)! * n = n!. The result is in AX for the caller
- 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 ENDPNeed a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- 8086 Procedures and CALL/RET (GeeksforGeeks)
- x86 Calling Conventions (Wikipedia)
- Assembly Procedures Explained (YouTube)
- Stack Frames in Assembly (Wikibooks)