The call stack is the part of a program that nobody designs and everybody depends on: a contiguous region of memory and one register that points into it. On x86-64 that register is rsp; on ARM and AVR it is sp. Everything else about it – frame layout, calling conventions, how far it is allowed to grow – is convention layered on top of that single pointer.
The mechanism underneath is smaller than most people expect. call pushes the address of the next instruction onto the stack and jumps to the target. ret pops that address back into the program counter and jumps to it. That is the whole of it. A subroutine call is a store, a return is a load, and the nesting discipline falls out for free because the store and the load are last-in, first-out.
The stack grows down on x86, ARM and AVR: every push decrements the pointer. The reason is less architectural elegance than address-space economy. If the stack starts high and grows down while the heap starts low and grows up, the two share one contiguous region and meet in the middle, and neither has to know in advance how much the other will want. A machine with an upward-growing stack has to reserve a fixed gap between them at link time and wastes whatever the program does not use.
What is actually in a frame
A frame is the slice of stack belonging to one invocation. Its contents are decided by the calling convention, not by the language:
- the return address, pushed by
call; - the arguments that did not fit in registers;
- the saved frame pointer, if the function keeps one;
- the callee-saved registers the function intends to clobber;
- local variables that could not stay in registers, and any temporaries spilled there;
- padding, because x86-64 requires
rspto be 16-byte aligned at a call boundary.
How arguments arrive varies more than the rest of the layout. The 32-bit x86 cdecl convention pushes them right to left and leaves cleanup to the caller – which is exactly what makes variadic functions possible, since only the caller knows how many arguments it passed. stdcall, used by the Win32 API, gives cleanup to the callee and saves a few bytes per call site at the cost of that flexibility. The System V AMD64 convention passes the first six integer arguments in rdi, rsi, rdx, rcx, r8 and r9 and the first eight floating-point arguments in xmm0 through xmm7, spilling only the remainder to memory. ARM's AAPCS puts the first four arguments in r0–r3 and, because ARM has a link register, keeps the return address in lr rather than on the stack – a leaf function there can return without touching memory at all.
The frame pointer, and why it disappears
Compilers traditionally keep a second register – rbp on x86-64 – pointing at the base of the current frame. Frames then form a linked list: from rbp you can walk to the caller's rbp, and so to the caller's caller, which is how a debugger produces a backtrace with no other information available.
At -O2 that register is usually reclaimed as a general-purpose register. On x86-64 there are only fifteen of them and one is a real cost, so the trade is made in favour of the optimiser. Backtraces then depend on DWARF call-frame information, a table the compiler emits describing how to recover the return address at any instruction address. It works well, but it needs the debug data to be present, and it makes a raw stack dump considerably harder to read. Embedded work often turns the frame pointer back on for exactly that reason: a crash on a device you cannot attach a debugger to is worth one register.
The red zone
One detail of System V AMD64 that surprises people writing signal handlers: the 128 bytes below rsp belong to the current function and may be written without adjusting the pointer at all. Leaf functions use this to avoid the two instructions a stack adjustment would cost. Signal handlers and interrupt entry code must not touch that region, because the interrupted function is entitled to it and will not expect it to have changed.
Overflow
The stack is finite and nothing in the instruction set enforces the bound. Recursion without a base case walks the pointer down until it leaves the region the operating system mapped – on a hosted system, usually into a guard page that faults cleanly; on a bare-metal microcontroller, into whatever else lives in RAM, silently. On a Cortex-M part the main stack and the process stack are separate and there is a stack limit register, which an RTOS can use to catch overflow at the next exception rather than at the next crash. On smaller parts there is nothing, and the failure mode is a corrupted variable somewhere else in memory, surfacing hours later.
The usual mitigation is a stack canary: the compiler places a known value between the locals and the saved return address and checks it on return. It catches the classic contiguous buffer overflow, and it does nothing at all about a non-adjacent write, an overwrite of a function pointer held further up, or a return-oriented attack that never needs to touch the canary. It is a useful speed bump, not a boundary.
Measuring it on a device
With no reliable runtime bound, the practical technique is a high-water mark. Fill the stack region with a known pattern – 0xA5 is the convention – run the firmware through its worst case, then scan upward from the bottom and find the deepest byte still holding the pattern. The distance from there to the top is what the program actually used.
The trap is that the answer is only true of the binary you measured. Debug and release builds have different frame sizes, an inlined function has no frame at all, and an interrupt taken at the worst possible moment adds a frame the main loop never produces. A stack that fits in testing and not in the field is not bad luck; it is a measurement taken over the wrong set of executions. The honest approach is to measure the deepest call chain with interrupts enabled at the optimisation level that ships, and then leave margin on top.
One structure, several names
The same object is called the call stack, the control stack and the run-time stack depending on which decade and which community is talking. Control stack is the oldest of the three and the most descriptive: the stack holds the control state of the program, the pending return addresses that define where execution resumes. Run-time distinguishes it from anything the compiler resolves statically. They are the same region of memory, and the differences between the names are entirely historical.