Have you ever written a C program, run it, and watched it print values you never assigned?
At first glance, it feels almost as if old data is haunting your program from beyond a function call, but what is happening under the hood is far more interesting:
stack frame reuse
compiler behaviour
memory persistence
and the performance tradeoffs built into modern systems
In this series, we'll go from high-level C code into stack frames, assembly instructions, and eventually real-world security implications.
Today, in Part 1, we investigate one of the most common low-level surprises in C: uninitialized local variables appearing to "remember" old values.
Consider the following program:
#include <stdio.h>
void Subtraction()
{
int a = 100;
int b = 35;
int c = a - b;
}
void PrintValues()
{
int i, j, k;
printf("First value is %d\nSecond value is %d\nThird value is %d\nThese are the values for PrintValues function\n", i, j, k);
}
int main()
{
Subtraction();
PrintValues();
return 0;
}
If we compile without optimization: gcc -O0 main.c, we may observe output similar to this:
First value is 100
Second value is 35
Third value is 65
These are the values for PrintValues function
At first glance, this looks impossible. PrintValues never initializes its variables. So why does it print the exact same values previously used by Subtraction()? Important Warning: This is undefined behaviour
Before going deeper, we need to clarify something important.
Reading uninitialized local variables in C is undefined behaviour.
That means:
the C standard does not guarantee what happens
different compilers might behave differently
optimization levels may change the result
future executions may produce different outputs
The behaviour shown above is simply one possible outcome observed under a particular compiler configuration.
The Dirty Whiteboard Analogy
To understand what's happening, forget the code for a moment. Imagine RAM as a whiteboard in a shared classroom. First teacher (Subtraction()). A math teacher walks into the room and writes: 100, 35, 65 on the whiteboard. After class ends, she leaves without erasing anything, because erasing takes time. For a brief moment, the room is empty. The writing is still visible. Nothing has overwritten it yet. Second teacher (PrintValues()). A history teacher enters the room immediately afterward. Instead of writing anything new, he simply reads whatever is already on the board. At a high level, this resembles what happens with stack memory. When a function returns, typical compiled code does not automatically erase the stack bytes previously used for local variables. Instead:
the stack space becomes available for reuse
old data often remains there temporarily
another function may reuse the same stack offsets
If the new function reads memory before writing new values, it may accidentally observe leftover bytes from an earlier stack frame.
INSPECT THE ASSEMBLY FOR SUBTRACTION()
Let's inspect the assembly generated for Subtraction(). Below is the disassembly produced by GDB on x86-64:
push %rbp
mov %rsp,%rbp
sub $0x30,%rsp
movl $0x64,-0x4(%rbp)
movl $0x23,-0x8(%rbp)
mov -0x4(%rbp),%eax
sub -0x8(%rb),%eax
mov %eax,-0xc(%rbp)
mov -0xc(%rbp),%ecx
mov -0x8(%rbp),%edx
mov -0x4(%rbp),%eax
add $0x30,%rsp
pop %rbp
ret
push %rbp - Save the base pointer onto the stack
mov %rsp,%rbp - Set the stack pointer as the base pointer.
sub $0x30,%rsp - Allocate 48 bytes of space on the stack
movl $0x64,-0x4(%rbp) - Store 100 into the first 4 bytes on the stack
movl $0x23,-0x8(%rbp) - Store 35 into the second slot
mov -0x4(%rbp),%eax - Load 100 into the eax register
sub -0x8(%rbp),%eax - Subtract 35 from eax, the result remains in eax
mov %eax,-0xc(%rbp) - Move the result from the register to memory
mov -0xc(%rbp),%ecx - Move to register
mov -0x8(%rbp),%edx - Move to register
mov -0x4(%rbp),%eax - Move to register
add $0x30,%rsp - Clean up allocated stack space
pop %rbp - Restore the previous stack frame
ret - Return control to the caller
After the function returns, the stack frame is released. The stack pointer simply moves back, making that region available for reuse. The previous values often remain there temporarily until another operation overwrites them. Typically compiled code does not automatically clear old stack contents because doing so would introduce additional instructions and reduce performance.
ENTER PrintValues()
Now let us examine the beginning assembly code generated for PrintValues()
push %rbp
mov %rsp,%rbp
sub $0x30,%rsp
mov -0xc(%rbp),%eax
mov -0x4(%rbp),%edx
mov -0x8(%rbp),%ecx
Notice something very important. PrintValues() allocates a very similar stack layout. So this instruction:
mov -0x4(%rbp),%eax means read whatever bytes that exists here. If those bytes still contain leftover data from Subtraction(), then PrintValues() may appear to "remember" the earlier values.
This teaches something deeper than uninitialized variables are dangerous. It reveals an important systems principle: memory is reused, not automatically cleaned.





















