The stack, push and pop
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V, Z80.
The stack, push and pop
Registers are convenient temporary storage, but there are only sixteen general-purpose register
families. The stack is an area of memory that a program can use for more temporary storage.
The register rsp, the stack pointer, holds the address of the current top of the stack.
Stack use works through cooperation. In nested use, one part of a program reserves stack space
and another part reserves more before the first part releases its space. Their current ranges are
distinct when each part releases exactly what it reserved. The mechanism cannot prevent code from
changing rsp incorrectly or writing to an arbitrary address and corrupting another range.
rsp and downward growth
The x86-64 stack grows toward lower addresses. This lesson uses the 64-bit general-purpose
register forms of push and pop, so each operation changes rsp by eight bytes:
push rax ; rsp -= 8, then store rax at [rsp]
pop rax ; load rax from [rsp], then rsp += 8
Eight bytes is the width of these particular forms, not a rule that every stack value has this
size. The 16-bit form, such as push ax, changes rsp by two bytes, and there is no ordinary
push eax form in 64-bit mode. A manual allocation with sub rsp, N can reserve any chosen number
of bytes.
Here two qwords are pushed and then popped:
In this playground, rsp begins at 0x4FFFFFFFFED0. Before either push, the nearby qwords look
like this (the green marker shows rsp):
| address | value |
|---|---|
0x4FFFFFFFFEC0 | 0000000000000000 |
0x4FFFFFFFFEC8 | 0000000000000000 |
0x4FFFFFFFFED0 | 🟢 0000000000000001 |
The first push subtracts 8, making rsp equal to 0x4FFFFFFFFEC8, and stores the first value
there:
| address | value |
|---|---|
0x4FFFFFFFFEC0 | 0000000000000000 |
0x4FFFFFFFFEC8 | 🟢 1111111111111111 |
0x4FFFFFFFFED0 | 0000000000000001 |
The second push subtracts another 8, making rsp equal to 0x4FFFFFFFFEC0, and stores the second
value:
| address | value |
|---|---|
0x4FFFFFFFFEC0 | 🟢 2222222222222222 |
0x4FFFFFFFFEC8 | 1111111111111111 |
0x4FFFFFFFFED0 | 0000000000000001 |
pop r8 reads the qword at the current rsp, so r8 receives
0x2222222222222222. It then raises rsp by 8. pop r9 reads the next qword, so r9 receives
0x1111111111111111, and rsp returns to 0x4FFFFFFFFED0.
The last value pushed is the first value popped. This order is called last in, first out, or LIFO.
A pop does not erase the bytes it read. After both pops, the two values are still visible at
0x4FFFFFFFFEC0 and 0x4FFFFFFFFEC8, but those bytes are no longer reserved by these pushes.
Later stack use may overwrite them, so a program must not treat a popped value as current storage.
Reserving several slots
You can reserve a block directly by subtracting its byte count from rsp. This example reserves
24 bytes and uses them as three qword slots:
Immediately after the subtraction, the three qword slots begin at [rsp], [rsp + 8], and
[rsp + 16]. These addresses stay at those offsets only while rsp is not changed again. Another
push, pop, subtraction, or addition of rsp would move the base used by the same expressions.
The matching add rsp, 24 restores the stack pointer to its value before the allocation. If the
subtraction and addition use different byte counts, later stack use begins from the wrong address.
As with popped values, releasing the block does not erase its bytes; it only ends this use of the
range.
Saving and restoring registers
A common use of push is to preserve a register while its current value is temporarily replaced:
The final values are again rbx = 100 and r12 = 200. When several saved values must return to
their original registers, pop them in the reverse order of their pushes. That reverse order follows
directly from LIFO.
Your turn
rbx and r12 hold 11 and 22. Swap them with exactly two push instructions followed by exactly
two pop instructions. Do not use xchg, a third register, or arithmetic for the swap. Restore
rsp to its starting value. Afterward, the released qwords at 0x4FFFFFFFFEC0 and
0x4FFFFFFFFEC8 should still contain 22 and 11.
Show solution
Now reserve 32 bytes with sub rsp, 32 and use all four qword slots. Store 5, 7, 8, and 9 at
offsets 0, 8, 16, and 24 respectively. Load the qword at offset 8 into r8 and the qword at offset
24 into r9. Finally, release the whole block with add rsp, 32, restoring rsp to its starting
value. The released bytes remain observable at their fixed addresses.