c++multithreadingexceptioncallstacklibunwind

set_terminate unexplained behaviour with exception thrown from std::thread and libunwind


I'm trying to add a std::terminate handler with std::set_terminate for debugging in my app but i ran into some unexplained behaviour. The basic idea is that if i'm throwing from constructors (or members running in the same thread) the backtrace (as retrieved with libunwind) is as expected, I can see the place where the throw is in the call stack, but if i create a std::thread and throw from it, the call stack only shows execute_native_thread_routine where I'm expecting to see the thrower. If I start a thread with pthread_create things work as expected. If I explicitly call std::terminate instead of throw from std::thread, I can see it on the stack, indicating that terminate is called from somewhere else. Trouble is I tried to replicate this in a stand-alone app and there the behaviour is correct in all situations and I don't know where to go from here. This is the code I'm testing with:

#define UNW_LOCAL_ONLY
#include <cxxabi.h>
#include <iomanip>
#include <iostream>
#include <libunwind.h>
#include <memory>
#include <stdlib.h>
#include <thread>
#include <unistd.h>

char const *get_demangled_name(char const *symbol) noexcept {
  if (!symbol) {
    return "<null>";
  }
  int status = -4;
  static std::unique_ptr<char, decltype(std::free) &> demangled_name{nullptr,
                                                                     std::free};
  demangled_name.reset(::abi::__cxa_demangle(symbol, demangled_name.release(),
                                             nullptr, &status));
  return ((status == 0) ? demangled_name.get() : symbol);
}

// Taken from https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/
void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    std::cout << pc << " : ";

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      std::cout << sym << " + " << offset << "\n   " << get_demangled_name(sym) << std::endl;
    } else {
      std::cout << " -- error: unable to obtain symbol name for this frame" << std::endl;
    }
  }
}

void backtrace_on_terminate() noexcept {
  backtrace();
  std::abort(); // or any other appropriate action
}

class C {
public:
  void thread_func_member() {
    sleep(1);
    throw std::bad_alloc();
  }
};

int main(int argc, char *argv[]) {
  std::set_terminate(backtrace_on_terminate);

  C c;
  std::thread thr = std::thread(&C::thread_func_member, &c);
  sleep(3);
}



Solution

  • Answering my own question, this is because of gcc implementation. Solution is to make the worker of the std::thread noexcept. See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=55917, https://gcc.gnu.org/legacy-ml/gcc-help/2013-01/msg00068.html and https://gcc.gnu.org/legacy-ml/gcc-help/2011-11/msg00141.html

    No, it's not a bug, but it could be improved.

    What happens in std::thread is that we catch the exception (at which point the stack has been unwound) then call std::terminate explicitly. That was done to ensure we onform to the standard and terminate as required.

    Now that the compiler support noexcept we should use that instead and not catch the exception, causing the runtime to call terminate without unwinding the stack.

    I'll make that change for 4.7.0

    Not fixed though:

    The reason for the unwound stack is that libstdc++'s function that calls the user's thread function contains a try/catch all statement. The supposed fix was to use noexcept on the internal thread main function, and the reason why it did not work out is described here:

    Because:

    On some targets using noexcept for the thread start function would be non-conforming.

    GNU libc-based systems use a special exception type for thread cancellation, and if thrown that exception must be allowed to leave the thread start function, so the function can't be noexcept.

    So although improving backtraces for noexcept functions would be good, it won't help std::thread