Introduction To Stack And Functions
August 15 and 16, 2025
September 3, 2025
Everyone knows stack as a data structure, and functions as reusable code blocks. So we are not going to repeat that theory.
The basic idea behind stack
We know that memory is a flat-array. Stack approaches that memory sequentially.
Stack as a memory management technique works exactly like a stack of plates.
- The first plate is at the bottom and every other plate comes above it (push).
- The last plate is the top one.
- When we take out plates, it happens from the top, not bottom (pop).
You may question that a stack of plates grow upwards while the stack in memory grows downwards.
- Actually, the addresses grow downwards with each push on stack.
- This feels counterintuitive because we don’t know how memory is structured.
- Checkout the process memory layout article for more information. It explains it the best.
The whole addressable memory is not managed with stack. There are multiple techniques for multiple purposes.
What makes something a function?
- A function has its own variable declarations and nested function calls.
- A function can receive arguments.
- A function has a return context.
- A function can return values to the caller.
If something can explain functions at best, it’s code reusability.
We know that labels combined with jump statements help us achieve control flow.
The problem with jump statements is that they are absolute in nature.
- There is no return context.
- If I have to return to the caller label, which called the callee label, the jump would be absolute. Meaning, I would return to the start of the caller label, not where the caller label had called that second label.
This lack of context limits code reusability, which is paramount to functions.
Now the question is, how can we create labels such that they can emulate a function?
- And the answer is stack.
How stack qualifies for function management?
Primarily there are 2 parts to function management.
- Managing the scope of a function.
- Managing the scope of nested function calls.
Everyone has some experience with C. You create main
function and call printf
from stdio.h
to print Hello, World!
.
printf
itself is a function, which uses other internal functions to perform the task it is meant to.
We would not appreciate one function accessing the variables declared inside another function, right? But we want functions to access a limited part of other functions.
- That’s the problem of scope.
When we call printf
from main
, we would not appreciate main
finishing before printf
. We want it to wait.
- That’s the problem of lifecycle.
When one function calls another function, we want the callee function to return to the caller function, so that it knows it has finished and can continue its execution. Maybe we want to return something to the caller function as well.
- That’s the problem of return context and return values.
How stack fits in?
Close your eyes and imagine a stack of plates.
- The last plate, which is on the top, is always taken out first.
- When more plates come after getting washed, they come on the top of the existing stack.
- Plates can’t be taken out from middle.
Isn’t this the same as order and lifecycle of function calls?
- Until the callee is not finished, caller can’t execute further.
- Until the plate on top is not taken, the bottom ones can’t be taken out.
Now imagine a stack of office files, organized from high priority (top) to low priority (bottom), each containing a number of pages.
- Each file has a case study and each case has its own data collection and outcomes.
- Similarly, each function gets its own universe where its declarations and nested calls reside.
Close your eyes one last time and imagine adding and removing plates from the stack.
Can you see stack smoothly lining up with our priorities?
- There is
main
which callsprintf
. A stack frame is created and the control is transferred to it. - When
printf
is finished, the control naturally returns tomain
, without any extra managerial logic. - When
scanf
is called, another stack frame is created and the control is transferred to it. And when it finishes, the control returns backmain
. - Each stack frame is isolated, so the previous frame or the next frame can’t mess with it.
I’ve deliberately kept it explicit and verbose so that we can have a broader overview of stack as a methodology, not just a data structure. I hope it was worth it.
Let’s see how functions actually exist in assembly.
Introducing Procedures
In simple words, a procedure is just a code symbol with jump statements and some “clever” usage of stack.
A procedure is a named, reusable block of code that performs a specific task, can accept input (arguments), has proper memory-management and returns a result.
Anatomy Of A Procedure
A procedure is composed of four core components:
- Header (label) is the name of the function.
- Prologue (entry setup) represents the clever use of stack.
- Body represents the function body.
- Epilogue (cleanup and return)
Management Pointers
The clever use of stack is about implementing stack frames and return context, which requires some general purpose registers, reserved for some specific purposes in the System V ABI.
Pointer Register | Purpose |
---|---|
rsp | Stack pointer register; holds a pointer to the top of the stack. |
rbp | Base pointer register; holds a pointer to the start of a stack frame, acts as a stable pointer asrspis volatile. |
rip | Global instruction pointer register; holds the address of the next instruction. |
Stack Frame
A stack frame is chunk of stack that belongs to a single procedure call.
When a function calls another function, a new stack frame is created and the instruction pointer register (rip
) is adjusted by the CPU to point to the instruction in the new procedure.
While the upper stack frame exists, the lower one can’t execute itself. Once the stack frame at top is done with its execution and it is killed, rip
is adjusted again to continue where it has left.
How is a stack frame structured?
Lets revise how user space memory is laid out. For more information, checkout virtual-memory-layout.md
User Space Memory Layout
*--------------------------*
| High Memory (~128 TiB) |
| *-----------------* |
| | Stack (↓) | |
| *-----------------* |
| | mmap region | |
| *-----------------* |
| | Free Space | |
| *-----------------* |
| | Heap (↑) | |
| *-----------------* |
| | Data (data/bss) | |
| *-----------------* |
| | Code | |
| *-----------------* |
| Low Memory (0..0) |
*--------------------------*
This is the general layout of a stack frame.
*---------------------*
| Function Arugments | <-- [rbp+16], [rbp+24], ....
| (beyond 6) |
*---------------------*
| Return Address |
| (next ins in prev.) | <-- [rbp+8]
*---------------------*
| Old Base Ptr Saved | <-- [rbp]: old base pointer && rbp: new base pointer
*---------------------*
| Local Variables | <-- [rbp-8], [rbp-16], ....
*---------------------*
| Empty Space |
| (for alignment) |
| (as required) |
*---------------------* <-- rsp
The first 6 arguments go in registers, we know that. Checkout calling-conventions.md
Shorthand Operations
A call instruction calls a procedure, which is shorthand for pushing the address of next instruction (rip
) to stack and jumping to the procedure’s label, like this:
push rip jmp label
Although this is not exactly how it is done, but we can consider it like this on surface.
push
is a shorthand for:
sub rsp, 8 mov [rsp], reg/imm
pop
is a shorthand for:
mov reg, [rsp] add rsp, 8
leave
restores the previous stack frame, which is a shorthand for:
mov rsp, rbp pop rbp
ret
is a shorthand to take the return address from stack and put it into rip
:
pop rip
How procedures are set up?
- Call to procedure
- Function prologue
- Body
- Function epilogue
Prologue
As per System V ABI, rsp
is guaranteed to be 16-bytes aligned.
Before calling a procedure, rsp
is 16-bytes aligned. The call
instruction is a shorthand for two instructions.
- Push the return address on stack, which is basically the next instruction in the current (caller’s) stack frame. This makes
rsp
misaligned by 8-bytes. - Jump on the procedure’s header (label).
After call
, we push the base pointer of the caller’s stack frame on stack, this is used to return to the caller function. This instruction makes the rsp
16-bytes aligned again.
After this, we setup the base pointer for the current stack frame. This is a little confusing so we will move a little slowly here.
A push instruction is a shorthand for “reserve 8 bytes, then populate them”.
When we push return address, the
rsp
now points to the memory where return address is stored.Similarly, when we push
rbp
on stack, thersp
now points to the memory whererbp
is stored.We have pushed
rbp
on stack but the original value ofrbp
is still inrbp
, right? When we do:mov rbp, rsp
We are updating the base pointer. The new base or the stack frame starts from here.
Take this:
- If the
rsp
was0d4000
beforecall
, pushing the return address would subtract it to0d3992
.0d3992
stores the return address. - When we push
rbp
, thersp
is subtracted by 8 again andrbp
goes on0d3984
. The stack also points at0d3984
now. - When we do
mov rbp, rsp
,rbp
now stores0d3984
. - When you ask what is
0d3984
, it would be the new base pointer. When you ask what is at0d3984
, it would be the old base pointer. I hope it is clear.
These two instructions form the function prologue.
push rbp
mov rbp, rsp
Body
Next comes the function body. It is made up of two parts.
- Reservation on stack.
- Everything else (instructions and static allocation).
Reservation on stack is a little tricky. This is where the concept of padding becomes important.
Stack pointer movement is word-aligned. Meaning, rsp
always moves in units of the machine’s word size, which is 64-bit for us.
But, there are some special instructions (SIMD
) which require the stack to be 16-bytes aligned.
- Why? Checkout simd.md, but it is not required here, so you can do that later.
So far, the stack is aligned. But now we have to reserve space for locals.
- To keep
rsp
16-bytes aligned, the total allocation on stack must be divisible by 16. - As long as the total allocation on stack is 16 divisible, no padding is required. If that’s not the case, the allocation is rounded up to the next 16-divisible digit.
If 100 bytes of locals were required, 112 bytes are reserved. The 12 bytes are for 16-bytes alignment.
There are leaf functions which are functions which don’t call any other functions inside them. For leaf functions, a concept called red zone exists in x64 System V ABI.
- Red zone is a small area of memory on the stack that a function can use for temporary storage without explicitly moving the stack pointer.
- The red zone is 128 bytes immediately below the
rsp
(stack pointer). - The red zone is guaranteed to be safe, nothing will write there unexpectedly.
Epilogue
It is about cleanup and return.
leave
ret
Stack does not need a deallocation process. To free the memory, we just make it inaccessible and it is quite efficient. How does that work?
- Technically, reducing
rsp
doesn’t clear the memory. The values are still there. - The point is that there are so many processes running on a machine constantly. You mark a memory inaccessible, ends the program and another process quickly overwrites the stack memory.
When we execute the leave
instruction,
- It restores the
rsp
by movingrbp
into it. Remember, what isrbp
pointing at? The old base pointer. - When we do
pop rbp
it restores the old base pointer inrbp
by poppingrsp
, moving its value intorbp
and adding 8-bytes torsp
to point at the return address.
At last we execute the ret
instruction, which pops the return address into rip
. And we are back into the old stack frame or old function context.
What about return value?
As per the ABI, the return goes in the accumulator (rax
).
When you write raw assembly, you can technically return multiple things as long as you keep ABI rules in check. But when you are writing C, only one return value is possible.
The Ultimate Question: Why functions In C return only one value?
I always have to use heap to return values. Why C is not like Python or JavaScript where your function can return multiple values?
A simple value can be returned in a register. Multiple values require multiple registers, which would need complex ABI rules. Although the existing ABI conventions are no simpler but that’s all I have found.
Although we can return a complex data structure like struct
, but again, that changes the situation.
Conclusion
This sets up the foundation for the next exploration. Now we know how functions exist at basic. From here we can study:
- Function arguments.
- How assembly sees function parameters. For clarity, parameters are declared in definition and arguments are passed in actual call. Although they are treated synonymous, they aren’t, at least at low level.
- Single return value.
- Returning a complex type (array, structure, union and pointer)
- Recursion
- Calling mechanisms: call by value and call by reference.
Always keep in mind, low level systems aren’t black magic. They are complex but can be understood if approached the right way.
By the way, this article is polished multiple times, which proves that I didn’t learned all this in one run, nor even in consecutive runs. Although the head offers a count of dates, you can always check the GitHub. I am mentioning this because I don’t want you mistreat your overwhelm. It is genuine. I too have faced that.