C/C++ Arena

Step 9 of 10

Where a program's data lives

You've used three kinds of storage: locals, globals and static variables, and malloc memory. They live in different regions of the program's memory, and knowing the map explains most lifetime rules and many bugs. On a typical Linux or macOS program, the regions are:

Region What's in it Lifetime
text the machine code of your functions whole run, read-only
rodata string literals, const globals whole run, read-only
data globals and static variables with a non-zero initial value whole run
bss globals and static variables that start at zero whole run
heap blocks from malloc, until free until you free it
stack locals, parameters, return addresses until the function returns
#include <stdlib.h>

int high_score = 100;          /* data */
int players;                   /* bss: zero-initialized */
const char *title = "Arena";   /* the pointer is in data; the text "Arena" is in rodata */

int main(void) {
    static int calls;          /* bss, lives for the whole run */
    int round = 1;             /* stack */
    int *scores = malloc(10 * sizeof *scores);   /* the pointer is on the stack; the block is on the heap */
    free(scores);
    return 0;
}

Stack frames and calling conventions

Each function call pushes a stack frame holding the return address, saved registers and the function's locals, and pops it on return. That's why returning a pointer to a local is a bug: the frame, and the local in it, is gone.

How arguments get passed is fixed by the platform's calling convention, part of its ABI (application binary interface, the rules compiled code from different compilers must share). On x86-64 Linux and Intel Macs (the System V convention), the first six integer or pointer arguments travel in registers (rdi, rsi, rdx, rcx, r8, r9), the return value comes back in rax, and further arguments go on the stack. 64-bit Windows uses four registers (rcx, rdx, r8, r9), and 64-bit ARM (phones, Apple silicon) uses eight (x0 to x7, with the result in x0). You rarely deal with this yourself, but it's why calls with a few arguments are cheap, and it's what you see when you read assembly or a debugger's register view.

Your turn: each function below must return something that is still valid after it returns. make_scores(n) returns a new heap array of n zeros (the caller frees it), level_name(lvl) returns the name for levels 1 to 3 ("easy", "normal", "hard") or "unknown", and next_ticket() returns 1, 2, 3, ... on successive calls. Pick the right storage for each: heap, string literal (rodata), and a static local (bss).

Previous: Who owns what Next: Challenge: a string builder