mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
fix: rescan performance overhaul, background thread, WinDbg regions
Move rescan to background thread via ScanEngine::startRescan() to prevent UI freeze. Fix populateTable bottleneck caused by QHeaderView::ResizeToContents iterating all rows (6s -> 0ms for 512 results). Add chunked batch reads (256KB spans), enumerateRegions() for WinDbg/ProcessMemory providers, cancel support, and diagnostic logging throughout the scanner pipeline.
This commit is contained in:
@@ -409,15 +409,15 @@ if(BUILD_TESTING)
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test)
|
||||
add_test(NAME test_scanner_ui COMMAND test_scanner_ui)
|
||||
|
||||
# Disabled: WinDbg provider test has build errors (lastError API changed)
|
||||
#if(WIN32)
|
||||
# add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
# plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||
# target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
# target_link_libraries(test_windbg_provider PRIVATE
|
||||
# ${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
# add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
#endif()
|
||||
if(WIN32)
|
||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp
|
||||
src/scanner.cpp)
|
||||
target_include_directories(test_windbg_provider PRIVATE src plugins/WinDbgMemory)
|
||||
target_link_libraries(test_windbg_provider PRIVATE
|
||||
${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32)
|
||||
add_test(NAME test_windbg_provider COMMAND test_windbg_provider)
|
||||
endif()
|
||||
|
||||
add_executable(bench_large_class tests/bench_large_class.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/addressparser.cpp
|
||||
|
||||
@@ -124,6 +124,51 @@ void ProcessMemoryProvider::cacheModules()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (!m_handle) return regions;
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uint64_t addr = 0;
|
||||
|
||||
while (VirtualQueryEx(m_handle, (LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi)) {
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
!(mbi.Protect & PAGE_NOACCESS) &&
|
||||
!(mbi.Protect & PAGE_GUARD))
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = (uint64_t)mbi.BaseAddress;
|
||||
region.size = mbi.RegionSize;
|
||||
region.readable = true;
|
||||
region.writable = (mbi.Protect & PAGE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_WRITECOPY) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READ) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
|
||||
// Match module name from cached module list
|
||||
for (const auto& mod : m_modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break; // overflow protection
|
||||
addr = next;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
@@ -282,6 +327,58 @@ void ProcessMemoryProvider::cacheModules()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (m_fd < 0) return regions;
|
||||
|
||||
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
|
||||
std::ifstream mapsFile(mapsPath.toStdString());
|
||||
if (!mapsFile.is_open()) return regions;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(mapsFile, line)) {
|
||||
std::istringstream iss(line);
|
||||
std::string addrRange, perms, offset, dev, inode, pathname;
|
||||
iss >> addrRange >> perms >> offset >> dev >> inode;
|
||||
std::getline(iss, pathname);
|
||||
|
||||
auto dash = addrRange.find('-');
|
||||
if (dash == std::string::npos) continue;
|
||||
uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16);
|
||||
uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16);
|
||||
|
||||
if (perms.size() < 4) continue;
|
||||
bool readable = (perms[0] == 'r');
|
||||
bool writable = (perms[1] == 'w');
|
||||
bool executable = (perms[2] == 'x');
|
||||
|
||||
if (!readable) continue;
|
||||
|
||||
rcx::MemoryRegion region;
|
||||
region.base = addrStart;
|
||||
region.size = addrEnd - addrStart;
|
||||
region.readable = readable;
|
||||
region.writable = writable;
|
||||
region.executable = executable;
|
||||
|
||||
// Extract module name from pathname
|
||||
size_t start = pathname.find_first_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
QString qpath = QString::fromStdString(pathname.substr(start));
|
||||
if (qpath.startsWith('/') && !qpath.startsWith("/dev/") &&
|
||||
!qpath.startsWith("/memfd:")) {
|
||||
QFileInfo fi(qpath);
|
||||
region.moduleName = fi.fileName();
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#endif // platform
|
||||
|
||||
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
|
||||
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
bool isLive() const override { return true; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
bool isReadable(uint64_t, int len) const override {
|
||||
#ifdef _WIN32
|
||||
return m_handle && len >= 0;
|
||||
|
||||
@@ -165,6 +165,10 @@ void WinDbgMemoryProvider::initInterfaces()
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugDataSpaces2, (void**)&m_dataSpaces2);
|
||||
qDebug() << "[WinDbg] IDebugDataSpaces2 hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_dataSpaces2;
|
||||
|
||||
hr = m_client->QueryInterface(IID_IDebugControl, (void**)&m_control);
|
||||
qDebug() << "[WinDbg] IDebugControl hr=" << Qt::hex << (unsigned long)hr
|
||||
<< "ptr=" << (void*)m_control;
|
||||
@@ -251,10 +255,11 @@ WinDbgMemoryProvider::~WinDbgMemoryProvider()
|
||||
void WinDbgMemoryProvider::cleanup()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
|
||||
if (m_control) { m_control->Release(); m_control = nullptr; }
|
||||
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
|
||||
if (m_client) { m_client->Release(); m_client = nullptr; }
|
||||
if (m_symbols) { m_symbols->Release(); m_symbols = nullptr; }
|
||||
if (m_control) { m_control->Release(); m_control = nullptr; }
|
||||
if (m_dataSpaces2) { m_dataSpaces2->Release(); m_dataSpaces2 = nullptr; }
|
||||
if (m_dataSpaces) { m_dataSpaces->Release(); m_dataSpaces = nullptr; }
|
||||
if (m_client) { m_client->Release(); m_client = nullptr; }
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -351,6 +356,112 @@ QString WinDbgMemoryProvider::getSymbol(uint64_t addr) const
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> WinDbgMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
#ifdef _WIN32
|
||||
if (!m_dataSpaces) return regions;
|
||||
|
||||
// Enumerate modules — used for tagging (user-mode) or as the primary
|
||||
// source of regions (kernel-mode, where QueryVirtual is unavailable).
|
||||
struct ModInfo { uint64_t base; uint64_t size; QString name; };
|
||||
QVector<ModInfo> modules;
|
||||
|
||||
if (m_symbols) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG loaded = 0, unloaded = 0;
|
||||
if (FAILED(m_symbols->GetNumberModules(&loaded, &unloaded)))
|
||||
return;
|
||||
for (ULONG i = 0; i < loaded; i++) {
|
||||
ULONG64 modBase = 0;
|
||||
if (FAILED(m_symbols->GetModuleByIndex(i, &modBase)))
|
||||
continue;
|
||||
DEBUG_MODULE_PARAMETERS params = {};
|
||||
if (FAILED(m_symbols->GetModuleParameters(1, &modBase, 0, ¶ms)))
|
||||
continue;
|
||||
char nameBuf[256] = {};
|
||||
ULONG nameSize = 0;
|
||||
m_symbols->GetModuleNames(i, 0,
|
||||
nullptr, 0, nullptr,
|
||||
nameBuf, sizeof(nameBuf), &nameSize,
|
||||
nullptr, 0, nullptr);
|
||||
ModInfo mi;
|
||||
mi.base = modBase;
|
||||
mi.size = params.Size;
|
||||
mi.name = QString::fromUtf8(nameBuf);
|
||||
modules.append(mi);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Try QueryVirtual first (user-mode debugging / user-mode dumps).
|
||||
// MSDN: "This method is not available in kernel-mode debugging."
|
||||
if (m_dataSpaces2) {
|
||||
dispatchToOwner([&]() {
|
||||
ULONG64 addr = 0;
|
||||
int safety = 0;
|
||||
constexpr int kMaxRegions = 500000;
|
||||
|
||||
while (safety++ < kMaxRegions) {
|
||||
MEMORY_BASIC_INFORMATION64 mbi = {};
|
||||
HRESULT hr = m_dataSpaces2->QueryVirtual(addr, &mbi);
|
||||
if (FAILED(hr))
|
||||
break;
|
||||
|
||||
if (mbi.State == MEM_COMMIT &&
|
||||
!(mbi.Protect & PAGE_NOACCESS) &&
|
||||
!(mbi.Protect & PAGE_GUARD))
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mbi.BaseAddress;
|
||||
region.size = mbi.RegionSize;
|
||||
region.readable = true;
|
||||
region.writable = (mbi.Protect & PAGE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_WRITECOPY) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
region.executable = (mbi.Protect & PAGE_EXECUTE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READ) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_READWRITE) ||
|
||||
(mbi.Protect & PAGE_EXECUTE_WRITECOPY);
|
||||
|
||||
for (const auto& mod : modules) {
|
||||
if (region.base >= mod.base && region.base < mod.base + mod.size) {
|
||||
region.moduleName = mod.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
ULONG64 next = mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback for kernel-mode debugging: QueryVirtual is unavailable,
|
||||
// so use loaded modules as scannable regions. Each module image
|
||||
// becomes one region — the scanner reads through module code/data.
|
||||
if (regions.isEmpty() && !modules.isEmpty()) {
|
||||
for (const auto& mod : modules) {
|
||||
if (mod.size == 0) continue;
|
||||
rcx::MemoryRegion region;
|
||||
region.base = mod.base;
|
||||
region.size = mod.size;
|
||||
region.readable = true;
|
||||
region.writable = false;
|
||||
region.executable = true;
|
||||
region.moduleName = mod.name;
|
||||
regions.append(region);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return regions;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WinDbgMemoryPlugin implementation
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -408,7 +519,7 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
{
|
||||
QDialog dlg(parent);
|
||||
dlg.setWindowTitle("WinDbg Settings");
|
||||
dlg.resize(460, 260);
|
||||
dlg.resize(460, 300);
|
||||
|
||||
QPalette dlgPal = qApp->palette();
|
||||
dlg.setPalette(dlgPal);
|
||||
@@ -418,7 +529,9 @@ bool WinDbgMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
||||
|
||||
layout->addWidget(new QLabel(
|
||||
"Connect to a running WinDbg debug server.\n"
|
||||
"In WinDbg, run: .server tcp:port=5055"));
|
||||
"In WinDbg, run: .server tcp:port=5055\n\n"
|
||||
"Non-invasive debug and dump files only.\n"
|
||||
"Execution control (bp, g, t, p) is not supported."));
|
||||
|
||||
layout->addSpacing(8);
|
||||
layout->addWidget(new QLabel("Connection string:"));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// Forward declarations for DbgEng COM interfaces
|
||||
struct IDebugClient;
|
||||
struct IDebugDataSpaces;
|
||||
struct IDebugDataSpaces2;
|
||||
struct IDebugControl;
|
||||
struct IDebugSymbols;
|
||||
|
||||
@@ -59,6 +60,7 @@ public:
|
||||
QString name() const override { return m_name; }
|
||||
QString kind() const override { return QStringLiteral("WinDbg"); }
|
||||
QString getSymbol(uint64_t addr) const override;
|
||||
QVector<rcx::MemoryRegion> enumerateRegions() const override;
|
||||
|
||||
bool isLive() const override { return m_isLive; }
|
||||
uint64_t base() const override { return m_base; }
|
||||
@@ -73,10 +75,11 @@ private:
|
||||
template<typename Fn>
|
||||
void dispatchToOwner(Fn&& fn) const;
|
||||
|
||||
IDebugClient* m_client = nullptr;
|
||||
IDebugDataSpaces* m_dataSpaces = nullptr;
|
||||
IDebugControl* m_control = nullptr;
|
||||
IDebugSymbols* m_symbols = nullptr;
|
||||
IDebugClient* m_client = nullptr;
|
||||
IDebugDataSpaces* m_dataSpaces = nullptr;
|
||||
IDebugDataSpaces2* m_dataSpaces2 = nullptr;
|
||||
IDebugControl* m_control = nullptr;
|
||||
IDebugSymbols* m_symbols = nullptr;
|
||||
|
||||
QString m_name;
|
||||
uint64_t m_base = 0;
|
||||
|
||||
111
src/main.cpp
111
src/main.cpp
@@ -440,6 +440,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
setCentralWidget(m_mdiArea);
|
||||
|
||||
createWorkspaceDock();
|
||||
createScannerDock();
|
||||
createMenus();
|
||||
createStatusBar();
|
||||
|
||||
@@ -611,6 +612,11 @@ void MainWindow::createMenus() {
|
||||
|
||||
view->addSeparator();
|
||||
view->addAction(m_workspaceDock->toggleViewAction());
|
||||
{
|
||||
auto* scanAct = m_scannerDock->toggleViewAction();
|
||||
scanAct->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S));
|
||||
view->addAction(scanAct);
|
||||
}
|
||||
|
||||
// Tools
|
||||
auto* tools = m_titleBar->menuBar()->addMenu("&Tools");
|
||||
@@ -1813,6 +1819,25 @@ void MainWindow::applyTheme(const Theme& theme) {
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
|
||||
// Scanner dock
|
||||
if (m_scannerPanel)
|
||||
m_scannerPanel->applyTheme(theme);
|
||||
if (m_scanDockTitle) {
|
||||
QPalette lp = m_scanDockTitle->palette();
|
||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||
m_scanDockTitle->setPalette(lp);
|
||||
}
|
||||
if (auto* titleBar = m_scannerDock ? m_scannerDock->titleBarWidget() : nullptr) {
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, theme.backgroundAlt);
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
if (m_scanDockCloseBtn)
|
||||
m_scanDockCloseBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(theme.textDim.name(), theme.indHoverSpan.name()));
|
||||
|
||||
// Rendered C/C++ views: update lexer colors, paper, margins
|
||||
for (auto& tab : m_tabs) {
|
||||
for (auto& pane : tab.panes) {
|
||||
@@ -1933,6 +1958,11 @@ void MainWindow::setEditorFont(const QString& fontName) {
|
||||
// Sync dock titlebar font
|
||||
if (m_dockTitleLabel)
|
||||
m_dockTitleLabel->setFont(f);
|
||||
// Sync scanner panel font
|
||||
if (m_scannerPanel)
|
||||
m_scannerPanel->setEditorFont(f);
|
||||
if (m_scanDockTitle)
|
||||
m_scanDockTitle->setFont(f);
|
||||
}
|
||||
|
||||
RcxController* MainWindow::activeController() const {
|
||||
@@ -2814,6 +2844,87 @@ void MainWindow::createWorkspaceDock() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Scanner Dock ──
|
||||
|
||||
void MainWindow::createScannerDock() {
|
||||
m_scannerDock = new QDockWidget("Scanner", this);
|
||||
m_scannerDock->setObjectName("ScannerDock");
|
||||
m_scannerDock->setAllowedAreas(
|
||||
Qt::BottomDockWidgetArea | Qt::RightDockWidgetArea | Qt::LeftDockWidgetArea);
|
||||
m_scannerDock->setFeatures(
|
||||
QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable |
|
||||
QDockWidget::DockWidgetFloatable);
|
||||
|
||||
// Custom titlebar: label + close button (matches workspace dock)
|
||||
{
|
||||
const auto& t = ThemeManager::instance().current();
|
||||
|
||||
auto* titleBar = new QWidget(m_scannerDock);
|
||||
titleBar->setFixedHeight(24);
|
||||
titleBar->setAutoFillBackground(true);
|
||||
{
|
||||
QPalette tbPal = titleBar->palette();
|
||||
tbPal.setColor(QPalette::Window, t.backgroundAlt);
|
||||
titleBar->setPalette(tbPal);
|
||||
}
|
||||
auto* layout = new QHBoxLayout(titleBar);
|
||||
layout->setContentsMargins(6, 2, 2, 2);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_scanDockTitle = new QLabel("Scanner", titleBar);
|
||||
{
|
||||
QPalette lp = m_scanDockTitle->palette();
|
||||
lp.setColor(QPalette::WindowText, t.textDim);
|
||||
m_scanDockTitle->setPalette(lp);
|
||||
}
|
||||
layout->addWidget(m_scanDockTitle);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
m_scanDockCloseBtn = new QToolButton(titleBar);
|
||||
m_scanDockCloseBtn->setText(QStringLiteral("\u2715"));
|
||||
m_scanDockCloseBtn->setAutoRaise(true);
|
||||
m_scanDockCloseBtn->setCursor(Qt::PointingHandCursor);
|
||||
m_scanDockCloseBtn->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: %1; border: none; padding: 0px 4px 2px 4px; font-size: 12px; }"
|
||||
"QToolButton:hover { color: %2; }")
|
||||
.arg(t.textDim.name(), t.indHoverSpan.name()));
|
||||
connect(m_scanDockCloseBtn, &QToolButton::clicked, m_scannerDock, &QDockWidget::close);
|
||||
layout->addWidget(m_scanDockCloseBtn);
|
||||
|
||||
m_scannerDock->setTitleBarWidget(titleBar);
|
||||
}
|
||||
|
||||
m_scannerPanel = new ScannerPanel(m_scannerDock);
|
||||
m_scannerPanel->applyTheme(ThemeManager::instance().current());
|
||||
{
|
||||
QSettings settings("Reclass", "Reclass");
|
||||
QString fontName = settings.value("font", "JetBrains Mono").toString();
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
m_scannerPanel->setEditorFont(f);
|
||||
m_scanDockTitle->setFont(f);
|
||||
}
|
||||
m_scannerDock->setWidget(m_scannerPanel);
|
||||
addDockWidget(Qt::BottomDockWidgetArea, m_scannerDock);
|
||||
m_scannerDock->hide();
|
||||
|
||||
// Wire provider getter: lazily captures the active tab's provider at scan time
|
||||
m_scannerPanel->setProviderGetter([this]() -> std::shared_ptr<rcx::Provider> {
|
||||
auto* ctrl = activeController();
|
||||
return ctrl ? ctrl->document()->provider : nullptr;
|
||||
});
|
||||
|
||||
// Wire "Go to Address" to rebase the active tab
|
||||
connect(m_scannerPanel, &ScannerPanel::goToAddress, this, [this](uint64_t addr) {
|
||||
auto* ctrl = activeController();
|
||||
if (!ctrl) return;
|
||||
ctrl->document()->tree.baseAddress = addr;
|
||||
ctrl->document()->tree.baseAddressFormula.clear();
|
||||
ctrl->refresh();
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::rebuildAllDocs() {
|
||||
m_allDocs.clear();
|
||||
for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "controller.h"
|
||||
#include "titlebar.h"
|
||||
#include "pluginmanager.h"
|
||||
#include "scannerpanel.h"
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
#include <QMdiSubWindow>
|
||||
@@ -152,6 +153,13 @@ private:
|
||||
void rebuildWorkspaceModel();
|
||||
void updateBorderColor(const QColor& color);
|
||||
|
||||
// Scanner dock
|
||||
QDockWidget* m_scannerDock = nullptr;
|
||||
ScannerPanel* m_scannerPanel = nullptr;
|
||||
QLabel* m_scanDockTitle = nullptr;
|
||||
QToolButton* m_scanDockCloseBtn = nullptr;
|
||||
void createScannerDock();
|
||||
|
||||
protected:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
#pragma once
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct MemoryRegion {
|
||||
uint64_t base = 0;
|
||||
uint64_t size = 0;
|
||||
bool readable = true;
|
||||
bool writable = false;
|
||||
bool executable = false;
|
||||
QString moduleName;
|
||||
};
|
||||
|
||||
class Provider {
|
||||
public:
|
||||
virtual ~Provider() = default;
|
||||
@@ -54,6 +64,11 @@ public:
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Enumerate committed/readable memory regions.
|
||||
// Used by the scan engine to know what address ranges to scan.
|
||||
// Default: returns empty (scan engine falls back to [0, size())).
|
||||
virtual QVector<MemoryRegion> enumerateRegions() const { return {}; }
|
||||
|
||||
// --- Derived convenience (non-virtual, never override) ---
|
||||
|
||||
bool isValid() const { return size() > 0; }
|
||||
|
||||
108
src/scanner.cpp
108
src/scanner.cpp
@@ -1,8 +1,11 @@
|
||||
#include "scanner.h"
|
||||
#include <QtConcurrent>
|
||||
#include <QMetaObject>
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
@@ -393,12 +396,20 @@ void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& re
|
||||
QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
const ScanRequest& req)
|
||||
{
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
QVector<ScanResult> results;
|
||||
|
||||
if (!prov || req.pattern.isEmpty())
|
||||
return results;
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
qDebug() << "[scan] regions:" << regions.size()
|
||||
<< " pattern:" << req.pattern.size() << "bytes"
|
||||
<< " align:" << req.alignment
|
||||
<< " filterExec:" << req.filterExecutable
|
||||
<< " filterWrite:" << req.filterWritable;
|
||||
|
||||
// Fallback for providers that don't enumerate regions (file/buffer)
|
||||
if (regions.isEmpty()) {
|
||||
@@ -424,6 +435,8 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
totalBytes += r.size;
|
||||
}
|
||||
|
||||
qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";
|
||||
|
||||
if (totalBytes == 0) return results;
|
||||
|
||||
uint64_t scannedBytes = 0;
|
||||
@@ -473,6 +486,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
ScanResult r;
|
||||
r.address = region.base + off + (uint64_t)i;
|
||||
r.regionModule = region.moduleName;
|
||||
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
||||
results.append(r);
|
||||
|
||||
if (results.size() >= req.maxResults)
|
||||
@@ -501,6 +515,100 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
||||
}
|
||||
|
||||
done:
|
||||
qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
|
||||
<< " scanned:" << (scannedBytes / 1024) << "KB";
|
||||
return results;
|
||||
}
|
||||
|
||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize) {
|
||||
if (isRunning()) return;
|
||||
|
||||
m_abort.store(false);
|
||||
|
||||
auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
|
||||
m_watcher = watcher;
|
||||
|
||||
connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
|
||||
auto results = watcher->result();
|
||||
watcher->deleteLater();
|
||||
if (m_watcher == watcher)
|
||||
m_watcher = nullptr;
|
||||
emit rescanFinished(results);
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run(
|
||||
[this, provider, results = std::move(results), readSize]() mutable {
|
||||
return runRescan(provider, std::move(results), readSize);
|
||||
}));
|
||||
}
|
||||
|
||||
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize) {
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
int total = results.size();
|
||||
if (total == 0 || !prov) return results;
|
||||
|
||||
qDebug() << "[rescan] start: " << total << "results, readSize:" << readSize;
|
||||
|
||||
// Save previous values
|
||||
for (auto& r : results)
|
||||
r.previousValue = r.scanValue;
|
||||
|
||||
// Sort indices by address for sequential chunked reads
|
||||
QVector<int> order(total);
|
||||
for (int i = 0; i < total; i++) order[i] = i;
|
||||
std::sort(order.begin(), order.end(), [&results](int a, int b) {
|
||||
return results[a].address < results[b].address;
|
||||
});
|
||||
|
||||
constexpr int kChunk = 256 * 1024;
|
||||
int updated = 0;
|
||||
int lastPct = -1;
|
||||
int chunks = 0;
|
||||
uint64_t totalBytesRead = 0;
|
||||
int i = 0;
|
||||
|
||||
while (i < total && !m_abort.load()) {
|
||||
uint64_t spanBase = results[order[i]].address;
|
||||
int spanEnd = i;
|
||||
|
||||
// Extend span while next result fits in the same chunk
|
||||
while (spanEnd + 1 < total) {
|
||||
uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
|
||||
if (endAddr - spanBase > (uint64_t)kChunk) break;
|
||||
spanEnd++;
|
||||
}
|
||||
|
||||
uint64_t spanLast = results[order[spanEnd]].address;
|
||||
int chunkLen = (int)(spanLast + readSize - spanBase);
|
||||
QByteArray chunk(chunkLen, '\0');
|
||||
prov->read(spanBase, chunk.data(), chunkLen);
|
||||
|
||||
for (int j = i; j <= spanEnd; j++) {
|
||||
auto& r = results[order[j]];
|
||||
int off = (int)(r.address - spanBase);
|
||||
r.scanValue = chunk.mid(off, readSize);
|
||||
}
|
||||
|
||||
chunks++;
|
||||
totalBytesRead += chunkLen;
|
||||
updated += (spanEnd - i + 1);
|
||||
i = spanEnd + 1;
|
||||
|
||||
int pct = updated * 100 / total;
|
||||
if (pct != lastPct) {
|
||||
lastPct = pct;
|
||||
QMetaObject::invokeMethod(this, "progress",
|
||||
Qt::QueuedConnection, Q_ARG(int, pct));
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
|
||||
<< timer.elapsed() << "ms |" << chunks << "chunks,"
|
||||
<< (totalBytesRead / 1024) << "KB read";
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,16 +65,21 @@ public:
|
||||
explicit ScanEngine(QObject* parent = nullptr);
|
||||
|
||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||
void startRescan(std::shared_ptr<Provider> provider,
|
||||
QVector<ScanResult> results, int readSize);
|
||||
void abort();
|
||||
bool isRunning() const;
|
||||
|
||||
signals:
|
||||
void progress(int percent);
|
||||
void finished(QVector<ScanResult> results);
|
||||
void rescanFinished(QVector<ScanResult> results);
|
||||
void error(QString message);
|
||||
|
||||
private:
|
||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
||||
QVector<ScanResult> results, int readSize);
|
||||
|
||||
std::atomic<bool> m_abort{false};
|
||||
QFutureWatcher<QVector<ScanResult>>* m_watcher = nullptr;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "scannerpanel.h"
|
||||
#include "addressparser.h"
|
||||
#include <cstring>
|
||||
#include <QElapsedTimer>
|
||||
#include <QDebug>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
@@ -135,8 +137,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
m_resultTable->setColumnCount(2);
|
||||
m_resultTable->horizontalHeader()->hide();
|
||||
m_resultTable->verticalHeader()->hide();
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_resultTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_resultTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_resultTable->setEditTriggers(QAbstractItemView::DoubleClicked);
|
||||
@@ -169,11 +171,12 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
|
||||
mainLayout->addLayout(actionRow);
|
||||
|
||||
// ── Initial visibility: signature mode ──
|
||||
// ── Initial state: signature mode ──
|
||||
m_typeLabel->hide();
|
||||
m_typeCombo->hide();
|
||||
m_valueLabel->hide();
|
||||
m_valueEdit->hide();
|
||||
m_execCheck->setChecked(true);
|
||||
|
||||
// ── Connections ──
|
||||
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
@@ -225,6 +228,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
||||
});
|
||||
connect(m_engine, &ScanEngine::finished,
|
||||
this, &ScannerPanel::onScanFinished);
|
||||
connect(m_engine, &ScanEngine::rescanFinished,
|
||||
this, &ScannerPanel::onRescanFinished);
|
||||
connect(m_engine, &ScanEngine::error, this, [this](const QString& msg) {
|
||||
m_statusLabel->setText(QStringLiteral("Error: %1").arg(msg));
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
@@ -240,6 +245,8 @@ void ScannerPanel::setEditorFont(const QFont& font) {
|
||||
m_resultTable->setFont(font);
|
||||
QFontMetrics fm(font);
|
||||
m_resultTable->verticalHeader()->setDefaultSectionSize(fm.height() + 6);
|
||||
// Address column width: "00000000`00000000" + padding
|
||||
m_resultTable->setColumnWidth(0, fm.horizontalAdvance(QStringLiteral("00000000`00000000")) + 20);
|
||||
m_patternEdit->setFont(font);
|
||||
m_valueEdit->setFont(font);
|
||||
m_modeCombo->setFont(font);
|
||||
@@ -275,15 +282,16 @@ void ScannerPanel::onModeChanged(int index) {
|
||||
m_typeCombo->setVisible(!isSig);
|
||||
m_valueLabel->setVisible(!isSig);
|
||||
m_valueEdit->setVisible(!isSig);
|
||||
|
||||
// Auto-toggle filters: signatures → executable code, values → writable data
|
||||
m_execCheck->setChecked(isSig);
|
||||
m_writeCheck->setChecked(!isSig);
|
||||
}
|
||||
|
||||
void ScannerPanel::onScanClicked() {
|
||||
if (m_engine->isRunning()) {
|
||||
m_engine->abort();
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_statusLabel->setText(QStringLiteral("Scan cancelled"));
|
||||
return;
|
||||
return; // finished/rescanFinished handler resets UI
|
||||
}
|
||||
|
||||
// Get provider
|
||||
@@ -344,41 +352,40 @@ ScanRequest ScannerPanel::buildRequest() {
|
||||
void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = results;
|
||||
m_results = std::move(results);
|
||||
|
||||
// Cache scan-time bytes
|
||||
if (m_lastScanMode == 1) {
|
||||
// Value mode — every result matched the same value, no re-read needed
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
// Bytes are cached by the engine during scan.
|
||||
// Value mode: override with exact search pattern (engine caches raw chunk bytes).
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
if (m_lastScanMode == 1)
|
||||
r.scanValue = m_lastPattern;
|
||||
}
|
||||
} else {
|
||||
// Signature mode — wildcards mean each match may differ, read actual bytes
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
prov = m_providerGetter();
|
||||
for (auto& r : m_results) {
|
||||
r.previousValue.clear();
|
||||
r.scanValue = prov ? prov->readBytes(r.address, 16) : QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
m_updateBtn->setEnabled(!m_results.isEmpty());
|
||||
populateTable(false);
|
||||
{
|
||||
QElapsedTimer pt;
|
||||
pt.start();
|
||||
populateTable(false);
|
||||
qDebug() << "[panel] populateTable(initial):" << m_results.size()
|
||||
<< "results," << pt.elapsed() << "ms";
|
||||
}
|
||||
|
||||
int n = m_results.size();
|
||||
m_statusLabel->setText(QStringLiteral("%1 result%2")
|
||||
.arg(m_results.size())
|
||||
.arg(m_results.size() == 1 ? "" : "s"));
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::populateTable(bool showPrevious) {
|
||||
constexpr int kMaxRows = 10000;
|
||||
|
||||
m_resultTable->blockSignals(true);
|
||||
int cols = showPrevious ? 3 : 2;
|
||||
m_resultTable->setColumnCount(cols);
|
||||
m_resultTable->setRowCount(m_results.size());
|
||||
int displayCount = qMin(m_results.size(), kMaxRows);
|
||||
m_resultTable->setRowCount(displayCount);
|
||||
|
||||
for (int i = 0; i < m_results.size(); i++) {
|
||||
for (int i = 0; i < displayCount; i++) {
|
||||
const auto& r = m_results[i];
|
||||
|
||||
// Address column — WinDbg backtick format: 00000000`00000000
|
||||
@@ -402,13 +409,11 @@ void ScannerPanel::populateTable(bool showPrevious) {
|
||||
}
|
||||
}
|
||||
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->blockSignals(false);
|
||||
}
|
||||
|
||||
void ScannerPanel::onUpdateClicked() {
|
||||
if (m_results.isEmpty()) return;
|
||||
if (m_results.isEmpty() || m_engine->isRunning()) return;
|
||||
|
||||
std::shared_ptr<Provider> prov;
|
||||
if (m_providerGetter)
|
||||
@@ -418,55 +423,34 @@ void ScannerPanel::onUpdateClicked() {
|
||||
return;
|
||||
}
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
|
||||
m_updateBtn->setEnabled(false);
|
||||
m_scanBtn->setEnabled(false);
|
||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||
m_statusLabel->setText(QStringLiteral("Re-scanning..."));
|
||||
m_progressBar->setValue(0);
|
||||
m_progressBar->show();
|
||||
|
||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
||||
int total = m_results.size();
|
||||
m_engine->startRescan(prov, m_results, readSize);
|
||||
}
|
||||
|
||||
// Single pass: read new values + build table rows
|
||||
m_resultTable->blockSignals(true);
|
||||
m_resultTable->setColumnCount(3);
|
||||
m_resultTable->setRowCount(total);
|
||||
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||
m_scanBtn->setText(QStringLiteral("Scan"));
|
||||
m_progressBar->hide();
|
||||
m_results = std::move(results);
|
||||
m_updateBtn->setEnabled(!m_results.isEmpty());
|
||||
|
||||
for (int i = 0; i < total; i++) {
|
||||
auto& r = m_results[i];
|
||||
r.previousValue = r.scanValue;
|
||||
r.scanValue = prov->readBytes(r.address, readSize);
|
||||
|
||||
QString hexPart = QStringLiteral("%1").arg(r.address, 16, 16, QLatin1Char('0')).toUpper();
|
||||
hexPart.insert(8, '`');
|
||||
auto* addrItem = new QTableWidgetItem(hexPart);
|
||||
addrItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 0, addrItem);
|
||||
|
||||
auto* valItem = new QTableWidgetItem(formatValue(r.scanValue));
|
||||
valItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
||||
m_resultTable->setItem(i, 1, valItem);
|
||||
|
||||
auto* prevItem = new QTableWidgetItem(formatValue(r.previousValue));
|
||||
prevItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
m_resultTable->setItem(i, 2, prevItem);
|
||||
|
||||
if ((i & 0xFF) == 0) {
|
||||
m_progressBar->setValue(i * 100 / total);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
{
|
||||
QElapsedTimer pt;
|
||||
pt.start();
|
||||
populateTable(true);
|
||||
qDebug() << "[panel] populateTable(rescan):" << m_results.size()
|
||||
<< "results," << pt.elapsed() << "ms";
|
||||
}
|
||||
|
||||
m_resultTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_resultTable->horizontalHeader()->setStretchLastSection(false);
|
||||
m_resultTable->blockSignals(false);
|
||||
|
||||
m_progressBar->setValue(100);
|
||||
m_progressBar->hide();
|
||||
m_scanBtn->setEnabled(true);
|
||||
m_updateBtn->setEnabled(true);
|
||||
int n = m_results.size();
|
||||
m_statusLabel->setText(QStringLiteral("Updated %1 result%2")
|
||||
.arg(total).arg(total == 1 ? "" : "s"));
|
||||
.arg(n).arg(n == 1 ? "" : "s"));
|
||||
}
|
||||
|
||||
void ScannerPanel::onGoToAddress() {
|
||||
|
||||
@@ -65,6 +65,7 @@ private slots:
|
||||
void onResultDoubleClicked(int row, int col);
|
||||
void onCellEdited(int row, int col);
|
||||
void onUpdateClicked();
|
||||
void onRescanFinished(QVector<ScanResult> results);
|
||||
|
||||
private:
|
||||
ScanRequest buildRequest();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QClipboard>
|
||||
#include <QElapsedTimer>
|
||||
#include <cstring>
|
||||
#include "scannerpanel.h"
|
||||
#include "scanner.h"
|
||||
@@ -29,6 +30,11 @@ private slots:
|
||||
void init() {
|
||||
m_panel = new ScannerPanel();
|
||||
m_panel->show();
|
||||
// Clear mode-dependent filter defaults so BufferProvider scans
|
||||
// (which have no executable regions) work without filter issues.
|
||||
// The initialState_filterCheckboxes test verifies defaults separately.
|
||||
m_panel->execCheck()->setChecked(false);
|
||||
m_panel->writeCheck()->setChecked(false);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
|
||||
@@ -77,8 +83,19 @@ private slots:
|
||||
}
|
||||
|
||||
void initialState_filterCheckboxes() {
|
||||
QVERIFY(!m_panel->execCheck()->isChecked());
|
||||
QVERIFY(!m_panel->writeCheck()->isChecked());
|
||||
// Verify defaults on a fresh panel (init() clears filters for test convenience)
|
||||
ScannerPanel fresh;
|
||||
// Signature mode default: executable checked, writable unchecked
|
||||
QVERIFY(fresh.execCheck()->isChecked());
|
||||
QVERIFY(!fresh.writeCheck()->isChecked());
|
||||
// Switching to value mode flips them
|
||||
fresh.modeCombo()->setCurrentIndex(1);
|
||||
QVERIFY(!fresh.execCheck()->isChecked());
|
||||
QVERIFY(fresh.writeCheck()->isChecked());
|
||||
// Switching back restores signature defaults
|
||||
fresh.modeCombo()->setCurrentIndex(0);
|
||||
QVERIFY(fresh.execCheck()->isChecked());
|
||||
QVERIFY(!fresh.writeCheck()->isChecked());
|
||||
}
|
||||
|
||||
void initialState_patternPlaceholder() {
|
||||
@@ -774,8 +791,10 @@ private slots:
|
||||
std::memcpy(newBytes.data(), &newVal, 4);
|
||||
prov->writeBytes(8, newBytes);
|
||||
|
||||
// Click update
|
||||
// Click update — runs async
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY(rescanSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 3);
|
||||
// Current value = 99, previous = 50
|
||||
@@ -821,16 +840,16 @@ private slots:
|
||||
prov->writeBytes(i * 4, nb);
|
||||
}
|
||||
|
||||
// Click Re-scan
|
||||
// Click Re-scan — runs async
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY(rescanSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
// Progress bar should be hidden (completed)
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
// Table should have 3 columns now
|
||||
QCOMPARE(m_panel->resultsTable()->columnCount(), 3);
|
||||
// All rows should be populated
|
||||
QCOMPARE(m_panel->resultsTable()->rowCount(), m_panel->resultsTable()->rowCount());
|
||||
// Spot check first and last row
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("21"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("7"));
|
||||
@@ -855,6 +874,7 @@ private slots:
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(0); // Signature
|
||||
m_panel->execCheck()->setChecked(false); // BufferProvider has no exec regions
|
||||
m_panel->patternEdit()->setText("48 8B ??");
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
@@ -867,8 +887,10 @@ private slots:
|
||||
mod[0] = 0x48; mod[1] = 0x8B; mod[2] = (char)0xFF;
|
||||
prov->writeBytes(0, mod);
|
||||
|
||||
// Re-scan
|
||||
// Re-scan — runs async
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY(rescanSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
@@ -908,8 +930,12 @@ private slots:
|
||||
QByteArray nb2(4, '\0');
|
||||
std::memcpy(nb2.data(), &v2, 4);
|
||||
prov->writeBytes(4, nb2);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
{
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY(rescanSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
}
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("20"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("10"));
|
||||
|
||||
@@ -918,8 +944,12 @@ private slots:
|
||||
QByteArray nb3(4, '\0');
|
||||
std::memcpy(nb3.data(), &v3, 4);
|
||||
prov->writeBytes(4, nb3);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QApplication::processEvents();
|
||||
{
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY(rescanSpy.wait(5000));
|
||||
QApplication::processEvents();
|
||||
}
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("30"));
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("20"));
|
||||
}
|
||||
@@ -928,6 +958,151 @@ private slots:
|
||||
// Provider getter is lazy (captures at scan time)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Benchmark: initial scan + 10 re-scans on a large buffer
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void bench_rescan10x() {
|
||||
// 1MB buffer, int32 value=42 placed every 512 bytes → 2048 results
|
||||
constexpr int kBufSize = 1 * 1024 * 1024;
|
||||
constexpr int kStride = 512;
|
||||
constexpr int32_t kVal = 42;
|
||||
|
||||
QByteArray data(kBufSize, '\0');
|
||||
int planted = 0;
|
||||
for (int off = 0; off + 4 <= kBufSize; off += kStride) {
|
||||
std::memcpy(data.data() + off, &kVal, 4);
|
||||
planted++;
|
||||
}
|
||||
qDebug() << "[bench] buffer:" << (kBufSize / 1024) << "KB,"
|
||||
<< planted << "planted values, stride:" << kStride;
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(1);
|
||||
for (int i = 0; i < m_panel->typeCombo()->count(); i++) {
|
||||
if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) {
|
||||
m_panel->typeCombo()->setCurrentIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_panel->valueEdit()->setText(QString::number(kVal));
|
||||
|
||||
// ── Initial scan ──
|
||||
QElapsedTimer totalTimer;
|
||||
totalTimer.start();
|
||||
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(30000));
|
||||
QApplication::processEvents();
|
||||
|
||||
int resultCount = m_panel->resultsTable()->rowCount();
|
||||
qDebug() << "[bench] initial scan:" << totalTimer.elapsed() << "ms,"
|
||||
<< resultCount << "results displayed";
|
||||
QVERIFY(resultCount > 0);
|
||||
QVERIFY(m_panel->updateButton()->isEnabled());
|
||||
|
||||
// ── 10 re-scans — mutate values each time ──
|
||||
for (int iter = 1; iter <= 10; iter++) {
|
||||
int32_t newVal = kVal + iter;
|
||||
for (int off = 0; off + 4 <= kBufSize; off += kStride)
|
||||
std::memcpy(prov->data().data() + off, &newVal, 4);
|
||||
|
||||
QElapsedTimer iterTimer;
|
||||
iterTimer.start();
|
||||
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY2(rescanSpy.wait(30000),
|
||||
qPrintable(QString("rescan #%1 timed out").arg(iter)));
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "[bench] rescan #" << iter << ":" << iterTimer.elapsed() << "ms"
|
||||
<< "| val:" << newVal
|
||||
<< "| rows:" << m_panel->resultsTable()->rowCount()
|
||||
<< "| status:" << m_panel->statusLabel()->text();
|
||||
|
||||
QVERIFY(m_panel->resultsTable()->item(0, 1) != nullptr);
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(),
|
||||
QString::number(newVal));
|
||||
if (m_panel->resultsTable()->columnCount() >= 3) {
|
||||
QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(),
|
||||
QString::number(newVal - 1));
|
||||
}
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan"));
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
QVERIFY(m_panel->updateButton()->isEnabled());
|
||||
}
|
||||
|
||||
qDebug() << "[bench] total (scan + 10 rescans):" << totalTimer.elapsed() << "ms";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Benchmark: signature re-scan (16 bytes per result)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void bench_rescanSignature10x() {
|
||||
// 1MB buffer with "MZ" planted every 4096 bytes → 256 results
|
||||
constexpr int kBufSize = 1 * 1024 * 1024;
|
||||
constexpr int kStride = 4096;
|
||||
|
||||
QByteArray data(kBufSize, '\0');
|
||||
int planted = 0;
|
||||
for (int off = 0; off + 2 <= kBufSize; off += kStride) {
|
||||
data[off] = 0x4D; data[off + 1] = 0x5A;
|
||||
planted++;
|
||||
}
|
||||
qDebug() << "[bench-sig] buffer:" << (kBufSize / 1024) << "KB,"
|
||||
<< planted << "planted sigs";
|
||||
|
||||
auto prov = std::make_shared<BufferProvider>(data);
|
||||
m_panel->setProviderGetter([prov]() { return prov; });
|
||||
|
||||
m_panel->modeCombo()->setCurrentIndex(0);
|
||||
m_panel->patternEdit()->setText("4D 5A");
|
||||
|
||||
QElapsedTimer totalTimer;
|
||||
totalTimer.start();
|
||||
|
||||
QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished);
|
||||
QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton);
|
||||
QVERIFY(finSpy.wait(30000));
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "[bench-sig] initial scan:" << totalTimer.elapsed() << "ms,"
|
||||
<< m_panel->resultsTable()->rowCount() << "results";
|
||||
QVERIFY(m_panel->resultsTable()->rowCount() > 0);
|
||||
|
||||
for (int iter = 1; iter <= 10; iter++) {
|
||||
for (int off = 0; off + 3 <= kBufSize; off += kStride)
|
||||
prov->data().data()[off + 2] = (char)(iter & 0xFF);
|
||||
|
||||
QElapsedTimer iterTimer;
|
||||
iterTimer.start();
|
||||
|
||||
QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished);
|
||||
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
|
||||
QVERIFY2(rescanSpy.wait(30000),
|
||||
qPrintable(QString("sig rescan #%1 timed out").arg(iter)));
|
||||
QApplication::processEvents();
|
||||
|
||||
qDebug() << "[bench-sig] rescan #" << iter << ":" << iterTimer.elapsed() << "ms"
|
||||
<< "| status:" << m_panel->statusLabel()->text();
|
||||
|
||||
QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan"));
|
||||
QVERIFY(!m_panel->progressBar()->isVisible());
|
||||
QVERIFY(m_panel->updateButton()->isEnabled());
|
||||
}
|
||||
|
||||
qDebug() << "[bench-sig] total:" << totalTimer.elapsed() << "ms";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Provider getter is lazy (captures at scan time)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
void providerGetter_lazy() {
|
||||
auto prov1 = std::make_shared<BufferProvider>(QByteArray(16, '\xAA'));
|
||||
auto prov2 = std::make_shared<BufferProvider>(QByteArray(16, '\xBB'));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <QTest>
|
||||
#include <QSignalSpy>
|
||||
#include <QByteArray>
|
||||
#include <QProcess>
|
||||
#include <QThread>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <chrono>
|
||||
|
||||
#include "providers/provider.h"
|
||||
#include "scanner.h"
|
||||
#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -481,6 +483,128 @@ private slots:
|
||||
delete raw;
|
||||
}
|
||||
|
||||
// ── enumerateRegions ──
|
||||
|
||||
void provider_enumerateRegions()
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
auto regions = prov.enumerateRegions();
|
||||
qDebug() << "enumerateRegions returned" << regions.size() << "regions";
|
||||
QVERIFY2(!regions.isEmpty(), "Should return at least one memory region");
|
||||
|
||||
// Every region should have sane values
|
||||
for (const auto& r : regions) {
|
||||
QVERIFY(r.size > 0);
|
||||
QVERIFY(r.readable);
|
||||
}
|
||||
}
|
||||
|
||||
void provider_enumerateRegions_hasModuleNames()
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
auto regions = prov.enumerateRegions();
|
||||
QVERIFY(!regions.isEmpty());
|
||||
|
||||
// At least one region should have a module name
|
||||
bool hasModule = false;
|
||||
for (const auto& r : regions) {
|
||||
if (!r.moduleName.isEmpty()) {
|
||||
hasModule = true;
|
||||
qDebug() << "Region base=0x" + QString::number(r.base, 16)
|
||||
<< "size=" << r.size
|
||||
<< "module=" << r.moduleName
|
||||
<< "r/w/x:" << r.readable << r.writable << r.executable;
|
||||
break;
|
||||
}
|
||||
}
|
||||
QVERIFY2(hasModule, "At least one region should have a module name");
|
||||
}
|
||||
|
||||
void provider_enumerateRegions_hasExecutable()
|
||||
{
|
||||
WinDbgMemoryProvider prov(m_connString);
|
||||
QVERIFY(prov.isValid());
|
||||
|
||||
auto regions = prov.enumerateRegions();
|
||||
QVERIFY(!regions.isEmpty());
|
||||
|
||||
bool hasExec = false;
|
||||
for (const auto& r : regions) {
|
||||
if (r.executable) { hasExec = true; break; }
|
||||
}
|
||||
QVERIFY2(hasExec, "Should have at least one executable region (code)");
|
||||
}
|
||||
|
||||
// ── Scanner integration ──
|
||||
|
||||
void scanner_signature_mz()
|
||||
{
|
||||
// Scan for the MZ header — should find at least one match
|
||||
auto prov = std::make_shared<WinDbgMemoryProvider>(m_connString);
|
||||
QVERIFY(prov->isValid());
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
QVERIFY2(!regions.isEmpty(), "Need regions for scan");
|
||||
|
||||
rcx::ScanRequest req;
|
||||
QString err;
|
||||
QVERIFY(rcx::parseSignature("4D 5A", req.pattern, req.mask, &err));
|
||||
req.alignment = 1;
|
||||
req.maxResults = 100;
|
||||
|
||||
rcx::ScanEngine engine;
|
||||
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
||||
|
||||
engine.start(prov, req);
|
||||
QVERIFY(spy.wait(30000));
|
||||
|
||||
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
||||
qDebug() << "MZ scan found" << results.size() << "results";
|
||||
QVERIFY2(!results.isEmpty(), "Should find at least one MZ header");
|
||||
|
||||
// Verify the first result is actually 'MZ'
|
||||
uint8_t buf[2] = {};
|
||||
prov->read(results[0].address, buf, 2);
|
||||
QCOMPARE(buf[0], (uint8_t)'M');
|
||||
QCOMPARE(buf[1], (uint8_t)'Z');
|
||||
}
|
||||
|
||||
void scanner_value_int32()
|
||||
{
|
||||
// Read a known 4-byte value from offset 0x3C (PE offset) then scan for it.
|
||||
// This only works for user-mode targets where address 0 is the main module.
|
||||
auto prov = std::make_shared<WinDbgMemoryProvider>(m_connString);
|
||||
QVERIFY(prov->isValid());
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
QVERIFY2(!regions.isEmpty(), "Need regions for scan");
|
||||
|
||||
uint32_t peOffset = prov->readU32(0x3C);
|
||||
if (peOffset == 0 || peOffset >= 0x1000)
|
||||
QSKIP("Address 0 not readable (kernel session) — value scan test requires user-mode target");
|
||||
|
||||
rcx::ScanRequest req;
|
||||
QString err;
|
||||
QVERIFY(rcx::serializeValue(rcx::ValueType::UInt32,
|
||||
QString::number(peOffset), req.pattern, req.mask, &err));
|
||||
req.alignment = 4;
|
||||
req.maxResults = 100;
|
||||
|
||||
rcx::ScanEngine engine;
|
||||
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
||||
|
||||
engine.start(prov, req);
|
||||
QVERIFY(spy.wait(30000));
|
||||
|
||||
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
||||
qDebug() << "Value scan for" << peOffset << "found" << results.size() << "results";
|
||||
QVERIFY2(!results.isEmpty(), "Should find the PE offset value somewhere");
|
||||
}
|
||||
|
||||
// ── Kernel/dump session tests ──
|
||||
// Set WINDBG_KERNEL_CONN to a target string:
|
||||
// "dump:F:/path/to/file.dmp" — open dump directly
|
||||
@@ -500,7 +624,7 @@ private slots:
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("Should connect, lastError: " + prov.lastError()));
|
||||
qPrintable("Should connect to " + target));
|
||||
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
||||
|
||||
qDebug() << "Kernel provider name:" << prov.name();
|
||||
@@ -520,7 +644,7 @@ private slots:
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
qPrintable("Failed to connect to " + target));
|
||||
|
||||
bool ok = false;
|
||||
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
||||
@@ -560,7 +684,7 @@ private slots:
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
qPrintable("Failed to connect to " + target));
|
||||
|
||||
if (addr == 0) addr = prov.base();
|
||||
if (addr == 0)
|
||||
@@ -589,6 +713,67 @@ private slots:
|
||||
.arg(buf[7], 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
void provider_kernel_enumerateRegions()
|
||||
{
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN");
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("Failed to connect to " + target));
|
||||
|
||||
auto regions = prov.enumerateRegions();
|
||||
qDebug() << "Kernel enumerateRegions returned" << regions.size() << "regions";
|
||||
QVERIFY2(!regions.isEmpty(), "Should return kernel memory regions");
|
||||
|
||||
// Log first few regions
|
||||
int logged = 0;
|
||||
for (const auto& r : regions) {
|
||||
if (logged++ >= 10) break;
|
||||
qDebug() << " base=0x" + QString::number(r.base, 16)
|
||||
<< "size=" << r.size
|
||||
<< "module=" << r.moduleName
|
||||
<< "r/w/x:" << r.readable << r.writable << r.executable;
|
||||
}
|
||||
}
|
||||
|
||||
void provider_kernel_scan_signature()
|
||||
{
|
||||
QString target = kernelTarget();
|
||||
if (target.isEmpty())
|
||||
QSKIP("Set WINDBG_KERNEL_CONN");
|
||||
|
||||
auto prov = std::make_shared<WinDbgMemoryProvider>(target);
|
||||
QVERIFY2(prov->isValid(),
|
||||
qPrintable("Failed to connect to " + target));
|
||||
|
||||
auto regions = prov->enumerateRegions();
|
||||
if (regions.isEmpty())
|
||||
QSKIP("No regions enumerated — QueryVirtual may not be supported for this target");
|
||||
|
||||
// Scan for MZ header in executable regions
|
||||
rcx::ScanRequest req;
|
||||
QString err;
|
||||
QVERIFY(rcx::parseSignature("4D 5A 90 00", req.pattern, req.mask, &err));
|
||||
req.alignment = 1;
|
||||
req.filterExecutable = true;
|
||||
req.maxResults = 50;
|
||||
|
||||
rcx::ScanEngine engine;
|
||||
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
||||
|
||||
engine.start(prov, req);
|
||||
QVERIFY(spy.wait(60000));
|
||||
|
||||
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
||||
qDebug() << "Kernel MZ scan (exec only) found" << results.size() << "results";
|
||||
for (const auto& r : results)
|
||||
qDebug() << " 0x" + QString::number(r.address, 16) << r.regionModule;
|
||||
|
||||
QVERIFY2(!results.isEmpty(), "Should find MZ headers in kernel modules");
|
||||
}
|
||||
|
||||
void provider_kernel_read_backgroundThread()
|
||||
{
|
||||
QString target = kernelTarget();
|
||||
@@ -605,7 +790,7 @@ private slots:
|
||||
|
||||
WinDbgMemoryProvider prov(target);
|
||||
QVERIFY2(prov.isValid(),
|
||||
qPrintable("lastError: " + prov.lastError()));
|
||||
qPrintable("Failed to connect to " + target));
|
||||
|
||||
// Simulate the controller's async refresh pattern
|
||||
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
||||
|
||||
Reference in New Issue
Block a user