Android 获取 Native 函数调用堆栈

949 阅读2分钟

在 Java 层中,可以通过 Thread.currentThread().getStackTrace() 方法获取当前线程的调用堆栈。这个方法会返回一个 StackTraceElement 数组,其中每一个元素都包含了调用堆栈中的一个帧的信息,如类名、方法名、文件名和行号等。很多开源框架都是类似做法来获取Java堆栈 , 使用如下:

StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stackTraceElements) {
    Log.d("TAG", element.toString());
}

另外在Native层,我们还可以通过 JNI 接口和 JNIEnv 指针在 Native 层获取当前线程的 Java 虚拟机堆栈信息。需要调用 env->GetJavaVM() 方法获取Java虚拟机实例,并使用 env->GetStackTrace() 方法获取当前线程的 Java 虚拟机堆栈信息。

但是获取Native的堆栈就比较麻烦了.

运行demo , 可以看到logcat打印如下:

检测正确性

通过NDK给我们提供的addr2line工具来验证 , 执行addr2line命令

aarch64-linux-android-addr2line -e libmyapplication.so 00023774 00023bfa 00023d18 00023d28 00023d38

可以看到当前函数调用的堆栈确实被还原出来了, 并且和我们实际代码行数确实是一致的.

关键代码如下

static _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) {
    std::vector<_Unwind_Word> &stack = *(std::vector<_Unwind_Word> *) arg;
    stack.push_back(_Unwind_GetIP(context));
    return _URC_NO_REASON;
}

void call_stack_dump(std::string &dump) {
    std::vector<_Unwind_Word> stack;
    _Unwind_Backtrace(unwind_callback, (void *) &stack);
    dump.append("backtrace:\n");
    char buff[256];
    for (size_t i = 0; i < stack.size(); i++) {
        Dl_info info;
        uintptr_t pc = stack[i];
        if (dladdr((void *) pc, &info) > 0) {
            int addr = (char *) pc - (char *) info.dli_fbase - 1;
            if (info.dli_sname == NULL || strlen(info.dli_sname) == 0) {
                sprintf(buff, "#%02x pc %08x  %s\n", i, addr, info.dli_fname);
            } else {
                char *demangled = abi::__cxa_demangle(info.dli_sname, nullptr, 0, nullptr);
                sprintf(buff, "#%02x pc %08x  %s  (%s+%" PRIuPTR")\n", i, addr, info.dli_fname,
                        demangled, pc - (uintptr_t) info.dli_saddr);
            }
            dump.append(buff);
        }
    }
}

_Unwind_Backtrace 是一个名为「回溯接口」(Unwinding API)的函数中的一部分,用于在 C/C++ 代码中获取函数调用堆栈。其实现方式是利用 GCC 的内部 ABI(Application Binary Interface)规范,在编译时向目标文件中插入相关的元数据,再在运行时解析这些元数据。因此,_Unwind_Backtrace 只能用于支持 GCC 内部 ABI 的编译器,如 GCC、Clang、ICC 等。

Android NDK 默认使用Clang 编译器作为 C/C++ 编译器 , 经测试android 5.0 和 13.0版本 , 使用_Unwind_Backtrace堆栈都能还原出来.

_Unwind_GetIP 函数用于获取当前堆栈帧程序计数器(Program Counter)的方法 , 程序计数器pc是一种处理器内部特殊寄存器。在CPU执行程序时,程序计数器指向下一条要执行的指令的地址,因此可以理解为程序计数器代表了CPU正在执行SO的地址 , 地址用16进制表示。