Introduction: Why Functions Are the Heart of Programming
Let me start with a confession: when I first learned assembly language, I thought functions were magical. I’d write CALL printf and some wizardry would happen, and somehow control would return to my code. I didn’t understand how it worked. I just knew that CALL pushed something on the stack, and RET popped something off, and if I got the stack wrong, my program would crash in spectacularly confusing ways.
If you’ve ever felt that way about functions in assembly, you’re not alone. Functions (also called subprograms, procedures, subroutines, or methods) are one of the most fundamental concepts in programming—and also one of the most misunderstood.
Here’s the thing: functions are not magical. They’re a convention. A set of rules that the caller and callee agree to follow. When you understand those rules—when you understand what CALL and RET actually do to the stack, what the different calling conventions mean, and how recursion really works—functions go from being mysterious to being a powerful tool in your toolkit.
In this article, we’re going to dissect functions in assembly language. We’ll cover:
- Why subprograms matter: Code reuse, readability, and the
CALL/RETmechanism - The
CALLinstruction deep dive: What happens to the stack when youCALL - The
RETinstruction: NearRET, farRET, andRET n(stack cleanup on return) - Calling conventions:
cdecl,stdcall,fastcall,thiscall—who cleans up the stack? - Register saving conventions: Which registers a called function must preserve
- Frame pointers (EBP/RBP): Why they exist, how they’re set up, and why compilers sometimes omit them
- Recursion in assembly: How the stack makes recursion possible
- Leaf functions vs. non-leaf functions: Optimization opportunities
- Inline functions vs. function calls: The speed vs. size tradeoff
- Complete example: Writing a
printf-like function in assembly
By the end of this article, you’ll understand functions so deeply that you’ll be able to write assembly code that interoperates with C, C++, and any other language that follows standard calling conventions. Let’s dive in.
Why Subprograms Matter: Code Reuse and the CALL/RET Mechanism
The Problem: Repetitive Code
Imagine you’re writing a program that needs to add two numbers in multiple places. Without functions, you’d write something like this:
; === Place 1: Add 5 + 10 ===
MOV EAX, 5
ADD EAX, 10
; Result in EAX = 15
; ... later in the code ...
; === Place 2: Add 20 + 30 ===
MOV EAX, 20
ADD EAX, 30
; Result in EAX = 50
; ... later in the code ...
; === Place 3: Add 100 + 200 ===
MOV EAX, 100
ADD EAX, 200
; Result in EAX = 300
This works, but it has problems:
- Repetition: The addition logic is duplicated three times. If you want to change how addition works (e.g., check for overflow), you need to change it in three places.
- Readability: Someone reading your code has to mentally parse
MOV EAX, 5/ADD EAX, 10each time to understand “oh, this is adding two numbers.” - Maintainability: If you want to reuse the addition logic in another program, you have to copy-paste the code. And if you copy-paste code, you will eventually forget to update one of the copies when you make a change.
The Solution: Subprograms
A subprogram (function, procedure, subroutine) is a named block of code that:
- Takes input (parameters)
- Does some work (computes a result, modifies memory, etc.)
- Returns control to the caller (the code that called it)
Here’s the same program using a function:
; === Place 1 ===
PUSH 10
PUSH 5
CALL add_numbers
ADD ESP, 8 ; Clean up stack (cdecl convention)
; Result in EAX = 15
; === Place 2 ===
PUSH 30
PUSH 20
CALL add_numbers
ADD ESP, 8
; Result in EAX = 50
; === Place 3 ===
PUSH 200
PUSH 100
CALL add_numbers
ADD ESP, 8
; Result in EAX = 300
; ============================================
; Function: add_numbers
; Parameters (cdecl): [EBP+8] = a, [EBP+12] = b
; Return value: EAX = a + b
; ============================================
add_numbers:
PUSH EBP
MOV EBP, ESP
MOV EAX, [EBP + 8] ; EAX = a
ADD EAX, [EBP + 12] ; EAX = a + b
MOV ESP, EBP
POP EBP
RET
Now the addition logic is defined once (in add_numbers) and reused three times (via CALL). If you need to change how addition works, you change it in one place.
What CALL and RET Actually Do
The magic of functions comes down to two instructions: CALL and RET. Let’s understand them in detail.
The CALL Instruction
CALL does two things:
- Pushes the return address onto the stack. The return address is the address of the next instruction after the
CALL. - Jumps to the target function (sets
EIP/RIPto the function’s address).
; Example
CALL add_numbers
MOV EBX, EAX ; <-- This is the "return address"
When the CPU executes CALL add_numbers, it:
- Pushes the address of
MOV EBX, EAXonto the stack - Jumps to
add_numbers
The RET Instruction
RET does the opposite:
- Pops the return address from the stack
- Jumps back to the caller (sets
EIP/RIPto the popped address)
add_numbers:
; ... do work ...
RET ; Pops return address, jumps back to caller
After RET executes, the CPU continues executing at the instruction right after the CALL.
The Stack Is Key
The reason functions work is because of the stack. The stack is a region of memory that grows downward (from high addresses to low addresses) and is used for:
- Storing return addresses (pushed by
CALL, popped byRET) - Passing parameters (pushed by the caller)
- Storing local variables (allocated by the callee)
- Saving registers (pushed by the callee)
If you mess up the stack—if you push when you should have popped, or forget to clean up parameters—your function will return to the wrong address, and your program will crash.
This is why understanding the stack is critical for assembly programming.



