Files
archived-Reclass/tests/test_disasm.cpp
IChooseYou 6a30e0a402 fix: replace remaining QList::append({}) in plugins and tests
Missed plugin and test directories in the previous Qt 6.8 compat fix.
2026-03-14 12:11:08 -06:00

468 lines
21 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <QtTest/QTest>
#include "disasm.h"
#include "core.h"
#include "providers/buffer_provider.h"
using namespace rcx;
// Helper: extract mnemonic portion from disassembly output (after "addr ")
static QString mnemonic(const QString& line) {
int sep = line.indexOf(" ");
return sep >= 0 ? line.mid(sep + 2) : line;
}
class TestDisasm : public QObject {
Q_OBJECT
private slots:
// ──────────────────────────────────────────────────
// disassemble() unit tests exact mnemonic match
// ──────────────────────────────────────────────────
void testDisasm64_pushMov() {
QByteArray code("\x55\x48\x89\xe5", 4);
QString result = disassemble(code, 0x401000, 64);
QStringList lines = result.split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[0].startsWith("0000000000401000"));
QVERIFY(lines[1].startsWith("0000000000401001"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
}
void testDisasm64_ret() { QCOMPARE(mnemonic(disassemble(QByteArray("\xc3",1), 0x7FF000, 64)), QStringLiteral("ret")); }
void testDisasm64_nop() { QCOMPARE(mnemonic(disassemble(QByteArray("\x90",1), 0, 64)), QStringLiteral("nop")); }
void testDisasm64_xorEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x31\xc0",2), 0, 64)), QStringLiteral("xor eax, eax")); }
void testDisasm64_subRsp() { QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x83\xec\x20",4), 0, 64)), QStringLiteral("sub rsp, 0x20")); }
void testDisasm64_int3() { QCOMPARE(mnemonic(disassemble(QByteArray("\xcc",1), 0, 64)), QStringLiteral("int3")); }
void testDisasm64_pushRdi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x57",1), 0, 64)), QStringLiteral("push rdi")); }
void testDisasm64_popRsi() { QCOMPARE(mnemonic(disassemble(QByteArray("\x5e",1), 0, 64)), QStringLiteral("pop rsi")); }
void testDisasm64_testEax() { QCOMPARE(mnemonic(disassemble(QByteArray("\x85\xc0",2), 0, 64)), QStringLiteral("test eax, eax")); }
void testDisasm64_leaRipRel() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8d\x05\x10\x00\x00\x00",7), 0x1000, 64)),
QStringLiteral("lea rax, [rip+0x10]"));
}
void testDisasm64_callRel() {
// call target = 0x1000 + 5 + 0x100 = 0x1105
QCOMPARE(mnemonic(disassemble(QByteArray("\xe8\x00\x01\x00\x00",5), 0x1000, 64)),
QStringLiteral("call 0x1105"));
}
void testDisasm64_jmpRel() {
// jmp target = 0x1000 + 2 + 0x10 = 0x1012
QCOMPARE(mnemonic(disassemble(QByteArray("\xeb\x10",2), 0x1000, 64)),
QStringLiteral("jmp 0x1012"));
}
void testDisasm64_movMemRead() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x8b\x43\x10",4), 0, 64)),
QStringLiteral("mov rax, qword ptr [rbx+0x10]"));
}
void testDisasm64_movMemWrite() {
QCOMPARE(mnemonic(disassemble(QByteArray("\x48\x89\x4c\x24\x08",5), 0, 64)),
QStringLiteral("mov qword ptr [rsp+0x8], rcx"));
}
void testDisasm64_functionPrologue() {
QByteArray code("\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
QStringList lines = disassemble(code, 0x140001000ULL, 64).split('\n');
QCOMPARE(lines.size(), 4);
QVERIFY(lines[0].startsWith("0000000140001000"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
}
void testDisasm64_multipleNops() {
QStringList lines = disassemble(QByteArray(5,'\x90'), 0x1000, 64).split('\n');
QCOMPARE(lines.size(), 5);
for (int i = 0; i < 5; i++) {
QCOMPARE(mnemonic(lines[i]), QStringLiteral("nop"));
QVERIFY(lines[i].startsWith(QStringLiteral("%1").arg(0x1000+i, 16, 16, QLatin1Char('0'))));
}
}
void testDisasm32_pushMov() {
QByteArray code("\x55\x89\xe5", 3);
QStringList lines = disassemble(code, 0x401000, 32).split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[0].startsWith("00401000"));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push ebp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov ebp, esp"));
}
void testDisasm_empty() { QVERIFY(disassemble({}, 0, 64).isEmpty()); QVERIFY(disassemble({}, 0, 32).isEmpty()); }
void testDisasm_invalidBitness() { QVERIFY(disassemble(QByteArray("\x90",1), 0, 16).isEmpty()); }
void testDisasm_maxBytes() { QCOMPARE(disassemble(QByteArray(200,'\x90'), 0, 64, 128).count('\n') + 1, 128); }
void testDisasm64_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 64).indexOf(" "), 16); }
void testDisasm32_addrWidth() { QCOMPARE(disassemble(QByteArray("\x90",1), 0, 32).indexOf(" "), 8); }
// ──────────────────────────────────────────────────
// hexDump() unit tests
// ──────────────────────────────────────────────────
void testHexDump_basic() {
QByteArray data; for (int i=0;i<32;i++) data.append((char)i);
QString r = hexDump(data, 0x1000, 128);
QCOMPARE(r.count('\n')+1, 2);
QVERIFY(r.startsWith("00001000"));
}
void testHexDump_ascii() {
QVERIFY(hexDump(QByteArray("Hello, World!xx",15), 0, 128).contains("Hello"));
}
void testHexDump_nonPrintable() {
QByteArray d(16,'\0'); d[0]='A'; d[15]='Z';
QVERIFY(hexDump(d, 0, 128).contains("A..............Z"));
}
void testHexDump_empty() { QVERIFY(hexDump({}, 0).isEmpty()); }
void testHexDump_maxBytes() { QCOMPARE(hexDump(QByteArray(200,'\xAA'), 0, 64).count('\n')+1, 4); }
void testHexDump_wideAddr() { QVERIFY(hexDump(QByteArray(16,'\0'), 0x100000000ULL, 128).startsWith("0000000100000000")); }
void testHexDump_hexValues() {
QByteArray d; d.append('\xDE'); d.append('\xAD'); d.append('\xBE'); d.append('\xEF');
while (d.size()<16) d.append('\0');
QVERIFY(hexDump(d, 0, 128).contains("de ad be ef", Qt::CaseInsensitive));
}
void testHexDump_secondLineAddr() {
QStringList lines = hexDump(QByteArray(32,'\x42'), 0x2000, 128).split('\n');
QCOMPARE(lines.size(), 2);
QVERIFY(lines[1].startsWith("00002010"));
}
// ──────────────────────────────────────────────────
// End-to-end: pointer-expanded VTable with FuncPtr64
// Verifies we read from the COMPOSED address, not node.offset
// ──────────────────────────────────────────────────
void testVTableDisasm_composedAddress() {
// Memory layout (absolute addresses, baseAddress = 0):
//
// [0x0000] Root "Obj" struct
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
//
// [0x0100] VTable (expanded via pointer deref)
// +0x00: func ptr 0 => value 0x200 (func0 code)
// +0x08: func ptr 1 => value 0x300 (func1 code)
//
// [0x0200] func0 code: push rbp; ret
// [0x0300] func1 code: xor eax, eax; ret
//
// Build a 4KB buffer
QByteArray mem(4096, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Root object at offset 0: __vptr points to vtable at 0x100
w64(0x00, 0x100);
// VTable at offset 0x100: two function pointers
w64(0x100, 0x200); // slot 0 -> func0
w64(0x108, 0x300); // slot 1 -> func1
// func0 at offset 0x200: push rbp; ret
mem[0x200] = '\x55';
mem[0x201] = '\xc3';
// func1 at offset 0x300: xor eax, eax; ret
mem[0x300] = '\x31';
mem[0x301] = '\xc0';
mem[0x302] = '\xc3';
BufferProvider prov(mem);
// Build node tree
NodeTree tree;
tree.baseAddress = 0;
// Root struct "Obj"
Node root;
root.kind = NodeKind::Struct;
root.name = "Obj";
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// VTable struct definition (template)
Node vtDef;
vtDef.kind = NodeKind::Struct;
vtDef.name = "VTable";
vtDef.parentId = 0;
vtDef.offset = 0x1000; // parked far away so it doesn't overlap
int vti = tree.addNode(vtDef);
uint64_t vtId = tree.nodes[vti].id;
// Two FuncPtr64 children inside VTable definition
Node fp0;
fp0.kind = NodeKind::FuncPtr64;
fp0.name = "func0";
fp0.parentId = vtId;
fp0.offset = 0;
tree.addNode(fp0);
Node fp1;
fp1.kind = NodeKind::FuncPtr64;
fp1.name = "func1";
fp1.parentId = vtId;
fp1.offset = 8;
tree.addNode(fp1);
// Pointer64 "__vptr" in root, pointing to VTable via refId
Node vptr;
vptr.kind = NodeKind::Pointer64;
vptr.name = "__vptr";
vptr.parentId = rootId;
vptr.offset = 0;
vptr.refId = vtId;
vptr.collapsed = false;
tree.addNode(vptr);
// Compose the tree
ComposeResult result = compose(tree, prov);
// Find the FuncPtr64 lines in the composed output that are inside the
// pointer-expanded VTable (near vtable address), not the standalone definition.
struct FuncInfo { int line; uint64_t offsetAddr; NodeKind kind; QString name; };
QVector<FuncInfo> funcPtrs;
for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i];
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
// Only include the pointer-expanded ones (near vtable at 0x100)
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
int nodeIdx = lm.nodeIdx;
funcPtrs.push_back(FuncInfo{i, lm.offsetAddr, lm.nodeKind,
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
}
}
}
QCOMPARE(funcPtrs.size(), 2);
// Verify composed addresses point to the vtable, NOT to the root struct
// func0 should be at 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
// func1 should be at 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
// Now simulate what the hover code should do:
// Read the function pointer VALUE from the correct provider address
for (const auto& fp : funcPtrs) {
// Provider reads at absolute address directly
uint64_t provAddr = fp.offsetAddr;
// Read the pointer value (the function address)
uint64_t ptrVal = prov.readU64(provAddr);
// Verify we got the right pointer values
if (fp.name == "func0") {
QCOMPARE(ptrVal, (uint64_t)0x200);
} else {
QCOMPARE(ptrVal, (uint64_t)0x300);
}
// Read code bytes at the pointer target (absolute address)
uint64_t codeProvAddr = ptrVal;
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
// Disassemble and verify
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for " + fp.name));
QStringList lines = asm_.split('\n');
if (fp.name == "func0") {
// Should decode: push rbp; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func0, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
// Verify address in output matches the real function address
QVERIFY2(lines[0].contains("200"),
qPrintable("func0 addr wrong: " + lines[0]));
} else {
// Should decode: xor eax, eax; ret
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
QVERIFY2(lines[0].contains("300"),
qPrintable("func1 addr wrong: " + lines[0]));
}
}
// CRITICAL: Verify that reading from node.offset (the WRONG way) gives
// different/wrong results. node.offset for func0=0, func1=8, which are
// inside the ROOT struct, not the vtable.
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, (uint64_t)0x100);
// This is the vtable address, not a function — disassembling it would be wrong
QVERIFY2(wrongVal0 != (uint64_t)0x200,
"node.offset reads the vptr, not the function pointer");
QVERIFY2(wrongVal1 != (uint64_t)0x300,
"node.offset=8 reads past vptr, not the second function pointer");
}
void testVTableDisasm_wrongAddressGivesWrongCode() {
// Demonstrate that using node.offset instead of composed address
// gives completely wrong disassembly results
QByteArray mem(1024, '\0');
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
// Root at 0: vptr -> 0x80
w64(0x00, (uint64_t)0x80);
// VTable at 0x80: one func ptr -> 0x100
w64(0x80, (uint64_t)0x100);
// Code at 0x100: sub rsp, 0x28; nop; ret
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
BufferProvider prov(mem);
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
uint64_t wrongPtrVal = prov.readU64(0);
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
// RIGHT: read from composed address (vtable + 0)
uint64_t rightPtrVal = prov.readU64(0x80);
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
// Disassemble the RIGHT target
QByteArray rightCode = prov.readBytes(0x100, 128);
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
QStringList rightLines = rightAsm.split('\n');
QVERIFY(rightLines.size() >= 3);
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
QCOMPARE(mnemonic(rightLines[1]), QStringLiteral("nop"));
QCOMPARE(mnemonic(rightLines[2]), QStringLiteral("ret"));
// Disassemble the WRONG target (vtable data, not code!)
QByteArray wrongCode = prov.readBytes(0x80, 128);
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
// The wrong bytes are the vtable entries (pointer values),
// which decode as garbage instructions, not sub/nop/ret
QVERIFY2(!wrongAsm.contains("sub rsp"),
qPrintable("Wrong address should NOT produce sub rsp: " + wrongAsm));
}
void testHoverFlow_fullSimulation() {
// Full simulation of the hover flow as implemented in editor.cpp:
//
// 1. Compose the tree to get LineMeta with correct offsetAddr
// 2. For each FuncPtr64 line, read pointer value from provider
// using lm.offsetAddr (absolute address)
// 3. Read code bytes from the REAL provider using ptrVal directly
// (the real provider can read any process address; snapshot cannot)
// 4. Disassemble the code bytes
//
// The key distinction: step 2 reads from composed tree addresses (in
// the snapshot), step 3 reads from arbitrary code addresses (needs
// the real provider, not snapshot).
QByteArray mem(8192, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Layout:
// [0x000] Root struct: __vptr -> vtable at 0x100
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
// [0x1800] func1 code: xor eax, eax; ret
w64(0x000, (uint64_t)0x100); // __vptr
w64(0x100, (uint64_t)0x1000); // vtable[0]
w64(0x108, (uint64_t)0x1800); // vtable[1]
// func0 code
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
// func1 code
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
// This provider represents the real process memory.
BufferProvider realProv(mem);
// Build a snapshot that only contains tree-data pages (like the
// async refresh does). The snapshot does NOT contain function code pages.
// This simulates the real scenario where SnapshotProvider only has
// pages for the root struct and pointer-expanded structs.
QByteArray snapData(0x200, '\0'); // only pages for root + vtable
memcpy(snapData.data(), mem.constData(), 0x200);
BufferProvider snapProv(snapData);
// Build node tree
NodeTree tree;
tree.baseAddress = 0;
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
root.parentId = 0; root.offset = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
Node vtDef; vtDef.kind = NodeKind::Struct; vtDef.name = "VTable";
vtDef.parentId = 0; vtDef.offset = 0x2000;
int vti = tree.addNode(vtDef);
uint64_t vtId = tree.nodes[vti].id;
Node fp0; fp0.kind = NodeKind::FuncPtr64; fp0.name = "func0";
fp0.parentId = vtId; fp0.offset = 0;
tree.addNode(fp0);
Node fp1; fp1.kind = NodeKind::FuncPtr64; fp1.name = "func1";
fp1.parentId = vtId; fp1.offset = 8;
tree.addNode(fp1);
Node vptr; vptr.kind = NodeKind::Pointer64; vptr.name = "__vptr";
vptr.parentId = rootId; vptr.offset = 0; vptr.refId = vtId;
vptr.collapsed = false;
tree.addNode(vptr);
// Compose with the snapshot (like production: compose uses snapshot)
ComposeResult result = compose(tree, snapProv);
// Find expanded FuncPtr64 lines
for (int i = 0; i < result.meta.size(); i++) {
const LineMeta& lm = result.meta[i];
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
continue;
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
continue; // skip standalone VTable definition entries
// --- Hover step 1: read pointer value from snapshot ---
uint64_t provAddr = lm.offsetAddr;
// The snapshot has this data (vtable pages are in it)
QVERIFY2(snapProv.isReadable(provAddr, 8),
qPrintable(QString("Snapshot should have vtable page at %1")
.arg(provAddr, 0, 16)));
uint64_t ptrVal = snapProv.readU64(provAddr);
QVERIFY2(ptrVal != 0, "Function pointer should not be zero");
// --- Hover step 2: read code from REAL provider ---
// The snapshot does NOT have the code pages:
uint64_t codeAddr = ptrVal;
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
"Snapshot should NOT have function code pages");
// But the real provider does:
QByteArray codeBytes(128, Qt::Uninitialized);
bool readOk = realProv.read(codeAddr, codeBytes.data(), 128);
QVERIFY2(readOk, "Real provider should be able to read code bytes");
// --- Hover step 3: disassemble ---
QString asm_ = disassemble(codeBytes, ptrVal, 64, 128);
QVERIFY2(!asm_.isEmpty(), qPrintable("Empty disasm for line " + QString::number(i)));
QStringList lines = asm_.split('\n');
const Node& node = tree.nodes[lm.nodeIdx];
if (node.name == "func0") {
QVERIFY(lines.size() >= 4);
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("mov rbp, rsp"));
QCOMPARE(mnemonic(lines[2]), QStringLiteral("sub rsp, 0x20"));
QCOMPARE(mnemonic(lines[3]), QStringLiteral("ret"));
} else if (node.name == "func1") {
QVERIFY(lines.size() >= 2);
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
}
}
}
};
QTEST_MAIN(TestDisasm)
#include "test_disasm.moc"