Skip to content

Functions, Stack and the ABI

A nested call changes the link register, and a callee may overwrite temporary registers. Preserve exactly the state your function needs, then restore the caller's stack.

Before you start

Complete addresses and relocations. You understand leaf returns, register widths, pointers and loops.

Outcome: write a non-leaf routine that keeps a live argument across a call and returns with its frame balanced. Work in a fresh directory on the Pi.

The AAPCS64 function boundary

For the ordinary integer/pointer calls in this course:

Registers Boundary obligation
x0–x7 Arguments; scalar integer/pointer result in x0
x8 Indirect-result location, not an extra ordinary argument
x9–x17 Caller-clobbered temporaries
x18 Platform role; avoid portable scratch use
x19–x29 Callee-preserved
x30 / LR Return address overwritten by BL
SP Restore it; keep 16-byte alignment

These are calling rules, not permanent hardware types. A live temporary needed after a call must be saved by its caller. Follow the AAPCS64 for aggregate, variadic and FP signatures rather than extrapolating this table. Flags are not preserved across public calls.

Why a leaf template is not enough

bl helper places the address after BL in LR. Without saving the incoming LR, a later RET can return into the current function instead of its caller. A leaf function that makes no calls does not automatically need a stack frame.

This example computes 2 * value + bonus modulo 2^64. It retains bonus in callee-preserved x19 while a helper deliberately overwrites x1. It also restores the caller's original x19.

// file: functions.S
.text
.global double_value
.type double_value, %function
double_value:
    add x0, x0, x0
    mov x1, #0                 // permitted caller-clobber
    ret
.size double_value, .-double_value

.global double_plus
.type double_plus, %function
double_plus:
    stp x29, x30, [sp, #-32]!
    mov x29, sp
    str x19, [sp, #16]
    mov x19, x1
    bl double_value
    add x0, x0, x19
    ldr x19, [sp, #16]
    ldp x29, x30, [sp], #32
    ret
.size double_plus, .-double_plus
.section .note.GNU-stack,"",%progbits
/* file: functions.c */
#include <stdint.h>
#include <stdio.h>
extern uint64_t double_plus(uint64_t, uint64_t);
int main(void)
{
    if (double_plus(10, 7) != 27 || double_plus(0, 0) != 0 ||
        double_plus(UINT64_MAX, 1) != UINT64_MAX) return 1;
    puts("PASS functions: nested call and live argument");
    return 0;
}
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -O0 functions.c functions.S -o functions
./functions
PASS functions: nested call and live argument

Draw the frame

After allocation, offsets 0 and 8 hold saved FP and LR, offset 16 holds saved x19, and offset 24 is padding. Total allocation is 32 bytes, preserving 16-byte alignment. Pre-index [sp, #-32]! allocates before the store; post-index [sp], #32 restores after the load.

x29 is a frame pointer here; it is not mandatory in every leaf. This teaching frame contains no unwind metadata, so do not assume full exception-unwinding support. Production mixed-language code needs an appropriate unwind contract as well as correct runtime registers.

Inspect preservation rather than guessing

In GDB, stop at the exact function entry, note SP and x19, step across the body and stop immediately before RET. They must match their entry values. The GDB lesson provides a repeatable command sequence.

If return flow repeats unexpectedly, check LR saving first. If bonus becomes zero, check whether it was left in x1 across the helper call. Do not repair either bug by assuming the helper will never change caller-saved registers.

Part 2 checkpoint

Trace one pointer load, one relocation pair and one nested call. Explain why register width, allocated object bounds and calling-convention preservation are separate obligations. Then predict double_plus(3, 4) (10) before adding the test.

Check your understanding

Why save x19 before putting bonus into it? Why allocate 32 bytes when the saved values occupy 24?

Compare your reasoning

The caller may already have a live x19 value, so this callee must restore it. Padding keeps SP aligned to 16 bytes while holding all three saved registers.

Continue the course

Previous: Addresses and Relocations · Course roadmap · Next: Calling Assembly from C and C++

Donate