Global versus Local Storage
Globals in .data, .bss, and .rodata reached by RIP-relative addressing, versus locals on the stack.
Two Places a Variable Can Live
Every variable you have read in assembly so far has lived in one of two places. Local variables and parameters live on the stack, reached through rbp or rsp with a small offset like -4[rbp]. We have leaned on that pattern since Chapter 1. But global variables, and anything with static storage, live somewhere else entirely: in fixed regions of the program's memory image that exist for the whole run. They are reached with a completely different addressing style, RIP-relative addressing, and learning to recognise it tells you instantly whether a value is a local or a global.
Globals Are Reached RIP-Relative
Here is a global and two functions that touch it:
int g_counter{100};
int readGlobal()
{
return g_counter;
}
void incrementGlobal()
{
++g_counter;
}
At -O2:
readGlobal():
mov eax, DWORD PTR g_counter[rip]
ret
incrementGlobal():
add DWORD PTR g_counter[rip], 1
ret
The operand g_counter[rip] is the new thing. rip is the instruction pointer, the register holding the address of the currently executing instruction. So g_counter[rip] means "the address of g_counter, expressed as a fixed distance from where this instruction is." The assembler and linker work out that distance for you; what matters for reading is that the variable is named directly in the operand.
That naming is the giveaway. A local variable never appears by name in the assembly; it is just an anonymous offset like -4[rbp]. A global appears by its symbol name next to [rip]. So g_counter[rip] reads plainly as "the global called g_counter." And notice incrementGlobal modifies it in place with a single add DWORD PTR g_counter[rip], 1: the global keeps its fixed home, so the increment reads and writes the same address.
Why RIP-Relative Instead of an Absolute Address
It would seem simpler to just write the global's absolute address into the instruction. Modern systems avoid that because of position-independent code: the operating system may load the program (or a shared library) at a different base address each time it runs, for security and flexibility. If instructions hard-coded absolute addresses, they would break whenever the load address changed.
RIP-relative addressing sidesteps the problem. The distance from an instruction to a nearby global is fixed at link time and never changes, no matter where the program is loaded, because the code and its globals move together. So the compiler encodes "this many bytes from here" rather than "this exact address." You do not need to compute the distance by hand; just read name[rip] as "the global name."
Locals on the Stack, Side by Side
To make the contrast concrete, here are a local-using function and a global-using function compiled together:
int g_value{7};
int useLocal(int n)
{
int local{n + 1};
return local * 2;
}
int useGlobal()
{
return g_value * 2;
}
At -O0:
useLocal(int):
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov eax, DWORD PTR -20[rbp]
add eax, 1
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
add eax, eax
pop rbp
ret
useGlobal():
push rbp
mov rbp, rsp
mov eax, DWORD PTR g_value[rip]
add eax, eax
pop rbp
ret
The difference jumps out. useLocal works entirely through rbp: the parameter goes to -20[rbp], the local local to -4[rbp], all anonymous stack slots that exist only while the function runs. useGlobal reaches straight for g_value[rip], naming the global directly, because it lives at a fixed location for the whole program. Stack offset versus name[rip]: that single distinction tells you the storage class of any variable in a listing.
Where Globals Actually Sit: .data, .bss, .rodata
Globals do not all live in the same section of the program image. The compiler sorts them into three areas depending on how they are initialised and whether they can change:
.dataholds globals with a non-zero initial value that can change at run time, like ourint g_counter{100};. The value100is stored in the program file and copied into memory at startup..bssholds globals that start at zero, likeint g_total{};. Storing a block of zeros in the file would be wasteful, so.bssjust records how many zero bytes are needed and the loader clears them at startup. The file stays smaller..rodata("read-only data") holds constants that must never change, such asconst int g_max{500};and string literals like"hello". The system marks these pages read-only, so a stray write to them faults instead of silently corrupting data.
You do not usually see these section names inside a function body, only the name[rip] reference. But knowing the three buckets explains why a global is reachable at a fixed location at all: it was placed in one of these sections, which are part of the program image and mapped into memory before main runs. A string literal you pass to std::cout, for instance, lives in .rodata and is reached RIP-relative, exactly like a named global.
A Note on the Unoptimized Global
The global access barely changes between -O0 and -O2. Here is readGlobal at -O0:
readGlobal():
push rbp
mov rbp, rsp
mov eax, DWORD PTR g_counter[rip]
pop rbp
ret
Aside from the prologue and epilogue, it is the same mov eax, DWORD PTR g_counter[rip]. Unlike a local, a global has nothing to store-and-reload through the stack: its home is already fixed, so even unoptimized code just reads it directly. The RIP-relative form is a property of where the variable lives, not of the optimization level.
Key Takeaways
- Variables live in two broad places: locals/parameters on the stack (anonymous offsets like
-4[rbp]) and globals/statics in fixed sections of the program image. - Globals are reached with RIP-relative addressing, written
name[rip], whereripis the instruction pointer. - The tell-tale sign of a global is that it appears by its symbol name in the operand; a local is just an offset from
rbp/rsp. - RIP-relative addressing enables position-independent code: the distance from an instruction to a global is fixed even when the program loads at a different base address.
- Globals are sorted into
.data(non-zero, mutable),.bss(zero-initialised), and.rodata(read-only constants and string literals). - A global's RIP-relative access looks nearly the same at
-O0and-O2, because its location is fixed rather than reconstructed each call.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Global versus Local Storage - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!