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

This commit is contained in:
rdb 2025-10-10 14:12:33 +02:00
parent 2540648ac9
commit 6ac2164c25
4 changed files with 193 additions and 13 deletions

View File

View File

@ -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

View File

@ -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

View File

@ -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'