The Life of Local Variables in C
1. High-Level vs. Low-Level View
- High-Level Languages (e.g., JavaScript): The idea of creating and destroying variables is straightforward. When a function returns, objects assigned to local variables lose a reference. If their reference count drops to zero, a Garbage Collector reclaims the memory.
- Low-Level Languages (e.g., C): C primarily manipulates numbers and memory addresses directly. To understand how local variables “die,” we need to look at how the microprocessor works.
2. How Microprocessors Handle Data
All microprocessors, from the 40-year-old MOS 6502 to a modern one, operate on data in two main ways:
- CPU Registers:
- These are a small number of extremely fast memory locations located directly inside the processor core.
- They are used for temporary storage and calculations.
- Instructions operate directly on them, and they are often named with letters (e.g.,
A,B,X,Y). - Crucially, registers are ephemeral. When a function finishes, or another function is called, the same registers will be overwritten and used for new purposes. The old value is lost unless explicitly saved somewhere else.
- Main Memory (RAM):
- The processor can move data from RAM into registers for processing and write the results from registers back into RAM for longer-term storage.
3. Example 1: The MOS 6502 Processor
The video demonstrates a program written in assembly for the 6502 (the CPU of the Commodore 64) to illustrate how registers are used for function arguments and local variables.
The goal is a function fill_screen(count) that draws count green pixels on the screen.
- Passing an Argument: The argument
count(the value 50) is loaded into theAregister (the Accumulator) before calling the function. TheAregister effectively acts as the function’s parameter. - Local Variables:
- Inside the function, the value from
A(50) is immediately transferred to theXregister to save it. TheXregister now holds the loop counter. - The
Aregister is immediately reused and overwritten with the value5(the color green). - The
Yregister is initialized to0and is used as an index or offset to determine where to draw the pixel.
- Inside the function, the value from
- The Loop:
- The value in
A(the color) is stored at memory address$0200 + Y. This screen is memory-mapped, meaning writing a byte to this memory location directly changes a pixel on the screen. Yis incremented to point to the next pixel location.Xis decremented.- If
Xis not zero, the loop repeats.
- The value in
- Conclusion: The argument passed in register
Awas “destroyed” (overwritten) as soon as the function started using it for something else. Its value only persisted because it was saved to another “local” register (X). When the function returns, all these registers are free to be used by other code, effectively erasing the local state.
4. Example 2: The Intel 386 Processor and The Stack
Modern architectures and C compilers primarily use a region of memory called the stack to manage function calls and local variables.
- What is the Stack?
- It’s a LIFO (Last-In, First-Out) data structure in RAM.
- A special register called the Stack Pointer (
SP) always points to the “top” of the stack. - The stack grows downwards in memory (pushing a value decrements the
SP). - Key Instructions:
PUSH: Decrements theSPand saves a value to the top of the stack.POP: Retrieves a value from the top of the stack and increments theSP.
The Function Call Convention
To ensure that code compiled by different compilers can work together, they follow a standard procedure for calling functions. The video shows the cdecl convention for the 386 architecture.
Here’s what happens when main calls sum(10, 20):
- Push Arguments: The arguments are pushed onto the stack in reverse order.
push 20push 10
- Call Function: The
CALL suminstruction does two things:- It pushes the return address (the address of the instruction right after the
CALL) onto the stack. This is how the CPU knows where to continue aftersumis finished. - It jumps to the code for the
sumfunction.
- It pushes the return address (the address of the instruction right after the
- Function Prologue (inside
sum):push ebp: The old Base Pointer (EBP) is saved to the stack. This is crucial for restoring the caller’s stack frame later.mov ebp, esp: The current Stack Pointer (ESP) is copied into theEBP. TheEBPnow becomes a stable reference point (a “frame pointer”) for the current function’s stack frame.
- Accessing Arguments: Inside
sum, the arguments are no longer in registers. They are on the stack and can be accessed at fixed offsets from the newEBP.- The first argument (10) is at
[ebp + 8]. - The second argument (20) is at
[ebp + 12]. The function performs the addition using these memory addresses.
- The first argument (10) is at
- Function Epilogue (leaving
sum):- The result of the sum is placed in the
EAXregister (the standard register for return values). pop ebp: The oldEBPis restored from the stack, effectively deleting thesumfunction’s stack frame.ret: This instruction pops the return address from the stack and jumps back to that location inmain.
- The result of the sum is placed in the
- Stack Cleanup (back in
main): The caller (main) is responsible for cleaning the arguments off the stack, usually by adding to theSP(add esp, 8), which effectively “deletes” the 10 and 20 that were pushed earlier.
Conclusion: The local variables (the arguments a and b) existed only as temporary values at specific locations on the stack. Once the function returns and the Stack Pointer is moved, that memory is considered free and will be immediately overwritten by the next function call. This is what it means for C local variables to be “created” and “destroyed.” They are simply transient values in registers or on the stack.