Backtracing Stack Frames on the ARM64 Architecture
A function's stack frame in ARM64 contains two key saved register values at its base, pointed to by the Frame Pointer (FP or X29) register. These are the saved Frame Pointer from the caller and the saved Link Register (LR or X30), which holds the return address.
The FP register points to the base of the curent stack frame. At that memory address (FP + 0) lies the saved FP of the calling function. At the next adress (FP + 8) lies the saved LR for the current function. This creates a linked list where each FP value points to the previous frame, enabling traversal of the call stack.
To unwind the stack:
- Retrieve the current frame's base from the FP register.
- Read the saved caller's FP from
[FP]. - Read the current function's return address from
[FP + 8](the saved LR). - The return address in LR is the instrucsion after the branch-and-link (BL) that called this function. To find the address of that BL instruction itself, subtract 4 (
LR - 4). - Set the current FP to the saved caller's FP value from step 2 and repeat. The chain ends when the saved FP value is 0.
Practical Unwinding Example
Consider the following call chain: main() -> func_a() -> func_b().
int func_b(int y) {
return y * 2;
}
int func_a(int x) {
int temp = x + 5;
return func_b(temp);
}
int main() {
int val = 10;
return func_a(val);
}
Using a debugger after breaking in func_b:
# Current context in func_b
(gdb) info registers x29 x30
x29 0x7ffffffee0 # FP for func_b's frame
x30 0x55555555c0 # LR: return to address in func_a
Step 1: Unwind from func_b to func_a
- func_b's frame base is
0x7ffffffee0. - Read caller's FP:
[0x7ffffffee0]contains0x7ffffffef0(func_a's FP). - Read return address:
[0x7ffffffee0 + 8]contains0x55555555c0. - Find calling instruction:
0x55555555c0 - 4 = 0x55555555bc.
(gdb) x/i 0x55555555bc
0x55555555bc <func_a+28>: bl 0x5555555590 <func_b>
Step 2: Unwind from func_a to main
- Now use func_a's FP:
0x7ffffffef0. - Read caller's FP:
[0x7ffffffef0]contains0x7fffffff00(main's FP). - Read return address:
[0x7ffffffef0 + 8]contains0x55555555f0. - Find calling instruction:
0x55555555f0 - 4 = 0x55555555ec.
(gdb) x/i 0x55555555ec
0x55555555ec <main+24>: bl 0x55555555a4 <func_a>
Step 3: Unwind from main to its caller
- Use main's FP:
0x7fffffff00. - Read caller's FP:
[0x7fffffff00]may contain0x0, indicating the start of the chain. - Read return address:
[0x7fffffff00 + 8]contains0x7ffff7e5c110. - Find calling instruction:
0x7ffff7e5c110 - 4 = 0x7ffff7e5c10c.
(gdb) x/i 0x7ffff7e5c10c
0x7ffff7e5c10c <__libc_start_main+228>: blr x3
This demonstrates the complete backtrace: __libc_start_main -> main -> func_a -> func_b.