Browse Source

notify: Print out C++ backtrace on assert-abort

rdb 2 months ago
parent
commit
6ac2164c25
4 changed files with 193 additions and 13 deletions
  1. 0 0
      dtool/src/parser-inc/DbgHelp.h
  2. 180 5
      dtool/src/prc/notify.cxx
  3. 10 8
      dtool/src/prc/pnotify.h
  4. 3 0
      makepanda/makepanda.py

+ 0 - 0
dtool/src/parser-inc/DbgHelp.h


+ 180 - 5
dtool/src/prc/notify.cxx

@@ -35,6 +35,18 @@
 #include "emscriptenLogStream.h"
 #endif
 
+#ifndef NDEBUG
+#ifdef PHAVE_EXECINFO_H
+#include <execinfo.h>  // for backtrace()
+#include <dlfcn.h>
+#include <cxxabi.h>
+#endif
+
+#ifdef _WIN32
+#include <dbghelp.h>
+#endif
+#endif  // NDEBUG
+
 using std::cerr;
 using std::cout;
 using std::ostream;
@@ -348,15 +360,16 @@ assert_failure(const char *expression, int line,
     << expression << " at line " << line << " of " << source_file;
   string message = message_str.str();
 
-  if (!_assert_failed) {
+  Notify *self = ptr();
+  if (!self->_assert_failed) {
     // We only save the first assertion failure message, as this is usually
     // the most meaningful when several occur in a row.
-    _assert_failed = true;
-    _assert_error_message = message;
+    self->_assert_failed = true;
+    self->_assert_error_message = message;
   }
 
-  if (has_assert_handler()) {
-    return (*_assert_handler)(expression, line, source_file);
+  if (self->has_assert_handler()) {
+    return (*self->_assert_handler)(expression, line, source_file);
   }
 
 #ifdef ANDROID
@@ -380,6 +393,33 @@ assert_failure(const char *expression, int line,
     // Make sure the error message has been flushed to the output.
     nout.flush();
 
+    // Capture and list a stack trace.
+#ifdef NDEBUG
+#elif defined(PHAVE_EXECINFO_H)
+    void *trace[64];
+    int size = backtrace(trace, 64);
+    if (size > 0) {
+      // Remove the frame(s) corresponding to the current function.
+      void **tracep = trace;
+      void *return_addr = __builtin_return_address(0);
+      for (int i = 0; i < size; ++i) {
+        if (trace[i] == return_addr) {
+          tracep = trace + (i + 1);
+          size -= i + 1;
+          break;
+        }
+      }
+      write_backtrace(tracep, size);
+    }
+#elif defined(_WIN32)
+    const ULONG max_size = 62;
+    void *trace[max_size];
+    int size = CaptureStackBackTrace(1, max_size, trace, nullptr);
+    if (size > 0) {
+      write_backtrace(trace, size);
+    }
+#endif
+
 #ifdef _MSC_VER
     // How to trigger an exception in VC++ that offers to take us into the
     // debugger?  abort() doesn't do it.  We used to be able to assert(false),
@@ -411,6 +451,141 @@ assert_failure(const char *expression, int line,
   return true;
 }
 
+/**
+ *
+ */
+void Notify::
+write_backtrace(void **trace, int size) {
+  std::ostream &out = nout;
+
+#ifdef NDEBUG
+#elif defined(PHAVE_EXECINFO_H)
+  char namebuf[128];
+
+  for (int i = 0; i < size; ++i) {
+    void *addr = trace[i];
+    Dl_info info;
+    if (dladdr((char *)addr - 1, &info) != 0) {
+      const char *name = nullptr;
+
+      if (info.dli_sname != nullptr) {
+        int status = 0;
+        size_t size = 0;
+        char *demangled = nullptr;
+        if (info.dli_sname[0] == '_') {
+          size = sizeof(namebuf) - 1;
+          demangled = abi::__cxa_demangle(info.dli_sname, namebuf, &size, &status);
+        }
+        if (status == 0 && demangled != nullptr) {
+          namebuf[size] = 0;
+          name = demangled;
+          //if (strncmp(name, "Notify::assert_failure", 22) == 0) {
+            //continue;
+          //}
+        } else {
+          name = info.dli_sname;
+        }
+      }
+
+      out << "[" << addr << "] ";
+
+      if (info.dli_fname != nullptr) {
+        const char *slash = strrchr(info.dli_fname, '/');
+        out << std::left << std::setw(30) << (slash ? slash + 1 : info.dli_fname) << " ";
+      }
+
+      if (name != nullptr) {
+        out << name;
+      } else {
+        out << info.dli_saddr;
+      }
+
+      int offset = reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(info.dli_saddr);
+      if (offset > 0) {
+        out << " + " << offset;
+      }
+      out << "\n";
+    } else {
+      out << "[" << addr << "]\n";
+    }
+  }
+#elif defined(_WIN32)
+  HMODULE handle = LoadLibraryA("dbghelp.dll");
+  if (!handle) {
+    return;
+  }
+
+  auto pSymInitialize = (BOOL (WINAPI *)(HANDLE, PCSTR, BOOL))GetProcAddress(handle, "SymInitialize");
+  auto pSymCleanup = (BOOL (WINAPI *)(HANDLE))GetProcAddress(handle, "SymCleanup");
+  auto pSymFromAddr = (BOOL (WINAPI *)(HANDLE, DWORD64, DWORD64 *, PSYMBOL_INFO))GetProcAddress(handle, "SymFromAddr");
+  auto pSymGetModuleInfo64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PIMAGEHLP_MODULE64))GetProcAddress(handle, "SymGetModuleInfo64");
+  auto pSymGetLineFromAddr64 = (BOOL (WINAPI *)(HANDLE, DWORD64, PDWORD, PIMAGEHLP_LINE64))GetProcAddress(handle, "SymGetLineFromAddr64");
+  if (!pSymInitialize || !pSymCleanup || !pSymFromAddr) {
+    FreeLibrary(handle);
+    return;
+  }
+
+  HANDLE process = GetCurrentProcess();
+  pSymInitialize(process, nullptr, TRUE);
+
+  for (int i = 0; i < size; ++i) {
+    DWORD64 addr = (DWORD64)trace[i];
+
+    alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 256] = {0};
+    SYMBOL_INFO *symbol = (SYMBOL_INFO *)buffer;
+    symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
+    symbol->MaxNameLen = 255;
+
+    out << "[" << (void *)addr << "]";
+
+    // Show the name of the library.
+    IMAGEHLP_MODULE64 module;
+    module.SizeOfStruct = sizeof(module);
+    const char *basename = "???";
+    if (pSymGetModuleInfo64 && pSymGetModuleInfo64(process, addr, &module)) {
+      const char *slash = strrchr(module.ImageName, '\\');
+      basename = (slash ? slash + 1 : module.ImageName);
+    }
+
+    // Look up the symbol name.  If the reported name comes from the export
+    // table and the address is way off, ignore it, it's probably not right.
+    if (pSymFromAddr(process, addr, nullptr, symbol) &&
+        ((symbol->Flags & SYMFLAG_EXPORT) == 0 || addr - (DWORD64)symbol->Address < 0x4000)) {
+
+      out << " " << std::left << std::setw(30) << basename << " " << symbol->Name;
+
+      DWORD64 offset = addr - (DWORD64)(symbol->Address);
+      if (offset > 0) {
+        out << " + " << offset;
+      }
+
+      // Look up filename + line number if available.
+      IMAGEHLP_LINE64 line;
+      DWORD displacement = 0;
+      line.SizeOfStruct = sizeof(line);
+      if (pSymGetLineFromAddr64(process, addr, &displacement, &line)) {
+        const char *slash = strrchr(line.FileName, '\\');
+        const char *basename = (slash ? slash + 1 : line.FileName);
+
+        out << " (" << basename << ":" << line.LineNumber;
+        //if (displacement > 0) {
+        //  out << " + " << displacement;
+        //}
+        out << ")";
+      }
+    } else {
+      out << " " << basename;
+    }
+    out << "\n";
+  }
+
+  pSymCleanup(process);
+  FreeLibrary(handle);
+#endif
+
+  out.flush();
+}
+
 /**
  * Given a string, one of "debug", "info", "warning", etc., return the
  * corresponding Severity level, or NS_unspecified if none of the strings

+ 10 - 8
dtool/src/prc/pnotify.h

@@ -76,10 +76,12 @@ PUBLISHED:
 public:
   static ios_fmtflags get_literal_flag();
 
-  bool assert_failure(const std::string &expression, int line,
-                      const char *source_file);
-  bool assert_failure(const char *expression, int line,
-                      const char *source_file);
+  static bool assert_failure(const std::string &expression, int line,
+                             const char *source_file);
+  static bool assert_failure(const char *expression, int line,
+                             const char *source_file);
+
+  static void write_backtrace(void **trace, int size);
 
   static NotifySeverity string_severity(const std::string &string);
 
@@ -204,7 +206,7 @@ private:
 #define nassertr(condition, return_value) \
   { \
     if (_nassert_check(condition)) { \
-      if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \
+      if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \
         return return_value; \
       } \
     } \
@@ -213,7 +215,7 @@ private:
 #define nassertv(condition) \
   { \
     if (_nassert_check(condition)) { \
-      if (Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__)) { \
+      if (Notify::assert_failure(#condition, __LINE__, __FILE__)) { \
         return; \
       } \
     } \
@@ -221,12 +223,12 @@ private:
 
 #define nassertd(condition) \
   if (_nassert_check(condition) && \
-      Notify::ptr()->assert_failure(#condition, __LINE__, __FILE__))
+      Notify::assert_failure(#condition, __LINE__, __FILE__))
 
 #define nassertr_always(condition, return_value) nassertr(condition, return_value)
 #define nassertv_always(condition) nassertv(condition)
 
-#define nassert_raise(message) Notify::ptr()->assert_failure(message, __LINE__, __FILE__)
+#define nassert_raise(message) Notify::assert_failure(message, __LINE__, __FILE__)
 
 #endif  // NDEBUG
 

+ 3 - 0
makepanda/makepanda.py

@@ -2424,6 +2424,7 @@ DTOOL_CONFIG=[
     ("PHAVE_DIRENT_H",                 'UNDEF',                  '1'),
     ("PHAVE_UCONTEXT_H",               'UNDEF',                  '1'),
     ("PHAVE_STDINT_H",                 '1',                      '1'),
+    ("PHAVE_EXECINFO_H",               'UNDEF',                  '1'),
     ("HAVE_RTTI",                      '1',                      '1'),
     ("HAVE_X11",                       'UNDEF',                  '1'),
     ("IS_LINUX",                       'UNDEF',                  '1'),
@@ -2560,6 +2561,7 @@ def WriteConfigSettings():
         dtool_config["PHAVE_GLOB_H"] = 'UNDEF'
         dtool_config["PHAVE_LOCKF"] = 'UNDEF'
         dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
+        dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF'
 
     if (GetTarget() == "emscripten"):
         # There are no threads in JavaScript, so don't bother using them.
@@ -2572,6 +2574,7 @@ def WriteConfigSettings():
         dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF'
         dtool_config["HAVE_X11"] = 'UNDEF'
         dtool_config["HAVE_GLX"] = 'UNDEF'
+        dtool_config["PHAVE_EXECINFO_H"] = 'UNDEF'
 
         # There are no environment vars either, or default prc files.
         prc_parameters["DEFAULT_PRC_DIR"] = 'UNDEF'