13. Stack and Procedures
Welcome to the final chapter of our x86-64 assembly journey! Now we’ll learn how to write structured, maintainable code using the stack and procedures - the foundation of functions, local variables, and organized programming.
The Stack: A Fundamental Data Structure
The stack is a Last-In, First-Out (LIFO) memory region that grows downward in memory. It’s managed by two special registers:
- RSP - Stack Pointer (points to the top of the stack)
- RBP - Base Pointer (points to the base of the current stack frame)**
; Memory visualization:
; Higher addresses: [ older data ]
; [ ... ]
; RBP → [ base frame ]
; [ local vars ]
; RSP → [ top of stack ]
; Lower addresses: [ ... ]
Stack Operations
1. PUSH - Add to Stack
Syntax: PUSH operand
How it works:
- Decrements RSP by 8 (in 64-bit mode)
- Stores operand at new
[RSP]
1
2
3
push rax ; Decrement RSP by 8, store RAX at [RSP]
push 42 ; Push immediate value
push qword [var] ; Push memory value
2. POP - Remove from Stack
Syntax: POP operand
How it works:
- Loads value from
[RSP]into operand - Increments RSP by 8
1
2
pop rbx ; Load [RSP] into RBX, increment RSP by 8
pop qword [var] ; Pop into memory location
Stack Frame Management
Each function call creates a stack frame containing:
- Return address (where to return after function)
- Previous RBP value
- Local variables
- Function parameters
Prologue (function start):
1
2
3
4
my_function:
push rbp ; Save old base pointer
mov rbp, rsp ; Set new base pointer
sub rsp, 16 ; Allocate space for local variables
Epilogue (function end):
1
2
3
mov rsp, rbp ; Restore stack pointer
pop rbp ; Restore old base pointer
ret ; Return to caller
Procedure Calls
1. CALL - Call a Procedure
Syntax: CALL target
How it works:
- Pushes return address (next instruction) onto stack
- Jumps to target address
1
2
call my_function ; Push return address, jump to my_function
; After function returns, execution continues here
2. RET - Return from Procedure
Syntax: RET or RET imm16
How it works:
- Pops return address from stack
- Jumps to that address
- Optional immediate value cleans stack parameters
1
2
ret ; Return to caller
ret 8 ; Return and remove 8 bytes of parameters
Calling Conventions
x86-64 uses a register-based calling convention for efficiency:
| Purpose | Register |
|---|---|
| Return value | RAX |
| 1st parameter | RDI |
| 2nd parameter | RSI |
| 3rd parameter | RDX |
| 4th parameter | RCX |
| 5th parameter | R8 |
| 6th parameter | R9 |
| Additional parameters | Stack |
Caller-saved registers: RAX, RCX, RDX, RSI, RDI, R8-R11
Callee-saved registers: RBX, RBP, R12-R15
Let’s write some code where we will call a function and return mathematical operation done on arguments passed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
section .data
result dq 0
section .text
global _start
; Function: add_numbers(a, b) → returns a + b
add_numbers:
; Prologue
push rbp
mov rbp, rsp
; Function body - parameters are in RDI and RSI
mov rax, rdi ; RAX = first parameter
add rax, rsi ; RAX += second parameter
; Return value is in RAX
; Epilogue
mov rsp, rbp
pop rbp
ret
; Function: multiply_numbers(a, b) → returns a × b
multiply_numbers:
push rbp
mov rbp, rsp
mov rax, rdi ; RAX = a
imul rax, rsi ; RAX = a × b
mov rsp, rbp
pop rbp
ret
_start:
; Call add_numbers(10, 20)
mov rdi, 10 ; First parameter
mov rsi, 20 ; Second parameter
call add_numbers
mov [result], rax ; Store result (30)
; Call multiply_numbers(5, 6)
mov rdi, 5
mov rsi, 6
call multiply_numbers
; RAX now contains 30
; Exit program
mov rax, 60
mov rdi, 0
syscall