Android Software Reverse Engineering & Decompilation

Troubleshooting Android Native Crashes: Disassembling ARM64 Core Dumps for Root Cause

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction

Android native crashes, often manifesting as Segmentation Faults (SIGSEGV) or Aborts (SIGABRT), can be a developer’s nightmare. Unlike Java crashes which provide relatively clear stack traces, native crashes offer cryptic addresses and register dumps, especially when symbols are stripped. For ARM64 architectures, the challenge is compounded by the intricacies of its assembly language. This expert-level guide delves into the powerful technique of disassembling ARM64 core dumps to precisely pinpoint the root cause of these elusive crashes, moving beyond mere stack traces to a deeper understanding of the faulting instruction and memory state.

Understanding Android Native Crashes

Native crashes occur when C/C++ code within an Android application attempts an illegal operation, such as accessing invalid memory, dereferencing a null pointer, or executing privileged instructions. The Android runtime’s debuggerd daemon intercepts these signals (e.g., SIGSEGV, SIGBUS, SIGILL) and generates a tombstone file. While invaluable, tombstone files often lack the complete memory state needed for complex issues, particularly when symbols are stripped or optimizations aggressively rearrange code. This is where core dumps shine, providing a complete snapshot of the process’s memory at the time of the crash.

Why Core Dumps are Essential

  • Complete Memory Snapshot: Core dumps capture the entire process memory, including heap, stack, and register values, offering a much richer context than a tombstone.
  • Post-mortem Debugging: They allow you to virtually “rewind” to the crash state and inspect variables, memory regions, and execution flow as if the program were still running in a debugger.
  • Symbol-Stripped Binaries: Even with stripped binaries, core dumps combined with unstripped versions (or symbol files) enable detailed analysis through address mapping.

Setting Up Your Disassembly Environment

Before diving into a core dump, you need the right tools and artifacts:

  1. Core Dump File: Obtain this from a crash reporting tool that supports core dump generation or manually using `gdbserver` and `gdb` to attach to a process and issue a `generate-core-file` command.
  2. Crashing Binary: The unstripped native library or executable that crashed. This is crucial for GDB to correctly map addresses to functions and lines of code. If you only have the stripped version, ensure you have the corresponding symbol file (`.sym` or unstripped `.so`).
  3. Android NDK Toolchain: Specifically, `aarch64-linux-android-gdb` (or `gdb-multiarch`) and `aarch64-linux-android-objdump` are vital. These are usually found within your Android NDK installation under `toolchains/llvm/prebuilt/linux-x86_64/bin`.

Let’s assume your core dump is named `core.12345` and the crashing library is `libmylibrary.so`. You’d typically pull these from your device or build output:

adb pull /data/vendor/bugreports/core.12345 .adb pull /data/app/com.example.myapp-XYZ/lib/arm64/libmylibrary.so .

Disassembling the Core Dump with GDB

The primary tool for core dump analysis is GDB. We’ll load the core dump and the crashing binary to reconstruct the execution state.

Loading the Core Dump

aarch64-linux-android-gdb -c core.12345 libmylibrary.so

GDB will load, indicating the program state at the crash. You’ll likely see a message like `Program terminated with signal SIGSEGV, Segmentation fault.`

Initial Investigation: Backtrace and Registers

First, get a backtrace to see the call stack:

(gdb) bt full

This shows the functions leading up to the crash. The `full` option also displays local variables. Next, inspect the CPU registers, especially the Program Counter (PC) and Link Register (LR):

(gdb) info registers

Pay close attention to `pc` (the instruction pointer at the crash) and `lr` (return address). The `sp` (stack pointer) is also critical for understanding stack frames.

Disassembling the Crash Site

Now, let’s look at the actual ARM64 instructions where the crash occurred. We use the `disassemble` command targeting the program counter (`$pc`):

(gdb) disassemble $pc

This will show a few instructions around the crash point. For more context, you can disassemble a wider range, for example, 20 instructions before and after:

(gdb) disassemble $pc-0x50, $pc+0x50

You can also examine the instruction directly at `$pc`:

(gdb) x/i $pc

Interpreting ARM64 Assembly

ARM64 instructions are 32-bit fixed length. Key concepts for crash analysis include:

  • Registers (x0-x30): General-purpose registers. `x0-x7` are often used for function arguments and return values. `x29` is the Frame Pointer (FP), `x30` is the Link Register (LR).
  • Stack Pointer (SP): Points to the top of the current stack frame.
  • Program Counter (PC): Points to the next instruction to be executed.
  • Load/Store Instructions: `ldr` (load register), `str` (store register). These are frequently involved in memory access violations.
  • Branch Instructions: `b` (unconditional branch), `bl` (branch with link – calls a function).

Example Scenario: Null Pointer Dereference

Imagine `disassemble $pc` reveals something like:

0x76xxxxxx <my_function+88>: ldr x0, [x19]

This instruction attempts to load a value into register `x0` from the memory address pointed to by `x19`. If this is a `SIGSEGV` and `info registers` shows `x19 = 0x0`, you’ve found a null pointer dereference. The `ldr` instruction tried to read from address `0x0`, which is typically disallowed.

Tracing Memory Access

If the crash involves a memory address (e.g., `ldr` or `str`), you can inspect the values in the registers involved and then `x` (examine memory) at those addresses.

For instance, if `ldr x0, [x19]` crashed and `x19` was `0x0`, you know the problem. But what if `x19` held a seemingly valid but out-of-bounds address, like `0x10000000`? You could examine it:

(gdb) x/10gx $x19

This command examines 10 8-byte (giant word) hexadecimal values starting from the address in `x19`. This can reveal if the pointer was corrupted or pointing to unmapped memory.

Advanced Analysis Techniques

Understanding Function Prologues and Epilogues

When a function is called, a prologue saves the caller’s frame pointer and return address, and allocates space on the stack. An epilogue restores these values and deallocates stack space. Observing these patterns helps identify function boundaries and local variable storage.

Typical ARM64 prologue:

stp x29, x30, [sp, #-16]! ; Save FP and LR, decrement SPmov x29, sp           ; Set new FP to current SP

Typical ARM64 epilogue:

ldp x29, x30, [sp], #16 ; Restore FP and LR, increment SPret                 ; Return to caller

Correlating Assembly to Source (with symbols)

If you have an unstripped binary, GDB can often show source code alongside assembly:

(gdb) list *(my_function+88)

This greatly aids in mapping the problematic assembly instruction back to your C/C++ code, allowing you to identify the line number causing the crash.

Practical Example: Out-of-Bounds Write

Let’s consider a hypothetical crash where `libmylibrary.so` attempts to write past the end of an allocated buffer.

(gdb) aarch64-linux-android-gdb -c core.12345 libmylibrary.so(gdb) bt full#0  0x00000076xxxxxx in my_write_function (buffer=0x70000000, size=10, index=12) at my_source.cpp:55#1  0x00000076yyyyyy in caller_function () at another_source.cpp:120...

From `bt full`, we see `my_write_function` crashed with `index=12` while `size=10`. This immediately suggests an out-of-bounds issue. Let’s inspect the exact crash point.

(gdb) frame 0(gdb) disassemble $pcDump of assembler code for function my_write_function:   ...   0x76xxxxxx <my_write_function+80>: add x8, x0, x2, lsl #2   ; Calculate address: buffer_base + index * 4   0x76xxxxxx <my_write_function+84>: str w1, [x8]         ; Store w1 (data) at calculated address   => 0x76xxxxxx <my_write_function+88>: nop                ; (crash occurred here, after the store)   ...

The crash happened *after* the `str w1, [x8]` instruction. This means the `str` itself likely caused the `SIGSEGV` by writing to an invalid memory region. Let’s check the registers before the crash, especially `x0` (buffer), `x2` (index), and the calculated address in `x8`.

(gdb) info registersx0  0x70000000x1  0xdeadbeef ; value being writtenx2  0xc        ; decimal 12 (index)x8  0x70000030 ; x0 + x2*4 = 0x70000000 + 12*4 = 0x70000000 + 0x30

The `str w1, [x8]` instruction tried to write `0xdeadbeef` to address `0x70000030`. If `buffer` at `0x70000000` was allocated for only 10 4-byte integers (size=10), then its valid range is `0x70000000` to `0x70000000 + 10*4 – 1 = 0x70000027`. Writing to `0x70000030` is clearly out-of-bounds. This confirms an array index out of bounds write, leading to the crash. The `SIGSEGV` often occurs when the memory page immediately *after* the allocated buffer is unmapped or protected.

Conclusion

Disassembling ARM64 core dumps is an indispensable skill for expert Android developers tackling native crashes. While challenging, this methodical approach allows you to reconstruct the exact state of your application at the point of failure, identify the faulting instruction, and understand the corrupted memory or register values. By combining the power of GDB with a solid understanding of ARM64 assembly, you can pinpoint root causes that simpler debugging methods might miss, ultimately leading to more robust and stable Android applications.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner