Have you ever called a function and wondered what really happens inside the computer?
package main
import "fmt"
func add(x int, y int) int {
result := x + y
return result
}
func main() {
a := 10
sum := add(a, 4)
fmt.Println(sum)
}
We know the output will be:
14
But inside the machine, a lot of quiet work happens.
The CPU needs to remember:
Where did this function start?
Where should it return?
Where are the arguments?
Where are the local variables?
Which function is currently running?
This is where the stack, stack frame, stack pointer, and base pointer come in.
Let’s break them down in a simple way.
First, What Is a Stack?
A stack is a special area of memory used while a program is running.
Think of it like a stack of plates.
You put a plate on top.
You remove the top plate first.
You cannot remove a plate from the middle without disturbing the others.
In programming, the stack works in a similar way.
When a function is called, the program creates a small memory area for that function. This small area is called a stack frame.
When the function finishes, its stack frame is removed.
So if main() calls add(), the stack may look like this:
add() stack frame
main() stack frame
add() is on top because it is currently running.
When add() finishes, its stack frame is removed, and the program returns to main().
What Is a Stack Frame?
A stack frame is the memory space a function gets while it is running.
It usually stores things like:
Function arguments
Return address
Old base pointer
Local variables
Temporary values
For our add() function:
func add(x int, y int) int {
result := x + y
return result
}
The stack frame may contain:
x = 10
y = 4
return address
old base pointer
result = 14
The exact layout depends on the CPU, compiler, operating system, and calling convention. But the main idea is the same:
A function needs a private workspace while it runs.
That workspace is its stack frame.
A Quick Note About Memory Addresses
Before we go deeper, let’s clear up one common confusion.
On most modern systems, memory is byte-addressable. That means every byte has its own address.
So memory addresses can look like this:
0, 1, 2, 3, 4, 5, 6, 7, 8 ...
But when we draw memory in larger chunks, like 4-byte chunks on a 32-bit-style example, we often label them like this:
0, 4, 8, 12, 16 ...
This does not mean addresses 1, 2, and 3 do not exist.
It simply means we are drawing memory in 4-byte blocks.
Address 0 -> byte 1 of first block
Address 1 -> byte 2 of first block
Address 2 -> byte 3 of first block
Address 3 -> byte 4 of first block
Address 4 -> byte 1 of second block
For this article, we will use a simple 32-bit mental model where each slot is 4 bytes.
So our stack addresses may move like this:
80
76
72
68
64
60
56
52
In many systems, the stack grows downward, which means the address gets smaller as new data is pushed onto the stack.
What Is the Stack Pointer?
The stack pointer, often called SP, points to the current top of the stack.
Imagine you are stacking plates.
The stack pointer is like your finger pointing at the top plate.
When a new value is pushed onto the stack, the stack pointer moves.
When a value is popped from the stack, the stack pointer moves back.
Before push:
Address 76 -> old value
Address 72 -> top of stack <- SP
Push new value:
Address 76 -> old value
Address 72 -> old top
Address 68 -> new value <- SP
Because our example stack grows downward, pushing data makes SP move from 72 to 68.
So the stack pointer is dynamic.
It keeps changing as the stack grows and shrinks.
What Is the Base Pointer?
The base pointer, often called BP or frame pointer, points to a stable location inside the current function’s stack frame.
Unlike the stack pointer, the base pointer usually stays fixed while the function is running.
Why do we need that?
Because the stack pointer keeps moving.
If the program only used SP, it would be hard to find local variables and arguments after more values are pushed or popped.
The base pointer solves this problem.
It gives the function a fixed reference point.
From that fixed point, the program can find values using offsets.
BP + 8 -> first argument
BP + 12 -> second argument
BP - 4 -> first local variable
BP - 8 -> second local variable
So the base pointer helps the program answer questions like:
Where is x?
Where is y?
Where is result?
Where should I return after this function ends?
Stack Pointer vs Base Pointer
Here is the simple difference:
| Register | Meaning | Behavior |
|---|---|---|
| Stack Pointer | Points to the current top of the stack | Changes often |
| Base Pointer | Points to a fixed place in the current stack frame | Usually stays fixed during the function |
A simple way to remember it:
SP tracks movement.
BP gives stability.
The stack pointer is like a moving bookmark.
The base pointer is like an anchor.
What Happens When main() Runs?
func main() {
a := 10
sum := add(a, 4)
fmt.Println(sum)
}
When main() starts, the program creates a stack frame for main.
Inside that frame, it stores local variables such as:
a = 10
sum = ?
At first, sum does not have its final value because add(a, 4) has not returned yet.
So the stack may look like this:
main stack frame
----------------
return address
old BP
a = 10
Now main() calls add(a, 4).
That means the program needs a new stack frame for add().
What Happens When add() Is Called?
This line is important:
sum := add(a, 4)
The program needs to call add() with two values:
x = 10
y = 4
So it creates a new stack frame for add().
A simplified stack frame may look like this:
add stack frame
---------------
y = 4
x = 10
return address
old BP
result = 0
The order can vary depending on the system, but the idea is the same.
The add() function needs:
Its arguments: x and y
A return address: where to go back after add() finishes
The old base pointer: so the caller’s stack frame can be restored
Its local variable: result
Why Do We Need a Return Address?
When main() calls add(), the CPU jumps to the code for add().
But after add() finishes, the CPU must know where to go back.
It needs to return to this line:
sum := add(a, 4)
More specifically, it needs to return to the next step after the function call, so the returned value can be stored in sum.
That “go back here” location is called the return address.
Without a return address, the program would get lost.
It would finish add() and then have no idea where to continue.
Why Save the Old Base Pointer?
Before add() starts, main() already has its own base pointer.
When add() starts, it gets a new base pointer for its own stack frame.
But when add() finishes, the program must restore the old base pointer so main() can continue correctly.
That is why the old base pointer is saved inside the new stack frame.
Think of it like leaving a breadcrumb.
Before entering a new function, the program says:
“Let me save where I came from, so I can restore it later.”
When the function ends, the old base pointer is loaded back.
Now the caller’s stack frame becomes active again.
How add() Finds x, y, and result
Inside add(), we have this line:
result := x + y
The CPU does not understand variable names like humans do.
It does not think:
“Oh, x means 10 and y means 4.”
Instead, the compiler maps variables to memory locations.
Using the base pointer, the program can find values by offsets.
BP + 8 -> x
BP + 12 -> y
BP - 4 -> result
So when the function needs x, it looks at something like:
BP + 8
When it needs y, it looks at:
BP + 12
When it needs to store result, it may use:
BP - 4
So the line:
result := x + y
becomes something like:
read value at BP + 8
read value at BP + 12
add them
store the answer at BP - 4
x = 10
y = 4
result = 14
Returning Back to main()
Now add() reaches this line:
return result
The function returns 14.
Then the add() stack frame is no longer needed.
So the program removes it from the stack.
The stack pointer moves back.
The old base pointer is restored.
The return address tells the CPU where to continue.
Now we are back inside main().
The returned value 14 is stored in sum.
sum := 14
Then this line runs:
fmt.Println(sum)
And we see:
14
A Simple Visual Summary
Here is a simplified version of what happens.
Before calling add():
main stack frame
----------------
a = 10
sum = ?
While add() is running:
add stack frame
---------------
x = 10
y = 4
result = 14
main stack frame
----------------
a = 10
sum = ?
After add() returns:
main stack frame
----------------
a = 10
sum = 14
Then main() prints:
14
Does This Always Work Exactly Like This?
Not always.
This article uses a simplified model to make the concept easy to understand.
Real systems can be more complex.
Modern compilers may optimize away the base pointer.
Function arguments may be passed through registers instead of the stack.
Go uses goroutine stacks, which can grow and move.
The exact stack layout depends on architecture, compiler version, and calling convention.
But the core idea is still useful:
Functions need stack frames.
The stack pointer tracks the top of the stack.
The base pointer gives a stable reference point inside a function frame.
Return addresses help the program continue from the correct place.
Old base pointers help restore the caller’s frame.
Once you understand this model, debugging, assembly, recursion, memory layout, and low-level programming all become much less mysterious.
A Real-Life Way to Remember It
Imagine you are reading a book and solving exercises.
You are on Chapter 5.
Suddenly, Chapter 5 says:
“Before continuing, go solve Example 2 from Chapter 3.”
So you place a bookmark in Chapter 5.
Then you go to Chapter 3 and solve the example.
After finishing, you return to the bookmark in Chapter 5.
In this story:
The bookmark is like the return address.
Your current page position is like the stack pointer.
The start of your current exercise notes is like the base pointer.
The notes for each exercise are like stack frames.
Every function call is like temporarily jumping into another task, while carefully remembering how to come back.


Top comments (0)