Understanding the Stack in Assembly
The stack is a special area of memory that is used as a Last In, First Out (LIFO) structure by the processor as it executes assembly code. It is an important component of function calls
The stack is a data structure that is built into x86-64 assembly, meaning that it doesn’t need to be initialized.
There are two basic operations that can be performed with a stack: push and pop. To push means to place an item onto the stack; pop means taking one off. As a LIFO structure, the last item to be pushed onto the stack is always the first to be popped off.
While stacks are common data structures used in many programming languages, when talking about the stack in x86-64 assembly, we are typically referring to a special stack known as the ‘program stack‘. The program stack is unique and necessary to understanding how assembly works.
One of the unique aspects of the stack in x86-64 assembly is that while the structure itself is always LIFO, the stack pointer (rsp) register can be manipulated, as well as the stack frame base pointer (rbp). Most importantly, this means that the ‘top’ of the stack can be changed, giving developers more control.
In this article, we will take a detailed look at the properties and functionality of the stack in assembly.
What is ‘The Stack‘?
The stack is a data structure stored in RAM (random access memory). To understand the stack, we will use the traditional example of a stack of plates:
The classic ‘stack of plates’ example: Stacking is a great way of storing plates. It maximizes our storing capacity and makes it easy to organize, place and retrieve plates. But we can’t easily get to plates in the middle of the stack; typically when we want a plate, we take the top plate off the stack. When we add a plate, it goes on top of the stack. If we want to get a plate from the middle, we need to remove all of the plates above it first. In other words, a stack of plates is a LIFO structure.
The stack in x86-64 is very similar. It’s an efficient structure that has a number of benefits. Like a stack of plates, the stack can grow when values are pushed onto it, and it can shrink as values are popped off. However as a LIFO structure, we can’t get to items in the middle without popping off everything above.
The stack occupies a continuous block of memory:
We can either push items onto the top of stack, or pop them off:
Note that different operating systems start the stack at different memory addresses. In addition, schemes like address space layout randomization (ASLR) can cause the location of the stack to change.
In general, the stack starts at higher memory addresses, higher than other sections like the heap, code, and data:
The stack starts at a high address and grows ‘down’, toward lower memory addresses:
This is in contrast with the heap, which starts at a relatively lower address and grows ‘up’. In other words, the stack and heap grow toward each other.
The x86-64 Stack Pointers
There are two stack pointer registers in x86-64 assembly:
- RBP – stack frame base pointer, which points to the ‘bottom’ of the stack frame. Note that by convention this is actually the highest memory address that is part of the stack frame, because the stack grows downward toward lower addresses.
- RSP – stack pointer, which points to the ‘top’ of the stack. This is the lowest memory address in the stack. As the stack grows, the RSP gets decremented.
We can think of the rsp as pointing to the ‘top plate’ of the stack. It changes every time we push a value onto or pop a value off the stack.
What’s Above the Stack? The stack doesn’t occupy the highest possible address in memory; as such, there will be data located ‘above’ the rbp, beyond the top of the stack. However this memory should be considered undefined and should not be used for program execution.
What’s On the Stack?
The stack is an essential part of the functionality of x86-64 assembly and we can find several things on the stack:
- Return addresses from a function call. This allows a called function to return to the function that called it.
- Local variables.
- Arguments passed from one function to another.
- Data stored on the stack to save space for registers. This allows functions to share registers without overwriting.
Data on the stack is stored in frames. Each function will have its own frame, including the main() function which has a corresponding main() frame.