FREE LESSON · Machine code & execution · 5 OF 8

A stack frame obeys an interface contract

A stack frame obeys an interface contract: Trace arguments, saved registers, local storage, and return addresses.

Prerequisites: Registers, loads, stores, and function calls.

Learning objectives

  • Distinguish caller-saved and callee-saved obligations.
  • Trace stack allocation and restoration.
  • Explain why source variables need not each occupy a stack slot.

The ABI connects separately compiled code

An application binary interface specifies conventions that allow independently compiled functions to cooperate. These can include argument registers, result registers, stack alignment, preserved registers, and object-file details. An instruction set says what a call-like instruction does; the ABI says how software uses the surrounding machine state.

Use the standard RISC-V integer calling convention as a concrete example. Argument registers a0 through a7 carry the first eligible integer arguments, with results ordinarily returned in a0 and sometimes a1. Registers s0 through s11 are callee-saved. Temporary and argument registers are caller-saved. The ordinary stack pointer is kept aligned to a 16-byte boundary under the standard convention. Specialized ABIs can differ, so a program must use the contract selected by its toolchain.

Caller and callee preserve different values

Suppose function f receives x in a0, calls g(x), and then adds the original x to g’s result. Because g may overwrite a0, f must preserve x somewhere before calling g. If f uses s0 to retain x, f must first preserve the caller’s original s0 and restore it before returning.

A nested call also changes the return-address register ra. Consequently f must preserve its own return address before calling g unless another valid strategy retains it. On a 64-bit target, a conceptual 32-byte frame can hold saved ra, saved s0, and padding or locals while preserving alignment. Allocating the frame subtracts 32 from sp. Deallocating it restores sp by adding 32. The exact offsets are compiler choices, but saving and restoring must agree.

PSEUDOCODE: conceptual non-leaf function
allocate aligned frame
save caller’s s0 and our incoming return address
s0 = incoming x
call g with x
result = g_result + s0
restore saved s0 and return address
release frame and return result

A frame is not a picture of the source

A local variable may live in a register, disappear after constant propagation, or share storage with a variable whose lifetime does not overlap. A leaf function that makes no calls may need no stack frame at all. Optimizations can omit a dedicated frame pointer or transform suitable tail calls.

Debuggers and exception unwinders therefore need reliable conventions or metadata rather than assuming every function has the same shape. Memory safety matters too: writing outside a local array can corrupt saved state, though the precise layout varies. The useful invariant is that the callee returns with the promised preserved state and a valid result, regardless of how it organized its temporary work.

Primary reading

Practice problem

Function f must retain a value in s1 while calling g. It also needs to return to its own caller. Explain which incoming values f must preserve if it changes s1 and uses the standard RISC-V return-address register for the nested call. Assume a 64-bit target and choose an aligned frame size for two eight-byte saved values.

Worked solution

Because s1 is callee-saved, f must preserve the value that its caller originally placed there before using s1 for its own temporary. Calling g through the ordinary link convention overwrites ra, so f must also retain its incoming return address before that nested call. Two eight-byte saved values occupy sixteen bytes, which is already a multiple of the required sixteen-byte alignment for the standard ABI. A sixteen-byte frame can therefore suffice if there are no additional locals, stack arguments, or ABI-specific requirements. On return, f restores the old s1 and its return address, then restores sp. The fact that g also preserves s1 does not excuse f from restoring the value expected by f’s caller.

Check your understanding

Why must f save its incoming s0 before using s0 as a local temporary?

  • Because every register must always be saved.
  • Because the ABI promises f’s caller that s0 is preserved.
  • Because s0 cannot hold integers.
  • Because stack memory is always faster.
Explanation

Callee-saved is an obligation toward the caller, not merely a convenient naming convention.

Which statement about local variables is correct?

  • Each always has a permanent stack slot.
  • A compiler may keep one in a register or eliminate it while preserving required behavior.
  • They all share the return-address register.
  • They must always survive after the function returns.
Explanation

The source-level abstraction does not mandate a fixed physical location for each variable.

Primary reading

Open the study and save your work →