C/C++ Systems & Memory Internals: A Deep-Dive Tutorial
📘 The Complete Tutorial
Introduction: Why Memory Matters
Every line of C/C++ you write translates into precise memory operations. Unlike managed languages, you are the architect of memory layout, lifecycle, and access patterns. Understanding what happens underneath your code is the difference between a programmer who writes code and an engineer who builds systems.
This tutorial walks you through the entire journey—from source code to running binary, from stack frames to heap allocators, from pointer arithmetic to cache lines.
Module 01: The C Compilation Pipeline
Before a single instruction executes, your .c file undergoes a four-stage transformation:
Source Code (.c/.cpp)
↓
Preprocessor → Expanded Source (.i)
↓
Compiler → Assembly (.s)
↓
Assembler → Object File (.o)
↓
Linker → Executable (a.out / ELF)
Key insight: Each stage is a distinct tool (cpp, cc1, as, ld) that you can invoke independently. Understanding this pipeline lets you debug at the right layer.
Module 02: The Preprocessor
The preprocessor is a text substitution engine, not a compiler. It handles:
#include— textual inclusion (beware of double inclusion!)#define— macro expansion (no type checking!)#ifdef/#pragma— conditional compilation
1#define SQUARE(x) ((x) * (x)) // Parentheses matter!
Trap: Macros don't respect scope or types. Prefer static inline functions in modern C.
Module 03: The Compiler
The compiler (gcc -S, clang -S) performs:
- Lexical Analysis — tokens from characters
- Parsing — Abstract Syntax Tree (AST)
- Semantic Analysis — type checking
- Intermediate Representation (IR) — platform-independent optimization
- Code Generation — target-specific assembly
Optimization levels: -O0 (debug), -O2 (balanced), -O3 (aggressive), -Os (size), -Ofast (unsafe).
Module 04: Assembly Generation
Assembly is the human-readable form of machine code. Key concepts:
- Registers:
%rax,%rbx,%rsp(stack pointer),%rbp(base pointer) - Instructions:
mov,push,pop,call,ret,lea - Addressing modes: immediate, register, direct, indirect
1movl $42, -4(%rbp) # Store 42 at stack offset -4
View assembly with: gcc -S -O0 -fno-asynchronous-unwind-tables file.c
Module 05: The Assembler
The assembler (as) converts .s → .o (object file). It:
- Translates mnemonics to opcodes
- Builds symbol tables
- Generates relocation entries for unresolved symbols
Object files contain sections: .text (code), .data (initialized globals), .bss (uninitialized globals), .rodata (constants).
Module 06: The Linker
The linker (ld) resolves symbols across object files and libraries:
- Static linking: Copies library code into the executable
- Dynamic linking: References resolved at load time (
.so/.dll)
Symbol resolution order matters. The linker is single-pass: gcc main.o libfoo.a works, but gcc libfoo.a main.o may fail if libfoo.a depends on main.o.
Module 07: The ELF Executable
Executable and Linkable Format (ELF) is the Linux standard. Key structures:
| Component | Purpose |
|---|---|
| ELF Header | Magic number, architecture, entry point |
| Program Headers | Segments for the loader |
| Section Headers | Sections for the linker |
| .text | Read-only executable code |
| .data | Read-write initialized data |
| .bss | Zero-initialized data (no disk space) |
| .rodata | Read-only constants |
Inspect with: readelf -a a.out, objdump -d a.out
Module 08: The Stack
The stack is a LIFO region that grows downward (toward lower addresses). Each function call creates a stack frame:
High Address
┌─────────────────┐
│ Return Addr │ ← pushed by CALL
├─────────────────┤
│ Old %rbp │ ← pushed by prologue
├─────────────────┤
│ Local Var 1 │
│ Local Var 2 │
│ ... │
├─────────────────┤
│ Arguments │ ← for next call (System V AMD64 ABI)
└─────────────────┘
Low Address ← %rsp grows down
Key facts:
- Stack allocation is O(1) — just subtract from
%rsp - Stack frames are automatically cleaned up on
ret - Stack overflow = writing past the stack's limit (segmentation fault)
Module 09: The Heap
The heap is a dynamic memory region managed by the runtime. Unlike the stack:
- Allocation is explicit (
malloc,new) - Lifetime is manual (you control when memory is freed)
- Fragmentation is a real concern
1void* ptr = malloc(1024); // Request 1KB from the heap 2free(ptr); // Return it
Behind the scenes: malloc doesn't always call sbrk()/mmap(). It maintains free lists, bins, and arenas to reuse memory efficiently.
Module 10: Global & Static Memory
Global and static variables live in the data segment:
- Initialized globals →
.datasection - Uninitialized globals →
.bsssection (zero-filled by the loader) staticlocals → Same as globals, but scope-restrictedconstglobals →.rodata(read-only, segfault on write)
1int global = 5; // .data 2int uninitialized; // .bss 3static int local_stat; // .bss (zero) 4const int ro = 10; // .rodata
Module 11: Pointers
A pointer is a memory address. Understanding pointers means understanding memory:
1int x = 42; 2int *p = &x; // p holds the address of x 3*p = 100; // Dereference: write 100 to that address
Pointer arithmetic is scaled by the type size:
1int arr[5] = {10, 20, 30, 40, 50}; 2int *p = arr; 3p++; // Advances by sizeof(int) bytes, not 1 byte
Pointer types matter for:
- Arithmetic scaling
- Dereference size
- Type safety (compiler warnings)
Module 12: Function Pointers
Functions live in .text. Their addresses can be stored and called indirectly:
1int add(int a, int b) { return a + b; } 2 3int (*op)(int, int) = add; 4int result = op(2, 3); // Calls add(2, 3)
Use cases: Callbacks, vtables (C++ polymorphism), state machines, plugin architectures.
Module 13: Struct Layout
Structs are contiguous memory blocks with fields in declaration order:
1struct Point { 2 int x; // offset 0 3 int y; // offset 4 4 char c; // offset 8 5}; // Size? Not 9! (see alignment)
Rule: The compiler lays out fields sequentially, but may insert padding.
Module 14: Alignment
CPUs read memory at aligned addresses for performance (and correctness on some architectures).
- Alignment requirement: A type must start at an address divisible by its alignment value
char: 1-byte alignedshort: 2-byte alignedint: 4-byte aligneddouble: 8-byte aligned (on 64-bit systems)
1struct Misaligned { 2 char c; // offset 0 3 double d; // offset 8 (not 1!), padded 4};
Module 15: Padding
Padding is inserted by the compiler to satisfy alignment:
1struct Example { 2 char a; // 1 byte + 3 bytes padding 3 int b; // 4 bytes 4 char c; // 1 byte + 3 bytes padding 5}; // Total: 12 bytes (not 6!)
Optimization: Reorder fields to minimize padding:
1struct Optimized { 2 int b; // 4 bytes 3 char a; // 1 byte 4 char c; // 1 byte 5 // 2 bytes padding at end (struct size = multiple of largest alignment) 6}; // Total: 8 bytes
Use __attribute__((packed)) or #pragma pack to override (with caution!).
Module 16: malloc/free Internals
malloc is not a system call—it's a user-space allocator built on brk()/mmap():
- Arena: A large chunk of heap memory
- Chunks: Allocated blocks with metadata (size, flags)
- Free lists: Reusable freed chunks
- Bins: Sorted lists of free chunks by size (fastbins, smallbins, largebins)
Chunk structure (simplified):
[prev_size | size | FD | BK | ... user data ... | size ]
free(ptr) marks the chunk as available and may coalesce adjacent free chunks.
Module 17: Memory Allocator Design
Building a custom allocator teaches you the tradeoffs:
Strategies:
- Bump allocator: Fast, no free(), no fragmentation
- Free list: O(1) alloc/free, fragmentation
- Buddy system: Power-of-2 splits/merges, low fragmentation
- Slab allocator: Fixed-size pools, cache-friendly
Key decisions:
- Allocation strategy (first-fit, best-fit, worst-fit)
- Coalescing policy (immediate vs. deferred)
- Thread safety (per-thread arenas, locks)
Module 18: Buffer Management
Buffers are contiguous memory regions for data:
1char buffer[1024]; // Stack buffer 2char *buffer = malloc(1024); // Heap buffer
Best practices:
- Always track size alongside pointer
- Use
snprintf, notsprintf - Prefer
std::vector/std::stringin C++ - Validate lengths before copying
Module 19: Memory Corruption
Memory corruption occurs when you write outside allocated bounds:
1char buf[10]; 2strcpy(buf, "This is way too long!"); // Stack smashing!
Types:
- Buffer overflow: Write past end of buffer
- Buffer underflow: Write before start of buffer
- Heap overflow: Overflow on heap-allocated memory
- Use-after-free: Access freed memory
- Double-free: Free same memory twice
Detection tools: Valgrind, AddressSanitizer (-fsanitize=address), GDB.
Module 20: Undefined Behavior (UB)
C/C++ has undefined behavior—the compiler assumes it never happens and optimizes accordingly:
| UB Type | Example | Consequence |
|---|---|---|
| Null dereference | *NULL | Segfault (or worse) |
| Signed overflow | INT_MAX + 1 | Wrap or trap (UB!) |
| Out-of-bounds access | arr[100] on arr[10] | Corruption |
| Uninitialized read | int x; printf("%d", x); | Garbage value |
| Strict aliasing violation | Cast int* to float* | Broken optimization |
Critical: UB can "work" in debug and explode in release due to aggressive optimization.
🛠 Projects
Project 1: Build a Mini malloc()
Implement a bump allocator with void* my_malloc(size_t) and void my_free(void*). Track allocations in a linked list. No coalescing needed—focus on metadata layout.
Project 2: Build a Mini Memory Allocator
Extend Project 1 with:
- Free lists by size class
- First-fit allocation
- Immediate coalescing on
free() - Boundary tags for O(1) coalescing
Project 3: Implement a Memory Pool
Create fixed-size object pools:
- Pre-allocate a slab of 100 objects
pool_alloc()returns from free listpool_free()returns to free list- Zero external fragmentation, O(1) operations
Project 4: Stack-Frame Visualizer
Write a C program that:
- Prints addresses of local variables
- Prints
%rspand%rbpusing inline assembly - Demonstrates stack growth direction
- Shows frame layout across nested calls
Project 5: Heap Debugger
Wrap malloc/free with macros:
1#define malloc(size) debug_malloc(size, __FILE__, __LINE__)
Track all allocations, detect leaks, detect double-free, and report unfreed blocks at exit.
Project 6: Buffer-Overflow Laboratory
In a controlled, isolated environment (VM or container):
- Write a simple program with
char buf[16]andgets(buf) - Use GDB to observe stack smashing
- Disable protections (
-fno-stack-protector -z execstack -no-pie) for learning - Never use this knowledge maliciously
🤖 AI Connection: From Tensors to GPU Memory
Modern AI frameworks (PyTorch, TensorFlow) are built on the same memory principles:
Tensor Memory
↓
Pointer (void* data_ptr)
↓
Contiguous Memory Block
↓
Strides — Byte offset to move along each dimension
↓
CPU Cache Lines — 64-byte blocks, stride-1 access is cache-friendly
↓
GPU Memory — CUDA allocates via cuMalloc, transfers via PCIe/NVLink
Key insight: A tensor's strides array is pointer arithmetic at scale. Understanding C pointer arithmetic makes tensor indexing intuitive:
1# PyTorch equivalent of C pointer math: 2# element = base_ptr + offset0 * stride0 + offset1 * stride1
Cache optimization: Row-major (C-style) vs. column-major (Fortran-style) affects cache hit rates in matrix operations.
🔒 Cybersecurity Connection: The Memory Attack Chain
Most low-level vulnerabilities follow this chain:
Pointer Bugs
↓
Buffer Overflow → Overwrite return address / function pointer
↓
Use-After-Free → Dangling pointer to freed memory
↓
Memory Corruption → Arbitrary read/write primitive
↓
Vulnerability Analysis → Exploit development / patching
Real-world examples:
- Heartbleed: Buffer over-read in OpenSSL
- Shellshock: Environment variable parsing flaw
- Stagefright: Integer overflow → heap corruption
Defenses:
- ASLR (Address Space Layout Randomization)
- DEP/NX (No-Execute bit on stack/heap)
- Stack Canaries
- Control-Flow Integrity (CFI)
- Safe languages (Rust) with memory safety guarantees
🎓 Summary: The Memory Mindset
| Concept | Key Takeaway |
|---|---|
| Compilation | Four stages, each inspectable and debuggable |
| Stack | Automatic, fast, scoped—but limited |
| Heap | Flexible, manual, powerful—but dangerous |
| Pointers | Addresses with type information and arithmetic |
| Alignment | Hardware requirement, affects size and speed |
| malloc | User-space optimization over raw system calls |
| UB | The compiler assumes you never trigger it |
| Security | Every memory bug is a potential vulnerability |