Data, pointers, and memory recap

Excellent work! You can now follow a piece of data wherever it lives: behind a pointer, inside an array, inside a struct, in a fixed global section, or on the heap. Every one of those turned out to be a variation on one idea from the first lesson: a memory access whose address comes from somewhere you can read. Let's review the key concepts from this chapter.

Pointers and References

A pointer is simply a 64-bit address held in a full register such as rdi, and dereferencing it is a memory load or store. The square brackets are the key: rdi alone is the address value, while [rdi] means "go to memory at that address". The lea instruction ("load effective address") is the assembly form of the & operator: it computes an address without touching memory, in contrast to mov, which fetches the contents.

mov eax, DWORD PTR [rdi]   ; *p   (load through the address)
lea rax, -4[rbp]           ; &x   (compute the address)

A reference compiles to exactly the same code as a pointer: an address in a register, dereferenced on each use. Only the demangled type in the label differs.

Indexing Into Arrays

arr[i] compiles to the addressing mode [base + index*scale], which computes arr + i*elementSize in a single operand. The scale equals the element size: *4 for int, *8 for long, double, or pointers, and an implicit *1 for char. An int index is first widened to 64 bits with movsx (signed) or movzx (unsigned) so it can take part in the address.

mov eax, DWORD PTR [rdi+rax*4]   ; arr[i] in an int array

When the index is a compile-time constant, the arithmetic folds into a fixed displacement: arr[3] becomes 12[rdi], with no index register or scale at all.

Struct Layout and Member Access

A struct is a contiguous block of bytes with each member at a fixed offset, so p->member compiles to a load or store at [base + offset], a fixed displacement just like a constant array index. The displacement in the listing is the member's byte offset, and the PTR size keyword is the member's size, so you can reconstruct a struct's layout straight from its accesses. The compiler inserts padding so each member meets its alignment requirement, which is why struct { char flag; int count; } places count at offset 4 and has a sizeof of 8, not 5. Member offsets are a property of the type, so they never change with the optimization level.

Global Versus Local Storage

Locals and parameters live on the stack as anonymous offsets like -4[rbp]; globals and statics live at fixed locations in the program image and are reached with RIP-relative addressing, written name[rip]. A variable appearing by its symbol name next to [rip] is the tell-tale sign of a global. RIP-relative addressing exists to support position-independent code: the distance from an instruction to a nearby global stays fixed even when the program loads at a different base address. Globals are sorted into .data (non-zero and mutable), .bss (zero-initialised), and .rodata (read-only constants and string literals).

A Glimpse of the Heap

The heap is a third home for data, requested at run time, and that request is visible as a function call: new compiles to a call to operator new with the byte size as its argument (returning the address in rax), and delete compiles to a call to operator delete, usually guarded by a null-pointer check. Stack allocation involves no call at all; a local just occupies a slot in the existing frame. An operator new with no matching operator delete is what a memory leak looks like in assembly, and containers and smart pointers make the same two calls underneath while guaranteeing the pairing for you.

Key Terminology

  • Address: A 64-bit number identifying a location in memory; what a pointer actually is
  • Dereference: A memory load or store through an address held in a register, written [reg]
  • lea (load effective address): Instruction that computes an address without accessing memory; the assembly form of &
  • base + index*scale: The addressing mode array indexing compiles to, computing base + index*elementSize
  • Scale: The multiplier in an addressing mode; always equal to the element size
  • Displacement: A constant byte offset baked into an operand, such as 12[rdi] or 4[rdi]
  • movsx / movzx: Instructions that widen a smaller value into a 64-bit register with sign or zero extension
  • Alignment: The rule that each type prefers to start at an address that is a multiple of its size
  • Padding: Unused bytes the compiler inserts between struct members to satisfy alignment
  • RIP-relative addressing: Reaching a global as a fixed distance from the current instruction, written name[rip]
  • Position-independent code: Code that works regardless of the address the program is loaded at
  • .data / .bss / .rodata: Sections holding initialised globals, zero-initialised globals, and read-only constants
  • Heap: Memory requested at run time via operator new and returned via operator delete
  • Memory leak: An allocation with no matching free; visible as an unmatched call operator new

Looking Forward

Everything in this chapter treated data as raw bytes reached through addresses, and that is exactly the toolkit C++'s object-oriented features are built from. In Chapter 6 you will see that a member function is an ordinary function whose hidden first argument, the this pointer, arrives in rdi; that constructors, destructors, virtual dispatch through vtables, template instantiations, and even exceptions all compile down to the loads, stores, displacements, and calls you have already mastered. Classes are about to dissolve before your eyes.