Android backtrace探索(一)
字数 1698 2025-08-22 12:22:48

Android Backtrace 技术深入解析

一、Java 层 Backtrace

Java 层的堆栈打印实现非常简单,通过构造异常并打印即可:

Exception e = new Exception();
e.printStackTrace();

这种方法适用于 Java 层的调试和分析,但本文重点将放在 Native 层的 backtrace 技术上。

二、Android Native Backtrace 发展历程

Android 系统在不同版本中使用了不同的 backtrace 库:

  1. libcorkscrew:仅用于 Android 4.1 - 4.4W
  2. libunwind:仅用于 Android 5.0 - 7.1.1
  3. libunwindstack:用于 Android 8.0 及以上版本

这些库主要用于配合系统 debuggerd 进程和调试器的工作。

三、C++ Backtrace 核心机制

1. 核心函数:_Unwind_Backtrace

_Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn, void*);

该函数的工作机制是:

  • 获取每一个堆栈帧
  • 在每个堆栈帧上调用第一个参数指定的回调函数
  • 第二个参数是传递给回调函数的额外数据指针

2. 回调函数原型

typedef _Unwind_Reason_Code (*_Unwind_Trace_Fn)(struct _Unwind_Context* context, void* arg);

其中 context 变量表示进程的上下文,主要包含寄存器信息。

3. _Unwind_Context 结构

struct _Unwind_Context {
    _Unwind_Context_Reg_Val reg[__LIBGCC_DWARF_FRAME_REGISTERS__ + 1];
    void* cfa;
    void* ra;
    void* lsda;
    struct dwarf_eh_bases bases;
    
    // Signal frame context
    #define SIGNAL_FRAME_BIT ((~(_Unwind_Word)0 >> 1) + 1)
    
    // Context which has version/args_size/by_value fields
    #define EXTENDED_CONTEXT_BIT ((~(_Unwind_Word)0 >> 2) + 1)
    
    _Unwind_Word flags;
    _Unwind_Word version;
    _Unwind_Word args_size;
    char by_value[__LIBGCC_DWARF_FRAME_REGISTERS__ + 1];
};

四、手动实现 Stack Trace

1. ARM64 寄存器基础

ARM64 架构有 31 个基础寄存器 X0-X30,以及一些特殊寄存器:

  • X0-X7:前 8 个寄存器可用于存储函数参数
  • SP 寄存器:Stack Pointer,指向栈顶
  • X29 (FP):Frame Pointer,指向栈底
  • X30 (LR):Link Register,存储函数返回地址
  • PC:Program Counter,保存下一条执行指令地址

2. ARM 指令集寄存器

ARM 指令集有 8 个通用寄存器 r0-r7,r8-r14 是分组寄存器:

  • r0-r3:用于存储函数参数
  • r11 (FP):通常作为 Frame Pointer
  • r13 (SP):Stack Pointer
  • r14 (LR):Link Register,函数返回地址
  • r15 (PC):Program Counter

Thumb 模式下,r7 寄存器作为 FP 寄存器。

3. 基于 FP 指针的手动回溯实现

栈回溯只需要 LR 指针和 FP 指针:

  • LR 指针:记录当前函数的返回地址
  • FP 指针:记录当前栈帧地址

通过逐层向上回溯可以实现完整的栈回溯。

示例代码分析

void func2() {
    // dump_backtrace();
}

void func1() {
    func2();
}

extern "C" JNIEXPORT jstring JNICALL 
Java_com_huawei_stacktraceunwnd_MainActivity_stringFromJNI(
    JNIEnv* env, jobject /* this */) {
    func1();
    return env->NewStringUTF("");
}

ARM64 函数入口/出口汇编

函数入口:

SUB SP, SP, #0x50
STP X29, X30, [SP, #0x40]
ADD X29, SP, #0x40

函数出口:

LDP X29, X30, [SP, #0x40]
ADD SP, SP, #0x50 ; 'P'
RET

从汇编可以看出:

  • 函数中获取的 FP 指针对应 SP 指针
  • SP 指针中存储着 FP 和 LR 指针
  • LR 指针是函数返回地址(近似 PC 寄存器值)
  • FP 指针对应 caller 的 SP 指针

手动回溯实现代码

auto lr = (uint64_t)__builtin_return_address(0);
auto fp = (uint64_t)__builtin_frame_address(0);

while ((0 != fp) && (0 != *(unsigned long*)fp) && (fp != *(unsigned long*)fp)) {
    lr = *(uint64_t*)(fp + sizeof(char*));
    Dl_info info;
    if (!dladdr((void*)lr, &info)) {
        break;
    }
    if (!info.dli_fname) {
        break;
    }
    LOG("backtrace pc = %p, module = %s", lr, info.dli_fname);
    fp = *((uint64_t*)fp);
}

4. 手动回溯的限制

手动回溯实现相比完整调用堆栈会少很多信息,主要原因包括:

  1. 无法穿过 JNI 和 OAT

    • 系统库(如 libart.so)不遵循标准规则
    • 在这些库中,X29 寄存器是 SP 寄存器
    • LR 寄存器位置可能在 SP + 0x20 处
    • 需要将获取的 FP 寄存器加上 0x20 才是正常的 FP 寄存器
  2. FP 寄存器优化

    • 使用 -fomit-frame-pointer 编译参数可以优化掉 FP 寄存器
    • 效果是只能获取当前函数信息

编译优化实现

在 CMake 中设置:

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fomit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fomit-frame-pointer")

# 或者针对特定目标
target_compile_options(${target} PUBLIC -fomit-frame-pointer)

五、参考资源

  1. Android NDK getting the backtrace
  2. AOSP backtrace 实现

六、总结

本文详细介绍了 Android 系统中 backtrace 技术的实现原理和方法,包括:

  1. Java 层简单的异常打印堆栈方法
  2. Native 层 backtrace 库的演进历史
  3. C++ backtrace 的核心机制和数据结构
  4. 基于 FP 指针的手动回溯实现及其限制
  5. 编译优化对 backtrace 的影响

理解这些原理对于 Android 逆向分析和安全研究至关重要,特别是在对抗堆栈打印保护机制时。

Android Backtrace 技术深入解析 一、Java 层 Backtrace Java 层的堆栈打印实现非常简单,通过构造异常并打印即可: 这种方法适用于 Java 层的调试和分析,但本文重点将放在 Native 层的 backtrace 技术上。 二、Android Native Backtrace 发展历程 Android 系统在不同版本中使用了不同的 backtrace 库: libcorkscrew :仅用于 Android 4.1 - 4.4W libunwind :仅用于 Android 5.0 - 7.1.1 libunwindstack :用于 Android 8.0 及以上版本 这些库主要用于配合系统 debuggerd 进程和调试器的工作。 三、C++ Backtrace 核心机制 1. 核心函数:_ Unwind_ Backtrace 该函数的工作机制是: 获取每一个堆栈帧 在每个堆栈帧上调用第一个参数指定的回调函数 第二个参数是传递给回调函数的额外数据指针 2. 回调函数原型 其中 context 变量表示进程的上下文,主要包含寄存器信息。 3. _ Unwind_ Context 结构 四、手动实现 Stack Trace 1. ARM64 寄存器基础 ARM64 架构有 31 个基础寄存器 X0-X30,以及一些特殊寄存器: X0-X7 :前 8 个寄存器可用于存储函数参数 SP 寄存器 :Stack Pointer,指向栈顶 X29 (FP) :Frame Pointer,指向栈底 X30 (LR) :Link Register,存储函数返回地址 PC :Program Counter,保存下一条执行指令地址 2. ARM 指令集寄存器 ARM 指令集有 8 个通用寄存器 r0-r7,r8-r14 是分组寄存器: r0-r3 :用于存储函数参数 r11 (FP) :通常作为 Frame Pointer r13 (SP) :Stack Pointer r14 (LR) :Link Register,函数返回地址 r15 (PC) :Program Counter Thumb 模式下,r7 寄存器作为 FP 寄存器。 3. 基于 FP 指针的手动回溯实现 栈回溯只需要 LR 指针和 FP 指针: LR 指针 :记录当前函数的返回地址 FP 指针 :记录当前栈帧地址 通过逐层向上回溯可以实现完整的栈回溯。 示例代码分析 ARM64 函数入口/出口汇编 函数入口: 函数出口: 从汇编可以看出: 函数中获取的 FP 指针对应 SP 指针 SP 指针中存储着 FP 和 LR 指针 LR 指针是函数返回地址(近似 PC 寄存器值) FP 指针对应 caller 的 SP 指针 手动回溯实现代码 4. 手动回溯的限制 手动回溯实现相比完整调用堆栈会少很多信息,主要原因包括: 无法穿过 JNI 和 OAT : 系统库(如 libart.so)不遵循标准规则 在这些库中,X29 寄存器是 SP 寄存器 LR 寄存器位置可能在 SP + 0x20 处 需要将获取的 FP 寄存器加上 0x20 才是正常的 FP 寄存器 FP 寄存器优化 : 使用 -fomit-frame-pointer 编译参数可以优化掉 FP 寄存器 效果是只能获取当前函数信息 编译优化实现 在 CMake 中设置: 五、参考资源 Android NDK getting the backtrace AOSP backtrace 实现 六、总结 本文详细介绍了 Android 系统中 backtrace 技术的实现原理和方法,包括: Java 层简单的异常打印堆栈方法 Native 层 backtrace 库的演进历史 C++ backtrace 的核心机制和数据结构 基于 FP 指针的手动回溯实现及其限制 编译优化对 backtrace 的影响 理解这些原理对于 Android 逆向分析和安全研究至关重要,特别是在对抗堆栈打印保护机制时。