// crash_handler.cpp - 崩溃现场捕获与死因日志实现 // // 设计约束:崩溃发生时堆/栈可能已损坏,因此处理路径上 // 1) 不使用 std::string / iostream / 动态分配; // 2) 只用栈缓冲 + vsnprintf + Win32 API 直接写文件并 FlushFileBuffers; // 3) 用 g_in_handler 防止 handler 内部二次崩溃造成无限递归。 #include "crash_handler.h" #include #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif // 崩溃日志最大单条长度 #define CH_MAX_TEXT 8192 namespace { char g_log_dir[512] = "data"; // 崩溃日志根目录 char g_milestone[512] = ""; // 最近一次里程碑说明 char g_exe_name[64] = "main.exe"; // 本程序文件名(生成 addr2line 命令用) // PE 文件头里的 ImageBase。addr2line 认的是 ImageBase+RVA,不是运行时的加载基址 // (ASLR 下两者不同),直接用崩溃时的虚拟地址会全部解析成 "??"。 uintptr_t g_image_base = 0x140000000; int g_installed = 0; int g_in_handler = 0; std::mutex& milestone_mtx() { static std::mutex m; // 函数内静态,保证初始化顺序安全 return m; } void now_str(char* buf, size_t cap) { std::time_t t = std::time(nullptr); struct tm tmv; #ifdef _WIN32 localtime_s(&tmv, &t); #else localtime_r(&t, &tmv); #endif std::strftime(buf, cap, "%Y-%m-%d %H:%M:%S", &tmv); } void stamp_str(char* buf, size_t cap) // YYYYMMDD_HHMMSS(文件名用) { std::time_t t = std::time(nullptr); struct tm tmv; #ifdef _WIN32 localtime_s(&tmv, &t); #else localtime_r(&t, &tmv); #endif std::strftime(buf, cap, "%Y%m%d_%H%M%S", &tmv); } void date_str(char* buf, size_t cap) // YYYYMMDD(目录名用) { std::time_t t = std::time(nullptr); struct tm tmv; #ifdef _WIN32 localtime_s(&tmv, &t); #else localtime_r(&t, &tmv); #endif std::strftime(buf, cap, "%Y%m%d", &tmv); } } // ==================== 日志落盘(多路径回退) ==================== #ifdef _WIN32 namespace { // UTF-8 -> UTF-16 int u8_to_w(const char* s, wchar_t* out, int cap) { int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, out, cap); if (n <= 0 && cap > 0) out[0] = L'\0'; return n; } bool append_file_w(const wchar_t* wpath, const char* text, size_t len) { HANDLE h = CreateFileW(wpath, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (h == INVALID_HANDLE_VALUE) return false; LARGE_INTEGER mv; mv.QuadPart = 0; SetFilePointerEx(h, mv, NULL, FILE_END); DWORD written = 0; BOOL ok = WriteFile(h, text, (DWORD)len, &written, NULL); FlushFileBuffers(h); CloseHandle(h); return ok && written > 0; } // 依次尝试: //crash.log -> /crash.log -> /simiradar_crash.log // 返回实际写入成功的目录(宽字符,供 minidump 复用);失败返回空。 bool write_crash_text(const char* text, wchar_t* out_dir, int cap) { wchar_t path[MAX_PATH * 2]; char date[16]; date_str(date, sizeof(date)); // 1) data/YYYYMMDD/crash.log wchar_t wdir[MAX_PATH * 2], wdate[32]; u8_to_w(g_log_dir, wdir, MAX_PATH * 2); u8_to_w(date, wdate, 32); CreateDirectoryW(wdir, NULL); // 已存在则失败,忽略 _snwprintf(path, MAX_PATH * 2, L"%s\\%s", wdir, wdate); CreateDirectoryW(path, NULL); _snwprintf(path, MAX_PATH * 2, L"%s\\%s\\crash.log", wdir, wdate); if (append_file_w(path, text, std::strlen(text))) { _snwprintf(out_dir, cap, L"%s\\%s", wdir, wdate); return true; } // 2) exe 同目录 crash.log(部署机上工作目录常常不是 exe 目录) wchar_t exe[MAX_PATH]; DWORD n = GetModuleFileNameW(NULL, exe, MAX_PATH); if (n > 0) { for (DWORD i = n - 1; i > 0; --i) if (exe[i] == L'\\' || exe[i] == L'/') { exe[i] = L'\0'; break; } _snwprintf(path, MAX_PATH * 2, L"%s\\crash.log", exe); if (append_file_w(path, text, std::strlen(text))) { _snwprintf(out_dir, cap, L"%s", exe); return true; } } // 3) 系统临时目录 wchar_t tmp[MAX_PATH]; if (GetTempPathW(MAX_PATH, tmp) > 0) { _snwprintf(path, MAX_PATH * 2, L"%ssimiradar_crash.log", tmp); if (append_file_w(path, text, std::strlen(text))) { _snwprintf(out_dir, cap, L"%s", tmp); return true; } } return false; } // ---- dbghelp:符号/行号/MiniDump,全部动态加载,缺库也不影响基本记录 ---- typedef BOOL (WINAPI *SymInitialize_t)(HANDLE, PCSTR, BOOL); typedef BOOL (WINAPI *SymFromAddr_t)(HANDLE, DWORD64, PDWORD64, PVOID); typedef BOOL (WINAPI *SymGetLineFromAddr64_t)(HANDLE, DWORD64, PDWORD, PVOID); typedef DWORD (WINAPI *SymSetOptions_t)(DWORD); typedef BOOL (WINAPI *MiniDumpWriteDump_t)(HANDLE, DWORD, HANDLE, DWORD, PVOID, PVOID, PVOID); typedef BOOL (WINAPI *SymCleanup_t)(HANDLE); struct SymPkg // dbghelp.h 的 SYMBOL_INFO(自绘,避免include) { ULONG SizeOfStruct; ULONG TypeIndex; ULONG64 Reserved[2]; ULONG Index; ULONG Size; ULONG64 ModBase; ULONG Flags; ULONG64 Value; ULONG64 Address; ULONG Register; ULONG Scope; ULONG Tag; ULONG NameLen; ULONG MaxNameLen; CHAR Name[1]; }; struct LinePkg // IMAGEHLP_LINE64 { DWORD SizeOfStruct; PVOID Key; DWORD64 LineNumber; PCHAR FileName; DWORD64 Address; }; struct DumpExcInfo // MINIDUMP_EXCEPTION_INFORMATION { DWORD ThreadId; PEXCEPTION_POINTERS ExceptionPointers; BOOL ClientPointers; }; HMODULE g_dbg = NULL; SymInitialize_t g_SymInit = NULL; SymFromAddr_t g_SymAddr = NULL; SymGetLineFromAddr64_t g_SymLine = NULL; SymSetOptions_t g_SymOpt = NULL; MiniDumpWriteDump_t g_Dump = NULL; void dbghelp_load() { if (g_dbg) return; g_dbg = LoadLibraryW(L"dbghelp.dll"); if (!g_dbg) return; g_SymInit = (SymInitialize_t)GetProcAddress(g_dbg, "SymInitialize"); g_SymAddr = (SymFromAddr_t)GetProcAddress(g_dbg, "SymFromAddr"); g_SymLine = (SymGetLineFromAddr64_t)GetProcAddress(g_dbg, "SymGetLineFromAddr64"); g_SymOpt = (SymSetOptions_t)GetProcAddress(g_dbg, "SymSetOptions"); g_Dump = (MiniDumpWriteDump_t)GetProcAddress(g_dbg, "MiniDumpWriteDump"); if (g_SymOpt) g_SymOpt(0x00000002 /*SYMOPT_UNDNAME*/ | 0x00000004 /*SYMOPT_DEFERRED_LOADS*/); if (g_SymInit) g_SymInit(GetCurrentProcess(), NULL, TRUE); } // 地址 -> "模块名+0x偏移 (符号) [文件:行]" void describe_addr(void* addr, char* out, size_t cap) { out[0] = '\0'; HMODULE hMod = NULL; char modname[64] = "?"; uintptr_t base = 0; if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)addr, &hMod) && hMod) { WCHAR wpath[MAX_PATH]; DWORD n = GetModuleFileNameW(hMod, wpath, MAX_PATH); if (n > 0) { const WCHAR* fn = wpath; for (DWORD i = 0; i < n; ++i) if (wpath[i] == L'\\' || wpath[i] == L'/') fn = wpath + i + 1; WideCharToMultiByte(CP_UTF8, 0, fn, -1, modname, sizeof(modname), NULL, NULL); } base = (uintptr_t)hMod; } uintptr_t off = (uintptr_t)addr - base; char sym[256] = "", loc[256] = ""; if (g_SymAddr) { char buf[sizeof(SymPkg) + 256]; std::memset(buf, 0, sizeof(buf)); SymPkg* si = (SymPkg*)buf; si->SizeOfStruct = sizeof(SymPkg); si->MaxNameLen = 256; DWORD64 disp = 0; if (g_SymAddr(GetCurrentProcess(), (DWORD64)(uintptr_t)addr, &disp, si)) _snprintf(sym, sizeof(sym), " %s+0x%llX", si->Name, (unsigned long long)disp); if (g_SymLine) { LinePkg ln; std::memset(&ln, 0, sizeof(ln)); ln.SizeOfStruct = sizeof(LinePkg); DWORD d = 0; if (g_SymLine(GetCurrentProcess(), (DWORD64)(uintptr_t)addr, &d, &ln) && ln.FileName) { const char* f = std::strrchr(ln.FileName, '\\'); if (!f) f = std::strrchr(ln.FileName, '/'); _snprintf(loc, sizeof(loc), " [%s:%lu]", f ? f + 1 : ln.FileName, (unsigned long)ln.LineNumber); } } } _snprintf(out, cap, " 0x%p %s+0x%llX%s%s", addr, modname, (unsigned long long)off, sym, loc); } typedef USHORT (WINAPI *CaptureStackBackTrace_t)(ULONG, ULONG, PVOID*, PULONG); // 输出调用栈;同时把属于本程序的帧地址收集到 addrs(供 addr2line 还原行号) void dump_stack(char* out, size_t cap, int skip, uintptr_t* addrs, int* naddr, int maxaddr) { HMODULE krnl = GetModuleHandleW(L"kernel32.dll"); CaptureStackBackTrace_t cap_fn = krnl ? (CaptureStackBackTrace_t)GetProcAddress(krnl, "RtlCaptureStackBackTrace") : NULL; if (!cap_fn) { std::strncat(out, " (不支持栈回溯)\n", cap - std::strlen(out) - 1); return; } void* frames[64]; USHORT n = cap_fn(0, 64, frames, NULL); if (n == 0) { std::strncat(out, " (空)\n", cap - std::strlen(out) - 1); return; } uintptr_t load_base = (uintptr_t)GetModuleHandleW(NULL); // 运行时加载基址(ASLR) for (USHORT i = (USHORT)skip; i < n && i < 64; ++i) { char line[512]; describe_addr(frames[i], line, sizeof(line)); std::strncat(out, line, cap - std::strlen(out) - 1); std::strncat(out, "\n", cap - std::strlen(out) - 1); if (addrs && naddr && *naddr < maxaddr && std::strstr(line, g_exe_name)) { // 转成 ImageBase+RVA:addr2line 只认这种形式 uintptr_t rva = (uintptr_t)frames[i] - load_base; addrs[(*naddr)++] = g_image_base + rva; } } } void write_minidump(const wchar_t* dir, PEXCEPTION_POINTERS ep) { if (!g_Dump || !dir || !dir[0]) return; char stamp[32]; stamp_str(stamp, sizeof(stamp)); wchar_t wstamp[64], path[MAX_PATH * 2]; u8_to_w(stamp, wstamp, 64); _snwprintf(path, MAX_PATH * 2, L"%s\\crash_%s.dmp", dir, wstamp); HANDLE hf = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hf == INVALID_HANDLE_VALUE) return; DumpExcInfo ei; ei.ThreadId = GetCurrentThreadId(); ei.ExceptionPointers = ep; ei.ClientPointers = FALSE; BOOL ok = g_Dump(GetCurrentProcess(), GetCurrentProcessId(), hf, 0 /*MiniDumpNormal*/, &ei, NULL, NULL); FlushFileBuffers(hf); DWORD sz = GetFileSize(hf, NULL); CloseHandle(hf); // 空的/失败的 dmp 只会误导排查,直接删掉(地址+addr2line 已足够定位) if (!ok || sz == 0) { DeleteFileW(path); char note[160]; _snprintf(note, sizeof(note), "[说明] minidump 生成失败(ok=%d size=%lu err=%lu)," "请用上面的 addr2line 命令还原行号。\n", (int)ok, (unsigned long)sz, (unsigned long)GetLastError()); wchar_t wpath2[MAX_PATH * 2]; _snwprintf(wpath2, MAX_PATH * 2, L"%s\\crash.log", dir); append_file_w(wpath2, note, std::strlen(note)); } } // 异常码 -> 中文说明 const char* exc_name(DWORD code) { switch (code) { case 0xC0000005: return "ACCESS_VIOLATION 访问违规(空指针/野指针/越界/已释放内存)"; case 0xC00000FD: return "STACK_OVERFLOW 栈溢出(递归过深或栈上大数组)"; case 0xC0000094: return "INT_DIVIDE_BY_ZERO 除零"; case 0xC0000095: return "INT_OVERFLOW 整数溢出"; case 0xC000001D: return "ILLEGAL_INSTRUCTION 非法指令"; case 0xC0000006: return "IN_PAGE_ERROR 内存页错误(读取映射文件失败)"; case 0xC000008C: return "ARRAY_BOUNDS_EXCEEDED 数组越界"; case 0xC0000135: return "DLL_NOT_FOUND 依赖的DLL未找到"; case 0xC0000142: return "DLL_INIT_FAILED DLL初始化失败"; case 0xC0000409: return "STACK_BUFFER_OVERRUN 栈缓冲区溢出(编译器安全检查触发)"; case 0xC0000374: return "HEAP_CORRUPTION 堆损坏(重复释放/越界写)"; case 0xC0000417: return "INVALID_CRUNTIME_PARAMETER CRT非法参数"; case 0xE06D7363: return "C++ 异常未捕获 (std::terminate)"; case 0x40010005: return "CTRL_CLOSE / 控制台被关闭"; case 0x80000003: return "BREAKPOINT 断点"; default: return "未知异常"; } } // 组装并落盘一条崩溃记录 void report_crash(const char* title, PEXCEPTION_POINTERS ep, DWORD code) { if (g_in_handler) return; // 防递归 g_in_handler = 1; char text[CH_MAX_TEXT]; char ts[32]; now_str(ts, sizeof(ts)); int len = _snprintf(text, sizeof(text), "\n==================== %s ====================\n" "时间 : %s\n" "进程/线程 : pid=%lu tid=%lu\n" "最后里程碑: %s\n", title, ts, (unsigned long)GetCurrentProcessId(), (unsigned long)GetCurrentThreadId(), g_milestone[0] ? g_milestone : "(无)"); if (code) len += _snprintf(text + len, sizeof(text) - len, "异常码 : 0x%08lX %s\n", (unsigned long)code, exc_name(code)); void* fault_addr = NULL; if (ep && ep->ExceptionRecord) { PEXCEPTION_RECORD r = ep->ExceptionRecord; fault_addr = r->ExceptionAddress; len += _snprintf(text + len, sizeof(text) - len, "异常地址 : 0x%p (%s 0x%p)\n" "异常标志 : 0x%lX 参数个数: %lu\n", r->ExceptionAddress, (r->ExceptionCode == 0xC0000005 ? (r->ExceptionInformation[0] ? "写入" : "读取") : "访问"), (r->ExceptionCode == 0xC0000005 && r->NumberParameters >= 2) ? (void*)r->ExceptionInformation[1] : (void*)0, (unsigned long)r->ExceptionFlags, (unsigned long)r->NumberParameters); char line[512]; describe_addr(fault_addr, line, sizeof(line)); len += _snprintf(text + len, sizeof(text) - len, "出错位置 :%s\n", line); if (ep->ContextRecord) { #ifdef _WIN64 len += _snprintf(text + len, sizeof(text) - len, "寄存器 : RIP=0x%llX RSP=0x%llX RBP=0x%llX RAX=0x%llX\n", (unsigned long long)ep->ContextRecord->Rip, (unsigned long long)ep->ContextRecord->Rsp, (unsigned long long)ep->ContextRecord->Rbp, (unsigned long long)ep->ContextRecord->Rax); #else len += _snprintf(text + len, sizeof(text) - len, "寄存器 : EIP=0x%lX ESP=0x%lX EBP=0x%lX EAX=0x%lX\n", (unsigned long)ep->ContextRecord->Eip, (unsigned long)ep->ContextRecord->Esp, (unsigned long)ep->ContextRecord->Ebp, (unsigned long)ep->ContextRecord->Eax); #endif } } len += _snprintf(text + len, sizeof(text) - len, "调用栈 :\n"); uintptr_t addrs[64]; int naddr = 0; dump_stack(text + len, sizeof(text) - len, 0, addrs, &naddr, 64); len = (int)std::strlen(text); len += _snprintf(text + len, sizeof(text) - len, "提示 : 同目录下 crash_*.dmp 可用 Visual Studio / WinDbg 打开查看完整现场。\n"); // MinGW 生成的是 DWARF 调试信息,Windows 的 dbghelp 读不了,用 addr2line 还原: // addr2line -e main.exe -f -C -i <地址...> if (naddr > 0 && len < (int)sizeof(text) - 64) { len += _snprintf(text + len, sizeof(text) - len, "还原行号 : addr2line -e %s -f -C -i", g_exe_name); for (int i = 0; i < naddr && len < (int)sizeof(text) - 24; ++i) len += _snprintf(text + len, sizeof(text) - len, " 0x%llX", (unsigned long long)addrs[i]); len += _snprintf(text + len, sizeof(text) - len, "\n"); } wchar_t dir[MAX_PATH * 2] = { 0 }; write_crash_text(text, dir, MAX_PATH * 2); // 控制台/调试器可见 fflush(stdout); std::fwrite(text, 1, std::strlen(text), stderr); fflush(stderr); OutputDebugStringA(text); write_minidump(dir, ep); // 弹窗提示(无人值守部署可设环境变量 SIMIRADAR_NO_CRASH_MSGBOX=1 关闭) char envbuf[8] = { 0 }; if (GetEnvironmentVariableA("SIMIRADAR_NO_CRASH_MSGBOX", envbuf, sizeof(envbuf)) == 0) { char msg[512]; _snprintf(msg, sizeof(msg), "%s\n\n异常码: 0x%08lX (%s)\n\n详细信息已写入:\n%ls\\crash.log", title, (unsigned long)code, exc_name(code), dir[0] ? dir : L"(写入失败)"); MessageBoxA(NULL, msg, "Pavo2 程序异常终止", MB_OK | MB_ICONERROR | MB_SETFOREGROUND | MB_TOPMOST); } g_in_handler = 0; } LONG WINAPI seh_filter(PEXCEPTION_POINTERS ep) { DWORD code = ep && ep->ExceptionRecord ? ep->ExceptionRecord->ExceptionCode : 0; if (code == 0x40010005) return EXCEPTION_CONTINUE_SEARCH; // 控制台关闭:正常退出 report_crash("崩溃: 未处理的系统异常(SEH)", ep, code); return EXCEPTION_EXECUTE_HANDLER; // 交给系统终止进程 } } #endif // _WIN32 // ==================== 非 Windows:只做 signal 兜底 ==================== namespace { void report_text(const char* title) { char text[CH_MAX_TEXT]; char ts[32]; now_str(ts, sizeof(ts)); _snprintf(text, sizeof(text), "\n==================== %s ====================\n时间: %s\n最后里程碑: %s\n", title, ts, g_milestone[0] ? g_milestone : "(无)"); #ifdef _WIN32 wchar_t dir[MAX_PATH * 2] = { 0 }; write_crash_text(text, dir, MAX_PATH * 2); #endif std::fwrite(text, 1, std::strlen(text), stderr); fflush(stderr); } void abort_signal(int sig) { if (g_in_handler) { std::signal(sig, SIG_DFL); std::raise(sig); return; } g_in_handler = 1; char title[128]; _snprintf(title, sizeof(title), "崩溃: 收到信号 %d (%s)", sig, sig == SIGABRT ? "abort()/未处理异常/std::thread未join" : (sig == SIGSEGV ? "段错误" : (sig == SIGFPE ? "浮点异常" : "非法指令"))); #ifdef _WIN32 report_crash(title, NULL, 0); // 仍记录调用栈与 minidump #else report_text(title); #endif g_in_handler = 0; std::signal(sig, SIG_DFL); std::raise(sig); // 保持默认退出行为,便于外部捕获退出码 } void on_terminate() { if (g_in_handler) std::abort(); g_in_handler = 1; std::string what; if (std::exception_ptr ep = std::current_exception()) { try { std::rethrow_exception(ep); } catch (const std::exception& e) { what = e.what(); } catch (...) { what = "非 std::exception 类型的异常"; } } std::string title = "崩溃: std::terminate 被调用"; if (!what.empty()) { title += " (未捕获异常: "; title += what; title += ")"; } else { // terminate 最常见的另一种诱因:std::thread 析构时仍 joinable(未 join/detach) title += " (无活动异常 —— 极可能是 std::thread 对象析构时线程仍在运行," "即某处 start 了线程却没有 join/detach)"; } #ifdef _WIN32 report_crash(title.c_str(), NULL, 0); // 记录调用栈 + minidump fflush(stderr); ExitProcess(3); #else report_text(title.c_str()); std::_Exit(3); #endif } void on_new_fail() { if (g_in_handler) return; g_in_handler = 1; report_text("崩溃: new 失败(std::bad_alloc) —— 内存不足或申请了超大内存"); g_in_handler = 0; throw std::bad_alloc(); } } // ==================== 对外接口 ==================== void install_crash_handlers(const char* log_dir) { if (g_installed) return; g_installed = 1; if (log_dir && log_dir[0]) std::strncpy(g_log_dir, log_dir, sizeof(g_log_dir) - 1); #ifdef _WIN32 char exe[MAX_PATH]; if (GetModuleFileNameA(NULL, exe, MAX_PATH) > 0) { const char* fn = std::strrchr(exe, '\\'); if (!fn) fn = std::strrchr(exe, '/'); std::strncpy(g_exe_name, fn ? fn + 1 : exe, sizeof(g_exe_name) - 1); g_exe_name[sizeof(g_exe_name) - 1] = '\0'; } // 读 PE 头里的 ImageBase,供 addr2line 还原行号 { char* base = (char*)GetModuleHandleW(NULL); uintptr_t load_base = (uintptr_t)base; // 运行时加载基址(ASLR,每次不同) if (base) { IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base; if (dos->e_magic == IMAGE_DOS_SIGNATURE) { IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew); if (nt->Signature == IMAGE_NT_SIGNATURE) { uintptr_t ib = (uintptr_t)nt->OptionalHeader.ImageBase; // 只有读到"合理且与加载基址不同"的静态值才采用; // 否则保持默认 0x140000000(MinGW 链接默认值) if (ib >= 0x10000 && ib != load_base) g_image_base = ib; } } } } dbghelp_load(); // 提前初始化符号,崩溃时只做查询 SetUnhandledExceptionFilter(seh_filter); // 关掉 Windows 的 "程序已停止工作" 系统弹窗,避免覆盖我们的提示 SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); #endif std::signal(SIGABRT, abort_signal); std::set_terminate(on_terminate); #ifndef _WIN32 // Windows 上 SEH 已覆盖 SIGSEGV/FPE/ILL:CRT 的 signal 映射会抢在 // UnhandledExceptionFilter 之前触发,反而拿不到异常地址和调用栈,故不注册。 std::signal(SIGSEGV, abort_signal); std::signal(SIGFPE, abort_signal); std::signal(SIGILL, abort_signal); #endif std::set_new_handler(on_new_fail); char ts[32]; now_str(ts, sizeof(ts)); CRASH_MILESTONE("崩溃日志已安装 (%s)", ts); } void crash_milestone(const char* fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); { std::lock_guard lk(milestone_mtx()); std::strncpy(g_milestone, buf, sizeof(g_milestone) - 1); g_milestone[sizeof(g_milestone) - 1] = '\0'; } // 同时进控制台与 run.log(便于对照时间线) char line[600]; char ts[32]; now_str(ts, sizeof(ts)); snprintf(line, sizeof(line), "[%s][里程碑] %s\n", ts, buf); std::fwrite(line, 1, std::strlen(line), stdout); fflush(stdout); } void crash_log(const char* fmt, ...) { char buf[CH_MAX_TEXT]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); char text[CH_MAX_TEXT + 64]; char ts[32]; now_str(ts, sizeof(ts)); snprintf(text, sizeof(text), "\n===== 严重错误 %s =====\n%s\n", ts, buf); #ifdef _WIN32 wchar_t dir[MAX_PATH * 2] = { 0 }; write_crash_text(text, dir, MAX_PATH * 2); #endif std::fwrite(text, 1, std::strlen(text), stderr); fflush(stderr); }