#include "profiler_internal.h" #include "Engine/Core/Log/logging.h" #include #include #include #include #include #include #include #include #include #include #include #include // The stack sampler. A sample suspends the target thread, takes its context and walks its stack, // then resumes it. While the target is suspended this thread must not take any lock the target // might hold -- the heap, the loader, DbgHelp -- or both stop for good. So the walk uses only // memory prepared beforehand: each module's own unwind table (found by binary search, not // RtlLookupFunctionEntry, which locks) and RtlVirtualUnwind, which only reads. Names are looked // up after sampling ends. namespace dingosdk::profiler { namespace { constexpr std::uint32_t max_depth = 96; constexpr std::uintptr_t ghidra_image_base = 0x140000000; struct Module { std::uintptr_t base{}, end{}; const RUNTIME_FUNCTION* functions{}; std::uint32_t count{}; std::wstring path; std::string name; bool exe{}; }; struct Modules { std::vector list; // sorted by base const Module* find(std::uintptr_t pc) const noexcept { std::size_t lo = 0, hi = list.size(); while (lo < hi) { const auto mid = (lo + hi) / 2; if (list[mid].end <= pc) lo = mid + 1; else hi = mid; } return lo < list.size() && list[lo].base <= pc && pc < list[lo].end ? &list[lo] : nullptr; } }; std::string narrow(std::wstring_view text) { std::string result; for (const auto c : text) result.push_back(c < 128 ? static_cast(c) : '?'); return result; } Modules snapshot_modules() { Modules result; std::vector handles(1024); DWORD needed{}; if (!EnumProcessModulesEx(GetCurrentProcess(), handles.data(), static_cast(handles.size() * sizeof(HMODULE)), &needed, LIST_MODULES_ALL)) return result; handles.resize(std::min(handles.size(), needed / sizeof(HMODULE))); const auto exe = GetModuleHandleW(nullptr); for (const auto handle : handles) { MODULEINFO info{}; if (!GetModuleInformation(GetCurrentProcess(), handle, &info, sizeof(info))) continue; Module module; module.base = reinterpret_cast(info.lpBaseOfDll); module.end = module.base + info.SizeOfImage; module.exe = handle == exe; wchar_t path[MAX_PATH]{}; if (GetModuleFileNameW(handle, path, MAX_PATH)) { module.path = path; const auto slash = module.path.find_last_of(L"\\/"); module.name = narrow(slash == std::wstring::npos ? module.path : module.path.substr(slash + 1)); } const auto* dos = reinterpret_cast(module.base); const auto* nt = reinterpret_cast(module.base + dos->e_lfanew); if (dos->e_magic == IMAGE_DOS_SIGNATURE && nt->Signature == IMAGE_NT_SIGNATURE && nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC && nt->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_EXCEPTION) { const auto& directory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION]; if (directory.VirtualAddress && directory.Size && directory.VirtualAddress + directory.Size <= info.SizeOfImage) { module.functions = reinterpret_cast(module.base + directory.VirtualAddress); module.count = directory.Size / sizeof(RUNTIME_FUNCTION); } } result.list.push_back(std::move(module)); } std::ranges::sort(result.list, {}, &Module::base); return result; } const RUNTIME_FUNCTION* lookup(const Module& module, std::uintptr_t pc) noexcept { if (!module.functions) return nullptr; const auto rva = static_cast(pc - module.base); std::uint32_t lo = 0, hi = module.count; while (lo < hi) { const auto mid = (lo + hi) / 2; if (module.functions[mid].EndAddress <= rva) lo = mid + 1; else hi = mid; } return lo < module.count && module.functions[lo].BeginAddress <= rva ? &module.functions[lo] : nullptr; } // Unwinds a suspended thread's context into frames. No allocation, no locks; a fault on a // corrupt stack just ends the walk. std::uint32_t walk(const Modules& modules, CONTEXT& context, std::uint64_t* frames) noexcept { std::uint32_t depth = 0; __try { while (depth < max_depth && context.Rip) { const auto pc = context.Rip, sp = context.Rsp; frames[depth++] = pc; const auto* module = modules.find(pc); if (!module) break; // Past the first frame the PC is a return address, which can sit just after the end of // its function: look it up one byte back. const auto* entry = lookup(*module, depth > 1 ? pc - 1 : pc); if (!entry) { // A leaf function: no frame, the return address is on top of the stack. context.Rip = *reinterpret_cast(context.Rsp); context.Rsp += 8; } else { void* handler_data{}; DWORD64 establisher{}; RtlVirtualUnwind(UNW_FLAG_NHANDLER, module->base, pc, const_cast(entry), &context, &handler_data, &establisher, nullptr); } if (context.Rsp <= sp) break; // a stack only unwinds upwards } } __except (EXCEPTION_EXECUTE_HANDLER) {} return depth; } // The function a frame belongs to: its primary unwind entry (chained entries describe parts of // the same function) -- the start Ghidra shows too. std::uintptr_t function_start(const Modules& modules, std::uintptr_t pc, const Module** owner) noexcept { const auto* module = modules.find(pc); if (owner) *owner = module; if (!module) return pc; const auto* entry = lookup(*module, pc); for (int guard = 0; entry && guard < 32; ++guard) { const auto* info = reinterpret_cast(module->base + entry->UnwindData); if (!(info[0] >> 3 & UNW_FLAG_CHAININFO)) break; const auto codes = info[2]; entry = reinterpret_cast(info + 4 + ((codes + 1) & ~1) * 2); } return entry ? module->base + entry->BeginAddress : pc; } // ---- names ---- std::mutex labels_mutex; std::vector