WinDbg plugin, ProcessMemoryWindows, dialog cleanup, and misc fixes

- Add WinDbgMemory plugin with debug server connection support
- Replace ProcessMemory plugin with Windows-specific ProcessMemoryWindows
- Simplify WinDbg dialog: single panel, no tabs, palette-based theming
- Fix example text visibility on dark themes (QPalette::Dark -> Disabled WindowText)
- Fix "file" -> "File" capitalization in source menu
- Add windbg_provider and com_security tests
This commit is contained in:
IChooseYou
2026-02-14 13:40:58 -07:00
committed by sysadmin
parent b44dc9e96b
commit c856ba2697
18 changed files with 1692 additions and 108 deletions

View File

@@ -459,8 +459,13 @@ void composeNode(ComposeState& state, const NodeTree& tree,
ptrVal = (node.kind == NodeKind::Pointer32)
? (uint64_t)prov.readU32(absAddr) : prov.readU64(absAddr);
if (ptrVal != 0) {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
// Treat sentinel values as invalid pointers
if (ptrVal == UINT64_MAX || (node.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF))
ptrVal = 0;
else {
uint64_t pBase = ptrToProviderAddr(tree, ptrVal);
if (pBase == UINT64_MAX) ptrVal = 0; // ptr below base: invalid
}
}
}

View File

@@ -293,6 +293,9 @@ void RcxController::connectEditor(RcxEditor* editor) {
break;
case EditTarget::BaseAddress: {
QString s = text.trimmed();
s.remove('`'); // WinDbg backtick separators (e.g. 7ff6`6cce0000)
s.remove('\n');
s.remove('\r');
// Support simple equations: 0x10+0x4, 0x100-0x10, etc.
uint64_t newBase = 0;
bool ok = true;
@@ -347,7 +350,7 @@ void RcxController::connectEditor(RcxEditor* editor) {
if (text.startsWith(QStringLiteral("#saved:"))) {
int idx = text.mid(7).toInt();
switchToSavedSource(idx);
} else if (text == QStringLiteral("file")) {
} else if (text == QStringLiteral("File")) {
auto* w = qobject_cast<QWidget*>(parent());
QString path = QFileDialog::getOpenFileName(w, "Load Binary Data", {}, "All Files (*)");
if (!path.isEmpty()) {
@@ -910,7 +913,7 @@ void RcxController::applyCommand(const Command& command, bool isUndo) {
qWarning() << "WriteBytes failed at address" << QString::number(c.addr, 16);
// Patch snapshot so compose sees the new value immediately
if (m_snapshotProv)
m_snapshotProv->patchSnapshot(c.addr, bytes.constData(), bytes.size());
m_snapshotProv->patchPages(c.addr, bytes.constData(), bytes.size());
} else if constexpr (std::is_same_v<T, cmd::ChangeArrayMeta>) {
int idx = tree.indexOfId(c.nodeId);
if (idx >= 0) {
@@ -1896,15 +1899,66 @@ void RcxController::pushSavedSourcesToEditors() {
void RcxController::setupAutoRefresh() {
m_refreshTimer = new QTimer(this);
m_refreshTimer->setInterval(2000);
m_refreshTimer->setInterval(660);
connect(m_refreshTimer, &QTimer::timeout, this, &RcxController::onRefreshTick);
m_refreshTimer->start();
m_refreshWatcher = new QFutureWatcher<QByteArray>(this);
connect(m_refreshWatcher, &QFutureWatcher<QByteArray>::finished,
m_refreshWatcher = new QFutureWatcher<PageMap>(this);
connect(m_refreshWatcher, &QFutureWatcher<PageMap>::finished,
this, &RcxController::onReadComplete);
}
// Recursively collect memory ranges for a struct and its pointer targets.
// memBase is the provider-relative address where this struct's data lives.
void RcxController::collectPointerRanges(
uint64_t structId, uint64_t memBase,
int depth, int maxDepth,
QSet<uint64_t>& visited,
QVector<QPair<uint64_t,int>>& ranges) const
{
if (depth >= maxDepth) return;
uint64_t key = memBase ^ (structId * 0x9E3779B97F4A7C15ULL);
if (visited.contains(key)) return;
visited.insert(key);
int span = m_doc->tree.structSpan(structId);
if (span <= 0) return;
ranges.append({memBase, span});
if (!m_snapshotProv) return;
// Walk children looking for non-collapsed pointers
QVector<int> children = m_doc->tree.childrenOf(structId);
for (int ci : children) {
const Node& child = m_doc->tree.nodes[ci];
if (child.kind != NodeKind::Pointer32 && child.kind != NodeKind::Pointer64)
continue;
if (child.collapsed || child.refId == 0) continue;
uint64_t ptrAddr = memBase + child.offset;
int ptrSize = child.byteSize();
if (!m_snapshotProv->isReadable(ptrAddr, ptrSize)) continue;
uint64_t ptrVal = (child.kind == NodeKind::Pointer32)
? (uint64_t)m_snapshotProv->readU32(ptrAddr)
: m_snapshotProv->readU64(ptrAddr);
if (ptrVal == 0 || ptrVal == UINT64_MAX || ptrVal < m_doc->tree.baseAddress) continue;
uint64_t pBase = ptrVal - m_doc->tree.baseAddress;
collectPointerRanges(child.refId, pBase, depth + 1, maxDepth,
visited, ranges);
}
// Embedded struct references (struct node with refId but no own children)
int idx = m_doc->tree.indexOfId(structId);
if (idx >= 0) {
const Node& sn = m_doc->tree.nodes[idx];
if (sn.kind == NodeKind::Struct && sn.refId != 0 && children.isEmpty())
collectPointerRanges(sn.refId, memBase, depth, maxDepth,
visited, ranges);
}
}
void RcxController::onRefreshTick() {
if (m_readInFlight) return;
if (!m_doc->provider || !m_doc->provider->isLive()) return;
@@ -1915,75 +1969,120 @@ void RcxController::onRefreshTick() {
int extent = computeDataExtent();
if (extent <= 0) return;
// Collect all needed ranges: main struct + pointer targets
QVector<QPair<uint64_t,int>> ranges;
ranges.append({0, extent});
if (m_snapshotProv) {
QSet<uint64_t> visited;
uint64_t rootId = m_viewRootId;
if (rootId == 0 && !m_doc->tree.nodes.isEmpty())
rootId = m_doc->tree.nodes[0].id;
collectPointerRanges(rootId, 0, 0, 4, visited, ranges);
}
m_readInFlight = true;
m_readGen = m_refreshGen;
// Capture shared_ptr copy — keeps provider alive during async read
auto prov = m_doc->provider;
uint64_t base = prov->base();
qDebug() << "[Refresh] reading" << extent << "bytes from base" << Qt::hex << base;
m_refreshWatcher->setFuture(QtConcurrent::run([prov, extent]() -> QByteArray {
return prov->readBytes(0, extent);
qDebug() << "[Refresh] reading" << ranges.size() << "ranges from base"
<< Qt::hex << prov->base();
m_refreshWatcher->setFuture(QtConcurrent::run([prov, ranges]() -> PageMap {
constexpr uint64_t kPageSize = 4096;
constexpr uint64_t kPageMask = ~(kPageSize - 1);
PageMap pages;
for (const auto& r : ranges) {
uint64_t pageStart = r.first & kPageMask;
uint64_t end = r.first + r.second;
uint64_t pageEnd = (end + kPageSize - 1) & kPageMask;
for (uint64_t p = pageStart; p < pageEnd; p += kPageSize) {
if (!pages.contains(p))
pages[p] = prov->readBytes(p, static_cast<int>(kPageSize));
}
}
return pages;
}));
}
void RcxController::onReadComplete() {
m_readInFlight = false;
// Stale read (provider changed while we were reading) — discard
if (m_readGen != m_refreshGen) return;
QByteArray newData = m_refreshWatcher->result();
PageMap newPages;
try {
newPages = m_refreshWatcher->result();
} catch (const std::exception& e) {
qWarning() << "[Refresh] async read threw:" << e.what();
return;
} catch (...) {
qWarning() << "[Refresh] async read threw unknown exception";
return;
}
// Fast path: no changes at all — skip full recompose
if (!m_prevSnapshot.isEmpty() && m_prevSnapshot.size() == newData.size()
&& memcmp(m_prevSnapshot.constData(), newData.constData(), newData.size()) == 0)
// All-zero guard: if page 0 is all zeros and we already have data, discard
if (!m_prevPages.isEmpty() && newPages.contains(0)) {
const QByteArray& p0 = newPages.value(0);
bool allZero = true;
for (int i = 0; i < p0.size(); ++i) {
if (p0[i] != 0) { allZero = false; break; }
}
if (allZero) {
qDebug() << "[Refresh] discarding all-zero page-0, keeping stale snapshot";
return;
}
}
// Fast path: no changes at all
if (newPages == m_prevPages)
return;
// Compute which byte offsets changed
// Compute which byte offsets changed (for change highlighting).
// Skip on first snapshot — nothing to compare against.
m_changedOffsets.clear();
if (!m_prevSnapshot.isEmpty()) {
int compareLen = qMin(m_prevSnapshot.size(), newData.size());
const char* oldP = m_prevSnapshot.constData();
const char* newP = newData.constData();
for (int i = 0; i < compareLen; i++) {
if (oldP[i] != newP[i])
m_changedOffsets.insert(i);
if (!m_prevPages.isEmpty()) {
for (auto it = newPages.constBegin(); it != newPages.constEnd(); ++it) {
uint64_t pageAddr = it.key();
const QByteArray& newPage = it.value();
auto oldIt = m_prevPages.constFind(pageAddr);
if (oldIt == m_prevPages.constEnd())
continue; // new page, no previous data to diff against
const QByteArray& oldPage = oldIt.value();
int cmpLen = qMin(oldPage.size(), newPage.size());
for (int i = 0; i < cmpLen; ++i) {
if (oldPage[i] != newPage[i])
m_changedOffsets.insert(static_cast<int64_t>(pageAddr) + i);
}
}
// Bytes beyond old snapshot are all "new"
for (int i = compareLen; i < newData.size(); i++)
m_changedOffsets.insert(i);
}
m_prevSnapshot = newData;
// Update or create snapshot provider
int mainExtent = computeDataExtent();
m_prevPages = newPages;
if (m_snapshotProv)
m_snapshotProv->updateSnapshot(std::move(newData));
m_snapshotProv->updatePages(std::move(newPages), mainExtent);
else
m_snapshotProv = std::make_unique<SnapshotProvider>(m_doc->provider, std::move(newData));
m_snapshotProv = std::make_unique<SnapshotProvider>(
m_doc->provider, std::move(newPages), mainExtent);
refresh();
// Clear changed offsets after refresh consumed them
m_changedOffsets.clear();
}
int RcxController::computeDataExtent() const {
// Prefer tree-based extent: exact bytes needed for rendering
static constexpr int64_t kMaxMainExtent = 16 * 1024 * 1024; // 16 MB cap
int64_t treeExtent = 0;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
const Node& node = m_doc->tree.nodes[i];
int64_t off = m_doc->tree.computeOffset(i);
// byteSize() returns 0 for Array-of-Struct/Array; use structSpan() for containers
int sz = (node.kind == NodeKind::Struct || node.kind == NodeKind::Array)
? m_doc->tree.structSpan(node.id) : node.byteSize();
int64_t end = off + sz;
if (end > treeExtent) treeExtent = end;
}
// Clamp to max int (readBytes takes int length)
if (treeExtent > 0) return (int)qMin(treeExtent, (int64_t)std::numeric_limits<int>::max());
if (treeExtent > 0) return static_cast<int>(qMin(treeExtent, kMaxMainExtent));
// Fallback: provider size (empty tree)
int provSize = m_doc->provider->size();
if (provSize > 0) return provSize;
return 0;
@@ -1993,7 +2092,7 @@ void RcxController::resetSnapshot() {
m_refreshGen++;
m_readInFlight = false;
m_snapshotProv.reset();
m_prevSnapshot.clear();
m_prevPages.clear();
m_changedOffsets.clear();
}

View File

@@ -141,10 +141,11 @@ private:
TypeSelectorPopup* m_cachedPopup = nullptr;
// ── Auto-refresh state ──
using PageMap = QHash<uint64_t, QByteArray>;
QTimer* m_refreshTimer = nullptr;
QFutureWatcher<QByteArray>* m_refreshWatcher = nullptr;
QFutureWatcher<PageMap>* m_refreshWatcher = nullptr;
std::unique_ptr<SnapshotProvider> m_snapshotProv;
QByteArray m_prevSnapshot;
PageMap m_prevPages;
QSet<int64_t> m_changedOffsets;
uint64_t m_refreshGen = 0;
uint64_t m_readGen = 0;
@@ -166,6 +167,10 @@ private:
void onReadComplete();
int computeDataExtent() const;
void resetSnapshot();
void collectPointerRanges(uint64_t structId, uint64_t memBase,
int depth, int maxDepth,
QSet<uint64_t>& visited,
QVector<QPair<uint64_t,int>>& ranges) const;
};
} // namespace rcx

View File

@@ -14,6 +14,7 @@
#include <QCursor>
#include <QMenu>
#include <QApplication>
#include <QClipboard>
#include "themes/thememanager.h"
namespace rcx {
@@ -1603,6 +1604,22 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) {
case Qt::Key_End:
m_sci->setCursorPosition(m_editState.line, editEndCol());
return true;
case Qt::Key_V:
if (ke->modifiers() & Qt::ControlModifier) {
// Sanitized paste: strip newlines (and backticks for base addresses)
QString clip = QApplication::clipboard()->text();
clip.remove('\n');
clip.remove('\r');
if (m_editState.target == EditTarget::BaseAddress)
clip.remove('`');
if (!clip.isEmpty()) {
QByteArray utf8 = clip.toUtf8();
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
(uintptr_t)0, utf8.constData());
}
return true;
}
return false;
default:
return false;
}
@@ -1961,7 +1978,7 @@ void RcxEditor::showSourcePicker() {
int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM);
menuFont.setPointSize(menuFont.pointSize() + zoom);
menu.setFont(menuFont);
menu.addAction("file");
menu.addAction("File");
// Add all registered providers from global registry
const auto& providers = ProviderRegistry::instance().providers();

View File

@@ -465,7 +465,7 @@ void MainWindow::styleTabCloseButtons() {
const auto& t = ThemeManager::instance().current();
QString style = QStringLiteral(
"QToolButton { color: %1; border: none; padding: 0px 4px; font-size: 12px; }"
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
"QToolButton:hover { color: %2; }")
.arg(t.textDim.name(), t.indHoverSpan.name());

View File

@@ -1,28 +1,65 @@
#pragma once
#include "provider.h"
#include <QHash>
#include <memory>
namespace rcx {
// Provider that reads from a cached QByteArray snapshot but delegates
// metadata (name, kind, getSymbol) to the underlying real provider.
// Used for async refresh: worker thread reads bulk data into a snapshot,
// UI thread composes against it without blocking.
// Page-based snapshot provider.
//
// During async refresh the controller reads pages for the main struct and
// every reachable pointer target. Compose reads entirely from this page
// table — no fallback to the real provider, no blocking I/O on the UI
// thread. Pages that were never fetched (truly invalid pointers) simply
// read as zeros.
class SnapshotProvider : public Provider {
std::shared_ptr<Provider> m_real;
QByteArray m_data;
QHash<uint64_t, QByteArray> m_pages; // page-aligned addr → 4096-byte page
int m_mainExtent = 0; // logical size of the main struct range
static constexpr uint64_t kPageSize = 4096;
static constexpr uint64_t kPageMask = ~(kPageSize - 1);
public:
SnapshotProvider(std::shared_ptr<Provider> real, QByteArray snapshot)
: m_real(std::move(real)), m_data(std::move(snapshot)) {}
using PageMap = QHash<uint64_t, QByteArray>;
SnapshotProvider(std::shared_ptr<Provider> real, PageMap pages, int mainExtent)
: m_real(std::move(real))
, m_pages(std::move(pages))
, m_mainExtent(mainExtent) {}
bool read(uint64_t addr, void* buf, int len) const override {
if (!isReadable(addr, len)) return false;
std::memcpy(buf, m_data.constData() + addr, len);
if (len <= 0) return false;
char* out = static_cast<char*>(buf);
uint64_t cur = addr;
int remaining = len;
while (remaining > 0) {
uint64_t pageAddr = cur & kPageMask;
int pageOff = static_cast<int>(cur - pageAddr);
int chunk = qMin(remaining, static_cast<int>(kPageSize - pageOff));
auto it = m_pages.constFind(pageAddr);
if (it != m_pages.constEnd()) {
std::memcpy(out, it->constData() + pageOff, chunk);
} else {
std::memset(out, 0, chunk);
}
out += chunk;
cur += chunk;
remaining -= chunk;
}
return true;
}
int size() const override { return m_data.size(); }
bool isReadable(uint64_t addr, int len) const override {
if (len <= 0) return (len == 0);
uint64_t end = addr + static_cast<uint64_t>(len);
for (uint64_t p = addr & kPageMask; p < end; p += kPageSize) {
if (!m_pages.contains(p)) return false;
}
return true;
}
int size() const override { return m_mainExtent; }
bool isWritable() const override { return m_real ? m_real->isWritable() : false; }
bool isLive() const override { return m_real ? m_real->isLive() : false; }
QString name() const override { return m_real ? m_real->name() : QString(); }
@@ -34,21 +71,36 @@ public:
bool write(uint64_t addr, const void* buf, int len) override {
if (!m_real) return false;
bool ok = m_real->write(addr, buf, len);
if (ok && isReadable(addr, len))
std::memcpy(m_data.data() + addr, buf, len);
if (ok) patchPages(addr, buf, len);
return ok;
}
// Update the entire snapshot (called after async read completes)
void updateSnapshot(QByteArray data) { m_data = std::move(data); }
// Patch specific bytes in the snapshot (called after user writes a value)
void patchSnapshot(uint64_t addr, const void* buf, int len) {
if (isReadable(addr, len))
std::memcpy(m_data.data() + addr, buf, len);
// Replace the entire page table (called after async read completes)
void updatePages(PageMap pages, int mainExtent) {
m_pages = std::move(pages);
m_mainExtent = mainExtent;
}
const QByteArray& snapshot() const { return m_data; }
// Patch specific bytes in existing pages (called after user writes a value)
void patchPages(uint64_t addr, const void* buf, int len) {
const char* src = static_cast<const char*>(buf);
uint64_t cur = addr;
int remaining = len;
while (remaining > 0) {
uint64_t pageAddr = cur & kPageMask;
int pageOff = static_cast<int>(cur - pageAddr);
int chunk = qMin(remaining, static_cast<int>(kPageSize - pageOff));
auto it = m_pages.find(pageAddr);
if (it != m_pages.end()) {
std::memcpy(it->data() + pageOff, src, chunk);
}
src += chunk;
cur += chunk;
remaining -= chunk;
}
}
const PageMap& pages() const { return m_pages; }
};
} // namespace rcx

View File

@@ -20,6 +20,7 @@ TitleBarWidget::TitleBarWidget(QWidget* parent)
// App name
m_appLabel = new QLabel(QStringLiteral("Reclass"), this);
m_appLabel->setContentsMargins(10, 0, 4, 0);
m_appLabel->setAlignment(Qt::AlignVCenter);
m_appLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
layout->addWidget(m_appLabel);