跪拜 Guibai
← All articles
iOS · Compiler Principles · Assembly Language

How a Thread Call Stack Actually Works on iOS

By _瑞 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Understanding the register-level mechanics of frame records, `bl`/`ret`, and the stack pointer removes the magic from crash logs and backtraces. When symbolication fails or a stack trace looks truncated, knowing that leaf functions omit frame records and that `x29` chains are compiler-optional explains the gaps.

Summary

Every function call on ARM64 allocates a stack frame by decrementing the stack pointer and, for non-leaf functions, writes a frame record containing the caller's FP and the return address. The `bl` instruction sets the link register (x30) to the next instruction before jumping, and `ret` uses that address to return. A debugger reconstructs the human-readable call stack by walking this linked list of frame records and mapping addresses back to Mach-O symbols.

Leaf functions that make no further calls can skip saving x30 and establishing a new frame record, which is why a stack trace may appear to skip frames. The compiler decides the exact layout, but the ARM64 calling convention mandates that x0–x7 carry integer arguments and return values, while x19–x28 must be preserved by the callee.

Stack overflow happens when recursive or deep call chains exhaust the per-thread stack space, causing a memory access fault. The runtime does not store function names; symbolication is a separate step that resolves raw addresses against the binary's symbol table.

Takeaways
ARM64 `bl` writes the return address into x30 (the link register) and jumps; it does not push anything to the stack.
Non-leaf functions save the caller's x29 and their own x30 into a frame record on the stack, forming a linked list that debuggers walk to produce a backtrace.
Leaf functions can omit saving x30 and skip establishing a new frame record, which is why some frames appear missing in a stack trace.
Stack overflow is a memory access fault caused when a thread's finite stack space is exhausted by deep or unbounded call chains.
Symbolication is a post-hoc step that maps raw code addresses to function names using the binary's symbol table; the stack itself contains only addresses.
Conclusions

Many developers treat the call stack as a given, but it is a convention enforced by the compiler and ABI, not a hardware feature. On ARM64, the frame pointer is optional, and a release build may omit frame records entirely, making backtracing rely on unwind info (CFI) instead.

The article's assembly walkthrough makes visible a subtle point: Swift's `Int` addition emits an overflow check (`adds` + `cset` + `tbnz`) that a C integer addition would not, adding a small but real cost to every arithmetic operation in debug builds.

Concepts & terms
Frame Record
A 16-byte structure on the stack containing the caller's frame pointer (x29) and the current function's return address (x30). The frame pointer chains these records together, enabling stack unwinding.
Leaf Function
A function that makes no further function calls. Because it never overwrites the link register (x30), it can skip saving x30 and establishing a new frame record, reducing stack usage.
Link Register (LR / x30)
An ARM64 register that holds the return address after a `bl` (branch with link) instruction. The `ret` instruction jumps to the address stored in LR.
Stack Overflow
A crash condition where a thread exhausts its allocated stack space, typically from unbounded recursion or very deep call chains, causing a memory access violation when the stack pointer moves beyond the guard page.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗