mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: add missing symbol store and PDB debug info sources
These files were referenced in CMakeLists.txt and main.cpp but never committed, breaking the CI build.
This commit is contained in:
@@ -147,6 +147,12 @@ add_executable(Reclass
|
|||||||
src/mcp/mcp_bridge.cpp
|
src/mcp/mcp_bridge.cpp
|
||||||
src/addressparser.h
|
src/addressparser.h
|
||||||
src/addressparser.cpp
|
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.h
|
||||||
src/disasm.cpp
|
src/disasm.cpp
|
||||||
third_party/fadec/decode.c
|
third_party/fadec/decode.c
|
||||||
@@ -415,7 +421,7 @@ if(BUILD_TESTING)
|
|||||||
if(BUILD_UI_TESTS)
|
if(BUILD_UI_TESTS)
|
||||||
|
|
||||||
add_executable(test_controller tests/test_controller.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_controller COMMAND test_controller)
|
||||||
|
|
||||||
add_executable(test_context_menu tests/test_context_menu.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_context_menu COMMAND test_context_menu)
|
||||||
|
|
||||||
add_executable(test_source_management tests/test_source_management.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_rendered_view COMMAND test_rendered_view)
|
||||||
|
|
||||||
add_executable(test_type_selector tests/test_type_selector.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_type_selector COMMAND test_type_selector)
|
||||||
|
|
||||||
add_executable(test_type_visibility tests/test_type_visibility.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
|
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_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||||
|
|
||||||
add_executable(test_source_provider tests/test_source_provider.cpp
|
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/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
|
||||||
src/typeselectorpopup.cpp
|
src/typeselectorpopup.cpp
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS}
|
||||||
|
|||||||
193
src/imports/pe_debug_info.cpp
Normal file
193
src/imports/pe_debug_info.cpp
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#include "pe_debug_info.h"
|
||||||
|
#include "../providers/provider.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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
|
||||||
20
src/imports/pe_debug_info.h
Normal file
20
src/imports/pe_debug_info.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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
|
||||||
123
src/symbol_downloader.cpp
Normal file
123
src/symbol_downloader.cpp
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#include "symbol_downloader.h"
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
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<int>(received), static_cast<int>(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
|
||||||
50
src/symbol_downloader.h
Normal file
50
src/symbol_downloader.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
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
|
||||||
171
src/symbolstore.cpp
Normal file
171
src/symbolstore.cpp
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
#include "symbolstore.h"
|
||||||
|
#include "providers/provider.h"
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
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<QPair<QString, uint32_t>>& 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<uint32_t>(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<uint32_t, QString>& 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
|
||||||
95
src/symbolstore.h
Normal file
95
src/symbolstore.h
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QPair>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
class Provider; // forward declaration
|
||||||
|
|
||||||
|
struct PdbSymbolSet {
|
||||||
|
QString pdbPath;
|
||||||
|
QString moduleName; // canonical lowercase name (e.g. "ntoskrnl")
|
||||||
|
QHash<QString, uint32_t> nameToRva;
|
||||||
|
QVector<QPair<uint32_t, QString>> 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<QPair<QString, uint32_t>>& 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<QString, PdbSymbolSet> m_modules; // canonical lowercase name → symbol set
|
||||||
|
QHash<QString, QString> m_aliases; // alias → canonical name
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
Reference in New Issue
Block a user