The first time I worked on ARM assembly after years of x86, I kept looking for a return address on the stack after a function call — and it just wasn’t there. That confusion led me straight to the link register, one of the cleverest small design decisions in RISC architecture. In this post, I’ll explain what the link register is, why it exists, how it differs from x86’s stack-based approach, and how it shapes performance, debugging, and even security.
What Is the Link Register?
The link register (LR, called X30 in AArch64 or R14 in AArch32) is a dedicated CPU register whose job is to hold the return address — the instruction address the processor should jump back to after a subroutine finishes executing.
When you call a function using the BL (Branch with Link) instruction, the processor automatically stores the address of the instruction immediately following the BL into the link register, then jumps to the subroutine.
BL my_function ; LR is set to the address of the next instruction
; execution continues here after my_function returns
Inside my_function, returning is as simple as:
my_function:
; ... function body ...
RET ; or BX LR / MOV PC, LR — jumps to address in LR
Why Does the Link Register Exist?
On CISC architectures like x86, a CALL instruction pushes the return address onto the stack, and RET pops it back off. That’s simple, but it means every single function call requires a memory write, even for the smallest, most frequently called leaf functions.
ARM’s RISC design philosophy avoids unnecessary memory traffic. By storing the return address in a register instead of memory, a simple, non-nested function call needs zero memory accesses for its call/return mechanism. This is a meaningful performance win, especially for leaf functions (functions that don’t call other functions) — which make up a large share of real-world code.
x86 vs. ARM: A Side-by-Side Comparison
| Aspect | x86/x86-64 | ARM |
|---|---|---|
| Return address storage | Pushed onto the stack automatically by CALL | Stored in the Link Register (LR/X30) by BL |
| Return mechanism | RET pops the stack | RET/BX LR jumps to the LR value |
| Memory access on call | Always at least one stack write | None, unless the function needs to preserve LR for nested calls |
| Nested calls | Stack naturally supports arbitrary nesting | Requires manually pushing LR before another BL |
| Simplicity for leaf functions | Same cost regardless of nesting | Faster — no memory access required |
What Happens with Nested Calls?
The link register holds exactly one return address. If a subroutine itself calls another subroutine, it must first save the current LR value somewhere (typically the stack), because the next BL will overwrite it.
outer_function:
STP X29, X30, [SP, #-16]! ; save frame pointer and LR to the stack
BL inner_function ; LR is now overwritten with return-to-outer address
LDP X29, X30, [SP], #16 ; restore frame pointer and LR
RET ; return using the restored LR
STP (Store Pair) here saves both the frame pointer (X29) and the link register (X30) in a single instruction — an efficient, idiomatic ARM64 prologue pattern.
Internal Working: Call and Return Flow
sequenceDiagram
participant PC as Program Counter
participant LR as Link Register
participant Stack as Stack Memory
participant Func as Subroutine
PC->>LR: BL instruction stores return address into LR
PC->>Func: Jump to subroutine entry point
Func->>Stack: (if nested) STP saves LR and frame pointer
Func->>Func: Executes subroutine body
Func->>LR: BL to nested function overwrites LR
Func->>Stack: (on return from nested call) LDP restores LR
Func->>PC: RET jumps to address held in LR
PC->>PC: Execution resumes after original BL
x86-64 Equivalent Mental Model
Even though x86-64 doesn’t have a link register, understanding the contrast helps clarify why ARM’s design matters:
call my_function ; pushes return address onto the stack, jumps to my_function
; ...
my_function:
; function body
ret ; pops return address from stack, jumps there
Here, the stack is doing exactly what the link register does on ARM, except it’s a memory-based mechanism rather than a register-based one. This is why x86-64 stack frames always contain a return address, while ARM leaf functions often don’t touch the stack at all.
Practical Use Cases
- Leaf function optimization: Compilers detect when a function doesn’t call anything else and skip saving LR entirely, since it will never be overwritten.
- Tail-call optimization: Some ARM compilers use the LR directly to implement efficient tail calls, avoiding a stack frame altogether.
- Interrupt and exception handling: On exception entry, ARM automatically saves the current execution state (including the return address) into a banked link register (e.g.,
LR_irqin AArch32), separate from the “normal” LR, so an interrupt handler can return cleanly. - Stack unwinding for debuggers: Debuggers and unwinders (like those used in
gdbor crash reporting tools) walk the chain of saved frame pointers and link registers to reconstruct a call stack, similar to how x86 debuggers walk saved return addresses.
Debugging with the Link Register
In GDB on an ARM target:
(gdb) info registers lr
lr 0x400620 0x400620 <main+56>
(gdb) bt
#0 inner_function () at file.c:12
#1 0x00400620 in outer_function () at file.c:20
#2 0x004006a0 in main () at file.c:30
The backtrace (bt) command relies heavily on correctly saved LR values on the stack. If a function corrupts its saved LR (a classic stack-buffer-overflow bug), the backtrace becomes garbage — which is actually one reason link-register corruption is a well-known security-relevant bug class on ARM systems, closely related to return-address overwrites on x86.
Security Considerations
Because the return address lives in a single register rather than being embedded automatically in every stack frame, certain classes of stack-based exploitation differ between architectures:
- On x86, overwriting the return address on the stack is the classic buffer-overflow attack vector.
- On ARM, an attacker must specifically target the saved copy of LR on the stack (since the live register itself isn’t normally reachable via a buffer overflow) — but once LR is saved via
STP, it becomes just as vulnerable to stack-smashing techniques.
This is part of why mitigations like Pointer Authentication (PAC) on ARMv8.3+ specifically sign the link register value before it’s pushed to the stack, making tampering detectable.
Optimization Considerations
- Avoiding unnecessary LR save/restore in leaf functions is one of the most common and effective ARM compiler optimizations (
-O2and above typically apply this automatically via leaf function detection). - Excessive nested calls without inlining can lead to repeated
STP/LDPpairs, adding memory traffic that a flatter call structure (or aggressive inlining) can avoid. - On some cores, the link register is also used with the return address predictor / return stack buffer, a hardware structure that predicts where a
RETwill jump, based on push/pop symmetry withBL/RETpairs — mismatches can cause pipeline flushes.
Common Mistakes
- Forgetting to save LR before a nested
BLcall, silently corrupting the return path of the outer function. - Assuming ARM functions always use the stack for return addresses (they don’t, for leaf functions).
- Confusing the link register with the frame pointer — they serve completely different purposes (LR is the return address; the frame pointer anchors local variable/stack-frame access).
Best Practices
- Always pair
STP/LDP(or equivalentPUSH/POP) around LR whenever a function makes further calls. - When hand-writing assembly, use
BLfor calls that need a return address recorded, and plainBfor tail calls/jumps that don’t. - Trust the compiler’s leaf-function detection rather than manually second-guessing whether to save LR — modern compilers are very good at this.
FAQs
Does x86-64 have anything like the link register? Not directly — x86-64’s CALL/RET pair always uses the stack. Some architectures like x86’s older ancestors or certain embedded chips do use link-register-like schemes, but mainstream x86-64 relies purely on the stack.
What happens if I overwrite LR without saving it first? The function loses its way home — attempting RET afterward jumps to whatever garbage address is currently in LR, typically causing a crash.
Is the link register the same as X30? Yes, on AArch64, X30 is architecturally defined as the link register; it’s a general-purpose register that also has this special role.
Summary and Key Takeaways
- The link register stores the return address for a subroutine call, set automatically by
BL/BLX. - It avoids a memory write on every function call, unlike x86-64’s stack-based
CALL/RET. - Nested calls require manually saving and restoring LR, typically alongside the frame pointer via
STP/LDP. - Understanding LR is essential for debugging (stack backtraces), security (return-address integrity), and performance (leaf function optimization).
References
- Arm® Architecture Reference Manual for A-profile architecture, Chapter on the Procedure Call Standard (AAPCS64)
- Arm Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64) documentation
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1 (CALL/RET semantics, for contrast)
- GNU Binutils documentation for ARM assembly syntax (GAS)