The Stack: push, pop, rsp
The one data structure the CPU loves
- › Explain what the stack is, where it lives, and why it grows downward
- › Describe exactly what push and pop do to rsp and to memory
- › Compute rsp and stack contents by hand across a sequence of pushes and pops
- › See why this structure is the foundation of function calls and, later, of exploitation
A region of memory with a discipline
The stack is not special memory — it's just part of the same byte-addressed RAM from day 4. What makes it "the stack" is a *discipline*: you add and remove data only at one end, last-in-first-out (LIFO), and one register — rsp, the stack pointer — always points at the current top.
The CPU adores this structure because it's exactly what function calls need: each call can carve out a little scratch space, use it, and give it back, perfectly nested. Today is the ground floor for call/ret tomorrow — and, months from now, the ground floor of the buffer overflow.
It grows downward
The one counterintuitive fact: on x86-64 the stack grows toward lower addresses. rsp starts high and *decreases* as you push. "Top of stack" is the *lowest* used address. Picture memory with high addresses up top:
high addresses
┌──────────────┐
│ ...older... │
│ 0x7ffe20 │ <- earlier pushes live up here
│ 0x7ffe18 │
│ 0x7ffe10 │ <- rsp points HERE (current top)
├──────────────┤
│ (unused) │ push moves rsp DOWN into here
│ (unused) │
└──────────────┘
low addressespush and pop, to the byte
Two instructions, and they are *precisely* defined — no hand-waving. On 64-bit, each pushes/pops 8 bytes:
- `push rax` = "
rsp = rsp - 8; then store rax at[rsp]." Make room by moving the top down, - then write there.
- `pop rbx` = "load
[rsp]into rbx; thenrsp = rsp + 8." Read the top, then shrink by - moving up.
That's the whole definition. push decrements-then-stores; pop loads-then-increments. Everything else about the stack is these two facts applied.
1 ; suppose rsp = 0x7ffe20 here2 push rax ; rsp -> 0x7ffe18, [0x7ffe18] = rax3 push rbx ; rsp -> 0x7ffe10, [0x7ffe10] = rbx4 pop rcx ; rcx = [0x7ffe10] (the rbx we pushed), rsp -> 0x7ffe185 pop rdx ; rdx = [0x7ffe18] (the rax we pushed), rsp -> 0x7ffe20pop rcx above gets back rbx (the most recent
push), and pop rdx gets rax. Push A then B, pop gives B then A. rsp ends exactly where it
started — pushes and pops must balance, or the stack "leaks."Two ways to use it, one manual
You can also move rsp *directly*. sub rsp, 32 reserves 32 bytes of scratch space in one step
(that's how functions make room for locals — day 14), and add rsp, 32 gives it back. push/pop are
just the packaged versions of "adjust rsp and access [rsp]." Underneath, allocating stack space is
literally subtracting from rsp.
push 0x11, push 0x22, push 0x33, then pop rax, pop rbx.
What are rax, rbx, and the final rsp?p $rsp prints the stack pointer, and x/4gx $rsp shows the
top four 8-byte slots. Watching rsp jump by 8 on each push/pop, and the values appear at [rsp], makes
this concrete in a way no diagram can.Prove push and pop to yourself
In stack.asm, push three distinct constants (say 0xAA, 0xBB, 0xCC), then pop them into three different registers and, using the LIFO rule, arrange it so a specific register ends up with 0xAA — predict which pop order does that before you write it. Run under gdb, and after each push/pop use `p $rsp` and `x/4gx $rsp` to confirm rsp moves by 8 and the values land where the definition says. You only need push, pop, mov, and the exact definitions from this lesson.
$ nasm -f elf64 stack.asm -o stack.o && ld stack.o -o stack$ gdb ./stack # break _start; stepi; p $rsp; x/4gx $rsp- › You predicted which register gets 0xAA before running (LIFO)
- › You watched rsp decrease by 8 on push and increase by 8 on pop
- › You saw the pushed values at [rsp] in memory
- › Your pushes and pops balanced — rsp returned to its start