DEV Community

Cover image for Stack Pointer vs Base Pointer: A Friendly Guide to How Function Calls Work
Tahsin Abrar
Tahsin Abrar

Posted on

Stack Pointer vs Base Pointer: A Friendly Guide to How Function Calls Work

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)
}
Enter fullscreen mode Exit fullscreen mode

We know the output will be:

14
Enter fullscreen mode Exit fullscreen mode

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.


diagram

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For our add() function:

func add(x int, y int) int {
    result := x + y
    return result
}
Enter fullscreen mode Exit fullscreen mode

The stack frame may contain:

x = 10
y = 4
return address
old base pointer
result = 14
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

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 ...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

When main() starts, the program creates a stack frame for main.

Inside that frame, it stores local variables such as:

a = 10
sum = ?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The program needs to call add() with two values:

x = 10
y = 4
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

So when the function needs x, it looks at something like:

BP + 8
Enter fullscreen mode Exit fullscreen mode

When it needs y, it looks at:

BP + 12
Enter fullscreen mode Exit fullscreen mode

When it needs to store result, it may use:

BP - 4
Enter fullscreen mode Exit fullscreen mode

So the line:

result := x + y
Enter fullscreen mode Exit fullscreen mode

becomes something like:

read value at BP + 8
read value at BP + 12
add them
store the answer at BP - 4
Enter fullscreen mode Exit fullscreen mode
x = 10
y = 4

result = 14
Enter fullscreen mode Exit fullscreen mode

Returning Back to main()

Now add() reaches this line:

return result
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then this line runs:

fmt.Println(sum)
Enter fullscreen mode Exit fullscreen mode

And we see:

14
Enter fullscreen mode Exit fullscreen mode

A Simple Visual Summary

Here is a simplified version of what happens.

Summary diagram

Before calling add():

main stack frame
----------------
a = 10
sum = ?
Enter fullscreen mode Exit fullscreen mode

While add() is running:

add stack frame
---------------
x = 10
y = 4
result = 14

main stack frame
----------------
a = 10
sum = ?
Enter fullscreen mode Exit fullscreen mode

After add() returns:

main stack frame
----------------
a = 10
sum = 14
Enter fullscreen mode Exit fullscreen mode

Then main() prints:

14
Enter fullscreen mode Exit fullscreen mode

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)