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 库:
- libcorkscrew:仅用于 Android 4.1 - 4.4W
- libunwind:仅用于 Android 5.0 - 7.1.1
- 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. 手动回溯的限制
手动回溯实现相比完整调用堆栈会少很多信息,主要原因包括:
-
无法穿过 JNI 和 OAT:
- 系统库(如 libart.so)不遵循标准规则
- 在这些库中,X29 寄存器是 SP 寄存器
- LR 寄存器位置可能在 SP + 0x20 处
- 需要将获取的 FP 寄存器加上 0x20 才是正常的 FP 寄存器
-
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)
五、参考资源
六、总结
本文详细介绍了 Android 系统中 backtrace 技术的实现原理和方法,包括:
- Java 层简单的异常打印堆栈方法
- Native 层 backtrace 库的演进历史
- C++ backtrace 的核心机制和数据结构
- 基于 FP 指针的手动回溯实现及其限制
- 编译优化对 backtrace 的影响
理解这些原理对于 Android 逆向分析和安全研究至关重要,特别是在对抗堆栈打印保护机制时。