How can I print stack trace for caught exceptions in C++ & code injection in C++
Asked Answered
F

5

29

I want to have stack trace not for my exceptions only but also for any descendants of std::exception

As I understand, stack trace is completely lost when exception is caught because of stack unwinding (unrolling).

So the only way I see to grab it is injection of code saving context info (stack trace) at the place of std::exception constructor call. Am I right?

If it is the case, please tell me how code injection can be done (if it can) in C++. Your method may be not completely safe because I need it for Debug version of my app only. May be I need to use assembler?

I'm interested only in solution for GCC. It can use c++0x features

Fillin answered 26/7, 2012 at 9:1 Comment(7)
This answer might help.Murillo
@user315052 That answer is for uncaught exceptions and doesn't work for caught.Fillin
True, but you can stuff the array of C strings into a std::string, and pass that into the constructor of your exception as the what (or a big part of it, anyway).Murillo
I tried to write a macro that would attach a backtrace to a caught exception via throw_with_nested, but alas, C++11 support in my compiler is lacking.Murillo
@user315052 After exception is caught stack state is already lost and you can do nothingFillin
Yes, but you can record a stack trace close to where the exception was thrown. You can get as close as you want, up to the actual call into the STL library code. This is as good as it gets with exceptions thrown by other code short of modifying library code.Murillo
There is an example here: gcc.gnu.org/bugzilla/show_bug.cgi?id=33903Trace
N
42

Since you mentioned that you're happy with something that is GCC specific I've put together an example of a way you might do this. It's pure evil though, interposing on internals of the C++ support library. I'm not sure I'd want to use this in production code. Anyway:

#include <iostream>
#include <dlfcn.h>
#include <execinfo.h>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>
#include <cstdlib>

namespace {
  void * last_frames[20];
  size_t last_size;
  std::string exception_name;

  std::string demangle(const char *name) {
    int status;
    std::unique_ptr<char,void(*)(void*)> realname(abi::__cxa_demangle(name, 0, 0, &status), &std::free);
    return status ? "failed" : &*realname;
  }
}

extern "C" {
  void __cxa_throw(void *ex, void *info, void (*dest)(void *)) {
    exception_name = demangle(reinterpret_cast<const std::type_info*>(info)->name());
    last_size = backtrace(last_frames, sizeof last_frames/sizeof(void*));

    static void (*const rethrow)(void*,void*,void(*)(void*)) __attribute__ ((noreturn)) = (void (*)(void*,void*,void(*)(void*)))dlsym(RTLD_NEXT, "__cxa_throw");
    rethrow(ex,info,dest);
  }
}

void foo() {
  throw 0;
}

int main() {
  try {
    foo();
  }
  catch (...) {
    std::cerr << "Caught a: " << exception_name << std::endl;
    // print to stderr
    backtrace_symbols_fd(last_frames, last_size, 2);
  }
}

We basically steal calls to the internal implementation function that GCC uses for dispatching thrown exceptions. At that point we take a stack trace and save it in a global variable. Then when we come across that exception later on in our try/catch we can work with the stacktrace to print/save or whatever it is you want to do. We use dlsym() to find the real version of __cxa_throw.

My example throws an int to prove that you can do this with literally any type, not just your own user defined exceptions.

It uses the type_info to get the name of the type that was thrown and then demangles it.

You could encapsulate the global variables that store the stacktrace a bit better if you wanted to.

I compiled and tested this with:

g++ -Wall -Wextra test.cc -g -O0 -rdynamic -ldl

Which gave the following when run:

./a.out
Caught a: int
./a.out(__cxa_throw+0x74)[0x80499be]
./a.out(main+0x0)[0x8049a61]
./a.out(main+0x10)[0x8049a71]
/lib/i686/cmov/libc.so.6(__libc_start_main+0xe6)[0xb75c2ca6]
./a.out[0x80497e1]

Please don't take this as an example of good advice though - it's an example of what you can do with a little bit of trickery and poking around at the internals!

Nitty answered 26/7, 2012 at 17:33 Comment(10)
Thank you very much! It's not a problem that it's unsafe because I need it just for faster development - seeing immediately where an error occurred when I'm testing and debugging, like in modern languages.Fillin
@user484936 The big risk here is that you don't notice when there's an ABI change and end up in a world of undefined behaviour pain. If you're interested though I can expand it to print the type of the exception, even within a catch(...) block.Nitty
Yes, I am interested, it will be greatFillin
@user484936 - done. It uses C++11 for unique_ptr now. I haven't quite figured how to get [[noreturn]] into the function pointer yet so compiling with C++11 adds a (spurious) warning.Nitty
Thanks. For those who are interested: I found more detailed code working on the same principleFillin
This hack is now a part of my library: code.google.com/p/libglim/source/browse/trunk/exception.hppMemoried
@user484936 the page you mentioned is gone but can be found at web.archive.org/web/20150706050621/http://blog.sjinks.pro/c-cpp/…. Unfortunately it looks much more involved and in Russian!Zachariah
I realized that this hack is used in our code and I want to know, how to disable it temporarily or for some exceptions? I guess I will solve it, but advice would be faster.Nicolis
I'd probably add an if into your __cxa_throw that checks a global or thread local flag.Nitty
You can make the printing of the stacktrace more portable (and more detailed) by using boost.stacktrace?Antonioantonius
M
5

On Linux this can be implemented by adding a call to backtrace() in the exception constructor to capture the stack trace into an exception's member variable. Unfortunately, it won't work for standard exceptions, only for the ones you define.

Metchnikoff answered 26/7, 2012 at 9:5 Comment(0)
W
3

Some years ago I wrote this: Unchaining chained exceptions in C++

Basically some macros log the place where the stack unwind happens when an exception is thrown.

An updated version of the framework can be found in the library Imebra (http://imebra.com).

I would reimplement some parts of it (like storing the stack trace on a thread local storage).

Wurst answered 26/7, 2012 at 9:19 Comment(0)
P
2

The solution from Flexo is very nice and works well. It also has the benefit that translation from backtrace addresses to procedure names is only performed in the catch part, so its up to the receiver of an exception if they care about the backtrace or not.

However there are also cases where a solution based on libunwind can be prefered, i.e. because libunwind can in some scenarios gather procedure names where the backtrace functions fail to do so.

Here I present an idea based on Flexo's answer, but with several extensions. It uses libunwind to generate the backtrace at the time of the throw, and directly prints to stderr. It uses libDL to identify the shared object file name. It uses DWARF debugging information from elfutils to gather the source code file name and line number. It uses the C++ API to demangle C++ exceptions. Users can set the mExceptionStackTrace variable to temporarily enable/disable the stack traces.

An important point about all solutions that intercept __cxa_throw is that they add potentially an overhead for walking the stack. This is especially true for my solution that adds significant overhead for accessing the debugger symbols to gather the source file name. This may be acceptable in i.e. automatic testing where you expect your code not to throw, and you want to have a powerful stack trace for (failed) tests that do throw.

// Our stack unwinding is a GNU C extension:
#if defined(__GNUC__)
// include elfutils to parse debugger information:
#include <elfutils/libdwfl.h>

// include libunwind to gather the stack trace:
#define UNW_LOCAL_ONLY
#include <libunwind.h>

#include <dlfcn.h>
#include <cxxabi.h>
#include <typeinfo>
#include <stdio.h>
#include <stdlib.h>

#define LIBUNWIND_MAX_PROCNAME_LENGTH 4096

static bool mExceptionStackTrace = false;


// We would like to print a stacktrace for every throw (even in
// sub-libraries and independent of the object thrown). This works
// only for gcc and only with a bit of trickery
extern "C" {
    void print_exception_info(const std::type_info* aExceptionInfo) {
        int vDemangleStatus;
        char* vDemangledExceptionName;

        if (aExceptionInfo != NULL) {
            // Demangle the name of the exception using the GNU C++ ABI:
            vDemangledExceptionName = abi::__cxa_demangle(aExceptionInfo->name(), NULL, NULL, &vDemangleStatus);
            if (vDemangledExceptionName != NULL) {
                fprintf(stderr, "\n");
                fprintf(stderr, "Caught exception %s:\n", vDemangledExceptionName);

                // Free the memory from __cxa_demangle():
                free(vDemangledExceptionName);
            } else {
                // NOTE: if the demangle fails, we do nothing, so the
                // non-demangled name will be printed. Thats ok.
                fprintf(stderr, "\n");
                fprintf(stderr, "Caught exception %s:\n", aExceptionInfo->name());
            }
        } else {
            fprintf(stderr, "\n");
            fprintf(stderr, "Caught exception:\n");
        }
    }

    void libunwind_print_backtrace(const int aFramesToIgnore) {
        unw_cursor_t vUnwindCursor;
        unw_context_t vUnwindContext;
        unw_word_t ip, sp, off;
        unw_proc_info_t pip;
        int vUnwindStatus, vDemangleStatus, i, n = 0;
        char vProcedureName[LIBUNWIND_MAX_PROCNAME_LENGTH];
        char* vDemangledProcedureName;
        const char* vDynObjectFileName;
        const char* vSourceFileName;
        int vSourceFileLineNumber;

        // This is from libDL used for identification of the object file names:
        Dl_info dlinfo;

        // This is from DWARF for accessing the debugger information:
        Dwarf_Addr addr;
        char* debuginfo_path = NULL;
        Dwfl_Callbacks callbacks = {};
        Dwfl_Line* vDWARFObjLine;


        // initialize the DWARF handling:
        callbacks.find_elf = dwfl_linux_proc_find_elf;
        callbacks.find_debuginfo = dwfl_standard_find_debuginfo;
        callbacks.debuginfo_path = &debuginfo_path;
        Dwfl* dwfl = dwfl_begin(&callbacks);
        if (dwfl == NULL) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
        }
        if ((dwfl != NULL) && (dwfl_linux_proc_report(dwfl, getpid()) != 0)) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
            dwfl = NULL;
        }
        if ((dwfl != NULL) && (dwfl_report_end(dwfl, NULL, NULL) != 0)) {
            fprintf(stderr, "libunwind_print_backtrace(): Error initializing DWARF.\n");
            dwfl = NULL;
        }


        // Begin stack unwinding with libunwnd:
        vUnwindStatus = unw_getcontext(&vUnwindContext);
        if (vUnwindStatus) {
            fprintf(stderr, "libunwind_print_backtrace(): Error in unw_getcontext: %d\n", vUnwindStatus);
            return;
        }

        vUnwindStatus = unw_init_local(&vUnwindCursor, &vUnwindContext);
        if (vUnwindStatus) {
            fprintf(stderr, "libunwind_print_backtrace(): Error in unw_init_local: %d\n", vUnwindStatus);
            return;
        }

        vUnwindStatus = unw_step(&vUnwindCursor);
        for (i = 0; ((i < aFramesToIgnore) && (vUnwindStatus > 0)); ++i) {
            // We ignore the first aFramesToIgnore stack frames:
            vUnwindStatus = unw_step(&vUnwindCursor);
        }


        while (vUnwindStatus > 0) {
            pip.unwind_info = NULL;
            vUnwindStatus = unw_get_proc_info(&vUnwindCursor, &pip);
            if (vUnwindStatus) {
                fprintf(stderr, "libunwind_print_backtrace(): Error in unw_get_proc_info: %d\n", vUnwindStatus);
                break;
            }

            // Resolve the address of the stack frame using libunwind:
            unw_get_reg(&vUnwindCursor, UNW_REG_IP, &ip);
            unw_get_reg(&vUnwindCursor, UNW_REG_SP, &sp);

            // Resolve the name of the procedure using libunwind:
            // unw_get_proc_name() returns 0 on success, and returns UNW_ENOMEM
            // if the procedure name is too long to fit in the buffer provided and
            // a truncated version of the name has been returned:
            vUnwindStatus = unw_get_proc_name(&vUnwindCursor, vProcedureName, LIBUNWIND_MAX_PROCNAME_LENGTH, &off);
            if (vUnwindStatus == 0) {
                // Demangle the name of the procedure using the GNU C++ ABI:
                vDemangledProcedureName = abi::__cxa_demangle(vProcedureName, NULL, NULL, &vDemangleStatus);
                if (vDemangledProcedureName != NULL) {
                    strncpy(vProcedureName, vDemangledProcedureName, LIBUNWIND_MAX_PROCNAME_LENGTH);
                    // Free the memory from __cxa_demangle():
                    free(vDemangledProcedureName);
                } else {
                    // NOTE: if the demangle fails, we do nothing, so the
                    // non-demangled name will be printed. Thats ok.
                }
            } else if (vUnwindStatus == UNW_ENOMEM) {
                // NOTE: libunwind could resolve the name, but could not store
                // it in a buffer of only LIBUNWIND_MAX_PROCNAME_LENGTH characters.
                // So we have a truncated procedure name that can not be demangled.
                // We ignore the problem and the truncated non-demangled name will
                // be printed.
            } else {
                vProcedureName[0] = '?';
                vProcedureName[1] = '?';
                vProcedureName[2] = '?';
                vProcedureName[3] = 0;
            }


            // Resolve the object file name using dladdr:
            if (dladdr((void *)(pip.start_ip + off), &dlinfo) && dlinfo.dli_fname && *dlinfo.dli_fname) {
                vDynObjectFileName = dlinfo.dli_fname;
            } else {
                vDynObjectFileName = "???";
            }


            // Resolve the source file name using DWARF:
            if (dwfl != NULL) {
                addr = (uintptr_t)(ip - 4);
                Dwfl_Module* module = dwfl_addrmodule(dwfl, addr);
                // Here we could also ask for the procedure name:
                //const char* vProcedureName = dwfl_module_addrname(module, addr);
                // Here we could also ask for the object file name:
                //vDynObjectFileName = dwfl_module_info(module, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
                vDWARFObjLine = dwfl_getsrc(dwfl, addr);
                if (vDWARFObjLine != NULL) {
                    vSourceFileName = dwfl_lineinfo(vDWARFObjLine, &addr, &vSourceFileLineNumber, NULL, NULL, NULL);
                    //fprintf(stderr, " %s:%d", strrchr(vSourceFileName, '/')+1, vSourceFileLineNumber);
                }
            }
            if (dwfl == NULL || vDWARFObjLine == NULL || vSourceFileName == NULL) {
                vSourceFileName = "???";
                vSourceFileLineNumber = 0;
            }


            // Print the stack frame number:
            fprintf(stderr, "#%2d:", ++n);

            // Print the stack addresses:
            fprintf(stderr, " 0x%016" PRIxPTR " sp=0x%016" PRIxPTR, static_cast<uintptr_t>(ip), static_cast<uintptr_t>(sp));

            // Print the source file name:
            fprintf(stderr, " %s:%d", vSourceFileName, vSourceFileLineNumber);

            // Print the dynamic object file name (that is the library name).
            // This is typically not interesting if we have the source file name.
            //fprintf(stderr, " %s", vDynObjectFileName);

            // Print the procedure name:
            fprintf(stderr, " %s", vProcedureName);

            // Print the procedure offset:
            //fprintf(stderr, " + 0x%" PRIxPTR, static_cast<uintptr_t>(off));

            // Print a newline to terminate the output:
            fprintf(stderr, "\n");


            // Stop the stack trace at the main method (there are some
            // uninteresting higher level functions on the stack):
            if (strcmp(vProcedureName, "main") == 0) {
                break;
            }

            vUnwindStatus = unw_step(&vUnwindCursor);
            if (vUnwindStatus < 0) {
                fprintf(stderr, "libunwind_print_backtrace(): Error in unw_step: %d\n", vUnwindStatus);
            }
        }
    }

    void __cxa_throw(void *thrown_exception, std::type_info *info, void (*dest)(void *)) {
        // print the stack trace to stderr:
        if (mExceptionStackTrace) {
            print_exception_info(info);
            libunwind_print_backtrace(1);
        }

        // call the real __cxa_throw():
        static void (*const rethrow)(void*,void*,void(*)(void*)) __attribute__ ((noreturn)) = (void (*)(void*,void*,void(*)(void*)))dlsym(RTLD_NEXT, "__cxa_throw");
        rethrow(thrown_exception,info,dest);
    }
}
#endif
Prado answered 12/7, 2017 at 12:15 Comment(0)
K
0

Check out backward at backward-cpp it does a good job and is well maintained

Example code

In trace.hxx

#define BACKWARD_HAS_DW 1 // or #define BACKWARD_HAS_BFD 1 check docs
#include <backward.hpp>

class recoverable_err final: std::runtime_error
  {
    backward::StackTrace stacktrace_;

  public:
    explicit recoverable_err(std::string msg) noexcept;

    auto
    print_stacktrace(std::ostream &stream)const noexcept -> void;

    [[nodiscard]] auto
    what() const noexcept -> const char * final;
  };

In trace.cxx

  #include "trace.hxx"
  
  recoverable_err::recoverable_err(std::string msg) noexcept
      : std::runtime_error{ msg }
      , stacktrace_{ backward::StackTrace() }
  {
    stacktrace_.load_here();
  }

  auto
  recoverable_err::print_stacktrace(std::ostream &stream)const  noexcept -> void
  {
    using namespace backward;
    Printer p;
    p.object = true;
    p.color_mode = ColorMode::always;
    p.address = true;
    p.print(stacktrace_, stream);
  }

  auto
  recoverable_err::what() const noexcept -> const char *
  {
    return std::runtime_error::what();
  }

Usage in main

auto
main() -> int
{
  try
    {
      throw recoverable_err("Recover from nasty error");
    }
  catch (recoverable_err const &ex)
    {
      std::cerr << ex.what();
      ex.print_stacktrace(std::cerr);
    }
  catch (std::exception const &ex)
    {
      std::cerr << "Using default class\n";
      std::cerr << ex.what();
    }
}
Keelboat answered 12/7, 2021 at 22:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.