Niko O
Niko O

Reputation: 437

How to get proper stack trace despite catch and throw; in std library

I use C++17, GCC, Qt Creator with its integrated GDB debugger.

I have code that simplifies down to this:

#include <iostream>
#include <iomanip>

// Example-implementation
#define assert(Condition) { if (!(Condition)) { std::cerr << "Assert failed, condition is false: " #Condition << std::endl; } }

#include <execinfo.h>
#include <signal.h>
#include <unistd.h>

void printStackTrace()
{
    constexpr int requestedFrameCount = 20;
    void* frames[requestedFrameCount];
    auto actualFrameCount = backtrace(frames, requestedFrameCount);
    std::cout << "Stack trace (" << actualFrameCount << " of " << requestedFrameCount << " requested frames):" << std::endl;
    backtrace_symbols_fd(frames, actualFrameCount, STDOUT_FILENO);
    std::cout << "End of stack trace." << std::endl;
}

void signalHandler(int signalNumber)
{
    std::cout << "Signal " << signalNumber << " (" << sys_siglist[signalNumber] << ") happened!" << std::endl;
    assert(signalNumber == SIGABRT);
    printStackTrace();
}

__attribute_noinline__ void someFunction()
{
    throw std::invalid_argument("Bad things happened");
}

__attribute_noinline__ void someFunctionInTheStandardLibraryThatICantChange()
{
    try
    {
        someFunction();
    }
    catch (...)
    {
        throw;
    }
}

__attribute_noinline__ int main()
{
    signal(SIGABRT, signalHandler);
    someFunctionInTheStandardLibraryThatICantChange();
    return 0;
}

someFunctionInTheStandardLibraryThatICantChange is a placeholder for this thing:

  template<bool _TrivialValueTypes>
    struct __uninitialized_copy
    {
      template<typename _InputIterator, typename _ForwardIterator>
        static _ForwardIterator
        __uninit_copy(_InputIterator __first, _InputIterator __last,
                      _ForwardIterator __result)
        {
          _ForwardIterator __cur = __result;
          __try
            {
              for (; __first != __last; ++__first, (void)++__cur)
                std::_Construct(std::__addressof(*__cur), *__first);
              return __cur;
            }
          __catch(...)
            {
              std::_Destroy(__result, __cur);
              __throw_exception_again;
            }
        }
    };

The program's output looks something like this:

On standard output:

Signal 6 (Aborted) happened!
Stack trace (13 of 20 requested frames):
/foo/Test(_Z15printStackTracev+0x1c)[0xaaaab9886d30]
/foo/Test(_Z13signalHandleri+0xbc)[0xaaaab9886e94]
linux-vdso.so.1(__kernel_rt_sigreturn+0x0)[0xffff95f3a5c8]
/lib64/libc.so.6(gsignal+0xc8)[0xffff94e15330]
/lib64/libc.so.6(abort+0xfc)[0xffff94e02b54]
/lib64/libstdc++.so.6(_ZN9__gnu_cxx27__verbose_terminate_handlerEv+0x188)[0xffff950d9358]
/lib64/libstdc++.so.6(_ZN10__cxxabiv111__terminateEPFvvE+0xc)[0xffff950d70ac]
/lib64/libstdc++.so.6(_ZN10__cxxabiv112__unexpectedEPFvvE+0x0)[0xffff950d7100]
/lib64/libstdc++.so.6(__cxa_rethrow+0x60)[0xffff950d7428]
/foo/Test(_Z47someFunctionInTheStandardLibraryThatICantChangev+0x1c)[0xaaaab9886f10]
/foo/Test(main+0x1c)[0xaaaab9886f48]
/lib64/libc.so.6(__libc_start_main+0xe4)[0xffff94e02fac]
/foo/Test(+0x2774)[0xaaaab9886774]
End of stack trace.

On standard error:

terminate called after throwing an instance of 'std::invalid_argument'
  what():  Bad things happened

Note how the stack trace goes directly from someFunctionInTheStandardLibraryThatICantChange to rethrow. someFunction was not inlined (call printStackTrace from someFunction if you don't trust me).

I can't change the library function, but I need to know where the exception was originally thrown. How do I get that information?

One possible way is to use the debugger and set a "Break when C++ exception is thrown" breakpoint. But that has the significant drawbacks that it only works when debugging, it's external to the program and it is only really viable if you don't throw a bunch of exceptions that you don't care about.

Upvotes: 0

Views: 797

Answers (1)

Niko O
Niko O

Reputation: 437

What @n.1.8e9-where's-my-sharem. suggested in the comments ended up working. When you throw an exception in C++, behind the scenes the function __cxa_throw is called. You can replace that function, look at the stack trace, then call the replaced function.

Here is a simple proof-of-concept:

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

typedef void (*ThrowFunction)(void*, void*, void(*)(void*)) __attribute__ ((__noreturn__));
ThrowFunction oldThrowFunction;

namespace __cxxabiv1
{
    extern "C" void __cxa_throw(void* thrownException, std::type_info* thrownTypeInfo, void (*destructor)(void *))
    {
        if (oldThrowFunction == nullptr)
        {
            oldThrowFunction = (ThrowFunction)dlsym(RTLD_NEXT, "__cxa_throw");
        }

        // At this point, you can get the current stack trace and do something with it (e.g. print it, like follows).
        // You can also set a break point here to have the debugger stop while the stack trace is still useful.
        std::cout << "About to throw an exception of type " << thrownTypeInfo->name() << "! Current stack trace is as follows:" << std::endl;
        printStackTrace();
        std::cout << std::endl;

        oldThrowFunction(thrownException, thrownTypeInfo, destructor);
    }
}

Integrated with the example in the question, the output is as follows:

About to throw an exception of type St16invalid_argument! Current stack trace is as follows:
Stack trace (7 of 20 requested frames):
/foo/Test(_Z15printStackTracev+0x3c)[0x55570996b385]
/foo/Test(__cxa_throw+0xa1)[0x55570996b653]
/foo/Test(_Z12someFunctionv+0x43)[0x55570996b555]
/foo/Test(_Z47someFunctionInTheStandardLibraryThatICantChangev+0x12)[0x55570996b581]
/foo/Test(main+0x21)[0x55570996b6ac]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7fb71560f0b3]
/foo/Test(_start+0x2e)[0x55570996b28e]
End of stack trace.

Signal 6 (Aborted) happened!
Stack trace (13 of 20 requested frames):
/foo/Test(_Z15printStackTracev+0x3c)[0x55570996b385]
/foo/Test(_Z13signalHandleri+0xbd)[0x55570996b50f]
/lib/x86_64-linux-gnu/libc.so.6(+0x46210)[0x7fb71562e210]
/lib/x86_64-linux-gnu/libc.so.6(gsignal+0xcb)[0x7fb71562e18b]
/lib/x86_64-linux-gnu/libc.so.6(abort+0x12b)[0x7fb71560d859]
/lib/x86_64-linux-gnu/libstdc++.so.6(+0x9e911)[0x7fb715893911]
/lib/x86_64-linux-gnu/libstdc++.so.6(+0xaa38c)[0x7fb71589f38c]
/lib/x86_64-linux-gnu/libstdc++.so.6(+0xaa3f7)[0x7fb71589f3f7]
/lib/x86_64-linux-gnu/libstdc++.so.6(__cxa_rethrow+0x4d)[0x7fb71589f6fd]
/foo/Test(_Z47someFunctionInTheStandardLibraryThatICantChangev+0x25)[0x55570996b594]
/foo/Test(main+0x21)[0x55570996b6ac]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x7fb71560f0b3]
/foo/Test(_start+0x2e)[0x55570996b28e]
End of stack trace.
terminate called after throwing an instance of 'std::invalid_argument'
  what():  Bad things happened

This can be improved in the following ways:

  • The printed exception type name can be demangled.
  • The actual exception object can be pulled out of the void* by examining the type_info.
  • The stack trace can be printed to a string (instead of the console). Note that this can be undesirable if an exception was thrown due to the process running out of memory. But it's an option for 99.9% of use cases.
  • The stack trace can be attached to the thrown exception object. Either by inserting your own class with an std::string field (to plonk the stack trace in) in the inheritance hierarchy of exceptions you are throwing (it's headache-inducing arcane magic, requires changing existing source code, is totally unportable, but works for me); or maybe by using thread-local storage, but I haven't touched that yet.
  • The attached stack trace can be grabbed and printed by the existing terminate handler.

If I find enough time and patience I'll add that to this answer, but I might die of frustration before that happens.

Upvotes: 2

Related Questions