.text:0040BDF0; =============== S U B R O U T I N E ===============
.text:0040BDF0
.text:0040BDF0

The stack, call and ret

.text:0040BDF0; author: Can Ozdemir date: 2024-09-14 size: 0xF00 tags: x86-64, stack, tutorial
.text:0040BDF0
.text:0040BDF0sub_40BDF0procnear
.text:0040BDF1

Almost every function you read starts by adjusting rsp and ends by putting it back. Once the stack is clear in your head, prologues and epilogues become noise you skip over, and local variables become easy to name.

.text:0040BEC9

The basics

.text:0040BED6

The stack grows down, towards lower addresses. rsp points at the last value pushed.

.text:0040BF2F
  • push rax: rsp -= 8, then store rax at [rsp]
  • pop rax: load [rsp] into rax, then rsp += 8
  • call f: push the address of the next instruction (the return address), then jump to f
  • ret: pop the return address into rip
.text:0040C026

So at the first instruction of any function, [rsp] holds the address it will return to.

.text:0040C07F

A frame at -O0

.text:0040C090
int total(int *v, int n);

int average3(int a, int b, int c)
{
    int v[3] = { a, b, c };
    return total(v, 3) / 3;
}
.text:0040C108

GCC 13, -O0:

.text:0040C116
0000:  push   rbp
0001:  mov    rbp, rsp
0004:  sub    rsp, 0x20                   ; room for locals
0008:  mov    DWORD PTR [rbp-0x14], edi   ; a
000b:  mov    DWORD PTR [rbp-0x18], esi   ; b
000e:  mov    DWORD PTR [rbp-0x1c], edx   ; c
0011:  mov    eax, DWORD PTR [rbp-0x14]
0014:  mov    DWORD PTR [rbp-0xc], eax    ; v[0] = a
0017:  mov    eax, DWORD PTR [rbp-0x18]
001a:  mov    DWORD PTR [rbp-0x8], eax    ; v[1] = b
001d:  mov    eax, DWORD PTR [rbp-0x1c]
0020:  mov    DWORD PTR [rbp-0x4], eax    ; v[2] = c
0023:  lea    rax, [rbp-0xc]              ; &v[0]
0027:  mov    esi, 0x3
002c:  mov    rdi, rax
002f:  call   total
0034:  ...                                ; / 3, see the bit-tricks post
0049:  leave
004a:  ret
.text:0040C3F0

Drawn as memory, higher addresses at the top:

.text:0040C41D
rbp+0x8   return address        (pushed by call)
rbp       saved rbp             (pushed by the prologue)
rbp-0x4   v[2]
rbp-0x8   v[1]
rbp-0xc   v[0]                  <- the array, in increasing addresses
rbp-0x14  a
rbp-0x18  b
rbp-0x1c  c
rsp = rbp-0x20
.text:0040C51D

leave is shorthand for mov rsp, rbp followed by pop rbp: it undoes the whole frame in one instruction.

.text:0040C589

Notice that v[0] is at the lowest address of the array. Arrays and structs always grow upwards in memory, even though the stack as a whole grows down. It's also why compilers add stack-protector checks (a guard value tested before ret) to functions with local arrays: you'll often see a __stack_chk_fail call at the end of such functions.

.text:0040C6E5

The same function at -O1

.text:0040C700
0000:  sub    rsp, 0x18
0004:  mov    DWORD PTR [rsp+0x4], edi
0008:  mov    DWORD PTR [rsp+0x8], esi
000c:  mov    DWORD PTR [rsp+0xc], edx
0010:  lea    rdi, [rsp+0x4]
0015:  mov    esi, 0x3
001a:  call   total
001f:  ...
0033:  add    rsp, 0x18
0037:  ret
.text:0040C802

No rbp frame: locals are addressed from rsp directly. The array still has to live in memory, because its address is passed to total.

.text:0040C88C

Why 0x18, not 0x10?

.text:0040C8A2

The ABI requires rsp to be a multiple of 16 at every call. On entry to a function, the call that got us here has just pushed 8 bytes, so rsp is 8 more than a multiple of 16. Subtracting 0x18 (24) makes it aligned again, with 12 bytes used for v and the rest padding. In the -O0 version, push rbp (8) + sub rsp, 0x20 (32) achieves the same thing. Odd-looking stack adjustments are almost always alignment.

.text:0040CA46

The red zone

.text:0040CA55

On Linux, leaf functions (ones that call nothing) may use the 128 bytes below rsp without moving it. If a small function writes to [rsp-0x10] with no sub rsp, that's the red zone, not a bug. Windows has no red zone; it has a 32-byte "shadow space" that the caller reserves above the return address instead.

.text:0040CB91

Reading habit

.text:0040CBA1

Skip the prologue and epilogue, then make a table of every [rbp-N] or [rsp+N] slot and what it holds. Decompilers do exactly this; doing it by hand a few times makes their output much easier to trust or correct.

.text:0040CC78
.text:0040CC78retn
.text:0040CC78sub_40BDF0endp
.text:0040CC78; © 2026 Can Ozdemir. All rights reserved. terms
.text:0040CC78
.text:0040CC79; -------- cross references --------
Output
Loading ozdc.us… 34 functions, 3 data segments.
Type help in the command line below and press Enter.