Compare commits

..

2 Commits

Author SHA1 Message Date
√(noham)²
b4727df3e9 Add macOS support for ProcessMemory plugin
Implement macOS-specific support for the ProcessMemory plugin and update plugin discovery/build.

- Add macOS build/install support: include plugins/ProcessMemory in top-level CMake and copy the built plugin into Reclass.app/Contents/PlugIns on macOS.
- Implement Apple-specific ProcessMemoryProvider: task_for_pid usage, mach_vm_read_overwrite/mach_vm_write, proc_pidpath/proc_regionfilename based module caching, region enumeration, symbol formatting, module enumeration, and proper cleanup (mach_port_deallocate).
- Extend plugin header to track m_task and adjust readability checks for macOS.
- Add macOS handling in getInitialBaseAddress and process enumeration to find base addresses and processes using proc APIs.
- Improve PluginManager to probe multiple plugin locations (including Contents/PlugIns inside macOS bundles), aggregate/log candidate counts, and continue scanning multiple dirs.
- Add macOS screenshot to README (docs/README_PIC6.png) and reference it in README.

These changes enable the ProcessMemory plugin to operate on macOS and make plugin discovery more robust on macOS app bundles.
2026-03-15 14:47:48 +01:00
IChooseYou
dc6963e0d5 feat: extract typeIndex from PDB symbols and add symbols.importType MCP tool
extractPdbSymbols() was reading S_GDATA32/S_GTHREAD32 records which
contain a typeIndex field linking the symbol to its type definition in
the TPI stream, but this field was discarded — only name + RVA were
kept. This meant loading symbols gave you address resolution but no
way to automatically import the type associated with a global variable.

Changes:
- PdbSymbol now carries typeIndex (0 = no type info / public symbol)
- extractPdbSymbols() captures typeIndex from all global data symbols
- PdbSymbolSet stores nameToTypeIndex mapping alongside nameToRva
- New importTypeForSymbol() follows LF_POINTER/LF_MODIFIER chains to
  find the underlying UDT/enum and imports it with full recursive children
- New symbols.importType MCP tool: given "ntdll!g_pShimEngineModule",
  resolves its typeIndex, imports the type definition from the PDB, and
  merges it into the active project
- loadPdbIntoStore() helper consolidates the extract+store pattern with
  type index support
2026-03-14 18:11:57 -06:00
15 changed files with 1055 additions and 49 deletions

View File

@@ -599,8 +599,8 @@ if(BUILD_TESTING)
endif() # BUILD_UI_TESTS endif() # BUILD_UI_TESTS
endif() endif()
add_subdirectory(plugins/ProcessMemory)
if(NOT APPLE) if(NOT APPLE)
add_subdirectory(plugins/ProcessMemory)
add_subdirectory(plugins/RemoteProcessMemory) add_subdirectory(plugins/RemoteProcessMemory)
endif() endif()
if(WIN32) if(WIN32)

View File

@@ -32,6 +32,8 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
![Memory scanner](docs/README_PIC3.png) ![Memory scanner](docs/README_PIC3.png)
![MacOS - Process Memory scanner](docs/README_PIC6.png)
## Features ## Features
### Editor ### Editor

BIN
docs/README_PIC6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

View File

@@ -45,3 +45,12 @@ set_target_properties(ProcessMemoryPlugin PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
) )
if(APPLE AND TARGET Reclass)
add_custom_command(TARGET ProcessMemoryPlugin POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/../PlugIns"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:ProcessMemoryPlugin>"
"$<TARGET_FILE_DIR:Reclass>/../PlugIns/"
COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns")
endif()

View File

@@ -10,6 +10,7 @@
#include <QImage> #include <QImage>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QMap>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32) #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
#include <QtWin> #include <QtWin>
#endif #endif
@@ -83,6 +84,13 @@ typedef struct alignas(8) _THREAD_BASIC_INFORMATION {
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cstring> #include <cstring>
#elif defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <libproc.h>
#include <sys/proc_info.h>
#include <unistd.h>
#include <cstring>
#endif #endif
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
@@ -476,8 +484,239 @@ QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
return regions; return regions;
} }
#elif defined(__APPLE__)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_task(0)
, m_pid(pid)
, m_processName(processName)
, m_writable(false)
, m_base(0)
{
mach_port_t task = MACH_PORT_NULL;
kern_return_t kr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
if (kr != KERN_SUCCESS || task == MACH_PORT_NULL)
return;
m_task = static_cast<uint32_t>(task);
m_writable = true;
proc_bsdinfo bsdInfo{};
int infoLen = proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
if (infoLen == (int)sizeof(bsdInfo)) {
#ifdef PROC_FLAG_LP64
m_pointerSize = (bsdInfo.pbi_flags & PROC_FLAG_LP64) ? 8 : 4;
#else
m_pointerSize = 8;
#endif
}
cacheModules();
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_task == 0 || len <= 0)
return false;
mach_vm_size_t outSize = 0;
kern_return_t kr = mach_vm_read_overwrite(
static_cast<mach_port_name_t>(m_task),
static_cast<mach_vm_address_t>(addr),
static_cast<mach_vm_size_t>(len),
reinterpret_cast<mach_vm_address_t>(buf),
&outSize);
if ((int)outSize < len)
memset((char*)buf + outSize, 0, len - outSize);
return kr == KERN_SUCCESS && outSize > 0;
}
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_task == 0 || !m_writable || len <= 0)
return false;
kern_return_t kr = mach_vm_write(
static_cast<mach_port_name_t>(m_task),
static_cast<mach_vm_address_t>(addr),
reinterpret_cast<vm_offset_t>(const_cast<void*>(buf)),
static_cast<mach_msg_type_number_t>(len));
return kr == KERN_SUCCESS;
}
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
void ProcessMemoryProvider::cacheModules()
{
if (m_task == 0)
return;
m_modules.clear();
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
QString mainPath;
if (proc_pidpath((int)m_pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
mainPath = QString::fromUtf8(mainPathBuf);
struct Range { uint64_t base; uint64_t end; };
QMap<QString, Range> moduleRanges;
mach_vm_address_t addr = 0;
uint32_t depth = 0;
for (;;) {
mach_vm_size_t size = 0;
vm_region_submap_info_data_64_t info{};
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
kern_return_t kr = mach_vm_region_recurse(
static_cast<mach_port_name_t>(m_task),
&addr,
&size,
&depth,
reinterpret_cast<vm_region_recurse_info_t>(&info),
&count);
if (kr != KERN_SUCCESS)
break;
if (info.is_submap) {
++depth;
continue;
}
if (size == 0) {
++addr;
continue;
}
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
int pathLen = proc_regionfilename((int)m_pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
if (pathLen > 0) {
QString fullPath = QString::fromUtf8(pathBuf);
uint64_t regionBase = (uint64_t)addr;
uint64_t regionEnd = regionBase + (uint64_t)size;
auto it = moduleRanges.find(fullPath);
if (it == moduleRanges.end()) {
moduleRanges.insert(fullPath, {regionBase, regionEnd});
} else {
if (regionBase < it->base) it->base = regionBase;
if (regionEnd > it->end) it->end = regionEnd;
}
if (m_base == 0 && !mainPath.isEmpty() && fullPath == mainPath && (info.protection & VM_PROT_EXECUTE))
m_base = regionBase;
}
uint64_t next = (uint64_t)addr + (uint64_t)size;
if (next <= (uint64_t)addr)
break;
addr = (mach_vm_address_t)next;
}
m_modules.reserve(moduleRanges.size());
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
{
QFileInfo fi(it.key());
m_modules.push_back(ModuleInfo{
fi.fileName(),
it.key(),
it->base,
it->end - it->base
});
}
if (m_base == 0 && !m_modules.isEmpty())
m_base = m_modules.front().base;
}
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
{
QVector<rcx::MemoryRegion> regions;
if (m_task == 0)
return regions;
mach_vm_address_t addr = 0;
uint32_t depth = 0;
for (;;) {
mach_vm_size_t size = 0;
vm_region_submap_info_data_64_t info{};
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
kern_return_t kr = mach_vm_region_recurse(
static_cast<mach_port_name_t>(m_task),
&addr,
&size,
&depth,
reinterpret_cast<vm_region_recurse_info_t>(&info),
&count);
if (kr != KERN_SUCCESS)
break;
if (info.is_submap) {
++depth;
continue;
}
if (size == 0) {
++addr;
continue;
}
bool readable = (info.protection & VM_PROT_READ) != 0;
if (readable)
{
rcx::MemoryRegion region;
region.base = (uint64_t)addr;
region.size = (uint64_t)size;
region.readable = readable;
region.writable = (info.protection & VM_PROT_WRITE) != 0;
region.executable = (info.protection & VM_PROT_EXECUTE) != 0;
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
int pathLen = proc_regionfilename((int)m_pid, region.base, pathBuf, sizeof(pathBuf));
if (pathLen > 0) {
QFileInfo fi(QString::fromUtf8(pathBuf));
region.moduleName = fi.fileName();
}
regions.append(region);
}
uint64_t next = (uint64_t)addr + (uint64_t)size;
if (next <= (uint64_t)addr)
break;
addr = (mach_vm_address_t)next;
}
return regions;
}
#endif // platform #endif // platform
#ifndef _WIN32
QVector<rcx::Provider::ModuleEntry> ProcessMemoryProvider::enumerateModules() const
{
QVector<ModuleEntry> result;
result.reserve(m_modules.size());
for (const auto& m : m_modules)
result.push_back(ModuleEntry{m.name, m.fullPath, m.base, m.size});
return result;
}
#endif
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
{ {
for (const auto& mod : m_modules) { for (const auto& mod : m_modules) {
@@ -495,6 +734,9 @@ ProcessMemoryProvider::~ProcessMemoryProvider()
#elif defined(__linux__) #elif defined(__linux__)
if (m_fd >= 0) if (m_fd >= 0)
::close(m_fd); ::close(m_fd);
#elif defined(__APPLE__)
if (m_task != 0)
mach_port_deallocate(mach_task_self(), static_cast<mach_port_name_t>(m_task));
#endif #endif
} }
@@ -504,6 +746,8 @@ int ProcessMemoryProvider::size() const
return m_handle ? 0x10000 : 0; return m_handle ? 0x10000 : 0;
#elif defined(__linux__) #elif defined(__linux__)
return (m_fd >= 0) ? 0x10000 : 0; return (m_fd >= 0) ? 0x10000 : 0;
#elif defined(__APPLE__)
return (m_task != 0) ? 0x10000 : 0;
#endif #endif
} }
@@ -654,6 +898,68 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
} }
} }
return 0; return 0;
#elif defined(__APPLE__)
QStringList parts = target.split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0)
return 0;
mach_port_t task = MACH_PORT_NULL;
kern_return_t tkr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
if (tkr != KERN_SUCCESS || task == MACH_PORT_NULL)
return 0;
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
QString mainPath;
if (proc_pidpath((int)pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
mainPath = QString::fromUtf8(mainPathBuf);
uint64_t base = 0;
mach_vm_address_t addr = 0;
uint32_t depth = 0;
for (;;) {
mach_vm_size_t size = 0;
vm_region_submap_info_data_64_t info{};
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
kern_return_t kr = mach_vm_region_recurse(task, &addr, &size, &depth,
reinterpret_cast<vm_region_recurse_info_t>(&info),
&count);
if (kr != KERN_SUCCESS)
break;
if (info.is_submap) {
++depth;
continue;
}
if (size == 0) {
++addr;
continue;
}
if ((info.protection & VM_PROT_EXECUTE) != 0) {
if (!mainPath.isEmpty()) {
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
int pathLen = proc_regionfilename((int)pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
if (pathLen > 0 && QString::fromUtf8(pathBuf) == mainPath) {
base = (uint64_t)addr;
break;
}
} else {
base = (uint64_t)addr;
break;
}
}
uint64_t next = (uint64_t)addr + (uint64_t)size;
if (next <= (uint64_t)addr)
break;
addr = (mach_vm_address_t)next;
}
mach_port_deallocate(mach_task_self(), task);
return base;
#else #else
Q_UNUSED(target); Q_UNUSED(target);
return 0; return 0;
@@ -797,6 +1103,61 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
::close(exeFd); ::close(exeFd);
} }
processes.append(info);
}
#elif defined(__APPLE__)
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
int bytes = proc_listpids(PROC_ALL_PIDS, 0, nullptr, 0);
if (bytes <= 0)
return processes;
int count = bytes / (int)sizeof(pid_t);
QVector<pid_t> pids(count);
bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), count * (int)sizeof(pid_t));
if (bytes <= 0)
return processes;
count = bytes / (int)sizeof(pid_t);
for (int i = 0; i < count; ++i) {
pid_t pid = pids[i];
if (pid <= 0)
continue;
mach_port_t task = MACH_PORT_NULL;
if (task_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS || task == MACH_PORT_NULL)
continue;
mach_port_deallocate(mach_task_self(), task);
char nameBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
int nameLen = proc_name(pid, nameBuf, sizeof(nameBuf));
QString procName;
if (nameLen > 0)
procName = QString::fromUtf8(nameBuf);
if (procName.isEmpty())
continue;
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
QString procPath;
if (proc_pidpath(pid, pathBuf, sizeof(pathBuf)) > 0)
procPath = QString::fromUtf8(pathBuf);
PluginProcessInfo info;
info.pid = static_cast<uint32_t>(pid);
info.name = procName;
info.path = procPath;
info.icon = defaultIcon;
proc_bsdinfo bsdInfo{};
int infoLen = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
if (infoLen == (int)sizeof(bsdInfo)) {
#ifdef PROC_FLAG_LP64
info.is32Bit = (bsdInfo.pbi_flags & PROC_FLAG_LP64) == 0;
#else
info.is32Bit = false;
#endif
}
processes.append(info); processes.append(info);
} }
#endif #endif

View File

@@ -35,6 +35,8 @@ public:
return m_handle && len >= 0; return m_handle && len >= 0;
#elif defined(__linux__) #elif defined(__linux__)
return m_fd >= 0 && len >= 0; return m_fd >= 0 && len >= 0;
#elif defined(__APPLE__)
return m_task != 0 && len >= 0;
#endif #endif
} }
@@ -53,6 +55,8 @@ private:
void* m_handle; void* m_handle;
#elif defined(__linux__) #elif defined(__linux__)
int m_fd; int m_fd;
#elif defined(__APPLE__)
uint32_t m_task;
#endif #endif
uint32_t m_pid; uint32_t m_pid;
QString m_processName; QString m_processName;

View File

@@ -1022,21 +1022,26 @@ PdbSymbolResult extractPdbSymbols(const QString& pdbPath, QString* errorMsg) {
const char* name = nullptr; const char* name = nullptr;
uint32_t rva = 0u; uint32_t rva = 0u;
uint32_t typeIdx = 0u;
if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32) { if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32) {
name = record->data.S_GDATA32.name; name = record->data.S_GDATA32.name;
typeIdx = record->data.S_GDATA32.typeIndex;
rva = imageSectionStream.ConvertSectionOffsetToRVA( rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_GDATA32.section, record->data.S_GDATA32.offset); record->data.S_GDATA32.section, record->data.S_GDATA32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32) { } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32) {
name = record->data.S_GTHREAD32.name; name = record->data.S_GTHREAD32.name;
typeIdx = record->data.S_GTHREAD32.typeIndex;
rva = imageSectionStream.ConvertSectionOffsetToRVA( rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset); record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32) { } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32) {
name = record->data.S_LDATA32.name; name = record->data.S_LDATA32.name;
typeIdx = record->data.S_LDATA32.typeIndex;
rva = imageSectionStream.ConvertSectionOffsetToRVA( rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_LDATA32.section, record->data.S_LDATA32.offset); record->data.S_LDATA32.section, record->data.S_LDATA32.offset);
} else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32) { } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32) {
name = record->data.S_LTHREAD32.name; name = record->data.S_LTHREAD32.name;
typeIdx = record->data.S_LTHREAD32.typeIndex;
rva = imageSectionStream.ConvertSectionOffsetToRVA( rva = imageSectionStream.ConvertSectionOffsetToRVA(
record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset); record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset);
} }
@@ -1046,7 +1051,7 @@ PdbSymbolResult extractPdbSymbols(const QString& pdbPath, QString* errorMsg) {
if (!name || name[0] == '\0') if (!name || name[0] == '\0')
continue; continue;
result.symbols.push_back(PdbSymbol{QString::fromUtf8(name), rva}); result.symbols.push_back(PdbSymbol{QString::fromUtf8(name), rva, typeIdx});
} }
} }
@@ -1232,6 +1237,119 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString*
return ctx.tree; return ctx.tree;
} }
// ── Public API: importTypeForSymbol ──
NodeTree importTypeForSymbol(const QString& pdbPath,
uint32_t typeIndex,
QString* typeName,
QString* errorMsg) {
auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; };
if (typeIndex == 0) {
setErr(QStringLiteral("Symbol has no associated type (typeIndex=0)"));
return {};
}
PdbFile pdb;
if (!pdb.open(pdbPath, errorMsg)) return {};
const TypeTable& tt = *pdb.typeTable;
// Walk through LF_MODIFIER and LF_POINTER chains to find the underlying UDT/enum
uint32_t ti = typeIndex;
int depth = 0;
while (ti >= tt.firstIndex() && depth < 16) {
const auto* rec = tt.get(ti);
if (!rec) break;
if (rec->header.kind == TRK::LF_MODIFIER) {
ti = rec->data.LF_MODIFIER.type;
depth++;
continue;
}
if (rec->header.kind == TRK::LF_POINTER) {
ti = rec->data.LF_POINTER.utype;
depth++;
continue;
}
break; // reached a non-wrapper type
}
// Check if we landed on a UDT or enum
if (ti < tt.firstIndex()) {
setErr(QStringLiteral("Symbol type resolves to a primitive (typeIndex %1)")
.arg(typeIndex));
return {};
}
const auto* rec = tt.get(ti);
if (!rec) {
setErr(QStringLiteral("Invalid type index %1").arg(ti));
return {};
}
bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE ||
rec->header.kind == TRK::LF_CLASS ||
rec->header.kind == TRK::LF_UNION);
bool isEnum = (rec->header.kind == TRK::LF_ENUM);
if (!isUDT && !isEnum) {
setErr(QStringLiteral("Symbol type is not a struct/class/union/enum (kind 0x%1)")
.arg((uint16_t)rec->header.kind, 0, 16));
return {};
}
// Extract the type name for the caller
if (typeName) {
const char* name = nullptr;
if (isEnum) {
name = rec->data.LF_ENUM.name;
} else if (rec->header.kind == TRK::LF_UNION) {
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
} else {
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
}
if (name) *typeName = QString::fromUtf8(name);
}
// If this is a forward reference, resolve to the full definition
PdbCtx ctx;
ctx.tt = &tt;
if (isUDT) {
bool fwdref = false;
if (rec->header.kind == TRK::LF_UNION)
fwdref = rec->data.LF_UNION.property.fwdref;
else
fwdref = rec->data.LF_CLASS.property.fwdref;
if (fwdref) {
// Build the definition index to find the real definition
ctx.buildUdtDefinitionIndex();
const char* name = nullptr;
if (rec->header.kind == TRK::LF_UNION)
name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data));
else
name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind);
if (name) {
uint32_t defTi = ctx.findUdtDefinitionIndex(rec->header.kind, name);
if (defTi != 0)
ti = defTi;
}
}
ctx.importUDT(ti);
} else {
ctx.importEnum(ti);
}
if (ctx.tree.nodes.isEmpty()) {
setErr(QStringLiteral("Failed to import type at index %1").arg(ti));
}
return ctx.tree;
}
} // namespace rcx } // namespace rcx
#else // !_WIN32 #else // !_WIN32
@@ -1259,6 +1377,11 @@ NodeTree importPdb(const QString&, const QString&, QString* errorMsg) {
return {}; return {};
} }
NodeTree importTypeForSymbol(const QString&, uint32_t, QString*, QString* errorMsg) {
if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows");
return {};
}
} // namespace rcx } // namespace rcx
#endif #endif

View File

@@ -10,6 +10,7 @@ namespace rcx {
struct PdbSymbol { struct PdbSymbol {
QString name; QString name;
uint32_t rva; uint32_t rva;
uint32_t typeIndex = 0; // TPI type index (0 = unknown / public symbol)
}; };
struct PdbSymbolResult { struct PdbSymbolResult {
@@ -51,4 +52,12 @@ NodeTree importPdb(const QString& pdbPath,
const QString& structFilter = {}, const QString& structFilter = {},
QString* errorMsg = nullptr); QString* errorMsg = nullptr);
// Import the type associated with a global symbol's typeIndex.
// Opens the PDB, resolves the typeIndex to a UDT/enum, and returns the imported tree.
// Returns empty tree if the symbol has no associated type or the type is a simple primitive.
NodeTree importTypeForSymbol(const QString& pdbPath,
uint32_t typeIndex,
QString* typeName = nullptr,
QString* errorMsg = nullptr);
} // namespace rcx } // namespace rcx

View File

@@ -1025,8 +1025,8 @@ protected:
const double r = 0.75, s = 3.0; const double r = 0.75, s = 3.0;
double cx = width() / 2.0; double cx = width() / 2.0;
double cy = height() / 2.0; double cy = height() / 2.0;
// 2 columns x 3 rows, centered // 2 columns x 4 rows, centered
for (int row = -1; row <= 1; row++) { for (int row = -2; row <= 1; row++) {
p.drawEllipse(QPointF(cx - s * 0.5, cy + row * s), r, r); p.drawEllipse(QPointF(cx - s * 0.5, cy + row * s), r, r);
p.drawEllipse(QPointF(cx + s * 0.5, cy + row * s), r, r); p.drawEllipse(QPointF(cx + s * 0.5, cy + row * s), r, r);
} }
@@ -4776,6 +4776,8 @@ void MainWindow::createSymbolsDock() {
m_symDownloadBtn = new QToolButton(titleBar); m_symDownloadBtn = new QToolButton(titleBar);
m_symDownloadBtn->setIcon(QIcon(QStringLiteral(":/vsicons/cloud-download.svg"))); m_symDownloadBtn->setIcon(QIcon(QStringLiteral(":/vsicons/cloud-download.svg")));
m_symDownloadBtn->setIconSize(QSize(14, 14)); m_symDownloadBtn->setIconSize(QSize(14, 14));
m_symDownloadBtn->setText(QStringLiteral("Download All"));
m_symDownloadBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_symDownloadBtn->setAutoRaise(true); m_symDownloadBtn->setAutoRaise(true);
m_symDownloadBtn->setCursor(Qt::PointingHandCursor); m_symDownloadBtn->setCursor(Qt::PointingHandCursor);
m_symDownloadBtn->setToolTip(QStringLiteral("Load/Download all symbols")); m_symDownloadBtn->setToolTip(QStringLiteral("Load/Download all symbols"));
@@ -4892,17 +4894,10 @@ void MainWindow::createSymbolsDock() {
return; return;
} }
// Helper to load a PDB file into the symbol store // Helper to load a PDB file into the symbol store (with type indices)
auto loadPdb = [this, name](const QString& pdbPath) -> bool { auto loadPdb = [this, name](const QString& pdbPath) -> bool {
QString symErr; int count = loadPdbIntoStore(pdbPath);
auto result = rcx::extractPdbSymbols(pdbPath, &symErr); if (count <= 0) return false;
if (result.symbols.isEmpty()) return false;
QVector<QPair<QString, uint32_t>> pairs;
pairs.reserve(result.symbols.size());
for (const auto& s : result.symbols)
pairs.emplaceBack(s.name, s.rva);
int count = rcx::SymbolStore::instance().addModule(
result.moduleName, pdbPath, pairs);
setAppStatus(QStringLiteral("Loaded %1 symbols for %2").arg(count).arg(name)); setAppStatus(QStringLiteral("Loaded %1 symbols for %2").arg(count).arg(name));
rebuildSymbolsModel(); rebuildSymbolsModel();
if (auto* c = activeController()) c->refresh(); if (auto* c = activeController()) c->refresh();
@@ -5341,6 +5336,28 @@ void MainWindow::createSymbolsDock() {
} }
} }
int MainWindow::loadPdbIntoStore(const QString& pdbPath) {
QString symErr;
auto result = rcx::extractPdbSymbols(pdbPath, &symErr);
if (result.symbols.isEmpty()) return 0;
QVector<QPair<QString, uint32_t>> pairs;
QHash<QString, uint32_t> typeIndices;
pairs.reserve(result.symbols.size());
for (const auto& s : result.symbols) {
pairs.emplaceBack(s.name, s.rva);
if (s.typeIndex != 0)
typeIndices.insert(s.name, s.typeIndex);
}
int count = rcx::SymbolStore::instance().addModule(
result.moduleName, pdbPath, pairs);
if (!typeIndices.isEmpty())
rcx::SymbolStore::instance().addModuleTypeIndices(
result.moduleName, typeIndices);
return count;
}
void MainWindow::rebuildSymbolsModel() { void MainWindow::rebuildSymbolsModel() {
if (!m_symbolsModel) return; if (!m_symbolsModel) return;
m_symbolsModel->clear(); m_symbolsModel->clear();

View File

@@ -221,6 +221,8 @@ private:
void rebuildSymbolsModel(); void rebuildSymbolsModel();
void rebuildModulesModel(); void rebuildModulesModel();
void downloadSymbolsForProcess(); void downloadSymbolsForProcess();
// Load PDB symbols + typeIndices into SymbolStore. Returns symbol count.
static int loadPdbIntoStore(const QString& pdbPath);
// Start page // Start page
StartPageWidget* m_startPage = nullptr; StartPageWidget* m_startPage = nullptr;

View File

@@ -5,7 +5,10 @@
#include "generator.h" #include "generator.h"
#include "mainwindow.h" #include "mainwindow.h"
#include "scanner.h" #include "scanner.h"
#include "symbolstore.h"
#include "imports/import_pdb.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QFile>
#include <QSettings> #include <QSettings>
#include <QTimer> #include <QTimer>
#include <QDebug> #include <QDebug>
@@ -323,8 +326,8 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&
"- To detect what changed after an in-game event: call ui.action with action:'reset_tracking', " "- To detect what changed after an in-game event: call ui.action with action:'reset_tracking', "
"then have the user perform the action, then call node.history on the relevant nodes " "then have the user perform the action, then call node.history on the relevant nodes "
"to see which ones have new timestamped entries.\n" "to see which ones have new timestamped entries.\n"
"- hex.read offset is relative to the struct base address by default. " "- hex.read offset is an absolute virtual address by default. "
"Use baseRelative=true for absolute virtual addresses in the process.\n" "Use baseRelative=true to make it relative to the struct base address (0 = start of struct).\n"
"- tree.apply operations are atomic (undo macro). Batch related changes into one call.\n" "- tree.apply operations are atomic (undo macro). Batch related changes into one call.\n"
"- Use tree.search to quickly find nodes by name instead of paging through project.state.\n" "- Use tree.search to quickly find nodes by name instead of paging through project.state.\n"
"- project.state returns structure metadata only (kinds, names, offsets), NOT live values. " "- project.state returns structure metadata only (kinds, names, offsets), NOT live values. "
@@ -385,7 +388,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
"Operations: " "Operations: "
"remove: {op:'remove', nodeId:'ID'}. " "remove: {op:'remove', nodeId:'ID'}. "
"rename: {op:'rename', nodeId:'ID', name:'newName'}. " "rename: {op:'rename', nodeId:'ID', name:'newName'}. "
"insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. " "insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0} "
"optional fields: structTypeName, classKeyword, strLen, elementKind, arrayLen, refId, "
"ptrDepth (0=struct ptr, 1=prim*, 2=prim**), isStatic (bool), offsetExpr (string), "
"isRelative (bool, RVA pointer), "
"enumMembers ([{name:'X',value:0},...]), bitfieldMembers ([{name:'X',bitOffset:0,bitWidth:1},...]). "
"change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. " "change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. "
"change_offset: {op:'change_offset', nodeId:'ID', offset:16}. " "change_offset: {op:'change_offset', nodeId:'ID', offset:16}. "
"change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. " "change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. "
@@ -394,9 +401,15 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
"change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. " "change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. "
"change_array_meta: {op:'change_array_meta', nodeId:'ID', elementKind:'UInt32', arrayLen:10}. " "change_array_meta: {op:'change_array_meta', nodeId:'ID', elementKind:'UInt32', arrayLen:10}. "
"collapse: {op:'collapse', nodeId:'ID', collapsed:true}. " "collapse: {op:'collapse', nodeId:'ID', collapsed:true}. "
"change_enum_members: {op:'change_enum_members', nodeId:'ID', members:[{name:'X',value:0},...]}. "
"change_offset_expr: {op:'change_offset_expr', nodeId:'ID', offsetExpr:'base + 0x10'}. "
"toggle_static: {op:'toggle_static', nodeId:'ID', isStatic:true}. "
"toggle_relative: {op:'toggle_relative', nodeId:'ID', isRelative:true}. "
"group_into_union: {op:'group_into_union', nodeIds:['ID1','ID2',...]} — groups siblings into a union. "
"dissolve_union: {op:'dissolve_union', nodeId:'ID'} — flattens a union back to parent scope. "
"Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. " "Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. "
"Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 " "Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 "
"Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"}, "Float Double Bool Pointer32 Pointer64 FuncPtr32 FuncPtr64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"},
{"inputSchema", QJsonObject{ {"inputSchema", QJsonObject{
{"type", "object"}, {"type", "object"},
{"properties", QJsonObject{ {"properties", QJsonObject{
@@ -451,16 +464,20 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
{"description", "Read raw bytes from provider (live process memory). Returns hex dump, ASCII, and multi-type " {"description", "Read raw bytes from provider (live process memory). Returns hex dump, ASCII, and multi-type "
"interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). " "interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). "
"Use this to see what actual values are in memory at any offset. " "Use this to see what actual values are in memory at any offset. "
"Offset is tree-relative (0-based, baseAddress added automatically) " "By default offset is an absolute virtual address in the target process. "
"unless baseRelative=true (offset is absolute virtual address in the process)."}, "Set baseRelative=true to make offset relative to the struct base address "
"(e.g. offset=0 reads at baseAddress, offset=0x10 reads at baseAddress+0x10)."},
{"inputSchema", QJsonObject{ {"inputSchema", QJsonObject{
{"type", "object"}, {"type", "object"},
{"properties", QJsonObject{ {"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"}, {"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}, {"description", "MDI tab index (0-based). Omit for active tab."}}},
{"offset", QJsonObject{{"type", "integer"}}}, {"offset", QJsonObject{{"type", "integer"},
{"length", QJsonObject{{"type", "integer"}}}, {"description", "Address to read from. Absolute VA by default, or relative to struct base if baseRelative=true."}}},
{"baseRelative", QJsonObject{{"type", "boolean"}}} {"length", QJsonObject{{"type", "integer"},
{"description", "Number of bytes to read (1-4096, default 64)."}}},
{"baseRelative", QJsonObject{{"type", "boolean"},
{"description", "If true, offset is relative to the tree's base address (added automatically). Default false (offset is absolute VA)."}}}
}}, }},
{"required", QJsonArray{"offset", "length"}} {"required", QJsonArray{"offset", "length"}}
}} }}
@@ -469,15 +486,20 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
// 5. hex.write // 5. hex.write
tools.append(QJsonObject{ tools.append(QJsonObject{
{"name", "hex.write"}, {"name", "hex.write"},
{"description", "Write raw bytes to provider (through undo stack). Hex string format: '4D5A9000'"}, {"description", "Write raw bytes to provider (through undo stack). Hex string format: '4D5A9000'. "
"By default offset is an absolute virtual address. "
"Set baseRelative=true to make offset relative to the struct base address."},
{"inputSchema", QJsonObject{ {"inputSchema", QJsonObject{
{"type", "object"}, {"type", "object"},
{"properties", QJsonObject{ {"properties", QJsonObject{
{"tabIndex", QJsonObject{{"type", "integer"}, {"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}, {"description", "MDI tab index (0-based). Omit for active tab."}}},
{"offset", QJsonObject{{"type", "integer"}}}, {"offset", QJsonObject{{"type", "integer"},
{"hexBytes", QJsonObject{{"type", "string"}}}, {"description", "Address to write to. Absolute VA by default, or relative to struct base if baseRelative=true."}}},
{"baseRelative", QJsonObject{{"type", "boolean"}}} {"hexBytes", QJsonObject{{"type", "string"},
{"description", "Hex byte string to write, e.g. '4D5A9000'. Spaces allowed."}}},
{"baseRelative", QJsonObject{{"type", "boolean"},
{"description", "If true, offset is relative to the tree's base address. Default false (absolute VA)."}}}
}}, }},
{"required", QJsonArray{"offset", "hexBytes"}} {"required", QJsonArray{"offset", "hexBytes"}}
}} }}
@@ -650,6 +672,86 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
}} }}
}} }}
}); });
// symbols.load
tools.append(QJsonObject{
{"name", "symbols.load"},
{"description", "Load PDB symbols from a file path into the global symbol store. "
"Symbols are used for address annotations (e.g. 'ntdll!RtlInitUnicodeString') "
"and can be resolved via symbols.lookup. "
"Returns the number of symbols loaded and the module name."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"pdbPath", QJsonObject{{"type", "string"},
{"description", "Absolute path to a .pdb file."}}},
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab. Used to refresh annotations after loading."}}}
}},
{"required", QJsonArray{"pdbPath"}}
}}
});
// symbols.lookup
tools.append(QJsonObject{
{"name", "symbols.lookup"},
{"description", "Resolve a symbol name to an absolute virtual address in the attached process. "
"Supports qualified names like 'ntdll!RtlInitUnicodeString' and bare names. "
"Requires symbols to be loaded (via symbols.load or the UI) and a live provider."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"symbol", QJsonObject{{"type", "string"},
{"description", "Symbol to resolve. Use 'module!name' for qualified lookup, or bare 'name' for unqualified."}}},
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}
}},
{"required", QJsonArray{"symbol"}}
}}
});
// symbols.importType
tools.append(QJsonObject{
{"name", "symbols.importType"},
{"description", "Import the type definition for a global symbol from its PDB into the active project. "
"Given a qualified symbol like 'ntdll!g_pShimEngineModule', resolves its typeIndex from "
"the PDB, follows pointer/modifier chains to find the underlying struct/class/union/enum, "
"and imports it with full recursive child types. "
"Requires symbols to be loaded first (via symbols.load). "
"Returns the imported type name and node count, or an error if the symbol has no type info."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"symbol", QJsonObject{{"type", "string"},
{"description", "Qualified symbol name (e.g. 'ntdll!g_pShimEngineModule'). Must include 'module!' prefix."}}},
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}
}},
{"required", QJsonArray{"symbol"}}
}}
});
// node.read_value
tools.append(QJsonObject{
{"name", "node.read_value"},
{"description", "Read the formatted typed value for one or more nodes. Unlike hex.read (which returns raw bytes), "
"this returns the value as the user sees it in the editor: e.g. '120.0f' for Float, '0x7FF61234' for Pointer64, "
"'true' for Bool, '1.0, 2.0, 3.0' for Vec3. "
"For Hex nodes returns the hex byte preview. For Struct/Array returns the computed size. "
"Requires a live provider for meaningful values."},
{"inputSchema", QJsonObject{
{"type", "object"},
{"properties", QJsonObject{
{"nodeIds", QJsonObject{{"type", "array"},
{"items", QJsonObject{{"type", "string"}}},
{"description", "Array of node IDs to read values for."}}},
{"tabIndex", QJsonObject{{"type", "integer"},
{"description", "MDI tab index (0-based). Omit for active tab."}}}
}},
{"required", QJsonArray{"nodeIds"}}
}}
});
return okReply(id, QJsonObject{{"tools", tools}}); return okReply(id, QJsonObject{{"tools", tools}});
} }
@@ -680,6 +782,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject&
else if (toolName == "scanner.scan_pattern") result = toolScannerScanPattern(args); else if (toolName == "scanner.scan_pattern") result = toolScannerScanPattern(args);
else if (toolName == "mcp.reconnect") result = toolReconnect(args); else if (toolName == "mcp.reconnect") result = toolReconnect(args);
else if (toolName == "process.info") result = toolProcessInfo(args); else if (toolName == "process.info") result = toolProcessInfo(args);
else if (toolName == "symbols.load") result = toolSymbolsLoad(args);
else if (toolName == "symbols.lookup") result = toolSymbolsLookup(args);
else if (toolName == "symbols.importType") result = toolSymbolsImportType(args);
else if (toolName == "node.read_value") result = toolNodeReadValue(args);
else return errReply(id, -32601, "Unknown tool: " + toolName); else return errReply(id, -32601, "Unknown tool: " + toolName);
m_mainWindow->clearMcpStatus(); m_mainWindow->clearMcpStatus();
@@ -965,6 +1071,31 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
n.strLen = qBound(1, (int)parseInteger(op.value("strLen"), 64), 1000000); n.strLen = qBound(1, (int)parseInteger(op.value("strLen"), 64), 1000000);
n.elementKind = kindFromString(op.value("elementKind").toString("UInt8")); n.elementKind = kindFromString(op.value("elementKind").toString("UInt8"));
n.arrayLen = qBound(1, (int)parseInteger(op.value("arrayLen"), 1), 1000000); n.arrayLen = qBound(1, (int)parseInteger(op.value("arrayLen"), 1), 1000000);
n.ptrDepth = qBound(0, (int)parseInteger(op.value("ptrDepth"), 0), 2);
n.isStatic = op.value("isStatic").toBool(false);
n.offsetExpr = op.value("offsetExpr").toString();
n.isRelative = op.value("isRelative").toBool(false);
// Enum members
if (op.contains("enumMembers")) {
QJsonArray emArr = op.value("enumMembers").toArray();
for (const auto& ev : emArr) {
QJsonObject eo = ev.toObject();
n.enumMembers.emplaceBack(eo.value("name").toString(),
(int64_t)parseInteger(eo.value("value")));
}
}
// Bitfield members
if (op.contains("bitfieldMembers")) {
QJsonArray bmArr = op.value("bitfieldMembers").toArray();
for (const auto& bv : bmArr) {
QJsonObject bo = bv.toObject();
BitfieldMember bm;
bm.name = bo.value("name").toString();
bm.bitOffset = (uint8_t)qBound(0, (int)parseInteger(bo.value("bitOffset")), 255);
bm.bitWidth = (uint8_t)qBound(1, (int)parseInteger(bo.value("bitWidth"), 1), 64);
n.bitfieldMembers.append(bm);
}
}
bool refOk; bool refOk;
QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk); QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk);
if (!refOk) { if (!refOk) {
@@ -1118,6 +1249,89 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) {
skippedOps.append(QStringLiteral("op[%1]: collapse nodeId '%2' not found").arg(i).arg(nid)); skippedOps.append(QStringLiteral("op[%1]: collapse nodeId '%2' not found").arg(i).arg(nid));
} }
} }
else if (opType == "change_enum_members") {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
QVector<QPair<QString, int64_t>> newMembers;
QJsonArray membersArr = op.value("members").toArray();
for (const auto& mv : membersArr) {
QJsonObject mo = mv.toObject();
newMembers.emplaceBack(mo.value("name").toString(),
(int64_t)parseInteger(mo.value("value")));
}
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeEnumMembers{tree.nodes[idx].id,
tree.nodes[idx].enumMembers, newMembers}));
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: change_enum_members nodeId '%2' not found").arg(i).arg(nid));
}
}
else if (opType == "change_offset_expr") {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeOffsetExpr{tree.nodes[idx].id,
tree.nodes[idx].offsetExpr,
op.value("offsetExpr").toString()}));
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: change_offset_expr nodeId '%2' not found").arg(i).arg(nid));
}
}
else if (opType == "toggle_static") {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
bool newVal = op.value("isStatic").toBool();
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ToggleStatic{tree.nodes[idx].id,
tree.nodes[idx].isStatic, newVal}));
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: toggle_static nodeId '%2' not found").arg(i).arg(nid));
}
}
else if (opType == "toggle_relative") {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
int idx = tree.indexOfId(nid.toULongLong());
if (idx >= 0) {
bool newVal = op.value("isRelative").toBool();
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ToggleRelative{tree.nodes[idx].id,
tree.nodes[idx].isRelative, newVal}));
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: toggle_relative nodeId '%2' not found").arg(i).arg(nid));
}
}
else if (opType == "group_into_union") {
QJsonArray idsArr = op.value("nodeIds").toArray();
QSet<uint64_t> ids;
for (const auto& v : idsArr) {
QString resolved = resolvePlaceholder(v.toString(), placeholders);
ids.insert(resolved.toULongLong());
}
if (ids.size() >= 2) {
ctrl->groupIntoUnion(ids);
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: group_into_union needs >= 2 nodeIds").arg(i));
}
}
else if (opType == "dissolve_union") {
QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders);
uint64_t unionId = nid.toULongLong();
int idx = tree.indexOfId(unionId);
if (idx >= 0) {
ctrl->dissolveUnion(unionId);
applied++;
} else {
skippedOps.append(QStringLiteral("op[%1]: dissolve_union nodeId '%2' not found").arg(i).arg(nid));
}
}
else { else {
skippedOps.append(QStringLiteral("op[%1]: unknown op '%2'").arg(i).arg(opType)); skippedOps.append(QStringLiteral("op[%1]: unknown op '%2'").arg(i).arg(opType));
} }
@@ -1820,6 +2034,224 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) {
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented))); return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
} }
// ════════════════════════════════════════════════════════════════════
// TOOL: symbols.load — load PDB symbols into the global store
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolSymbolsLoad(const QJsonObject& args) {
QString pdbPath = args.value("pdbPath").toString();
if (pdbPath.isEmpty())
return makeTextResult("pdbPath is required", true);
if (!QFile::exists(pdbPath))
return makeTextResult("File not found: " + pdbPath, true);
QString symErr;
auto result = extractPdbSymbols(pdbPath, &symErr);
if (result.symbols.isEmpty())
return makeTextResult(symErr.isEmpty()
? QStringLiteral("No symbols found in PDB") : symErr, true);
QVector<QPair<QString, uint32_t>> pairs;
QHash<QString, uint32_t> typeIndices;
pairs.reserve(result.symbols.size());
for (const auto& s : result.symbols) {
pairs.emplaceBack(s.name, s.rva);
if (s.typeIndex != 0)
typeIndices.insert(s.name, s.typeIndex);
}
int count = SymbolStore::instance().addModule(result.moduleName, pdbPath, pairs);
if (!typeIndices.isEmpty())
SymbolStore::instance().addModuleTypeIndices(result.moduleName, typeIndices);
// Refresh the active tab so annotations pick up new symbols
auto* tab = resolveTab(args);
if (tab && tab->ctrl)
tab->ctrl->refresh();
m_mainWindow->rebuildSymbolsModel();
QJsonObject out;
out["moduleName"] = result.moduleName;
out["symbolCount"] = count;
out["pdbPath"] = pdbPath;
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════
// TOOL: symbols.lookup — resolve symbol name to address
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolSymbolsLookup(const QJsonObject& args) {
QString symbol = args.value("symbol").toString();
if (symbol.isEmpty())
return makeTextResult("symbol is required", true);
auto* tab = resolveTab(args);
if (!tab || !tab->doc->provider)
return makeTextResult("No active tab or provider", true);
auto* prov = tab->doc->provider.get();
bool ok = false;
uint64_t addr = SymbolStore::instance().resolve(symbol, prov, &ok);
if (!ok || addr == 0)
return makeTextResult("Symbol not found: " + symbol, true);
QJsonObject out;
out["symbol"] = symbol;
out["address"] = "0x" + QString::number(addr, 16).toUpper();
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════
// TOOL: symbols.importType — import type definition for a symbol
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolSymbolsImportType(const QJsonObject& args) {
QString symbol = args.value("symbol").toString();
if (symbol.isEmpty())
return makeTextResult("symbol is required", true);
int bangIdx = symbol.indexOf('!');
if (bangIdx <= 0 || bangIdx >= symbol.size() - 1)
return makeTextResult("Symbol must be qualified: 'module!name'", true);
QString modPart = symbol.left(bangIdx);
// Look up the typeIndex from the symbol store
uint32_t typeIdx = SymbolStore::instance().typeIndexForSymbol(symbol);
if (typeIdx == 0)
return makeTextResult("No type info for symbol '" + symbol +
"'. The PDB may not have been loaded with symbols.load, or this "
"is a public symbol (S_PUB32) without type metadata.", true);
// Find the PDB path for this module
QString canonical = SymbolStore::instance().resolveAlias(modPart);
const auto* modData = SymbolStore::instance().moduleData(canonical);
if (!modData || modData->pdbPath.isEmpty())
return makeTextResult("No PDB path found for module '" + modPart + "'", true);
// Import the type from the PDB
QString importedTypeName;
QString importErr;
NodeTree importedTree = importTypeForSymbol(modData->pdbPath, typeIdx,
&importedTypeName, &importErr);
if (importedTree.nodes.isEmpty())
return makeTextResult(importErr.isEmpty()
? QStringLiteral("Failed to import type for typeIndex %1").arg(typeIdx)
: importErr, true);
// Merge imported nodes into the active document
auto* tab = resolveTab(args);
if (!tab)
return makeTextResult("No active tab", true);
auto& tree = tab->doc->tree;
tab->ctrl->setSuppressRefresh(true);
tab->doc->undoStack.beginMacro(
QStringLiteral("Import type for ") + symbol);
// Map old IDs to new IDs to preserve parent-child relationships
QHash<uint64_t, uint64_t> idMap;
for (const auto& node : importedTree.nodes) {
uint64_t newId = tree.reserveId();
idMap[node.id] = newId;
}
for (const auto& node : importedTree.nodes) {
Node copy = node;
copy.id = idMap.value(node.id, node.id);
copy.parentId = idMap.value(node.parentId, node.parentId);
if (copy.refId != 0)
copy.refId = idMap.value(node.refId, node.refId);
tab->doc->undoStack.push(new RcxCommand(tab->ctrl,
cmd::Insert{copy}));
}
tab->doc->undoStack.endMacro();
tab->ctrl->setSuppressRefresh(false);
tab->ctrl->refresh();
m_mainWindow->rebuildWorkspaceModel();
QJsonObject out;
out["symbol"] = symbol;
out["typeName"] = importedTypeName;
out["typeIndex"] = (int)typeIdx;
out["nodesImported"] = importedTree.nodes.size();
return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════
// TOOL: node.read_value — read formatted typed values for nodes
// ════════════════════════════════════════════════════════════════════
QJsonObject McpBridge::toolNodeReadValue(const QJsonObject& args) {
auto* tab = resolveTab(args);
if (!tab) return makeTextResult("No active tab", true);
const auto& tree = tab->doc->tree;
auto* prov = tab->doc->provider.get();
if (!prov) return makeTextResult("No provider", true);
QJsonArray requestedIds = args.value("nodeIds").toArray();
if (requestedIds.isEmpty())
return makeTextResult("nodeIds array is required", true);
QJsonObject result;
for (const auto& idVal : requestedIds) {
QString idStr = idVal.toString();
uint64_t nodeId = idStr.toULongLong();
int idx = tree.indexOfId(nodeId);
if (idx < 0) {
QJsonObject entry;
entry["error"] = "node not found";
result[idStr] = entry;
continue;
}
const Node& node = tree.nodes[idx];
// Compute absolute address
int64_t signedOff = tree.computeOffset(idx);
uint64_t addr = (signedOff >= 0)
? tree.baseAddress + static_cast<uint64_t>(signedOff) : 0;
QJsonObject entry;
entry["kind"] = kindToString(node.kind);
entry["name"] = node.name;
entry["offset"] = node.offset;
entry["address"] = "0x" + QString::number(addr, 16).toUpper();
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) {
// Containers don't have scalar values — return computed size
int span = tree.structSpan(node.id);
entry["computedSize"] = span;
entry["value"] = QStringLiteral("(container, size=0x%1)")
.arg(QString::number(span, 16).toUpper());
} else if (addr != 0 && prov->isReadable(addr, node.byteSize())) {
// Read formatted value using the same formatting as the editor
int numLines = linesForKind(node.kind);
if (numLines <= 1) {
entry["value"] = fmt::readValue(node, *prov, addr, 0);
} else {
// Multi-line types (Mat4x4): return all sub-lines
QJsonArray lines;
for (int sub = 0; sub < numLines; sub++)
lines.append(fmt::readValue(node, *prov, addr, sub));
entry["value"] = lines;
}
} else {
entry["value"] = QJsonValue();
entry["error"] = "not readable";
}
result[idStr] = entry;
}
return makeTextResult(QString::fromUtf8(QJsonDocument(result).toJson(QJsonDocument::Indented)));
}
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════
// Notifications (call from MainWindow/Controller hooks) // Notifications (call from MainWindow/Controller hooks)
// ════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════

View File

@@ -85,6 +85,10 @@ private:
QJsonObject toolScannerScanPattern(const QJsonObject& args); QJsonObject toolScannerScanPattern(const QJsonObject& args);
QJsonObject toolReconnect(const QJsonObject& args); QJsonObject toolReconnect(const QJsonObject& args);
QJsonObject toolProcessInfo(const QJsonObject& args); QJsonObject toolProcessInfo(const QJsonObject& args);
QJsonObject toolSymbolsLoad(const QJsonObject& args);
QJsonObject toolSymbolsLookup(const QJsonObject& args);
QJsonObject toolSymbolsImportType(const QJsonObject& args);
QJsonObject toolNodeReadValue(const QJsonObject& args);
// Helpers // Helpers
QJsonObject makeTextResult(const QString& text, bool isError = false); QJsonObject makeTextResult(const QString& text, bool isError = false);

View File

@@ -12,16 +12,15 @@ PluginManager::~PluginManager()
void PluginManager::LoadPlugins() void PluginManager::LoadPlugins()
{ {
// Get the Plugins directory relative to the executable // Probe plugin locations relative to the executable.
QString appDir = QCoreApplication::applicationDirPath(); QString appDir = QCoreApplication::applicationDirPath();
QString pluginsDir = appDir + "/Plugins"; QStringList pluginDirs;
pluginDirs << (appDir + "/Plugins");
QDir dir(pluginsDir); #ifdef __APPLE__
if (!dir.exists()) // In macOS app bundles, plugins may live in Contents/PlugIns or in
{ // the top-level build/Plugins directory during local development.
qWarning() << "PluginManager: Plugins directory not found:" << pluginsDir; pluginDirs << QDir::cleanPath(appDir + "/../PlugIns");
return; #endif
}
// Find all DLL files // Find all DLL files
QStringList filters; QStringList filters;
@@ -33,12 +32,20 @@ void PluginManager::LoadPlugins()
filters << "*.so"; filters << "*.so";
#endif #endif
int totalCandidates = 0;
bool foundAnyDir = false;
for (const QString& pluginsDir : pluginDirs)
{
QDir dir(pluginsDir);
if (!dir.exists())
continue;
foundAnyDir = true;
dir.setNameFilters(filters); dir.setNameFilters(filters);
QFileInfoList files = dir.entryInfoList(QDir::Files); QFileInfoList files = dir.entryInfoList(QDir::Files);
totalCandidates += files.count();
qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir; qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir;
qDebug() << "PluginManager: Found" << files.count() << "potential plugin(s)";
for (const QFileInfo& fileInfo : files) for (const QFileInfo& fileInfo : files)
{ {
// Skip the remote-inject payload binary — it's not a plugin and // Skip the remote-inject payload binary — it's not a plugin and
@@ -48,6 +55,12 @@ void PluginManager::LoadPlugins()
LoadPlugin(fileInfo.absoluteFilePath()); LoadPlugin(fileInfo.absoluteFilePath());
} }
}
if (!foundAnyDir)
qWarning() << "PluginManager: Plugins directory not found. Searched:" << pluginDirs;
else
qDebug() << "PluginManager: Found" << totalCandidates << "potential plugin(s)";
qDebug() << "PluginManager: Loaded" << m_plugins.count() << "plugin(s)"; qDebug() << "PluginManager: Loaded" << m_plugins.count() << "plugin(s)";
} }

View File

@@ -52,6 +52,26 @@ int SymbolStore::addModule(const QString& moduleName, const QString& pdbPath,
return count; return count;
} }
void SymbolStore::addModuleTypeIndices(const QString& moduleName,
const QHash<QString, uint32_t>& nameToTypeIndex) {
QString canonical = resolveAlias(moduleName);
auto it = m_modules.find(canonical);
if (it == m_modules.end()) return;
it->nameToTypeIndex = nameToTypeIndex;
}
uint32_t SymbolStore::typeIndexForSymbol(const QString& qualifiedSymbol) const {
int bangIdx = qualifiedSymbol.indexOf('!');
if (bangIdx <= 0 || bangIdx >= qualifiedSymbol.size() - 1)
return 0;
QString modPart = qualifiedSymbol.left(bangIdx);
QString symPart = qualifiedSymbol.mid(bangIdx + 1);
QString canonical = resolveAlias(modPart);
auto modIt = m_modules.find(canonical);
if (modIt == m_modules.end()) return 0;
return modIt->nameToTypeIndex.value(symPart, 0);
}
void SymbolStore::unloadModule(const QString& moduleName) { void SymbolStore::unloadModule(const QString& moduleName) {
QString canonical = resolveAlias(moduleName); QString canonical = resolveAlias(moduleName);
m_modules.remove(canonical); m_modules.remove(canonical);

View File

@@ -14,6 +14,7 @@ struct PdbSymbolSet {
QString pdbPath; QString pdbPath;
QString moduleName; // canonical lowercase name (e.g. "ntoskrnl") QString moduleName; // canonical lowercase name (e.g. "ntoskrnl")
QHash<QString, uint32_t> nameToRva; QHash<QString, uint32_t> nameToRva;
QHash<QString, uint32_t> nameToTypeIndex; // symbol name → TPI typeIndex (0 = no type info)
QVector<QPair<uint32_t, QString>> rvaToName; // sorted by RVA for binary search QVector<QPair<uint32_t, QString>> rvaToName; // sorted by RVA for binary search
void sortRvaIndex() { void sortRvaIndex() {
@@ -35,6 +36,15 @@ public:
int addModule(const QString& moduleName, const QString& pdbPath, int addModule(const QString& moduleName, const QString& pdbPath,
const QVector<QPair<QString, uint32_t>>& symbols); const QVector<QPair<QString, uint32_t>>& symbols);
// Store symbol→typeIndex mapping for a previously-added module.
// Called after addModule with the typeIndex data from PdbSymbol records.
void addModuleTypeIndices(const QString& moduleName,
const QHash<QString, uint32_t>& nameToTypeIndex);
// Look up the TPI typeIndex for a qualified symbol (e.g. "ntdll!g_pShimEngineModule").
// Returns 0 if not found or no type info available.
uint32_t typeIndexForSymbol(const QString& qualifiedSymbol) const;
// Unload symbols for a module. // Unload symbols for a module.
void unloadModule(const QString& moduleName); void unloadModule(const QString& moduleName);