diff --git a/CMakeLists.txt b/CMakeLists.txt index 678bd3f..510329f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,12 @@ add_executable(Reclass src/mcp/mcp_bridge.cpp src/addressparser.h src/addressparser.cpp + src/symbolstore.h + src/symbolstore.cpp + src/symbol_downloader.h + src/symbol_downloader.cpp + src/imports/pe_debug_info.h + src/imports/pe_debug_info.cpp src/disasm.h src/disasm.cpp third_party/fadec/decode.c @@ -415,7 +421,7 @@ if(BUILD_TESTING) if(BUILD_UI_TESTS) add_executable(test_controller tests/test_controller.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -429,7 +435,7 @@ if(BUILD_TESTING) add_test(NAME test_controller COMMAND test_controller) add_executable(test_context_menu tests/test_context_menu.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -443,7 +449,7 @@ if(BUILD_TESTING) add_test(NAME test_context_menu COMMAND test_context_menu) add_executable(test_source_management tests/test_source_management.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -475,7 +481,7 @@ if(BUILD_TESTING) add_test(NAME test_rendered_view COMMAND test_rendered_view) add_executable(test_type_selector tests/test_type_selector.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -489,7 +495,7 @@ if(BUILD_TESTING) add_test(NAME test_type_selector COMMAND test_type_selector) add_executable(test_type_visibility tests/test_type_visibility.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}) @@ -509,7 +515,7 @@ if(BUILD_TESTING) add_test(NAME test_options_dialog COMMAND test_options_dialog) add_executable(test_source_provider tests/test_source_provider.cpp - src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp + src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp src/controller.cpp src/symbolstore.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp src/typeselectorpopup.cpp src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS} diff --git a/src/imports/pe_debug_info.cpp b/src/imports/pe_debug_info.cpp new file mode 100644 index 0000000..e7fc04c --- /dev/null +++ b/src/imports/pe_debug_info.cpp @@ -0,0 +1,193 @@ +#include "pe_debug_info.h" +#include "../providers/provider.h" +#include + +namespace rcx { + +// Minimal PE structures (no Windows SDK dependency) +#pragma pack(push, 1) +struct DosHeader { + uint16_t e_magic; // 'MZ' + uint8_t pad[58]; + int32_t e_lfanew; // offset to PE signature +}; + +struct CoffHeader { + uint16_t Machine; + uint16_t NumberOfSections; + uint32_t TimeDateStamp; + uint32_t PointerToSymbolTable; + uint32_t NumberOfSymbols; + uint16_t SizeOfOptionalHeader; + uint16_t Characteristics; +}; + +struct DataDirectory { + uint32_t VirtualAddress; + uint32_t Size; +}; + +// Only the fields we need from the optional header +struct OptionalHeader32 { + uint16_t Magic; // 0x10b = PE32, 0x20b = PE32+ + uint8_t pad[90]; + uint32_t NumberOfRvaAndSizes; + // DataDirectory[0] = Export, [1] = Import, ... [6] = Debug +}; + +struct OptionalHeader64 { + uint16_t Magic; // 0x20b = PE32+ + uint8_t pad[106]; + uint32_t NumberOfRvaAndSizes; +}; + +struct DebugDirectory { + uint32_t Characteristics; + uint32_t TimeDateStamp; + uint16_t MajorVersion; + uint16_t MinorVersion; + uint32_t Type; + uint32_t SizeOfData; + uint32_t AddressOfRawData; // RVA when loaded + uint32_t PointerToRawData; // file offset (not used for memory reads) +}; + +struct CvInfoPdb70 { + uint32_t Signature; // 'RSDS' + uint8_t Guid[16]; + uint32_t Age; + // char PdbFileName[] follows +}; +#pragma pack(pop) + +static constexpr uint16_t kMZ = 0x5A4D; +static constexpr uint32_t kPE = 0x00004550; +static constexpr uint16_t kPE32 = 0x10b; +static constexpr uint16_t kPE32P = 0x20b; +static constexpr uint32_t kRSDS = 0x53445352; +static constexpr uint32_t kDebugType_CodeView = 2; + +static QString guidToString(const uint8_t guid[16]) { + // Windows GUID is mixed-endian: Data1(4B LE), Data2(2B LE), Data3(2B LE), Data4(8B sequential) + // MS symbol server expects native integer values for Data1/2/3, sequential for Data4 + uint32_t d1; memcpy(&d1, guid, 4); + uint16_t d2; memcpy(&d2, guid + 4, 2); + uint16_t d3; memcpy(&d3, guid + 6, 2); + QString s = QStringLiteral("%1%2%3") + .arg(d1, 8, 16, QLatin1Char('0')) + .arg(d2, 4, 16, QLatin1Char('0')) + .arg(d3, 4, 16, QLatin1Char('0')); + for (int i = 8; i < 16; i++) + s += QStringLiteral("%1").arg(guid[i], 2, 16, QLatin1Char('0')); + return s.toUpper(); +} + +PdbDebugInfo extractPdbDebugInfo(const Provider& prov, uint64_t moduleBase) { + PdbDebugInfo result; + + // Read DOS header + DosHeader dos; + if (!prov.read(moduleBase, &dos, sizeof(dos))) + return result; + if (dos.e_magic != kMZ) + return result; + + uint64_t peOffset = moduleBase + dos.e_lfanew; + + // Read PE signature + uint32_t peSig = 0; + if (!prov.read(peOffset, &peSig, 4)) + return result; + if (peSig != kPE) + return result; + + // Read COFF header + uint64_t coffOffset = peOffset + 4; + CoffHeader coff; + if (!prov.read(coffOffset, &coff, sizeof(coff))) + return result; + + // Read optional header magic to determine PE32 vs PE32+ + uint64_t optOffset = coffOffset + sizeof(CoffHeader); + uint16_t optMagic = 0; + if (!prov.read(optOffset, &optMagic, 2)) + return result; + + // Locate debug data directory (index 6) + uint32_t numRvaAndSizes = 0; + uint64_t dataDirsOffset = 0; + + if (optMagic == kPE32) { + // PE32: NumberOfRvaAndSizes at offset 92, data dirs at offset 96 + if (!prov.read(optOffset + 92, &numRvaAndSizes, 4)) + return result; + dataDirsOffset = optOffset + 96; + } else if (optMagic == kPE32P) { + // PE32+: NumberOfRvaAndSizes at offset 108, data dirs at offset 112 + if (!prov.read(optOffset + 108, &numRvaAndSizes, 4)) + return result; + dataDirsOffset = optOffset + 112; + } else { + return result; + } + + if (numRvaAndSizes <= 6) + return result; // no debug directory + + DataDirectory debugDir; + if (!prov.read(dataDirsOffset + 6 * sizeof(DataDirectory), &debugDir, sizeof(debugDir))) + return result; + + if (debugDir.VirtualAddress == 0 || debugDir.Size == 0) + return result; + + // Read debug directory entries + int numEntries = debugDir.Size / sizeof(DebugDirectory); + for (int i = 0; i < numEntries; i++) { + DebugDirectory entry; + uint64_t entryAddr = moduleBase + debugDir.VirtualAddress + i * sizeof(DebugDirectory); + if (!prov.read(entryAddr, &entry, sizeof(entry))) + continue; + + if (entry.Type != kDebugType_CodeView) + continue; + + // Read CodeView info (RSDS) + if (entry.AddressOfRawData == 0 || entry.SizeOfData < sizeof(CvInfoPdb70) + 1) + continue; + + CvInfoPdb70 cv; + uint64_t cvAddr = moduleBase + entry.AddressOfRawData; + if (!prov.read(cvAddr, &cv, sizeof(cv))) + continue; + + if (cv.Signature != kRSDS) + continue; + + // Read PDB filename (null-terminated string after the struct) + int nameMaxLen = entry.SizeOfData - sizeof(CvInfoPdb70); + if (nameMaxLen > 260) nameMaxLen = 260; + char nameBuf[261] = {}; + if (!prov.read(cvAddr + sizeof(CvInfoPdb70), nameBuf, nameMaxLen)) + continue; + nameBuf[nameMaxLen] = '\0'; + + result.pdbName = QString::fromLatin1(nameBuf); + // Extract just the filename if it contains a path + int lastSlash = result.pdbName.lastIndexOf('\\'); + if (lastSlash >= 0) + result.pdbName = result.pdbName.mid(lastSlash + 1); + int lastFwdSlash = result.pdbName.lastIndexOf('/'); + if (lastFwdSlash >= 0) + result.pdbName = result.pdbName.mid(lastFwdSlash + 1); + + result.guidString = guidToString(cv.Guid); + result.age = cv.Age; + result.valid = true; + return result; + } + + return result; +} + +} // namespace rcx diff --git a/src/imports/pe_debug_info.h b/src/imports/pe_debug_info.h new file mode 100644 index 0000000..6a26c3e --- /dev/null +++ b/src/imports/pe_debug_info.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +namespace rcx { + +class Provider; + +struct PdbDebugInfo { + QString pdbName; // e.g. "ntoskrnl.pdb" + QString guidString; // 32 hex chars, no dashes, uppercase + uint32_t age = 0; + bool valid = false; +}; + +// Extract PDB debug info (GUID, age, filename) from a PE module in memory. +// Reads DOS header → PE header → debug directory → CodeView RSDS record. +PdbDebugInfo extractPdbDebugInfo(const Provider& prov, uint64_t moduleBase); + +} // namespace rcx diff --git a/src/symbol_downloader.cpp b/src/symbol_downloader.cpp new file mode 100644 index 0000000..ba13bbd --- /dev/null +++ b/src/symbol_downloader.cpp @@ -0,0 +1,123 @@ +#include "symbol_downloader.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rcx { + +SymbolDownloader::SymbolDownloader(QObject* parent) + : QObject(parent) + , m_nam(new QNetworkAccessManager(this)) +{ +} + +QString SymbolDownloader::cacheDir() { + QString base = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + return base + QStringLiteral("/SymbolCache"); +} + +QString SymbolDownloader::findCached(const DownloadRequest& req) const { + // Cache layout: cacheDir/pdbName/GUID+age/pdbName + QString path = cacheDir() + QStringLiteral("/%1/%2%3/%1") + .arg(req.pdbName, req.guidString, QString::number(req.age, 16)); + if (QFile::exists(path)) + return path; + return {}; +} + +QString SymbolDownloader::findLocal(const QString& moduleFullPath, const QString& pdbName) { + if (moduleFullPath.isEmpty() || pdbName.isEmpty()) + return {}; + // Check same directory as the module + QString dir = QFileInfo(moduleFullPath).absolutePath(); + QString candidate = dir + QStringLiteral("/") + pdbName; + if (QFile::exists(candidate)) + return candidate; + return {}; +} + +void SymbolDownloader::download(const DownloadRequest& req) { + // URL: https://msdl.microsoft.com/download/symbols/{pdbName}/{GUID}{age}/{pdbName} + QString url = QStringLiteral("https://msdl.microsoft.com/download/symbols/%1/%2%3/%1") + .arg(req.pdbName, req.guidString, QString::number(req.age, 16)); + + QUrl reqUrl(url); + QNetworkRequest netReq(reqUrl); + netReq.setHeader(QNetworkRequest::UserAgentHeader, + QStringLiteral("Microsoft-Symbol-Server/10.0.0.0")); + netReq.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + + cancel(); // cancel any previous + m_activeReply = m_nam->get(netReq); + + QString moduleName = req.moduleName; + QString pdbName = req.pdbName; + QString guidString = req.guidString; + uint32_t age = req.age; + + connect(m_activeReply, &QNetworkReply::downloadProgress, + this, [this, moduleName](qint64 received, qint64 total) { + emit progress(moduleName, static_cast(received), static_cast(total)); + }); + + connect(m_activeReply, &QNetworkReply::finished, + this, [this, moduleName, pdbName, guidString, age]() { + auto* reply = m_activeReply; + m_activeReply = nullptr; + + if (!reply) return; + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + emit finished(moduleName, {}, false, + QStringLiteral("Download failed: %1").arg(reply->errorString())); + return; + } + + int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (httpStatus != 200) { + emit finished(moduleName, {}, false, + QStringLiteral("HTTP %1").arg(httpStatus)); + return; + } + + QByteArray data = reply->readAll(); + if (data.isEmpty()) { + emit finished(moduleName, {}, false, QStringLiteral("Empty response")); + return; + } + + // Save to cache + QString dir = cacheDir() + QStringLiteral("/%1/%2%3") + .arg(pdbName, guidString, QString::number(age, 16)); + QDir().mkpath(dir); + QString path = dir + QStringLiteral("/") + pdbName; + + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + emit finished(moduleName, {}, false, + QStringLiteral("Cannot write: %1").arg(f.errorString())); + return; + } + f.write(data); + f.close(); + + emit finished(moduleName, path, true, {}); + }); +} + +void SymbolDownloader::cancel() { + if (m_activeReply) { + m_activeReply->abort(); + m_activeReply->deleteLater(); + m_activeReply = nullptr; + } +} + +} // namespace rcx diff --git a/src/symbol_downloader.h b/src/symbol_downloader.h new file mode 100644 index 0000000..b6406e5 --- /dev/null +++ b/src/symbol_downloader.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include + +class QNetworkAccessManager; +class QNetworkReply; + +namespace rcx { + +class SymbolDownloader : public QObject { + Q_OBJECT +public: + explicit SymbolDownloader(QObject* parent = nullptr); + + struct DownloadRequest { + QString moduleName; // display name (e.g. "ntoskrnl.exe") + QString pdbName; // PDB filename (e.g. "ntoskrnl.pdb") + QString guidString; // 32 hex chars, no dashes + uint32_t age = 0; + }; + + // Check if PDB exists in local cache. Returns path or empty. + QString findCached(const DownloadRequest& req) const; + + // Check if PDB exists next to the module on disk. Returns path or empty. + static QString findLocal(const QString& moduleFullPath, const QString& pdbName); + + // Start downloading a PDB from MS symbol server. + // Emits finished() when done (success or failure). + void download(const DownloadRequest& req); + + // Cancel any in-progress download. + void cancel(); + + // Local symbol cache directory. + static QString cacheDir(); + +signals: + void progress(const QString& moduleName, int bytesReceived, int bytesTotal); + void finished(const QString& moduleName, const QString& localPath, + bool success, const QString& error); + +private: + QNetworkAccessManager* m_nam = nullptr; + QNetworkReply* m_activeReply = nullptr; +}; + +} // namespace rcx diff --git a/src/symbolstore.cpp b/src/symbolstore.cpp new file mode 100644 index 0000000..0ba3316 --- /dev/null +++ b/src/symbolstore.cpp @@ -0,0 +1,171 @@ +#include "symbolstore.h" +#include "providers/provider.h" +#include + +namespace rcx { + +uint64_t SymbolStore::getModuleBase(const Provider* provider, const QString& canonical) const { + if (!provider) + return 0; + uint64_t base = provider->symbolToAddress(canonical); + if (base == 0) + base = provider->symbolToAddress(canonical + QStringLiteral(".exe")); + if (base == 0) + base = provider->symbolToAddress(canonical + QStringLiteral(".dll")); + if (base == 0) + base = provider->symbolToAddress(canonical + QStringLiteral(".sys")); + return base; +} + +int SymbolStore::addModule(const QString& moduleName, const QString& pdbPath, + const QVector>& symbols) { + QString canonical = resolveAlias(moduleName); + + PdbSymbolSet set; + set.pdbPath = pdbPath; + set.moduleName = canonical; + set.nameToRva.reserve(symbols.size()); + set.rvaToName.reserve(symbols.size()); + + for (const auto& sym : symbols) { + if (set.nameToRva.contains(sym.first)) + continue; + set.nameToRva.insert(sym.first, sym.second); + set.rvaToName.append({sym.second, sym.first}); + } + + set.sortRvaIndex(); + int count = set.nameToRva.size(); + + // Register the raw module name as an alias if it differs from canonical + QString rawLower = moduleName.toLower(); + if (rawLower.endsWith(QStringLiteral(".exe")) || rawLower.endsWith(QStringLiteral(".dll")) || + rawLower.endsWith(QStringLiteral(".sys"))) + rawLower = rawLower.left(rawLower.lastIndexOf('.')); + if (rawLower != canonical) + m_aliases[rawLower] = canonical; + + m_modules[canonical] = std::move(set); + + qDebug() << "[SymbolStore] loaded" << count << "symbols for module" << canonical + << "(from" << pdbPath << ")"; + return count; +} + +void SymbolStore::unloadModule(const QString& moduleName) { + QString canonical = resolveAlias(moduleName); + m_modules.remove(canonical); +} + +uint64_t SymbolStore::resolve(const QString& token, const Provider* provider, bool* ok) const { + *ok = false; + + // Check for "module!symbol" syntax + int bangIdx = token.indexOf('!'); + if (bangIdx > 0 && bangIdx < token.size() - 1) { + QString modPart = token.left(bangIdx); + QString symPart = token.mid(bangIdx + 1); + QString canonical = resolveAlias(modPart); + + auto modIt = m_modules.find(canonical); + if (modIt == m_modules.end()) + return 0; + + auto symIt = modIt->nameToRva.find(symPart); + if (symIt == modIt->nameToRva.end()) + return 0; + + uint32_t rva = *symIt; + uint64_t moduleBase = getModuleBase(provider, canonical); + // Also try the user-supplied module name form + if (moduleBase == 0) + moduleBase = getModuleBase(provider, modPart); + + *ok = true; + return moduleBase + rva; + } + + // Bare symbol — search all loaded modules + uint32_t foundRva = 0; + QString foundModule; + int matches = 0; + + for (auto it = m_modules.begin(); it != m_modules.end(); ++it) { + auto symIt = it->nameToRva.find(token); + if (symIt != it->nameToRva.end()) { + foundRva = *symIt; + foundModule = it.key(); + matches++; + if (matches > 1) + return 0; // ambiguous + } + } + + if (matches == 1) { + uint64_t moduleBase = getModuleBase(provider, foundModule); + *ok = true; + return moduleBase + foundRva; + } + + // Fallback: treat bare token as a module name (e.g. "ntdll" → ntdll base) + if (matches == 0) { + QString canonical = resolveAlias(token); + uint64_t moduleBase = getModuleBase(provider, canonical); + if (moduleBase != 0) { + *ok = true; + return moduleBase; + } + } + + return 0; +} + +QString SymbolStore::getSymbolForAddress(uint64_t addr, const Provider* provider) const { + if (m_modules.isEmpty() || !provider) + return {}; + + for (auto it = m_modules.begin(); it != m_modules.end(); ++it) { + const PdbSymbolSet& set = *it; + + uint64_t moduleBase = getModuleBase(provider, set.moduleName); + if (moduleBase == 0) + continue; + + if (addr < moduleBase) + continue; + + uint32_t rva = static_cast(addr - moduleBase); + + if (set.rvaToName.isEmpty()) + continue; + + // Binary search: find last entry with RVA <= target + auto upper = std::upper_bound(set.rvaToName.begin(), set.rvaToName.end(), rva, + [](uint32_t val, const QPair& entry) { + return val < entry.first; + }); + + if (upper == set.rvaToName.begin()) + continue; + + --upper; + uint32_t displacement = rva - upper->first; + + static constexpr uint32_t kMaxDisplacement = 0x1000; + if (displacement > kMaxDisplacement) + continue; + + if (displacement == 0) + return set.moduleName + QStringLiteral("!") + upper->second; + return set.moduleName + QStringLiteral("!") + upper->second + + QStringLiteral("+0x") + QString::number(displacement, 16); + } + + return {}; +} + +void SymbolStore::addAlias(const QString& alias, const QString& canonicalModule) { + m_aliases[alias.toLower()] = canonicalModule.toLower(); +} + +} // namespace rcx diff --git a/src/symbolstore.h b/src/symbolstore.h new file mode 100644 index 0000000..e1eb545 --- /dev/null +++ b/src/symbolstore.h @@ -0,0 +1,95 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace rcx { + +class Provider; // forward declaration + +struct PdbSymbolSet { + QString pdbPath; + QString moduleName; // canonical lowercase name (e.g. "ntoskrnl") + QHash nameToRva; + QVector> rvaToName; // sorted by RVA for binary search + + void sortRvaIndex() { + std::sort(rvaToName.begin(), rvaToName.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + } +}; + +class SymbolStore { +public: + static SymbolStore& instance() { + static SymbolStore s; + return s; + } + + // Add a pre-extracted symbol set for a module. + // moduleName is the canonical name (e.g. "ntoskrnl"). + // Returns the number of unique symbols stored. + int addModule(const QString& moduleName, const QString& pdbPath, + const QVector>& symbols); + + // Unload symbols for a module. + void unloadModule(const QString& moduleName); + + // Resolve a token from the expression parser. + // Handles "module!symbol" (qualified) and bare "symbol" (unqualified). + // Uses provider->symbolToAddress() to get the module's runtime base address. + uint64_t resolve(const QString& token, const Provider* provider, bool* ok) const; + + // Reverse lookup: given an absolute address and a provider, find the nearest symbol. + // Returns "module!symbol" or "module!symbol+0xN", or empty if no match. + QString getSymbolForAddress(uint64_t addr, const Provider* provider) const; + + // Check if any symbols are loaded. + bool hasSymbols() const { return !m_modules.isEmpty(); } + + // List loaded module names. + QStringList loadedModules() const { return m_modules.keys(); } + + // Number of loaded modules. + int moduleCount() const { return m_modules.size(); } + + // Access module data by name (returns nullptr if not found). + const PdbSymbolSet* moduleData(const QString& moduleName) const { + QString canonical = resolveAlias(moduleName); + auto it = m_modules.find(canonical); + return it != m_modules.end() ? &*it : nullptr; + } + + // Add a module alias (e.g. "nt" → "ntoskrnl"). + void addAlias(const QString& alias, const QString& canonicalModule); + + // Resolve alias to canonical module name (public for callers that need it) + QString resolveAlias(const QString& name) const { + QString lower = name.toLower(); + if (lower.endsWith(QStringLiteral(".exe")) || lower.endsWith(QStringLiteral(".dll")) || + lower.endsWith(QStringLiteral(".sys"))) + lower = lower.left(lower.lastIndexOf('.')); + auto it = m_aliases.find(lower); + return it != m_aliases.end() ? *it : lower; + } + +private: + SymbolStore() { + // Common Windows kernel aliases + m_aliases[QStringLiteral("nt")] = QStringLiteral("ntoskrnl"); + m_aliases[QStringLiteral("ntkrnlmp")] = QStringLiteral("ntoskrnl"); + m_aliases[QStringLiteral("ntkrnlpa")] = QStringLiteral("ntoskrnl"); + m_aliases[QStringLiteral("ntkrpamp")] = QStringLiteral("ntoskrnl"); + } + + // Get the module base address, trying various name forms + uint64_t getModuleBase(const Provider* provider, const QString& canonical) const; + + QHash m_modules; // canonical lowercase name → symbol set + QHash m_aliases; // alias → canonical name +}; + +} // namespace rcx