A stack frame (or activation record) is the portion of the stack used by a single subroutine call. It holds everything the subroutine needs that doesn’t fit in registers: saved registers, the return address, parameters, and local variables.

Each call gets its own frame, pushed onto the stack on entry and popped on return. Recursive calls naturally get distinct frames, so each recursive instance has its own copy of locals.

Typical contents

A frame for a single subroutine call:

Image: Call stack layout showing stack frames and frame pointer, public domain

Reading a single frame top-to-bottom (higher to lower addresses): arguments passed by the caller (via the stack when there are too many for registers), the saved caller frame pointer, the saved return address, saved callee-saved registers, local variables, then temporary work space, with the stack pointer (SP) at the lowest address.

The stack grows down, so newer items have lower addresses. The frame’s “top” (highest address) holds the oldest items (arguments passed in by the caller); the “bottom” (lowest address, at SP) holds the newest items.

Frame pointer (FP)

A frame pointer is a register that points to a fixed location within the current frame — typically just below the saved return address. While the stack pointer (SP) may move up and down during the subroutine’s execution (pushing/popping temporary values), FP stays fixed for the duration of the call.

This makes it possible to access frame elements at known offsets. Using the convention “FP points at the saved-FP slot” (which the prologue below sets up):

ldw r1, 8(fp)        # arg passed by caller via stack (positive offset, into caller's frame)
ldw r2, 4(fp)        # saved return address
ldw r3, 0(fp)        # saved caller FP
ldw r4, -4(fp)       # callee-saved register r16
ldw r5, -12(fp)      # local variable (after the saved registers)

Positive offsets from FP reach into the caller’s frame (incoming args, beyond what fits in argument registers). Negative offsets reach into this subroutine’s saved registers and locals. The exact split depends on the prologue you wrote — pick a convention and apply it consistently throughout the function.

Without FP, you’d have to track SP’s current offset manually, which gets fragile when the subroutine pushes and pops things mid-execution.

Why each call needs its own frame

When a subroutine calls another subroutine, the callee must not corrupt the caller’s local data. Separate frames isolate this:

  • Caller pushes its frame (args, locals).
  • Callee pushes its frame on top.
  • Callee runs, manipulating only its own frame.
  • Callee pops its frame on return.
  • Caller’s frame is intact, exactly as it was before the call.

For recursion, the same subroutine appears at multiple call levels — each level has its own frame, so each instance’s locals are independent. This is why recursive functions can hold different values of the same variable at different recursion levels.

Frame setup (prologue)

A subroutine’s prologue sets up the frame on entry:

MySub:
    subi sp, sp, 16          # allocate 16 bytes
    stw  ra,  12(sp)         # save return address
    stw  fp,   8(sp)         # save caller's FP
    addi fp, sp, 8           # set new FP to point above the saved FP
    stw  r16,  4(sp)         # save callee-saved register
    stw  r17,  0(sp)         # save another
    
    # ... subroutine body ...

The stack grows down, so SP is decremented to allocate space, then the subroutine writes into the allocated region.

Frame teardown (epilogue)

The epilogue tears down the frame before returning:

    # ... subroutine body ...
    
    ldw  r17,  0(sp)         # restore saved register
    ldw  r16,  4(sp)
    ldw  fp,   8(sp)         # restore caller's FP
    ldw  ra,  12(sp)         # restore return address
    addi sp, sp, 16          # release frame
    ret                       # return

The reverse of the prologue. After this, the frame is gone — SP is back where it started, the caller’s FP is restored, and ret jumps to the saved return address.

When FP is optional

In simple functions where SP doesn’t change after the prologue, FP is redundant — you can address everything as offsets from SP directly. Many compilers omit FP in optimized builds for small functions (“frame pointer omission”), which saves a register and a couple of instructions per call.

The cost is debugging: stack traces are easier to construct when FP exists, since each frame’s saved FP forms a linked list of frames. Without FP, debuggers have to consult debug info to figure out frame boundaries.

In Nios II, fp (r28) is the conventional frame pointer when one is used. In hand-written assembly for Computer Architecture, you may or may not maintain it depending on the subroutine’s complexity.