CSSE 132 Mini-reading

The Stack and Procedure Calls in 10 Minutes

Review how stack frames keep function calls separate, how ARM64 passes values, and why a function sometimes saves x30.

Why This Matters

Every C function needs its own place for local variables and a way to return to the instruction that called it. In CSSE 132, you will use the stack to trace procedure calls, interpret the compiler's ARM64 output, and explain why local variables with the same name in different functions do not interfere with each other. Worksheet 10 applies these ideas to a short function call.

Core Idea

The stack is a region of memory used while a program runs. When a function begins, it reserves a private region called a stack frame. The stack pointer, sp, identifies the current top of the stack. A function can use its frame for local variables and, when needed, saved register values such as the return address.

One function call

main calls my_func. A frame for my_func is created above main's frame, so its local variables are separate from main's locals.

A nested call

If my_func calls do_nothing, another frame is created. When that call finishes, its frame is released first. This last-in, first-out behavior is why the structure is called a stack.

In the current ARM64 examples, a C int value uses a 32-bit wN register. An address or saved return address uses a 64-bit xN register. The link register is x30; it holds the address to return to after a bl instruction.

Worked Examples

Example 1: Reserve and release a stack frame

sub sp, sp, 16
str w0, [sp, 12]
...
add sp, sp, 16
ret

sub sp, sp, 16 reserves 16 bytes for this function's frame. The store writes the 32-bit value in w0 at offset 12 within that frame. Before returning, add sp, sp, 16 gives the 16 bytes back. The allocation is 16 bytes even though this particular function stores only a 4-byte int; compilers may allocate extra space for alignment or other implementation needs.

Example 2: Follow a simple call

int my_func(int x) {
    return x + 1;
}

int main() {
    int x = 56;
    x = my_func(x);
}
mov w0, 56       // caller puts the argument in w0
bl  my_func      // branch; the next instruction address goes in x30
str w0, [sp, 28] // caller receives the return value from w0

The caller is main, because it calls the procedure. It puts the first int argument in w0. bl my_func branches to the callee and replaces x30 with the return address. The callee, my_func, uses w0 for the input and places its result back in w0. When it returns, the caller reads 57 from w0.

Example 3: Why a non-leaf function saves x30

Now suppose my_func calls another function:

my_func:
  str x30, [sp, -32]!  // reserve 32 bytes and save caller's return address
  str w0, [sp, 28]
  bl  do_nothing       // overwrites x30 with a new return address
  ldr w0, [sp, 28]
  add w0, w0, 1
  ldr x30, [sp], 32    // restore x30 and release the frame
  ret

After main calls my_func, x30 holds the address in main where execution must resume. But bl do_nothing needs x30 for a different return address. Therefore my_func stores its old x30 on the stack before the nested call and restores it afterward. A function that calls another function is a non-leaf function; a leaf function makes no further calls and may not need to save x30.

Example 4: Read the combined instructions

str x30, [sp, -32]!
...
ldr x30, [sp], 32
InstructionEquivalent two-step idea
str x30, [sp, -32]!Subtract 32 from sp, then store x30 at the new top of the stack.
ldr x30, [sp], 32Load x30 from the current top of the stack, then add 32 to sp.

The ! in the first instruction means the updated address is written back to sp before the store. In the second instruction, the update happens after the load. Together, these instructions make matching entry and exit steps for the stack frame.

Common Mistakes

Confusing the caller and callee

The caller invokes the function; the callee is the invoked function. Their responsibilities occur on opposite sides of bl.

Thinking every function saves x30

A leaf function that makes no calls may return using its unchanged x30. A non-leaf function must preserve its caller's return address before using bl again.

Mixing up w0 and x30

w0 carries a 32-bit int argument or return value; x30 carries a 64-bit return address.

Forgetting to restore sp

Every reservation of stack space needs a matching release before the function returns. Otherwise the caller's stack frame is no longer found at the expected location.

Quick Reference

ConceptRemember
Stack frameA function's private, temporary stack memory.
spStack pointer; moving it down reserves space and moving it up releases space.
bl labelBranch to label and save the return address in x30.
retReturn to the address held in x30.
First int argument / return valueUse w0.
Non-leaf functionSave and later restore x30 if another call would overwrite it.

Check Yourself

Try these before opening the answers. The examples differ from Worksheet 10.

  1. Why can two functions each have a local variable named count without sharing one value?
  2. What does bl helper do to execution and to x30?
  3. In a call that passes and returns a C int, which register is used for the first argument and return value?
  4. Why should a function that calls helper save x30 before its bl helper instruction?
  5. A function executes sub sp, sp, 16 when it begins. What must it do to sp before returning?

Answers

Show the answers
  1. Each call has its own stack frame, so each local variable has a different memory location.
  2. It branches to helper and stores the address of the following instruction in x30.
  3. w0. It holds the first 32-bit int argument before the call and the 32-bit return value afterward.
  4. The nested bl overwrites x30. Saving the old value lets the function return to its own caller after helper finishes.
  5. Release the same 16 bytes, such as with add sp, sp, 16.

Next Step

Complete Worksheet 10: Stack / Procedure Call. As you trace each call, keep a small table of the current function, sp, w0, and x30.