First, a word of warning: This is not portable. Secondly… being able to produce stack traces (outside of the debugger) is something that’s usually reserved for languages like Python or Java… but it’s quite nice to have them in C++. There’s several hurdles to overcome, however.

Acquire Stack

This part is pretty easy, but unless you’re nosy with the header files in /usr/include, it’s not likely to stumble upon this by chance.

#include <execinfo.h>
void print_trace(FILE *out, const char *file, int line)
{
    const size_t max_depth = 100;
    size_t stack_depth;
    void *stack_addrs[max_depth];
    char **stack_strings;

    stack_depth = backtrace(stack_addrs, max_depth);
    stack_strings = backtrace_symbols(stack_addrs, stack_depth);

    fprintf(out, "Call stack from %s:%d:\n", file, line);

    for (size_t i = 1; i < stack_depth; i++) {
        fprintf(out, "    %s\n", stack_strings[i]);
    }
    free(stack_strings); // malloc()ed by backtrace_symbols
    fflush(out);
}

Demangle C++ Names

GCC also provides access to the C++ name (de)mangler. There are some pretty hairy details to learn about memory ownership, and interfacing with the stack trace output requires a bit of string parsing, but it boils down to replacing the above inner loop with this:

#include <cxxabi.h>
...
for (size_t i = 1; i < stack.depth; i++) {
    size_t sz = 200; // just a guess, template names will go much wider
    char *function = static_cast(malloc(sz));
    char *begin = 0, *end = 0;
    // find the parentheses and address offset surrounding the mangled name
    for (char *j = stack.strings[i]; *j; ++j) {
        if (*j == '(') {
            begin = j;
        }
        else if (*j == '+') {
            end = j;
        }
    }
    if (begin && end) {
        *begin++ = '';
        *end = '';
        // found our mangled name, now in [begin, end)

        int status;
        char *ret = abi::__cxa_demangle(begin, function, &sz, &status);
        if (ret) {
            // return value may be a realloc() of the input
            function = ret;
        }
        else {
            // demangling failed, just pretend it's a C function with no args
            std::strncpy(function, begin, sz);
            std::strncat(function, "()", sz);
            function[sz-1] = '';
        }
        fprintf(out, "    %s:%s\n", stack.strings[i], function);
    }
    else
    {
        // didn't find the mangled name, just print the whole line
        fprintf(out, "    %s\n", stack.strings[i]);
    }
    free(function);
}

There. You could do a bit more optimization, but I’ll leave that as an exercise to the reader. The important thing is to obey exactly what the ABI requires regarding dynamic memory:

  • you pass me a buffer created by malloc, along with the current size of the buffer.
  • i might realloc your buffer to make space for the whole name, and I’ll return the result, which may be different. Or I’ll fail and return NULL, because you didn’t pass in a mangled name I could understand.
  • you free my return value when you’re done (unless I returned NULL).

Otherwise, you’ll get a segmentation fault somewhere along the line, and a stack trace that blows up the program isn’t useful except for post-mortem analysis in gdb.

Export Symbols

Even if you go through all of the other steps, if you don’t account for this in your build phase, you will get a pretty useless stack trace. Here’s what I got from my experiment initially:

Call stack from backtrace.cxx:105:
    debug/backtrace:__gxx_personality_v0()
    debug/backtrace:__gxx_personality_v0()
    debug/backtrace:__gxx_personality_v0()
    debug/backtrace:__gxx_personality_v0()
    /lib/tls/i686/cmov/libc.so.6:__libc_start_main()
    debug/backtrace:__gxx_personality_v0()

After poking around, I realized I had to add -rdynamic to my linker flags so that all symbols would be exported into the executable. I haven’t experimented, but I would guess this applies to shared-object building as well.

With -rdynamic, my stack trace looks a lot nicer:

Call stack from backtrace.cxx:105:
    debug/backtrace:hot_potato::pass(double, double)
    debug/backtrace:hot_potato::pass(int)
    debug/backtrace:hot_potato::pass()
    debug/backtrace:main()
    /lib/tls/i686/cmov/libc.so.6:__libc_start_main()
    debug/backtrace:__gxx_personality_v0()

Bingo! I only get a source file and line number from the call site, but it’s not too difficult to trace back through callers from this point. In this case, my executable is named debug/backtrace and hot_potato is just an example class that calls its own functions to give me a pretty chain to look at.

Usage

Being able to create a stack trace is only half of it. Now you actually have to use it to get any value out of it. Here’s the rub: This is most useful when an exception gets caught, but the data is already gone by that time, because the exception’s been thrown. Logically then, it seems to make sense to encapsulate this functionality into an exception class (e.g. class app_exception : public std::exception) that gets thrown. Simply replace all of the fprintf calls with something that generates a string. execute it in the exception class’s constructor, and print it out in the catch block.

Another option would be to allocate thread-local storage for stack traces (similar to the current thread-local errno), and then have all of your important functions call set_thread_stacktrace, which populates that thread-local storage. Then the exception handlers can just pull that data regardless of the type of exception thrown. I think this is better from the flexibility aspect, but I haven’t actually investigated the feasability of it nor the performance impact of recalculating this all the time.

About these ads