Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Backtracing Stack Frames on the ARM64 Architecture

Tech May 13 2

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:

  1. Retrieve the current frame's base from the FP register.
  2. Read the saved caller's FP from [FP].
  3. Read the current function's return address from [FP + 8] (the saved LR).
  4. 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).
  5. 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

  1. func_b's frame base is 0x7ffffffee0.
  2. Read caller's FP: [0x7ffffffee0] contains 0x7ffffffef0 (func_a's FP).
  3. Read return address: [0x7ffffffee0 + 8] contains 0x55555555c0.
  4. 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

  1. Now use func_a's FP: 0x7ffffffef0.
  2. Read caller's FP: [0x7ffffffef0] contains 0x7fffffff00 (main's FP).
  3. Read return address: [0x7ffffffef0 + 8] contains 0x55555555f0.
  4. 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

  1. Use main's FP: 0x7fffffff00.
  2. Read caller's FP: [0x7fffffff00] may contain 0x0, indicating the start of the chain.
  3. Read return address: [0x7fffffff00 + 8] contains 0x7ffff7e5c110.
  4. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.