HomeARM CourseDay 15
DAY 15 · THE INSTRUCTION SET

Subroutines & the AAPCS Calling Convention

By EcrioniX · Updated Jun 6, 2026

A function call sounds simple — jump there, jump back. But how does the caller pass arguments, get a result, and trust its registers survive? The answer is a shared rulebook: the AAPCS. Master it and your assembly can call C, and C can call yours.

1. Call and return

From Day 12: BL calls (jump + save return address in LR), and you return with BX LR (or by popping LR into PC):

BL myfunc ; call: LR = address of next instruction, then jump ; ... execution continues here after myfunc returns ... myfunc: ; ... do work ... BX lr ; return to wherever we were called from

2. The problem a convention solves

If everyone invented their own way to pass arguments and use registers, no two pieces of code could call each other. So ARM defines the AAPCS (ARM Architecture Procedure Call Standard) — the contract every compiler and library follows. Obey it and your code interoperates with the whole ecosystem.

3. The register roles

RegistersRoleWho preserves
r0–r3arguments 1–4; r0 (& r1) = return value; scratchcaller-saved
r4–r11local variablescallee-saved
r12 (IP)scratch / intra-call tempcaller-saved
r13 (SP)stack pointerspecial
r14 (LR)return addressspecial
r15 (PC)program counterspecial

Caller-saved vs callee-saved

💡 Why two kinds?

It splits the work fairly. Scratch registers (r0–r3) are for quick throwaway use — no one saves them, so calls are cheap. Long-lived values go in r4–r11, which the callee promises to protect, so the caller can rely on them surviving any call.

4. Passing arguments & returning values

; int sum = add(10, 20); MOV r0, #10 ; arg1 in r0 MOV r1, #20 ; arg2 in r1 BL add ; call ; result now in r0 add: ADD r0, r0, r1 ; r0 = r0 + r1 BX lr ; return value is in r0

5. Nested calls — save LR!

Here's the catch that bites beginners. BL overwrites LR. So if your function calls another function, the second BL destroys your own return address — unless you save LR on the stack first (Day 14):

outer: PUSH {r4, lr} ; save LR (and any callee regs we use) BL inner ; this would clobber LR — but we saved it ; ... more work ... POP {r4, pc} ; restore and return (saved LR → PC)

A leaf function (one that calls nothing) can skip this and just BX LR. Any function that makes a call must preserve LR. This is the single most common cause of "my function returns to the wrong place."

6. The complete function template

func: PUSH {r4-r6, lr} ; prologue: save callee-saved regs + LR ; r0-r3 = args; use r4-r6 for locals; call others freely ; ... body ... ; put result in r0 POP {r4-r6, pc} ; epilogue: restore + return

✅ The mental model

The AAPCS is the handshake between functions: r0–r3 carry args, r0 returns the result, r4–r11 are protected by the callee, and LR holds the return address — which you must save on the stack before any nested call. Follow it and your assembly and C interoperate perfectly.

🎯 Day 15 takeaways

Quick check

  1. Where does the first argument go, and where does the return value come back?
  2. Which registers must a called function preserve?
  3. Why must a non-leaf function push LR?

FAQ

What is the AAPCS?

The ARM Architecture Procedure Call Standard — the convention for argument/return registers, register preservation and stack use that lets ARM code interoperate.

Caller-saved vs callee-saved?

Caller-saved (r0–r3, r12) can be clobbered by the callee; callee-saved (r4–r11) must be preserved by the called function.

Why save LR?

BL overwrites LR, so a function that calls another must push LR first or it loses its own return address.

Previous
← Day 14: The stack

← Back to the full course roadmap