Home

Pointers, Dereferencing, and the Ghost of Uninitialized Memory

A field guide to a few C gotchas, built from a debugging session around one small main() full of pointers — from the concept, down through the assembly, to the actual bytes printed on screen.

Part 1 — The concept: pa = 8; vs *pa = 8;

Given:

int a;
int *pa = &a;

pa now holds the address of a. From here, two very different statements are easy to confuse:

pa = 8;    // pa now points at memory address 8
*pa = 8;   // the value 8 is written into a

The asterisk is doing real work: it’s the difference between “change where I’m pointing” and “change what’s at the address I’m already pointing to.”

A related trap: declaring the same pointer twice in one scope,

int *pa = &a;
...
int *pa = 8;   // compile error: redefinition of 'pa'

won’t compile at all — a good reminder to read compiler errors before chasing runtime behavior.

Part 2 — Uninitialized variables aren’t “random,” they’re noise

int a, b, c, d;
a = 0;
printf("%d %d %d %d\n", a, b, c, d);

b, c, and d are never assigned. Printing them doesn’t invoke some random number generator — it prints whatever bit pattern already occupies that stack memory. That noise depends on things outside the program’s logic entirely:

The rest of this post pins that second point down with a controlled experiment: two versions of the same idea, one with four extra pointer variables, one without — same compiler, same machine — compared at the source, assembly, and output level.

Part 3 — Case study: two versions, side by side

Version A — with pointers pa, pb, pc, pd

#include <stdio.h>
int main(void) {
    int a, b, c, d;
    a = 0;
    printf("%d %d %d %d\n", a, b, c, d);
    printf("%p %p %p %p\n", &a, &b, &c, &d);
    int *pa = &a;
    int *pb = &b;
    int *pc = &c;
    int *pd = &d;
    printf("%p %p %p %p\n", pa, pb, pc, pd);
    printf("Addresses of pointers:\n");
    printf("%p %p %p %p\n", &pa, &pb, &pc, &pd);
    *pa = 8;
    printf("%d\n", a);
    return 0;
}

Compiled with gcc -S (GCC 13.3.0, Ubuntu 24.04), the relevant assembly:

subq $64, %rsp
...
movl -44(%rbp), %esi   ; -> d
movl -48(%rbp), %ecx   ; -> c
movl -52(%rbp), %edx   ; -> b
movl -56(%rbp), %eax   ; -> a

A 64-byte stack frame. a lives at -56(%rbp), b at -52, c at -48, d at -44. The rest of the frame (-16 through -40) holds the four pointers, and -8 holds the stack-protector canary.

Actual output when run:

0 0 0 0
0x7ffee16594a8 0x7ffee16594ac 0x7ffee16594b0 0x7ffee16594b4
0x7ffee16594a8 0x7ffee16594ac 0x7ffee16594b0 0x7ffee16594b4
Addresses of pointers:
0x7ffee16594b8 0x7ffee16594c0 0x7ffee16594c8 0x7ffee16594d0
8

b, c, d happen to print 0, and the final line correctly prints 8 — that’s *pa = 8; writing through the pointer into a.

Version B — pointers removed

#include <stdio.h>
int main(void) {
    int a, b, c, d;
    a = 0;
    printf("%d %d %d %d\n", a, b, c, d);
    printf("%p %p %p %p\n", &a, &b, &c, &d);
    return 0;
}

Same compiler, same flags. Assembly:

subq $32, %rsp
...
movl -12(%rbp), %esi   ; -> d
movl -16(%rbp), %ecx   ; -> c
movl -20(%rbp), %edx   ; -> b
movl -24(%rbp), %eax   ; -> a

Only a 32-byte frame now. a is at -24(%rbp), b at -20, c at -16, d at -12.

Actual output when run:

0 32711 1477634672 32765
0x7ffd5812e978 0x7ffd5812e97c 0x7ffd5812e980 0x7ffd5812e984

This time b, c, d print 32711, 1477634672, 32765 — visibly not zero.

Reading the diff

a, b, c, d moved from -56/-52/-48/-44 to -24/-20/-16/-12 purely because four unrelated pointer variables were removed from the function — variables that never read or wrote a, b, c, d’s values at all. The compiler simply no longer needed to reserve room for pa..pd, so it shrank the frame, and every later variable’s offset shifted with it.

Since b, c, d are never initialized in either version, printf reads whatever bytes happen to sit at that offset relative to %rbp when the call executes. Different offset → different leftover bytes → different printed “garbage” — despite the logical program (declare four ints, print three of them uninitialized) being identical in both versions.

One more run, for good measure

Running Version A a second time:

0 0 0 0
0x7fff669ac558 0x7fff669ac55c 0x7fff669ac560 0x7fff669ac564
0x7fff669ac558 0x7fff669ac55c 0x7fff669ac560 0x7fff669ac564
Addresses of pointers:
0x7fff669ac568 0x7fff669ac570 0x7fff669ac578 0x7fff669ac580
8

The printed addresses shifted between runs (0x7ffee... vs 0x7fff6...) — that’s ASLR (address space layout randomization) relocating the stack on each process launch. Yet b, c, d still printed 0 both times, because on this particular machine that stack region happens to reliably come back freshly zeroed.

That consistency is a property of this machine and this program shape, right now — not a guarantee from the C language. Change the compiler, the OS, the optimization level, or (as shown above) just the unrelated code around the variables, and it can just as easily stop being true.

Takeaway

There’s no single “the garbage value” tied to an uninitialized variable. It’s not a property of the variable — it’s a property of whichever stack address the compiler happened to assign it this time, combined with whatever bits already sat there. That combination is sensitive to compiler flags, unrelated code elsewhere in the function, optimization level, ASLR, and build environment.

This is exactly why the C standard calls it undefined behavior rather than “unspecified but consistent” behavior: the language makes zero promises, and a program can look perfectly consistent on one machine today and print something completely different tomorrow — or with the very next unrelated line of code.

To reproduce this yourself: gcc -S yourfile.c and diff the assembly between two versions of a program. The stack offsets will tell the real story.

Tags: CPointersMemoryUndefined-BehaviorAssembly