【问题标题】:Android NDK: getting the backtraceAndroid NDK:获取回溯
【发布时间】:2011-12-28 05:58:07
【问题描述】:

我正在开发通过 NDK 与 Android 配合使用的本机应用程序。 发生崩溃时我需要调用backtrace() 函数。问题是NDK 没有<execinfo.h>

还有其他方法可以获取该回溯吗?

【问题讨论】:

  • 您可以尝试对 C 使用 和 _Unwind_Backtrace(),但对我来说它不适用于 C++。

标签: android c++ c android-ndk


【解决方案1】:

Android 没有backtrace(),但unwind.h 可以为您服务。可以通过dladdr() 进行符号化。

下面的代码是我对回溯的简单实现(没有拆解):

#include <iostream>
#include <iomanip>

#include <unwind.h>
#include <dlfcn.h>

namespace {

struct BacktraceState
{
    void** current;
    void** end;
};

static _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* context, void* arg)
{
    BacktraceState* state = static_cast<BacktraceState*>(arg);
    uintptr_t pc = _Unwind_GetIP(context);
    if (pc) {
        if (state->current == state->end) {
            return _URC_END_OF_STACK;
        } else {
            *state->current++ = reinterpret_cast<void*>(pc);
        }
    }
    return _URC_NO_REASON;
}

}

size_t captureBacktrace(void** buffer, size_t max)
{
    BacktraceState state = {buffer, buffer + max};
    _Unwind_Backtrace(unwindCallback, &state);

    return state.current - buffer;
}

void dumpBacktrace(std::ostream& os, void** buffer, size_t count)
{
    for (size_t idx = 0; idx < count; ++idx) {
        const void* addr = buffer[idx];
        const char* symbol = "";

        Dl_info info;
        if (dladdr(addr, &info) && info.dli_sname) {
            symbol = info.dli_sname;
        }

        os << "  #" << std::setw(2) << idx << ": " << addr << "  " << symbol << "\n";
    }
}

它可以用于像 LogCat 一样回溯

#include <sstream>
#include <android/log.h>

void backtraceToLogcat()
{
    const size_t max = 30;
    void* buffer[max];
    std::ostringstream oss;

    dumpBacktrace(oss, buffer, captureBacktrace(buffer, max));

    __android_log_print(ANDROID_LOG_INFO, "app_name", "%s", oss.str().c_str());
}

【讨论】:

  • 但是我可以把这段代码放在哪里来获取实际的堆栈跟踪?如果我在 SIGSEGV 处理程序中执行此操作,我得到的只是处理程序本身,因为堆栈已经展开。
  • 它有效,使用 addr2line -Ciape ./binaryname 然后使用十六进制地址列表。这将显示与这些行匹配的源代码行。
  • 一定要在dumpBacktrace()中把buffer改成addrs,我做的版本被拒绝了,没有按原样编译。
  • @Stranger:在我的任何一个 Android 设备(Android 5.0 和 4.4)上都不能这样工作。我在回溯中得到的只是信号处理程序本身。
  • 这似乎可行。不过,通过 dladdr 的符号化对我不起作用。我需要做些什么特别的事情才能让它工作吗?可能是特定的编译器标志?
【解决方案2】:

这里有一些工作和完整的代码,它通过从 Eugene Shapovalov 的答案开始实现 dump_stack() 并在设备上进行符号查找和 C++ 名称解构。这个解决方案:

  • 适用于 NDK r10e(您不需要完整的 Android AOSP 源代码树)
  • 不需要任何额外的第三方库(没有 libunwind、libbacktrace、corkscrew、CallStack)
  • 不依赖于设备上安装的任何共享库(例如,在 Android 5 中被淘汰的开瓶器)
  • 不会强制您将地址映射到开发机器上的符号;所有符号名称都会在您的代码中显示在 Android 设备上

它使用这些设施,这些设施内置于 NDK:

  • &lt;unwind.h&gt; NDK 工具链/目录中的标头(不是 libunwind)
  • dladdr()
  • __cxxabiv1::__cxa_demangle() 来自 &lt;cxxabi.h&gt;(请参阅下面的 STLport 说明)

到目前为止,我仅使用基于 arm 的 Android 5.1 设备对此进行了测试,并且仅从我的主程序(而不是从信号处理程序)调用它。我使用的是默认的 ndk-build,它为 arm 平台选择 gcc。

如果你能完成这项工作,请发表评论

  • 在其他 Android 操作系统上
  • 来自崩溃时的 SIGSEGV 处理程序(我的目标只是在断言失败时打印堆栈跟踪)
  • 使用 clang 工具集而不是 gcc

请注意,r10e NDK 在 gcc 和 clang 工具集中为许多架构提供 &lt;unwind.h&gt; 代码,因此支持看起来很广泛。

C++ 符号名称解构支持取决于来自 NDK 中包含的 C++ STL 的 __cxxabiv1::__cxa_demangle() 函数。如果您使用 GNU STL 进行 Android 构建(Application.mk 中的APP_STL := gnustl_staticgnustl_shared;请参阅 this page 了解更多信息),这应该可以正常工作。如果您当前根本不使用 STL,只需将 APP_STL := gnustl_staticgnustl_shared 添加到 Application.mk。如果您使用的是 STLport,则必须享受一种特殊的乐趣(更多内容见下文)。

重要提示:要使此代码正常工作,您不得使用-fvisibility=hidden gcc 编译器选项(至少在您的调试版本中)。该选项通常用于在发布版本中隐藏符号以防窥探。

许多人已经注意到 ndk-build 脚本会从您的 NDK .so 中删除符号,同时将其复制到项目的 libs/ 目录中。这是真的(在.so 的两个副本上使用nm 会得到非常不同的结果)但是这个特殊的剥离层惊人地不会阻止下面的代码工作。不知何故,即使在剥离之后仍然有符号(只要你记得不要用-fvisibility=hidden 编译)。他们以nm -D 出现。

关于此主题的其他帖子讨论了其他编译​​器选项,例如 -funwind-tables。我没有发现我需要设置任何这样的选项。默认的 ndk-build 选项有效。

要使用此代码,请将_my_log() 替换为您喜欢的日志记录或字符串函数。

STLport 用户请参阅下面的特别说明。

#include <unwind.h>
#include <dlfcn.h>
#include <cxxabi.h>

struct android_backtrace_state
{
    void **current;
    void **end;
};

_Unwind_Reason_Code android_unwind_callback(struct _Unwind_Context* context, 
                                            void* arg)
{
    android_backtrace_state* state = (android_backtrace_state *)arg;
    uintptr_t pc = _Unwind_GetIP(context);
    if (pc) 
    {
        if (state->current == state->end) 
        {
            return _URC_END_OF_STACK;
        } 
        else 
        {
            *state->current++ = reinterpret_cast<void*>(pc);
        }
    }
    return _URC_NO_REASON;
}

void dump_stack(void)
{
    _my_log("android stack dump");

    const int max = 100;
    void* buffer[max];

    android_backtrace_state state;
    state.current = buffer;
    state.end = buffer + max;

    _Unwind_Backtrace(android_unwind_callback, &state);

    int count = (int)(state.current - buffer);

    for (int idx = 0; idx < count; idx++) 
    {
        const void* addr = buffer[idx];
        const char* symbol = "";

        Dl_info info;
        if (dladdr(addr, &info) && info.dli_sname) 
        {
            symbol = info.dli_sname;
        }
        int status = 0; 
        char *demangled = __cxxabiv1::__cxa_demangle(symbol, 0, 0, &status); 

        _my_log("%03d: 0x%p %s",
                idx,
                addr,
                (NULL != demangled && 0 == status) ?
                demangled : symbol);

        if (NULL != demangled)
            free(demangled);        
    }

    _my_log("android stack dump done");
}

如果您使用的是 STLport STL 而不是 GNU STL,该怎么办?

做你(和我)真糟糕。有两个问题:

  • 第一个问题是STLport 缺少来自&lt;cxxabi.h&gt;__cxxabiv1::__cxa_demangle() 调用。您需要从this repository 下载两个源文件cp-demangle.ccp-demangle.h,并将它们放在源下的demangle/ 子目录中,然后执行此操作而不是#include &lt;cxxabi.h&gt;

    #define IN_LIBGCC2 1 // means we want to define __cxxabiv1::__cxa_demangle
    namespace __cxxabiv1
    {
    extern "C"
    {
    #include "demangle/cp-demangle.c"
    }
    }
    
  • 第二个问题更糟糕。事实证明,NDK 中的&lt;unwind.h&gt; 不是一种,也不是两种,而是三种不同的不兼容类型。您猜对了,STLport 中的&lt;unwind.h&gt;(实际上是在您选择 STLport 时附带的 gabi++ 库中)是不兼容的。 STLport/gabi++ 包含在工具链包含之前(请参阅您的 ndk-build 输出的 -I 选项)这一事实意味着 STLport 正在阻止您使用真正的 &lt;unwind.h&gt;。除了进入并破解已安装的 NDK 中的文件名之外,我找不到任何更好的解决方案:

    • sources/cxx-stl/gabi++/include/unwind.hsources/cxx-stl/gabi++/include/unwind.h.NOT
    • sources/cxx-stl/gabi++/include/unwind-arm.hsources/cxx-stl/gabi++/include/unwind-arm.h.NOT
    • sources/cxx-stl/gabi++/include/unwind-itanium.hsources/cxx-stl/gabi++/include/unwind-itanium.h.NOT

我确信有一些更优雅的解决方案,但是我怀疑切换 -I 编译器选项的顺序可能会产生其他问题,因为 STL 通常希望覆盖工具链包含文件。

享受吧!

【讨论】:

  • 这里是关于 unwind.h 和 STLPort 混乱的问题:code.google.com/p/android/issues/detail?id=68081。谷歌人将其标记为过时,所以我们坚持使用丑陋的解决方案,我猜 =(
  • 链接器抱怨:错误:(52, 28) 错误:冲突声明 'typedef long unsigned int* _Unwind_Ptr' platformVersion = 21 stl = "gnustl_static" toolchain = 'gcc' //也与 clang跨度>
【解决方案3】:

backtrace() 是一个非标准的 Glibc 扩展,即使在 ARM 上也有些不稳定(我认为你需要用 -funwind-tables 构建所有东西,然后再有一个新的 Glibc?)

据我所知,Android使用的Bionic C库中不包含这个函数。

您可以尝试将 Glibc 回溯的源代码拉入您的项目,然后使用展开表重建有趣的东西,但这对我来说听起来很辛苦。

如果您有调试信息,您可以尝试使用附加到您的进程的脚本启动 GDB,并以这种方式打印回溯,但我不知道 GDB 是否适用于 Android(尽管 Android 基本上是 Linux,所以很多很好,安装细节可能有问题?)您可以通过以某种方式转储核心(仿生支持吗?)并在事后分析它来进一步了解。

【讨论】:

  • 谢谢! -funwind-tables 帮助了我。
  • @zxcat:所以你使用了 _Unwind_Backtrace 和一些手动工作或来自 glibc 的代码?
  • 他们使用 -funwind-tables 的事实意味着它只能是 glibc,因为这个参数与仿生无关。只有仿生访问,您需要使用 _Unwind_Backtrace (如果在信号处理程序中,您需要将堆栈指针从 ucontext 对象传递给它)以获得无符号回溯,然后您可以通过 addr2line 运行它以获得符号回来了。 我遇到的问题 是我找不到任何人正确使用 _Unwind_Backtrace 将旧堆栈指针传递给它....如果我传递错误的参数,我会得到垃圾或另一个让我的应用崩溃的信号。
  • @codetaku 他们从来没有说过任何关于-funwind-tables的事情。我这么说,然后只是作为旁白。
【解决方案4】:

这是一种疯狂的单行方法,用于获取包含 C/C++(本机)和 Java 的极其详细的堆栈跟踪:abuse JNI

env->FindClass(NULL);

只要您的应用经过编译调试,或者以其他方式使用 Android 的 CheckJNI,此错误调用将触发 Android 的内置 JNI 检查器,该检查器将在控制台上生成华丽的堆栈跟踪(来自“艺术”日志源)。这个堆栈跟踪是在 Android 的 libart.so 内完成的,使用了所有最新的技术和花里胡哨的东西,这些技术对于像我们这样的低级 NDK 用户来说是不容易获得的。

即使对于未编译调试的应用程序,您也可以启用 CheckJNI。详情请见this google FAQ

我不知道这个技巧是否适用于 SIGSEGV 处理程序(从 SIGSEGV 你可能会得到错误堆栈的堆栈跟踪,或者可能根本不会触发 art),但值得一试。

如果您需要使堆栈跟踪在您的代码中可用的解决方案(例如,以便您可以通过网络发送或记录它),请参阅我在同一个问题中的其他答案。

【讨论】:

    【解决方案5】:

    您可以使用 CallStack:

    #include <utils/CallStack.h>
    
    void log_backtrace()
    {
        CallStack cs;
        cs.update(2);
        cs.dump();
    }
    

    结果需要通过c++filt 或类似的方式进行处理:

    D/CallStack( 2277): #08  0x0x40b09ac8: <_ZN7android15TimedEventQueue11threadEntryEv>+0x0x40b09961
    D/CallStack( 2277): #09  0x0x40b09b0c: <_ZN7android15TimedEventQueue13ThreadWrapperEPv>+0x0x40b09af9
    

    you@work>$ c++filt _ZN7android15TimedEventQueue11threadEntryEv _ZN7android15TimedEventQueue13ThreadWrapperEPv

        android::TimedEventQueue::threadEntry()
        android::TimedEventQueue::ThreadWrapper(void*)
    

    【讨论】:

    • 致命错误:utils/CallStack.h: No such file or directory #include 还有什么需要进入 Android.mk 之类的吗?
    • CallStack.h 的实际位置是 ./frameworks/native/include/utils/CallStack.h 所以它应该类似于 LOCAL_C_INCLUDES:=$(TOP)/frameworks/native/include,但在我的情况下,它在没有这样的规范的情况下工作。我可以在一些高级 Android.mk 中得到这个。
    • 整个NDK文件夹中没有文件CallStack.h
    • @VioletGiraffe,frameworks/native 是 android AOSP 树中的一个文件夹,而不是 NDK,因此如果您将代码构建为 AOSP 树中的 android 模块,那么这个建议很有帮助,但不是如果您尝试针对 NDK 进行构建,这将非常有帮助。但是,可以将相关代码拉出并静态链接到它。
    • 由于某种原因,它不会向 logcat 打印任何内容。即使流到达信号处理程序。
    【解决方案6】:

    以下是使用与现代 Android NDK(例如 NDK r16b)捆绑的 libunwind 在 32 位 ARM 上捕获回溯的方法。


    // Android NDK r16b contains "libunwind.a" for armeabi-v7a ABI.
    // This library is even silently linked in by the ndk-build,
    // so we don't have to add it manually in "Android.mk".
    // We can use this library, but we need matching headers,
    // namely "libunwind.h" and "__libunwind_config.h".
    // For NDK r16b, the headers can be fetched here:
    // https://android.googlesource.com/platform/external/libunwind_llvm/+/ndk-r16/include/
    #include "libunwind.h"
    
    struct BacktraceState {
        const ucontext_t*   signal_ucontext;
        size_t              address_count = 0;
        static const size_t address_count_max = 30;
        uintptr_t           addresses[address_count_max] = {};
    
        BacktraceState(const ucontext_t* ucontext) : signal_ucontext(ucontext) {}
    
        bool AddAddress(uintptr_t ip) {
            // No more space in the storage. Fail.
            if (address_count >= address_count_max)
                return false;
    
            // Add the address to the storage.
            addresses[address_count++] = ip;
            return true;
        }
    };
    
    void CaptureBacktraceUsingLibUnwind(BacktraceState* state) {
        assert(state);
    
        // Initialize unw_context and unw_cursor.
        unw_context_t unw_context = {};
        unw_getcontext(&unw_context);
        unw_cursor_t  unw_cursor = {};
        unw_init_local(&unw_cursor, &unw_context);
    
        // Get more contexts.
        const ucontext_t* signal_ucontext = state->signal_ucontext;
        assert(signal_ucontext);
        const sigcontext* signal_mcontext = &(signal_ucontext->uc_mcontext);
        assert(signal_mcontext);
    
        // Set registers.
        unw_set_reg(&unw_cursor, UNW_ARM_R0,  signal_mcontext->arm_r0);
        unw_set_reg(&unw_cursor, UNW_ARM_R1,  signal_mcontext->arm_r1);
        unw_set_reg(&unw_cursor, UNW_ARM_R2,  signal_mcontext->arm_r2);
        unw_set_reg(&unw_cursor, UNW_ARM_R3,  signal_mcontext->arm_r3);
        unw_set_reg(&unw_cursor, UNW_ARM_R4,  signal_mcontext->arm_r4);
        unw_set_reg(&unw_cursor, UNW_ARM_R5,  signal_mcontext->arm_r5);
        unw_set_reg(&unw_cursor, UNW_ARM_R6,  signal_mcontext->arm_r6);
        unw_set_reg(&unw_cursor, UNW_ARM_R7,  signal_mcontext->arm_r7);
        unw_set_reg(&unw_cursor, UNW_ARM_R8,  signal_mcontext->arm_r8);
        unw_set_reg(&unw_cursor, UNW_ARM_R9,  signal_mcontext->arm_r9);
        unw_set_reg(&unw_cursor, UNW_ARM_R10, signal_mcontext->arm_r10);
        unw_set_reg(&unw_cursor, UNW_ARM_R11, signal_mcontext->arm_fp);
        unw_set_reg(&unw_cursor, UNW_ARM_R12, signal_mcontext->arm_ip);
        unw_set_reg(&unw_cursor, UNW_ARM_R13, signal_mcontext->arm_sp);
        unw_set_reg(&unw_cursor, UNW_ARM_R14, signal_mcontext->arm_lr);
        unw_set_reg(&unw_cursor, UNW_ARM_R15, signal_mcontext->arm_pc);
    
        unw_set_reg(&unw_cursor, UNW_REG_IP, signal_mcontext->arm_pc);
        unw_set_reg(&unw_cursor, UNW_REG_SP, signal_mcontext->arm_sp);
    
        // unw_step() does not return the first IP,
        // the address of the instruction which caused the crash.
        // Thus let's add this address manually.
        state->AddAddress(signal_mcontext->arm_pc);
    
        // Unwind frames one by one, going up the frame stack.
        while (unw_step(&unw_cursor) > 0) {
            unw_word_t ip = 0;
            unw_get_reg(&unw_cursor, UNW_REG_IP, &ip);
    
            bool ok = state->AddAddress(ip);
            if (!ok)
                break;
        }
    }
    
    void SigActionHandler(int sig, siginfo_t* info, void* ucontext) {
        const ucontext_t* signal_ucontext = (const ucontext_t*)ucontext;
        assert(signal_ucontext);
    
        BacktraceState backtrace_state(signal_ucontext);
        CaptureBacktraceUsingLibUnwind(&backtrace_state);
    
        exit(0);
    }
    

    这是一个示例回溯测试应用程序,其中包含 3 种实现的回溯方法,包括上面显示的方法。

    https://github.com/alexeikh/android-ndk-backtrace-test

    【讨论】:

    • 是的,GitHub 上的代码适用于 64 位设备。刚刚在三星 Galaxy S9 上试过。帖子中的上述代码仅适用于 ARM32。
    • 请提供非c-people 的任何用法示例。是否可以将您的实用程序与.apk 集成以查看发送到崩溃日志收集服务(如SentryACRA)的崩溃日志?
    【解决方案7】:

    如果您只需要几个(例如 2 到 5 个)最顶层的调用帧,并且您的 GCC 足够新,您可以考虑使用一些 return address or frame address builtins.

    (但我对 Android 不太了解,所以我可能是错的)

    【讨论】:

    • 谢谢,可惜Android只支持0级,不支持更高。
    • 这可能意味着Android不保留后帧指针,所以你被卡住了。 (或者我猜错了)。
    • 这个运气好吗?我们也在尝试获取原生 c/c++ 回溯
    • @givi:会不会是因为编译默认为-fomit-frame-pointer?也许取消该选项会使其发挥作用。
    猜你喜欢
    • 2020-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-28
    • 2011-09-01
    • 2012-10-24
    相关资源
    最近更新 更多