Android NDK: getting the backtrace
Asked Answered
S

8

55

I'm developing the native application that works with Android via the NDK. I need to call the backtrace() function when there is a crash. The problem is that there is no <execinfo.h> for the NDK.

Is there any other way to get that back trace?

Shelton answered 13/11, 2011 at 22:18 Comment(1)
you can try to use <unwind.h> and _Unwind_Backtrace() for C, but it don't work with C++ for me.Footpound
S
22

backtrace() is a non-standard Glibc extension, and even then somewhat shaky on ARM (you need to have built everything with -funwind-tables, I think, and then have a somewhat new Glibc?)

As far as I know, this function is not included in the Bionic C library used by Android.

You could try pulling the source for Glibc backtrace into your project, and then rebuilding the interesting things with the unwind table, but it sounds like hard work to me.

If you have debug info, you could try launching GDB with a script that attaches to your process, and prints a backtrace that way, but I have no idea if GDB works on Android (although Android is basically Linux, so that much id fine, the installation details may be problematic?) You may get further by dumping core somehow (does Bionic support that?) and analysing it after-the-fact.

Snooty answered 28/11, 2011 at 11:36 Comment(4)
@zxcat: So you used the _Unwind_Backtrace and some manual work or the code from glibc?Castile
The fact that they used -funwind-tables means it could only be glibc, because this argument is irrelevant to bionic. With only bionic access, you need to use _Unwind_Backtrace (and if in a signal handler, you need to pass it the stack pointer from the ucontext object) to get a symbol-free backtrace, then you can run that through addr2line in order to get the symbols back. The problem I'm having is that I can't find anyone using _Unwind_Backtrace correctly to pass it the old stack pointer.... If I pass it the wrong arguments, I'll either get garbage or another signal to crash my app.Donalt
@Donalt They never said anything about -funwind-tables. I said that, and then only as an aside.Snooty
-funwind-tables work in musl in my environment.Barrington
F
48

Android have no backtrace(), but unwind.h is here to serve. Symbolization is possible via dladdr().

The following code is my simple implementation of backtrace (with no demangling):

#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";
    }
}

It may be used for backtracing into LogCat like

#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());
}
Fairman answered 4/3, 2015 at 15:56 Comment(13)
But where can I put this code to get the actual stack trace? If I do it in the SIGSEGV handler, all I get is the handler itself because the stack is already unwound.Airship
It works, use addr2line -Ciape ./binaryname and then the list of hex addresses. This will show the source code lines that match those lines.Hoeg
Make sure to change buffer to addrs in dumpBacktrace(), the edition I made was rejected, and it's not compiling as it is.Hoeg
@SantiagoVillafuerte Thank you for pointing to the mistake. I corrected the original answer source.Fairman
Thanks, this code really helps! @VioletGiraffe I put this code simply as signal(SIGSEGV, backtraceToLogcat); and I have full stack trace. But I also had to add exit(EXIT_FAILURE); at the end of backtraceToLogcat(), otherwise app never exists and stack dumping loops forever.Lottery
@Stranger: doesn't work like that on either of my Android devices (Android 5.0 and 4.4). All I get in the backtrace is the signal handler itself.Airship
This almost seems to work. The symbolization via dladdr doesn't work for me though. Is there anything special I need to do to get this working? Specific compiler flags perhaps?Redeemer
on NDK 10e with clang3.6 the output is # <index>: <same-address> _Unwind_Backtrace throughout all of the stacktrace, has anybody encountered this issue? or has any idea how to resolve this?Scumble
btw, to demangle names you should use #include <cxxabi.h> int status = 0; auto demangled = abi::__cxa_demangle(symbol, 0, 0, &status); ... free(demangled);Scumble
As to "The symbolization via dladdr doesn't work for me....Specific compiler flags perhaps?" I found that I needed to remove the -fvisibility=hidden compiler option from debug builds in order to get symbols in the call stack (it was on for release builds in order to hide private symbols). "ant" build also supposedly strips symbols as it copies your .so to the libs/ directory, however this does not seem to prevent this method from giving meaningful symbols.Eclampsia
I took Eugene's answer and added in C++ name demangling plus more info about how to make it work with STLport and compiler options in my answer. https://mcmap.net/q/333162/-android-ndk-getting-the-backtraceEclampsia
When building solution i get the error: conflicting declaration 'typedef long unsigned int* _Unwind_Ptr' platformVersion = 21 stl = "gnustl_static" toolchain = 'gcc'Mckissick
this solution doesn't work if used inside a signal handler for SIGSEGV or similar, because it only shows the stack visible within signal handler and not the stack above itMortmain
E
31

Here is some working and complete code that implements dump_stack() by starting with Eugene Shapovalov's answer and does symbol lookups and C++ name demangling right on the device. This solution:

  • works with the NDK r10e (you don't need the complete Android AOSP source tree)
  • does NOT require any extra third-party libraries (no libunwind, libbacktrace, corkscrew, CallStack)
  • does NOT depend on any shared libraries being installed on the device (e.g. corkscrew, which got axed in Android 5)
  • does NOT force you to map addresses to symbols on your development machine; all symbol names are revealed on the Android device in your code

It uses these facilities, which are built into the NDK:

  • <unwind.h> header that is in the NDK toolchain/ dirs (NOT libunwind)
  • dladdr()
  • __cxxabiv1::__cxa_demangle() from <cxxabi.h> (see STLport note below)

So far, I tested this only with an arm-based Android 5.1 device and I called it only from my main program (not from a signal handler). I was using the default ndk-build which chooses gcc for the arm platform.

Please comment if you are able to make this work

  • on other Android OSes
  • from a SIGSEGV handler on crash (my goal was simply to print a stack trace on assertion failure)
  • using clang toolsets instead of gcc

Note the r10e NDK has <unwind.h> code for many architectures in both gcc and clang toolsets so the support looks broad.

The C++ symbol name demangling support depends on an __cxxabiv1::__cxa_demangle() function that comes from the C++ STL that is included with the NDK. This should work as-is if you are doing your Android build with the GNU STL (APP_STL := gnustl_static or gnustl_shared in Application.mk; see this page for more info). If you are currrently using no STL at all, simply add APP_STL := gnustl_static or gnustl_shared to Application.mk. If you are using STLport, you have to enjoy a special kind of fun (more below).

IMPORTANT: for this code to work, you must not use the -fvisibility=hidden gcc compiler option (at least in your debug builds). That option is commonly used to hide symbols from prying eyes in release builds.

Many people have noted that the ndk-build script strips symbols from your NDK .so whilst copying it to the libs/ directory of your project. That is true (using nm on the two copies of the .so gives very different results) HOWEVER this particular layer of stripping amazingly does not prevent the code below from working. Somehow even after stripping there are still symbols (as long as you remembered not to compile with -fvisibility=hidden). They show up with nm -D.

Other posts on this topic have discussed other compiler options like -funwind-tables. I didn't find that I needed to set any such option. The default ndk-build options worked.

To use this code, replace _my_log() with your favorite logging or string function.

STLport users see special notes below.

#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");
}

What if you are using STLport STL instead of GNU STL?

Sucks to be you (and me). There are two problems:

  • The first problem is that STLport lacks the __cxxabiv1::__cxa_demangle() call from <cxxabi.h>. You will need to download two source files cp-demangle.c and cp-demangle.h from this repository and place them in a demangle/ subdirectory under your source, then do this instead of #include <cxxabi.h>:

    #define IN_LIBGCC2 1 // means we want to define __cxxabiv1::__cxa_demangle
    namespace __cxxabiv1
    {
    extern "C"
    {
    #include "demangle/cp-demangle.c"
    }
    }
    
  • The second problem is more nasty. It turns out there's not one, not two, but THREE different, incompatible types of <unwind.h> in the NDK. And you guessed it, the <unwind.h> in STLport (actually it's in the gabi++ library that comes along for a ride when you choose STLport) is incompatible. The fact that the STLport/gabi++ includes come before the toolchain includes (see your ndk-build output's -I options) means that STLport is preventing you from using the real <unwind.h>. I could not find any better solution than to go in and hack the filenames inside my installed NDK:

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

I'm sure there's some more elegant solution, however I suspect switching the order of the -I compiler options will probably create other problems, since STLs generally want to override toolchain include files.

Enjoy!

Eclampsia answered 23/2, 2016 at 18:50 Comment(2)
Here's issue about this mess with unwind.h and STLPort: code.google.com/p/android/issues/detail?id=68081. Google guys marked it as obsolete, so we stuck with ugly solutions, I guess =(Telium
linker complains : Error:(52, 28) error: conflicting declaration 'typedef long unsigned int* _Unwind_Ptr' platformVersion = 21 stl = "gnustl_static" toolchain = 'gcc' //also with clangMckissick
S
22

backtrace() is a non-standard Glibc extension, and even then somewhat shaky on ARM (you need to have built everything with -funwind-tables, I think, and then have a somewhat new Glibc?)

As far as I know, this function is not included in the Bionic C library used by Android.

You could try pulling the source for Glibc backtrace into your project, and then rebuilding the interesting things with the unwind table, but it sounds like hard work to me.

If you have debug info, you could try launching GDB with a script that attaches to your process, and prints a backtrace that way, but I have no idea if GDB works on Android (although Android is basically Linux, so that much id fine, the installation details may be problematic?) You may get further by dumping core somehow (does Bionic support that?) and analysing it after-the-fact.

Snooty answered 28/11, 2011 at 11:36 Comment(4)
@zxcat: So you used the _Unwind_Backtrace and some manual work or the code from glibc?Castile
The fact that they used -funwind-tables means it could only be glibc, because this argument is irrelevant to bionic. With only bionic access, you need to use _Unwind_Backtrace (and if in a signal handler, you need to pass it the stack pointer from the ucontext object) to get a symbol-free backtrace, then you can run that through addr2line in order to get the symbols back. The problem I'm having is that I can't find anyone using _Unwind_Backtrace correctly to pass it the old stack pointer.... If I pass it the wrong arguments, I'll either get garbage or another signal to crash my app.Donalt
@Donalt They never said anything about -funwind-tables. I said that, and then only as an aside.Snooty
-funwind-tables work in musl in my environment.Barrington
E
11

Here is a crazy one-line method for getting a fantastically detailed stack trace that includes both C/C++ (native) and Java: abuse JNI

env->FindClass(NULL);

As long as your app is compiled debug, or otherwise uses Android's CheckJNI, this erroneous call will trigger Android's built-in JNI checker which will produce a gorgeous stack trace on the console (from the "art" log source). This stack trace is done inside Android's libart.so using all the latest technologies and bells and whistles that are not easily available to lowly NDK users like us.

You can enable CheckJNI even for apps that are not compiled debug. See this google FAQ for details.

I do not know if this trick works from a SIGSEGV handler (from SIGSEGV you might get a stack trace of the wrong stack, or maybe art will not be triggered at all) but it is worth a try.

If you need a solution that makes the stack trace available in your code (e.g. so you can send it over the net or log it), see my other answer in this same question.

Eclampsia answered 23/2, 2016 at 19:12 Comment(0)
P
7

You can use the CallStack:

#include <utils/CallStack.h>

void log_backtrace()
{
    CallStack cs;
    cs.update(2);
    cs.dump();
}

Results will need de-mangling by c++filt or something similar:

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*)
Polity answered 30/9, 2013 at 7:51 Comment(6)
fatal error: utils/CallStack.h: No such file or directory #include <utils/CallStack.h> Is there anything else that needs to go into the Android.mk or something?Obsequent
The actual location of the CallStack.h is ./frameworks/native/include/utils/CallStack.h so it should be something like LOCAL_C_INCLUDES:=$(TOP)/frameworks/native/include, but it is working without such specifications in my case. May I've got this in some upperlevel Android.mk.Polity
There's no file CallStack.h in the whole NDK folder.Airship
@VioletGiraffe, frameworks/native is a folder in the android AOSP tree, not the NDK, so that advice is helpful if you're building your code as an android module in the AOSP tree, but not so helpful if you're trying to build against the NDK. It may be possible to pull the relevant code out and statically link to it however.Dogmatize
For some reason it does not print anything to logcat. Even if flow gets to signal handler.Noted
If you're using the AOSP tree, you also have access to the stack tool. Run stack, type an optional ABI line (e.g. ABI: 'arm64') like you see in native crashes in the log, and then paste your stack lines. It will demangle and addr-to-line the text for you.Roehm
W
6

Here is how you capture backtrace on 32-bit ARM, using libunwind, that is bundled with modern Android NDKs (such as NDK r16b).


// 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);
}

Here is a sample backtrace testing app with 3 implemented backtracing methods, including the method shown above.

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

Whithersoever answered 25/4, 2018 at 17:8 Comment(2)
Yes, the code at GitHub works on 64-bit devices. Just tried on Samsung Galaxy S9. The code above in the post, is ARM32-only.Whithersoever
Please provide any usage example for non c-people. Is it possible to integrate your utility with .apk to view a crash log being sent to crash log collection services like Sentry or ACRA?Reynalda
A
1

If you just want a few (eg 2 - 5) topmost call frames and if your GCC is recent enough, you might consider using some return address or frame address builtins.

(But I don't know much about Android, so I could be wrong)

Apivorous answered 13/11, 2011 at 22:48 Comment(4)
Thanks, but unfortunately Android only supports level 0 and not higher.Shelton
This probably means that Android does not keep back frame pointers, so you are stuck. (Or I am guessing wrong).Apivorous
Any luck with this? We're also trying to get a native c/c++ backtraceHonkytonk
@givi: Would that be because the compilation defaults to -fomit-frame-pointer? Perhaps eliminating that option would make it work.Castile
B
0

The Bionic execinfo.h header has become public since API level 33 (Android 14), allowing you to collect backtraces at runtime as on regular Linux. An example is available on the man page for backtrace: https://man7.org/linux/man-pages/man3/backtrace.3.html

For older versions of Android, you can reuse the code at: https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/execinfo.cpp.

Bufflehead answered 26/5, 2023 at 14:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.