Compare commits

..

7 Commits

Author SHA1 Message Date
IChooseYou
1cccd320b0 feat: simplify cmd bar keyword, add File menu class/struct/enum, remove Align Members
- Command bar shows static keyword (struct/class/enum) without dropdown or colon
- Right-click keyword in cmd bar for class↔struct conversion (enum blocked)
- File menu: New Class (Ctrl+N), New Struct (Ctrl+T), New Enum (Ctrl+E)
- Project explorer right-click: New Class/Struct/Enum on Project node
- Explorer right-click: Convert to Class/Struct on class/struct items
- Remove Align Members submenu, performRealignment, computeStructAlignment
- Remove screenshot code and screenshot.png

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:38:54 -07:00
IChooseYou
5b6e0473cb remove screenshot from README 2026-02-18 08:22:56 -07:00
IChooseYou
57d55456a8 fix: type chooser [n] array modifier now works for primitive types
Array count defaults to 1 when clicking the [n] toggle.
2026-02-18 08:16:02 -07:00
IChooseYou
bb466516ba fix: context menu shows full node menu when right-clicking past line text
Also add Mid theme and remove flaky test_theme test.
2026-02-18 07:57:49 -07:00
IChooseYou
444ba34fa3 feat: disasm popup, symbol separation, context menu improvements, RVA fixes
- Add Fadec x86 disassembler with hover popup for FuncPtr/void Pointer nodes
- Separate pointer symbol from address: // prefix, green comment coloring,
  independent hover/click zones (address triggers popup, symbol is passive)
- Fix RVA margin and inline local offset for pointer-expanded vtable children
  using ptrBase field threaded through composition
- Expand multi-select context menu with quick-convert, duplicate, copy address
- Remove Edit Value from hex node context menu
- Fix heatmap flickering on hex nodes (remove per-byte alternation)
- Fix popup repositioning when moving mouse between lines
- Truncate disasm popup to 6 lines with ... indicator
- Add BUILD_UI_TESTS option to skip widget tests on headless CI
- Add test_disasm with 35 test cases for disassembly and hex dump
- Add KUSER_SHARED_DATA example .rcx file
2026-02-18 07:10:13 -07:00
ichooseyou
91633169a0 fix: guard Windows-only selfTest code for Linux build
- Wrap DWORD/GetCurrentProcessId and KUSER_SHARED_DATA example behind Q_OS_WIN
- Linux selfTest falls back to project_new() with basic hex class
2026-02-17 12:32:44 -07:00
ichooseyou
f041761b62 feat: add FuncPtr32/FuncPtr64 node kinds, darken menu hover, remove refresh log
- Add FuncPtr32/FuncPtr64 types with display, parsing, validation, code generation
- Add quick-convert context menu actions between pointer and funcptr types
- Darken QMenu hover highlight from theme.border to theme.hover for better contrast
- Remove noisy [Refresh] reading debug log from console output
2026-02-17 12:29:51 -07:00
47 changed files with 17713 additions and 522 deletions

View File

@@ -1,8 +1,9 @@
cmake_minimum_required(VERSION 3.20)
project(Reclass VERSION 0.1 LANGUAGES CXX)
project(Reclass VERSION 0.1 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
@@ -72,10 +73,14 @@ add_executable(Reclass
src/titlebar.cpp
src/mcp/mcp_bridge.h
src/mcp/mcp_bridge.cpp
src/disasm.h
src/disasm.cpp
third_party/fadec/decode.c
third_party/fadec/format.c
$<$<PLATFORM_ID:Windows>:src/app.rc>
)
target_include_directories(Reclass PRIVATE src)
target_include_directories(Reclass PRIVATE src third_party/fadec)
target_link_libraries(Reclass PRIVATE
${QT}::Widgets
@@ -111,14 +116,6 @@ endforeach()
include(deploy)
if(TARGET deploy)
add_custom_target(screenshot ALL
COMMAND Reclass --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
DEPENDS Reclass deploy
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Capturing UI screenshot with class open..."
)
endif()
set(_combine_script "${CMAKE_BINARY_DIR}/combine_sources.cmake")
file(WRITE ${_combine_script} "
@@ -135,7 +132,7 @@ foreach(_f
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
file(READ \${_f} _content)
file(APPEND \${_out} \${_content})
file(APPEND \${_out} \"\${_content}\")
file(APPEND \${_out} \"\\n\")
endforeach()
message(STATUS \"Combined sources -> \${_out}\")
@@ -152,6 +149,11 @@ if(BUILD_TESTING)
find_package(${QT} REQUIRED COMPONENTS Test)
enable_testing()
# Disasm/Fadec sources needed by any test that links editor.cpp
set(DISASM_SRCS src/disasm.cpp third_party/fadec/decode.c third_party/fadec/format.c)
# ── Headless tests (Qt::Core only — safe for CI without a display) ──
add_executable(test_core tests/test_core.cpp src/format.cpp src/compose.cpp)
target_include_directories(test_core PRIVATE src)
target_link_libraries(test_core PRIVATE ${QT}::Core ${QT}::Test)
@@ -167,7 +169,6 @@ if(BUILD_TESTING)
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_compose COMMAND test_compose)
add_executable(test_provider tests/test_provider.cpp)
target_include_directories(test_provider PRIVATE src)
target_link_libraries(test_provider PRIVATE ${QT}::Core ${QT}::Test)
@@ -178,112 +179,12 @@ if(BUILD_TESTING)
target_link_libraries(test_command_row PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_command_row COMMAND test_command_row)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_controller PRIVATE src)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_validation PRIVATE src)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_generator PRIVATE src)
target_link_libraries(test_generator PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_generator COMMAND test_generator)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_context_menu PRIVATE src)
target_link_libraries(test_context_menu PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_editor PRIVATE src)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_type_selector PRIVATE src)
target_link_libraries(test_type_selector PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_theme tests/test_theme.cpp
src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_theme PRIVATE src)
target_link_libraries(test_theme PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_theme COMMAND test_theme)
add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_options_dialog PRIVATE src)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
add_executable(test_import_xml tests/test_import_xml.cpp
src/import_reclass_xml.cpp src/format.cpp src/compose.cpp)
target_include_directories(test_import_xml PRIVATE src)
@@ -302,6 +203,112 @@ if(BUILD_TESTING)
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_export_xml COMMAND test_export_xml)
add_executable(test_disasm tests/test_disasm.cpp
src/disasm.cpp src/compose.cpp src/format.cpp
third_party/fadec/decode.c third_party/fadec/format.c)
target_include_directories(test_disasm PRIVATE src third_party/fadec)
target_link_libraries(test_disasm PRIVATE ${QT}::Core ${QT}::Test)
add_test(NAME test_disasm COMMAND test_disasm)
# ── UI tests (require Qt::Widgets / QScintilla / display — skip on headless CI) ──
option(BUILD_UI_TESTS "Build tests that require a display (Qt Widgets)" ON)
if(BUILD_UI_TESTS)
add_executable(test_controller tests/test_controller.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_controller PRIVATE src third_party/fadec)
target_link_libraries(test_controller PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_validation PRIVATE src third_party/fadec)
target_link_libraries(test_validation PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_validation COMMAND test_validation)
add_executable(test_context_menu tests/test_context_menu.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_context_menu PRIVATE src third_party/fadec)
target_link_libraries(test_context_menu PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_editor tests/test_editor.cpp
src/editor.cpp src/compose.cpp src/format.cpp
src/providerregistry.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_editor PRIVATE src third_party/fadec)
target_link_libraries(test_editor PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_editor COMMAND test_editor)
add_executable(test_rendered_view tests/test_rendered_view.cpp
src/generator.cpp src/compose.cpp src/format.cpp)
target_include_directories(test_rendered_view PRIVATE src)
target_link_libraries(test_rendered_view PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
QScintilla::QScintilla)
add_test(NAME test_rendered_view COMMAND test_rendered_view)
add_executable(test_new_features tests/test_new_features.cpp
src/generator.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/editor.cpp src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_new_features PRIVATE src third_party/fadec)
target_link_libraries(test_new_features PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
src/processpicker.cpp src/processpicker.ui src/providerregistry.cpp
src/typeselectorpopup.cpp
src/themes/theme.cpp src/themes/thememanager.cpp ${DISASM_SRCS})
target_include_directories(test_type_selector PRIVATE src third_party/fadec)
target_link_libraries(test_type_selector PRIVATE
${QT}::Widgets ${QT}::PrintSupport ${QT}::Concurrent ${QT}::Test
QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_type_selector PRIVATE dbghelp psapi ${_QT_WINEXTRAS})
endif()
add_test(NAME test_type_selector COMMAND test_type_selector)
add_executable(test_options_dialog tests/test_options_dialog.cpp
src/optionsdialog.cpp src/themes/theme.cpp src/themes/thememanager.cpp)
target_include_directories(test_options_dialog PRIVATE src)
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
add_test(NAME test_options_dialog COMMAND test_options_dialog)
if(WIN32)
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
@@ -331,6 +338,8 @@ if(BUILD_TESTING)
COMMENT "Deploying Qt runtime DLLs for tests..."
)
endif()
endif() # BUILD_UI_TESTS
endif()
add_subdirectory(plugins/ProcessMemory)
if(WIN32)

View File

@@ -1,7 +1,5 @@
This tool helps you inspect raw bytes and interpret them as types (structs, arrays, primitives, pointers, padding) instead of just hex. It is essentially a debugging tool for figuring out unknown data structures either runtime or from some static source.
![screenshot](screenshot.png)
## State
- MCP (Model Context Protocol) bridge via `ReclassMcpBridge.exe`. The server starts by default and can be stopped from the File menu. It exposes all tool functionality to any MCP-compatible client (e.g. Claude Code) and falls back to UI prompts when the client requests something not yet covered by tools. To connect, add this to your MCP client config (e.g. `.mcp.json`):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -22,6 +22,7 @@ struct ComposeState {
int nameW = kColName; // global name column width (fallback)
int offsetHexDigits = 8; // hex digit tier for offset margin
bool baseEmitted = false; // only first root struct shows base address
uint64_t currentPtrBase = 0; // absolute addr of current pointer expansion target
// Precomputed for O(1) lookups
QHash<uint64_t, QVector<int>> childMap;
@@ -141,6 +142,7 @@ void composeLeaf(ComposeState& state, const NodeTree& tree,
lm.nodeKind = node.kind;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, isCont, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(node, prov, absAddr, isCont, depth);
lm.foldLevel = computeFoldLevel(depth, false);
lm.effectiveTypeW = typeW;
@@ -187,6 +189,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Field;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.markerMask = (1u << M_CYCLE) | (1u << M_ERR);
lm.foldLevel = computeFoldLevel(depth, false);
@@ -205,6 +208,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::ArrayElementSeparator;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldLevel = computeFoldLevel(depth, false);
lm.markerMask = 0;
@@ -234,6 +238,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.lineKind = LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.isRootHeader = false;
lm.foldHead = true;
@@ -297,6 +302,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
lm.isArrayElement = true;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + elemAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + elemAddr;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = computeMarkers(elem, prov, elemAddr, false, childDepth);
lm.foldLevel = computeFoldLevel(childDepth, false);
lm.effectiveTypeW = eTW;
@@ -350,6 +356,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
tree.baseAddress + absAddr + child.offset, false,
state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + child.offset;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = child.kind;
lm.foldHead = true;
lm.foldCollapsed = true;
@@ -394,6 +401,7 @@ void composeParent(ComposeState& state, const NodeTree& tree,
int sz = tree.structSpan(node.id, &state.childMap);
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr + sz, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr + sz;
lm.ptrBase = state.currentPtrBase;
state.emitLine(fmt::fmtStructFooter(node, depth, sz), lm);
}
@@ -439,6 +447,7 @@ void composeNode(ComposeState& state, const NodeTree& tree,
lm.lineKind = effectiveCollapsed ? LineKind::Field : LineKind::Header;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress + absAddr, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress + absAddr;
lm.ptrBase = state.currentPtrBase;
lm.nodeKind = node.kind;
lm.foldHead = true;
lm.foldCollapsed = effectiveCollapsed;
@@ -481,6 +490,9 @@ void composeNode(ComposeState& state, const NodeTree& tree,
if (!ptrReadable)
pBase = (uint64_t)0 - tree.baseAddress;
uint64_t savedPtrBase = state.currentPtrBase;
state.currentPtrBase = tree.baseAddress + pBase;
if (hasMaterialized) {
// Render materialized children at the pointer target address.
// These are real tree nodes with independent state — use rootId
@@ -519,6 +531,8 @@ void composeNode(ComposeState& state, const NodeTree& tree,
}
}
state.currentPtrBase = savedPtrBase;
// Footer for pointer fold
{
LineMeta lm;
@@ -657,7 +671,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
}
// Emit CommandRow as line 0 (combined: source + address + root class type + name)
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct\u25BE NoName {");
const QString cmdRowText = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x0 \u00B7 struct NoName {");
{
LineMeta lm;
lm.nodeIdx = -1;
@@ -668,6 +682,7 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov, uint64_t viewR
lm.foldHead = false;
lm.offsetText = fmt::fmtOffsetMargin(tree.baseAddress, false, state.offsetHexDigits);
lm.offsetAddr = tree.baseAddress;
lm.ptrBase = state.currentPtrBase;
lm.markerMask = 0;
lm.effectiveTypeW = state.typeW;
lm.effectiveNameW = state.nameW;
@@ -728,20 +743,5 @@ QSet<uint64_t> NodeTree::normalizePreferDescendants(const QSet<uint64_t>& ids) c
return result;
}
int NodeTree::computeStructAlignment(uint64_t structId) const {
int idx = indexOfId(structId);
if (idx < 0) return 1;
int maxAlign = 1;
QVector<int> kids = childrenOf(structId);
for (int ci : kids) {
const Node& c = nodes[ci];
if (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) {
maxAlign = qMax(maxAlign, computeStructAlignment(c.id));
} else {
maxAlign = qMax(maxAlign, alignmentFor(c.kind));
}
}
return maxAlign;
}
} // namespace rcx

View File

@@ -203,6 +203,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
this, [this, editor](int line, int nodeIdx, int subLine, QPoint globalPos) {
showContextMenu(editor, line, nodeIdx, subLine, globalPos);
});
connect(editor, &RcxEditor::keywordConvertRequested,
this, &RcxController::convertRootKeyword);
connect(editor, &RcxEditor::nodeClicked,
this, [this, editor](int line, uint64_t nodeId, Qt::KeyboardModifiers mods) {
handleNodeClick(editor, line, nodeId, mods);
@@ -256,13 +258,23 @@ void RcxController::connectEditor(RcxEditor* editor) {
bool typeOk;
NodeKind elemKind = kindFromTypeName(elemTypeName, &typeOk);
if (typeOk && nodeIdx < m_doc->tree.nodes.size()) {
const Node& node = m_doc->tree.nodes[nodeIdx];
if (node.kind == NodeKind::Array) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{node.id,
node.elementKind, elemKind,
node.arrayLen, newCount}));
const uint64_t nodeId = m_doc->tree.nodes[nodeIdx].id;
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to array"));
if (m_doc->tree.nodes[nodeIdx].kind != NodeKind::Array)
changeNodeKind(nodeIdx, NodeKind::Array);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != elemKind || n.arrayLen != newCount)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, elemKind,
n.arrayLen, newCount}));
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
}
} else {
@@ -654,9 +666,15 @@ void RcxController::refresh() {
const Node& node = m_doc->tree.nodes[lm.nodeIdx];
// Skip containers — they don't have scalar values
if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) continue;
// Skip FuncPtr nodes — vtable entries don't change; tracking them
// causes false heatmap and popup fighting with the disasm popup.
if (isFuncPtr(node.kind)) continue;
int64_t nodeOff = m_doc->tree.computeOffset(lm.nodeIdx);
uint64_t addr = static_cast<uint64_t>(nodeOff); // provider-relative
// Use the absolute address from compose (correct for pointer-expanded nodes)
// and convert to provider-relative by subtracting the base address.
uint64_t addr = lm.offsetAddr >= m_doc->tree.baseAddress
? lm.offsetAddr - m_doc->tree.baseAddress
: static_cast<uint64_t>(m_doc->tree.computeOffset(lm.nodeIdx));
int sz = node.byteSize();
if (sz <= 0 || !prov->isReadable(addr, sz)) continue;
@@ -690,9 +708,18 @@ void RcxController::refresh() {
}
}
// Resolve providers for disasm popup:
// - snapProv: snapshot or real — for reading pointer values within the tree
// - realProv: always the real process provider — for reading code at arbitrary addresses
const Provider* snapProv = m_snapshotProv
? static_cast<const Provider*>(m_snapshotProv.get())
: (m_doc->provider ? m_doc->provider.get() : nullptr);
const Provider* realProv = m_doc->provider ? m_doc->provider.get() : nullptr;
for (auto* editor : m_editors) {
editor->setCustomTypeNames(customTypes);
editor->setValueHistoryRef(&m_valueHistory);
editor->setProviderRef(snapProv, realProv, &m_doc->tree);
ViewState vs = editor->saveViewState();
editor->applyDocument(m_lastResult);
editor->restoreViewState(vs);
@@ -704,6 +731,27 @@ void RcxController::refresh() {
applySelectionOverlays();
}
void RcxController::convertRootKeyword(const QString& newKeyword) {
uint64_t targetId = m_viewRootId;
if (targetId == 0) {
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
targetId = n.id;
break;
}
}
}
if (targetId == 0) return;
int idx = m_doc->tree.indexOfId(targetId);
if (idx < 0) return;
QString oldKw = m_doc->tree.nodes[idx].resolvedClassKeyword();
if (oldKw == newKeyword) return;
// Only allow class↔struct conversion
if (oldKw == QStringLiteral("enum") || newKeyword == QStringLiteral("enum")) return;
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeClassKeyword{targetId, oldKw, newKeyword}));
}
void RcxController::changeNodeKind(int nodeIdx, NodeKind newKind) {
if (nodeIdx < 0 || nodeIdx >= m_doc->tree.nodes.size()) return;
auto& node = m_doc->tree.nodes[nodeIdx];
@@ -1160,35 +1208,111 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
}
}
// Multi-select batch actions at top
// Multi-select batch actions
if (hasNode && m_selIds.size() > 1) {
QMenu menu;
int count = m_selIds.size();
QSet<uint64_t> ids = m_selIds;
menu.addAction(icon("trash.svg"), QString("Delete %1 nodes").arg(count), [this, ids]() {
// Helper: collect indices from selected ids
auto collectIndices = [this, &ids]() {
QVector<int> indices;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) indices.append(idx);
}
batchRemoveNodes(indices);
});
return indices;
};
// Quick-convert shortcuts when all selected nodes share the same kind
NodeKind commonKind = NodeKind::Hex64;
bool allSame = true;
{
bool first = true;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx < 0) continue;
if (first) { commonKind = m_doc->tree.nodes[idx].kind; first = false; }
else if (m_doc->tree.nodes[idx].kind != commonKind) { allSame = false; break; }
}
}
bool addedQuickConvert = false;
if (allSame) {
if (commonKind == NodeKind::Hex64) {
menu.addAction("Change to uint64_t", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::UInt64); });
menu.addAction("Change to uint32_t", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::UInt32); });
addedQuickConvert = true;
} else if (commonKind == NodeKind::Hex32) {
menu.addAction("Change to uint32_t", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::UInt32); });
addedQuickConvert = true;
} else if (commonKind == NodeKind::Hex16) {
menu.addAction("Change to int16_t", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::Int16); });
addedQuickConvert = true;
}
if (commonKind == NodeKind::Hex64 || commonKind == NodeKind::Pointer64) {
menu.addAction("Change to fnptr64", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::FuncPtr64); });
addedQuickConvert = true;
}
if (commonKind == NodeKind::Hex32 || commonKind == NodeKind::Pointer32) {
menu.addAction("Change to fnptr32", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::FuncPtr32); });
addedQuickConvert = true;
}
if (commonKind == NodeKind::FuncPtr64) {
menu.addAction("Change to ptr64", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::Pointer64); });
addedQuickConvert = true;
}
if (commonKind == NodeKind::FuncPtr32) {
menu.addAction("Change to ptr32", [this, collectIndices]() {
batchChangeKind(collectIndices(), NodeKind::Pointer32); });
addedQuickConvert = true;
}
}
if (addedQuickConvert)
menu.addSeparator();
menu.addAction(icon("symbol-structure.svg"), QString("Change type of %1 nodes...").arg(count),
[this, ids]() {
[this, ids, collectIndices]() {
QStringList types;
for (const auto& e : kKindMeta) types << e.name;
bool ok;
QString sel = QInputDialog::getItem(nullptr, "Change Type", "Type:",
types, 0, false, &ok);
if (ok) {
QVector<int> indices;
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) indices.append(idx);
}
batchChangeKind(indices, kindFromString(sel));
if (ok)
batchChangeKind(collectIndices(), kindFromString(sel));
});
menu.addSeparator();
menu.addAction(icon("files.svg"), QString("Duplicate %1 nodes").arg(count), [this, ids]() {
for (uint64_t id : ids) {
int idx = m_doc->tree.indexOfId(id);
if (idx >= 0) duplicateNode(idx);
}
});
menu.addAction(icon("trash.svg"), QString("Delete %1 nodes").arg(count), [this, collectIndices]() {
batchRemoveNodes(collectIndices());
});
menu.addSeparator();
menu.addAction(icon("link.svg"), "Copy &Address", [this, ids]() {
QStringList addrs;
for (uint64_t id : ids) {
int ni = m_doc->tree.indexOfId(id);
if (ni < 0) continue;
uint64_t addr = m_doc->tree.baseAddress + m_doc->tree.computeOffset(ni);
addrs << QStringLiteral("0x") + QString::number(addr, 16).toUpper();
}
QApplication::clipboard()->setText(addrs.join('\n'));
});
menu.exec(globalPos);
return;
}
@@ -1226,10 +1350,39 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
addedQuickConvert = true;
}
if (node.kind == NodeKind::Hex64 || node.kind == NodeKind::Pointer64) {
menu.addAction("Change to fnptr64", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::FuncPtr64);
});
addedQuickConvert = true;
}
if (node.kind == NodeKind::Hex32 || node.kind == NodeKind::Pointer32) {
menu.addAction("Change to fnptr32", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::FuncPtr32);
});
addedQuickConvert = true;
}
if (node.kind == NodeKind::FuncPtr64) {
menu.addAction("Change to ptr64", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::Pointer64);
});
addedQuickConvert = true;
}
if (node.kind == NodeKind::FuncPtr32) {
menu.addAction("Change to ptr32", [this, nodeId]() {
int ni = m_doc->tree.indexOfId(nodeId);
if (ni >= 0) changeNodeKind(ni, NodeKind::Pointer32);
});
addedQuickConvert = true;
}
if (addedQuickConvert)
menu.addSeparator();
bool isEditable = node.kind != NodeKind::Struct && node.kind != NodeKind::Array
&& !isHexNode(node.kind)
&& m_doc->provider->isWritable();
if (isEditable) {
menu.addAction(icon("edit.svg"), "Edit &Value\tEnter", [editor, line]() {
@@ -1308,22 +1461,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
});
}
// Align Members submenu
if (node.kind == NodeKind::Struct) {
int curAlign = m_doc->tree.computeStructAlignment(nodeId);
auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members");
static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128};
for (int av : alignValues) {
QString label = (av == 1)
? QStringLiteral("1 (packed)")
: QString::number(av);
auto* act = alignMenu->addAction(label, [this, nodeId, av]() {
performRealignment(nodeId, av);
});
act->setCheckable(true);
act->setChecked(av == curAlign);
}
}
}
menu.addAction(icon("files.svg"), "D&uplicate\tCtrl+D", [this, nodeId]() {
@@ -1358,33 +1495,6 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx,
// ── Always-available actions ──
// Root struct alignment (always available if a root struct exists)
{
uint64_t rootStructId = 0;
for (const auto& n : m_doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
rootStructId = n.id;
break;
}
}
if (rootStructId != 0) {
int curAlign = m_doc->tree.computeStructAlignment(rootStructId);
auto* alignMenu = menu.addMenu(icon("symbol-ruler.svg"), "Align &Members");
static const int alignValues[] = {1, 2, 4, 8, 16, 32, 64, 128};
for (int av : alignValues) {
QString label = (av == 1)
? QStringLiteral("1 (packed)")
: QString::number(av);
auto* act = alignMenu->addAction(label, [this, rootStructId, av]() {
performRealignment(rootStructId, av);
});
act->setCheckable(true);
act->setChecked(av == curAlign);
}
menu.addSeparator();
}
}
menu.addAction(icon("diff-added.svg"), "Append 128 bytes", [this]() {
uint64_t target = m_viewRootId ? m_viewRootId : 0;
m_suppressRefresh = true;
@@ -1540,112 +1650,6 @@ void RcxController::applySelectionOverlays() {
editor->applySelectionOverlay(m_selIds);
}
void RcxController::performRealignment(uint64_t structId, int targetAlign) {
auto& tree = m_doc->tree;
int rootIdx = tree.indexOfId(structId);
if (rootIdx < 0) return;
// Gather direct children sorted by offset
QVector<int> kids = tree.childrenOf(structId);
std::sort(kids.begin(), kids.end(), [&](int a, int b) {
return tree.nodes[a].offset < tree.nodes[b].offset;
});
// Separate into real nodes (non-hex) and hex filler nodes
struct NodeInfo { uint64_t id; int offset; int size; };
QVector<NodeInfo> realNodes;
QVector<uint64_t> hexIds;
for (int ci : kids) {
const Node& child = tree.nodes[ci];
int sz = (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
? tree.structSpan(child.id) : child.byteSize();
if (isHexNode(child.kind))
hexIds.append(child.id);
else
realNodes.append({child.id, child.offset, sz});
}
auto roundUp = [](int x, int align) -> int {
return align <= 1 ? x : ((x + align - 1) / align) * align;
};
// Compute new offsets for real nodes
struct OffChange { uint64_t id; int oldOff; int newOff; };
QVector<OffChange> offChanges;
int cursor = 0;
for (auto& rn : realNodes) {
int newOff = roundUp(cursor, targetAlign);
if (newOff != rn.offset)
offChanges.append({rn.id, rn.offset, newOff});
rn.offset = newOff; // update local copy for gap computation
cursor = newOff + rn.size;
}
// Compute where padding is needed (gaps between consecutive nodes)
struct PadInsert { int offset; int size; };
QVector<PadInsert> padsNeeded;
for (int i = 0; i < realNodes.size(); i++) {
int gapStart = (i == 0) ? 0 : realNodes[i - 1].offset + realNodes[i - 1].size;
int gapEnd = realNodes[i].offset;
if (gapEnd > gapStart)
padsNeeded.append({gapStart, gapEnd - gapStart});
}
// Check if anything actually changes
if (offChanges.isEmpty() && hexIds.isEmpty() && padsNeeded.isEmpty())
return;
// Apply as undoable macro
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Realign to %1").arg(targetAlign));
// 1. Remove all existing hex filler nodes (no offset adjustments — we recompute)
for (uint64_t hid : hexIds) {
int idx = tree.indexOfId(hid);
if (idx < 0) continue;
QVector<Node> subtree;
subtree.append(tree.nodes[idx]);
m_doc->undoStack.push(new RcxCommand(this,
cmd::Remove{hid, subtree, {}}));
}
// 2. Reposition real nodes
for (const auto& oc : offChanges) {
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeOffset{oc.id, oc.oldOff, oc.newOff}));
}
// 3. Insert hex nodes to fill gaps (largest first for alignment)
for (const auto& pi : padsNeeded) {
int padOffset = pi.offset;
int gap = pi.size;
while (gap > 0) {
NodeKind padKind;
int padSize;
if (gap >= 8) { padKind = NodeKind::Hex64; padSize = 8; }
else if (gap >= 4) { padKind = NodeKind::Hex32; padSize = 4; }
else if (gap >= 2) { padKind = NodeKind::Hex16; padSize = 2; }
else { padKind = NodeKind::Hex8; padSize = 1; }
Node pad;
pad.kind = padKind;
pad.parentId = structId;
pad.offset = padOffset;
pad.name = QString("pad_%1").arg(padOffset, 2, 16, QChar('0'));
pad.id = tree.reserveId();
m_doc->undoStack.push(new RcxCommand(this, cmd::Insert{pad}));
padOffset += padSize;
gap -= padSize;
}
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
}
void RcxController::updateCommandRow() {
// -- Source label: driven by provider metadata --
@@ -1691,7 +1695,7 @@ void RcxController::updateCommandRow() {
const auto& n = m_doc->tree.nodes[vi];
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1\u25BE %2 {")
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
}
}
@@ -1702,14 +1706,14 @@ void RcxController::updateCommandRow() {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
QString keyword = n.resolvedClassKeyword();
QString className = n.structTypeName.isEmpty() ? n.name : n.structTypeName;
row2 = QStringLiteral("%1\u25BE %2 {")
row2 = QStringLiteral("%1 %2 {")
.arg(keyword, className.isEmpty() ? QStringLiteral("NoName") : className);
break;
}
}
}
if (row2.isEmpty())
row2 = QStringLiteral("struct\u25BE NoName {");
row2 = QStringLiteral("struct NoName {");
QString combined = QStringLiteral("[\u25B8] ") + row + QStringLiteral(" \u00B7 ") + row2;
@@ -1931,8 +1935,28 @@ void RcxController::applyTypePopupResult(TypePopupMode mode, int nodeIdx,
if (mode == TypePopupMode::FieldType) {
if (entry.entryKind == TypeEntry::Primitive) {
if (entry.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, entry.primitiveKind);
if (spec.arrayCount > 0) {
// Primitive array: e.g. "int32_t[10]"
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
m_doc->undoStack.beginMacro(QStringLiteral("Change to primitive array"));
if (nodeKind != NodeKind::Array)
changeNodeKind(nodeIdx, NodeKind::Array);
int idx = m_doc->tree.indexOfId(nodeId);
if (idx >= 0) {
auto& n = m_doc->tree.nodes[idx];
if (n.elementKind != entry.primitiveKind || n.arrayLen != spec.arrayCount)
m_doc->undoStack.push(new RcxCommand(this,
cmd::ChangeArrayMeta{nodeId, n.elementKind, entry.primitiveKind,
n.arrayLen, spec.arrayCount}));
}
m_doc->undoStack.endMacro();
m_suppressRefresh = wasSuppressed;
if (!m_suppressRefresh) refresh();
} else {
if (entry.primitiveKind != nodeKind)
changeNodeKind(nodeIdx, entry.primitiveKind);
}
} else if (entry.entryKind == TypeEntry::Composite) {
bool wasSuppressed = m_suppressRefresh;
m_suppressRefresh = true;
@@ -2182,8 +2206,6 @@ void RcxController::onRefreshTick() {
m_readGen = m_refreshGen;
auto prov = m_doc->provider;
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);

View File

@@ -85,6 +85,7 @@ public:
void removeSplitEditor(RcxEditor* editor);
QList<RcxEditor*> editors() const { return m_editors; }
void convertRootKeyword(const QString& newKeyword);
void changeNodeKind(int nodeIdx, NodeKind newKind);
void renameNode(int nodeIdx, const QString& newName);
void insertNode(uint64_t parentId, int offset, NodeKind kind, const QString& name);
@@ -160,7 +161,6 @@ private:
void connectEditor(RcxEditor* editor);
void handleMarginClick(RcxEditor* editor, int margin, int line, Qt::KeyboardModifiers mods);
void updateCommandRow();
void performRealignment(uint64_t structId, int targetAlign);
void switchToSavedSource(int idx);
void pushSavedSourcesToEditors();
void showTypePopup(RcxEditor* editor, TypePopupMode mode, int nodeIdx, QPoint globalPos);

View File

@@ -26,6 +26,7 @@ enum class NodeKind : uint8_t {
UInt8, UInt16, UInt32, UInt64,
Float, Double, Bool,
Pointer32, Pointer64,
FuncPtr32, FuncPtr64,
Vec2, Vec3, Vec4, Mat4x4,
UTF8, UTF16,
Struct, Array
@@ -78,6 +79,8 @@ inline constexpr KindMeta kKindMeta[] = {
{NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None},
{NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None},
{NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None},
{NodeKind::FuncPtr32, "FuncPtr32", "fnptr32", 4, 1, 4, KF_None},
{NodeKind::FuncPtr64, "FuncPtr64", "fnptr64", 8, 1, 8, KF_None},
{NodeKind::Vec2, "Vec2", "vec2", 8, 1, 4, KF_Vector},
{NodeKind::Vec3, "Vec3", "vec3", 12, 1, 4, KF_Vector},
{NodeKind::Vec4, "Vec4", "vec4", 16, 1, 4, KF_Vector},
@@ -136,6 +139,9 @@ inline constexpr bool isVectorKind(NodeKind k) {
inline constexpr bool isMatrixKind(NodeKind k) {
return k == NodeKind::Mat4x4;
}
inline constexpr bool isFuncPtr(NodeKind k) {
return k == NodeKind::FuncPtr32 || k == NodeKind::FuncPtr64;
}
inline QStringList allTypeNamesForUI(bool stripBrackets = false) {
QStringList out;
@@ -374,9 +380,6 @@ struct NodeTree {
return qMax(declaredSize, maxEnd);
}
// Compute natural alignment of a struct (max alignment of direct children)
int computeStructAlignment(uint64_t structId) const;
// Batch selection normalizers
QSet<uint64_t> normalizePreferAncestors(const QSet<uint64_t>& ids) const;
QSet<uint64_t> normalizePreferDescendants(const QSet<uint64_t>& ids) const;
@@ -481,6 +484,7 @@ struct LineMeta {
int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element)
QString offsetText;
uint64_t offsetAddr = 0; // Raw absolute address (for margin toggle)
uint64_t ptrBase = 0; // Pointer expansion base (non-zero = use for RVA)
uint32_t markerMask = 0;
bool dataChanged = false; // true if any byte in this node changed since last refresh
int heatLevel = 0; // 0=static, 1=cold, 2=warm, 3=hot (from ValueHistory)
@@ -653,16 +657,17 @@ inline ColumnSpan commandRowAddrSpan(const QString& lineText) {
}
// ── CommandRow root-class spans ──
// Combined CommandRow format ends with: " struct ClassName {"
// Combined CommandRow format ends with: " struct ClassName {"
inline int commandRowRootStart(const QString& lineText) {
int best = -1;
int i;
i = lineText.lastIndexOf(QStringLiteral("struct\u25BE"));
// Match "struct " / "class " / "enum " as whole words before the class name
i = lineText.lastIndexOf(QStringLiteral("struct "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("class\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("class "));
if (i > best) best = i;
i = lineText.lastIndexOf(QStringLiteral("enum\u25BE"));
i = lineText.lastIndexOf(QStringLiteral("enum "));
if (i > best) best = i;
return best;
}
@@ -671,8 +676,7 @@ inline ColumnSpan commandRowRootTypeSpan(const QString& lineText) {
int start = commandRowRootStart(lineText);
if (start < 0) return {};
int end = start;
while (end < lineText.size() && lineText[end] != QChar(' ')
&& lineText[end] != QChar(0x25BE)) end++;
while (end < lineText.size() && lineText[end] != QChar(' ')) end++;
if (end <= start) return {};
return {start, end, true};
}

76
src/disasm.cpp Normal file
View File

@@ -0,0 +1,76 @@
#include "disasm.h"
extern "C" {
#include <fadec.h>
}
namespace rcx {
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes) {
if (bytes.isEmpty() || (bitness != 32 && bitness != 64))
return {};
int len = qMin((int)bytes.size(), maxBytes);
const auto* buf = reinterpret_cast<const uint8_t*>(bytes.constData());
QString result;
int off = 0;
while (off < len) {
FdInstr instr;
int ret = fd_decode(buf + off, len - off, bitness, baseAddr + off, &instr);
if (ret < 0)
break;
char fmtBuf[128];
fd_format(&instr, fmtBuf, sizeof(fmtBuf));
if (!result.isEmpty())
result += QLatin1Char('\n');
result += QStringLiteral("%1 %2")
.arg(baseAddr + off, bitness == 64 ? 16 : 8, 16, QLatin1Char('0'))
.arg(QString::fromLatin1(fmtBuf));
off += ret;
}
return result;
}
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes) {
if (bytes.isEmpty())
return {};
int len = qMin((int)bytes.size(), maxBytes);
QString result;
for (int off = 0; off < len; off += 16) {
int lineLen = qMin(16, len - off);
if (!result.isEmpty())
result += QLatin1Char('\n');
// Address
bool wide = (baseAddr + len > 0xFFFFFFFFULL);
result += QStringLiteral("%1 ").arg(baseAddr + off, wide ? 16 : 8, 16, QLatin1Char('0'));
// Hex bytes
for (int i = 0; i < 16; i++) {
if (i < lineLen) {
uint8_t b = static_cast<uint8_t>(bytes[off + i]);
result += QStringLiteral("%1 ").arg(b, 2, 16, QLatin1Char('0'));
} else {
result += QStringLiteral(" ");
}
if (i == 7) result += QLatin1Char(' ');
}
// ASCII
result += QLatin1Char(' ');
for (int i = 0; i < lineLen; i++) {
char c = bytes[off + i];
result += (c >= 0x20 && c < 0x7f) ? QLatin1Char(c) : QLatin1Char('.');
}
}
return result;
}
} // namespace rcx

15
src/disasm.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include <QString>
#include <QByteArray>
#include <cstdint>
namespace rcx {
// Disassemble up to maxBytes of x86 code, returning formatted asm lines.
// bitness: 32 or 64. Returns one line per instruction, prefixed with offset.
QString disassemble(const QByteArray& bytes, uint64_t baseAddr, int bitness, int maxBytes = 128);
// Format bytes as hex dump lines (16 bytes per line with ASCII sidebar).
QString hexDump(const QByteArray& bytes, uint64_t baseAddr, int maxBytes = 128);
} // namespace rcx

View File

@@ -1,4 +1,5 @@
#include "editor.h"
#include "disasm.h"
#include "providerregistry.h"
#include <QDebug>
#include <Qsci/qsciscintilla.h>
@@ -24,6 +25,9 @@
namespace rcx {
// Forward declaration (defined below, after RcxEditor constructor)
static QString getLineText(QsciScintilla* sci, int line);
// ── Value history popup (styled like TypeSelectorPopup) ──
class ValueHistoryPopup : public QFrame {
@@ -131,7 +135,6 @@ public:
}
void showAt(const QPoint& globalPos) {
if (isVisible()) return;
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
@@ -141,7 +144,7 @@ public:
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - 4;
move(x, y);
show();
if (!isVisible()) show();
}
void dismiss() {
@@ -152,6 +155,106 @@ public:
}
};
// ── Disassembly / hex-dump hover popup ──
class DisasmPopup : public QFrame {
uint64_t m_nodeId = 0;
QString m_body;
QLabel* m_titleLabel = nullptr;
QLabel* m_bodyLabel = nullptr;
public:
explicit DisasmPopup(QWidget* parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setAttribute(Qt::WA_DeleteOnClose, false);
setAttribute(Qt::WA_ShowWithoutActivating, true);
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
auto* vbox = new QVBoxLayout(this);
vbox->setContentsMargins(8, 6, 8, 6);
vbox->setSpacing(2);
m_titleLabel = new QLabel;
QFont bold = m_titleLabel->font();
bold.setBold(true);
m_titleLabel->setFont(bold);
vbox->addWidget(m_titleLabel);
auto* sep = new QFrame;
sep->setFrameShape(QFrame::HLine);
sep->setFrameShadow(QFrame::Plain);
sep->setFixedHeight(1);
vbox->addWidget(sep);
m_bodyLabel = new QLabel;
m_bodyLabel->setTextFormat(Qt::PlainText);
m_bodyLabel->setWordWrap(false);
vbox->addWidget(m_bodyLabel);
}
uint64_t nodeId() const { return m_nodeId; }
void populate(uint64_t nodeId, const QString& title, const QString& body,
const QFont& font) {
if (nodeId == m_nodeId && body == m_body && isVisible())
return;
m_nodeId = nodeId;
m_body = body;
const auto& theme = ThemeManager::instance().current();
QPalette pal;
pal.setColor(QPalette::Window, theme.backgroundAlt);
pal.setColor(QPalette::WindowText, theme.text);
setPalette(pal);
QFont bold = font;
bold.setBold(true);
m_titleLabel->setFont(bold);
m_titleLabel->setText(title);
m_titleLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.text.name()));
// Find and style the separator
for (auto* child : findChildren<QFrame*>()) {
if (child->frameShape() == QFrame::HLine) {
QPalette sp;
sp.setColor(QPalette::WindowText, theme.border);
child->setPalette(sp);
break;
}
}
m_bodyLabel->setFont(font);
m_bodyLabel->setText(body);
m_bodyLabel->setStyleSheet(
QStringLiteral("color: %1;").arg(theme.syntaxNumber.name()));
setMaximumWidth(600);
adjustSize();
}
void showAt(const QPoint& globalPos) {
QSize sz = sizeHint();
QRect screen = QApplication::screenAt(globalPos)
? QApplication::screenAt(globalPos)->availableGeometry()
: QRect(0, 0, 1920, 1080);
int x = qMin(globalPos.x(), screen.right() - sz.width());
int y = globalPos.y();
if (y + sz.height() > screen.bottom())
y = globalPos.y() - sz.height() - 4;
move(x, y);
if (!isVisible()) show();
}
void dismiss() {
if (isVisible()) hide();
m_nodeId = 0;
m_body.clear();
}
};
static constexpr int IND_EDITABLE = 8;
static constexpr int IND_HEX_DIM = 9;
static constexpr int IND_BASE_ADDR = 10; // Default text color override for command row address
@@ -225,7 +328,35 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
}
return;
}
int line = m_sci->lineAt(pos);
HitInfo hi = hitTest(pos);
int line = hi.line;
// Right-click on command row keyword → show conversion menu
if (line == 0 && hi.col >= 0 && !m_meta.isEmpty()
&& m_meta[0].lineKind == LineKind::CommandRow) {
QString lineText = getLineText(m_sci, 0);
ColumnSpan rts = commandRowRootTypeSpan(lineText);
if (rts.valid && hi.col >= rts.start && hi.col < rts.end) {
// Extract current keyword from span text
QString kw = lineText.mid(rts.start, rts.end - rts.start).trimmed();
QMenu menu;
if (kw == QStringLiteral("class"))
menu.addAction("Convert to Struct");
else if (kw == QStringLiteral("struct"))
menu.addAction("Convert to Class");
// enum: no conversion options
if (!menu.isEmpty()) {
QAction* chosen = menu.exec(m_sci->mapToGlobal(pos));
if (chosen) {
QString newKw = chosen->text().contains("Class")
? QStringLiteral("class") : QStringLiteral("struct");
emit keywordConvertRequested(newKw);
}
}
return;
}
}
int nodeIdx = -1;
int subLine = 0;
if (line >= 0 && line < m_meta.size()) {
@@ -240,8 +371,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
if (!m_editState.active) return;
if (id == 1 && (m_editState.target == EditTarget::Type
|| m_editState.target == EditTarget::ArrayElementType
|| m_editState.target == EditTarget::PointerTarget
|| m_editState.target == EditTarget::RootClassType)) {
|| m_editState.target == EditTarget::PointerTarget)) {
auto info = endInlineEdit();
emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text);
}
@@ -570,6 +700,7 @@ void RcxEditor::applyDocument(const ComposeResult& result) {
applyFoldLevels(result.meta);
applyHexDimming(result.meta);
applyHeatmapHighlight(result.meta);
applySymbolColoring(result.meta);
applyCommandRowPills();
// Reset hint line - applySelectionOverlay will repaint indicators
@@ -626,7 +757,8 @@ void RcxEditor::reformatMargins() {
lm.lineKind == LineKind::CommandRow) {
lm.offsetText = QString(hexDigits + 1, ' ');
} else {
uint64_t rel = lm.offsetAddr >= base ? lm.offsetAddr - base : 0;
uint64_t rvaBase = lm.ptrBase ? lm.ptrBase : base;
uint64_t rel = lm.offsetAddr >= rvaBase ? lm.offsetAddr - rvaBase : 0;
lm.offsetText = (QStringLiteral("+") +
QString::number(rel, 16).toUpper())
.rightJustified(hexDigits, ' ') + QChar(' ');
@@ -663,17 +795,22 @@ void RcxEditor::reformatMargins() {
};
if (m_relativeOffsets) {
// Derive local offset: find enclosing header or array element separator
// Derive local offset: for pointer-expanded children use ptrBase,
// otherwise find enclosing header or array element separator
uint64_t parentAddr = base;
for (int j = i - 1; j >= 0; j--) {
const auto& pLm = m_meta[j];
if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) {
parentAddr = pLm.offsetAddr;
break;
}
if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) {
parentAddr = pLm.offsetAddr;
break;
if (lm.ptrBase != 0) {
parentAddr = lm.ptrBase;
} else {
for (int j = i - 1; j >= 0; j--) {
const auto& pLm = m_meta[j];
if (pLm.lineKind == LineKind::Header && pLm.depth < lm.depth) {
parentAddr = pLm.offsetAddr;
break;
}
if (pLm.lineKind == LineKind::ArrayElementSeparator && pLm.depth <= lm.depth) {
parentAddr = pLm.offsetAddr;
break;
}
}
}
uint64_t localOff = lm.offsetAddr >= parentAddr ? lm.offsetAddr - parentAddr : 0;
@@ -908,6 +1045,22 @@ ColumnSpan RcxEditor::typeSpan(const LineMeta& lm, int typeW) { return typeSpan
ColumnSpan RcxEditor::nameSpan(const LineMeta& lm, int typeW, int nameW) { return nameSpanFor(lm, typeW, nameW); }
ColumnSpan RcxEditor::valueSpan(const LineMeta& lm, int lineLength, int typeW, int nameW) { return valueSpanFor(lm, lineLength, typeW, nameW); }
// For pointer-like nodes, narrow value span to just the address portion
// (before the " // " separator that precedes the symbol like "Module+0x1234").
static ColumnSpan narrowPtrValueSpan(const LineMeta& lm, const ColumnSpan& vs,
const QString& lineText) {
if (!vs.valid) return vs;
if (!isFuncPtr(lm.nodeKind)
&& lm.nodeKind != NodeKind::Pointer32
&& lm.nodeKind != NodeKind::Pointer64)
return vs;
QString valText = lineText.mid(vs.start, vs.end - vs.start);
int sep = valText.indexOf(QLatin1String(" // "));
if (sep > 0)
return {vs.start, vs.start + sep, true};
return vs;
}
// ── Multi-selection ──
QSet<int> RcxEditor::selectedNodeIndices() const {
@@ -956,28 +1109,10 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
// Pick the right indicator for this heat level (1→cold, 2→warm, 3→hot)
int activeInd = heatIndicators[qBound(0, heat - 1, 2)];
// For hex preview nodes: per-byte heat coloring on changed bytes
if (isHexPreview(lm.nodeKind) && lm.dataChanged && !lm.changedByteIndices.isEmpty()) {
int ind = kFoldCol + lm.depth * 3;
int asciiStart = ind + typeW + kSepWidth;
int hexStart = asciiStart + nameW + kSepWidth;
for (int byteIdx : lm.changedByteIndices) {
fillIndicatorCols(activeInd, i, asciiStart + byteIdx, asciiStart + byteIdx + 1);
int hexCol = hexStart + byteIdx * 3;
fillIndicatorCols(activeInd, i, hexCol, hexCol + 2);
}
// Clear the other two heat indicators on this line
for (int hi : heatIndicators) {
if (hi != activeInd)
clearIndicatorLine(hi, i);
}
continue;
}
// Non-hex nodes: apply heat-level indicator to value span
// Apply heat-level indicator to value span (narrowed for pointer-like nodes)
QString lineText = getLineText(m_sci, i);
ColumnSpan vs = valueSpan(lm, lineText.size(), typeW, nameW);
ColumnSpan vs = narrowPtrValueSpan(lm,
valueSpan(lm, lineText.size(), typeW, nameW), lineText);
if (!vs.valid) continue;
fillIndicatorCols(activeInd, i, vs.start, vs.end);
@@ -990,6 +1125,28 @@ void RcxEditor::applyHeatmapHighlight(const QVector<LineMeta>& meta) {
}
}
void RcxEditor::applySymbolColoring(const QVector<LineMeta>& meta) {
for (int i = 0; i < meta.size(); i++) {
const LineMeta& lm = meta[i];
if (!isFuncPtr(lm.nodeKind)
&& lm.nodeKind != NodeKind::Pointer32
&& lm.nodeKind != NodeKind::Pointer64)
continue;
QString lineText = getLineText(m_sci, i);
// Find " // " within the value region and color "// sym" portion green
ColumnSpan vs = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
if (!vs.valid) continue;
int searchFrom = vs.start;
int sep = lineText.indexOf(QLatin1String(" // "), searchFrom);
if (sep < 0 || sep >= vs.end) continue;
int symStart = sep + 2; // start of "// sym"
int symEnd = vs.end;
while (symEnd > symStart && lineText[symEnd - 1] == ' ') symEnd--;
if (symEnd > symStart)
fillIndicatorCols(IND_HINT_GREEN, i, symStart, symEnd);
}
}
void RcxEditor::applyBaseAddressColoring(const QVector<LineMeta>& meta) {
if (meta.isEmpty() || meta[0].lineKind != LineKind::CommandRow) return;
@@ -1341,8 +1498,7 @@ static bool hitTestTarget(QsciScintilla* sci,
ColumnSpan as = commandRowAddrSpan(lineText);
if (inSpan(as)) { outTarget = EditTarget::BaseAddress; outLine = line; return true; }
ColumnSpan rts = commandRowRootTypeSpan(lineText);
if (inSpan(rts)) { outTarget = EditTarget::RootClassType; outLine = line; return true; }
// RootClassType is no longer clickable — use right-click to convert
ColumnSpan rns = commandRowRootNameSpan(lineText);
if (inSpan(rns)) { outTarget = EditTarget::RootClassName; outLine = line; return true; }
return false;
@@ -1354,7 +1510,8 @@ static bool hitTestTarget(QsciScintilla* sci,
ColumnSpan ts = RcxEditor::typeSpan(lm, typeW);
ColumnSpan ns = RcxEditor::nameSpan(lm, typeW, nameW);
ColumnSpan vs = RcxEditor::valueSpan(lm, textLen, typeW, nameW);
ColumnSpan vs = narrowPtrValueSpan(lm,
RcxEditor::valueSpan(lm, textLen, typeW, nameW), lineText);
// Pointer fields/headers: check sub-spans within type column first
if (lm.nodeKind == NodeKind::Pointer32 || lm.nodeKind == NodeKind::Pointer64) {
@@ -2022,23 +2179,7 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) {
// and exit early above (never reach here).
if (target == EditTarget::Source)
QTimer::singleShot(0, this, &RcxEditor::showSourcePicker);
if (target == EditTarget::RootClassType) {
QTimer::singleShot(0, this, [this]() {
if (!m_editState.active || m_editState.target != EditTarget::RootClassType) return;
// Replace text with spaces and show picker
int len = m_editState.original.size();
QString spaces(len, ' ');
m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL,
m_editState.posStart, m_editState.posEnd);
m_sci->SendScintilla(QsciScintillaBase::SCI_REPLACESEL,
(uintptr_t)0, spaces.toUtf8().constData());
m_sci->SendScintilla(QsciScintillaBase::SCI_GOTOPOS, m_editState.posStart);
m_sci->SendScintilla(QsciScintillaBase::SCI_AUTOCSETSEPARATOR, (long)'\n');
m_sci->SendScintilla(QsciScintillaBase::SCI_USERLISTSHOW,
(uintptr_t)1, "struct\nclass\nenum");
m_sci->viewport()->setCursor(Qt::ArrowCursor);
});
}
// RootClassType is no longer editable via click — use right-click conversion instead
// Refresh hover cursor so value history popup appears with Set buttons immediately
if (target == EditTarget::Value)
QTimer::singleShot(0, this, &RcxEditor::applyHoverCursor);
@@ -2315,8 +2456,7 @@ void RcxEditor::paintEditableSpans(int line) {
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::BaseAddress, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
if (resolvedSpanFor(line, EditTarget::RootClassType, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
// RootClassType no longer shown as editable — right-click conversion instead
if (resolvedSpanFor(line, EditTarget::RootClassName, norm))
fillIndicatorCols(IND_EDITABLE, line, norm.start, norm.end);
return;
@@ -2440,14 +2580,19 @@ void RcxEditor::applyHoverCursor() {
if (!showPopup && m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
// Always dismiss disasm popup during inline editing
if (m_disasmPopup && m_disasmPopup->isVisible())
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
return;
}
// Mouse left viewport - set Arrow, dismiss history popup
// Mouse left viewport - set Arrow, dismiss popups
// (but not during applyDocument — the Leave is synthetic from setText)
if (!m_hoverInside) {
if (m_historyPopup && !m_applyingDocument)
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
if (m_disasmPopup && !m_applyingDocument)
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
m_sci->viewport()->setCursor(Qt::ArrowCursor);
return;
}
@@ -2522,6 +2667,18 @@ void RcxEditor::applyHoverCursor() {
m_hoverSpanLines.append(line);
}
}
// Narrow pointer-like nodes to address portion only (exclude symbol)
if (!narrowed && (isFuncPtr(lm.nodeKind)
|| lm.nodeKind == NodeKind::Pointer32
|| lm.nodeKind == NodeKind::Pointer64)) {
ColumnSpan full = valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW);
ColumnSpan narrow = narrowPtrValueSpan(lm, full, lineText);
if (h.col >= narrow.start && h.col < narrow.end) {
fillIndicatorCols(IND_HOVER_SPAN, line, narrow.start, narrow.end);
m_hoverSpanLines.append(line);
}
narrowed = true;
}
}
if (!narrowed && h.col >= span.start && h.col < span.end) {
fillIndicatorCols(IND_HOVER_SPAN, line, span.start, span.end);
@@ -2537,11 +2694,16 @@ void RcxEditor::applyHoverCursor() {
}
// Value history popup on hover (read-only, no buttons)
// Skip FuncPtr and void-Pointer nodes — they use the disasm popup instead.
{
bool showPopup = false;
if (m_valueHistory && h.line >= 0 && h.line < m_meta.size()) {
const LineMeta& lm = m_meta[h.line];
if (lm.heatLevel > 0 && lm.nodeId != 0) {
bool skipForDisasm = isFuncPtr(lm.nodeKind)
|| ((lm.nodeKind == NodeKind::Pointer32
|| lm.nodeKind == NodeKind::Pointer64)
&& lm.pointerTargetName.isEmpty());
if (lm.heatLevel > 0 && lm.nodeId != 0 && !skipForDisasm) {
auto it = m_valueHistory->find(lm.nodeId);
if (it != m_valueHistory->end() && it->uniqueCount() > 1) {
QString lineText = getLineText(m_sci, h.line);
@@ -2571,6 +2733,110 @@ void RcxEditor::applyHoverCursor() {
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
// Disasm / hex-dump popup on hover for FuncPtr and void Pointer nodes
{
bool showDisasm = false;
if (m_disasmProvider && m_disasmTree && h.line >= 0 && h.line < m_meta.size()) {
const LineMeta& lm = m_meta[h.line];
bool isFP = isFuncPtr(lm.nodeKind);
bool isVoidPtr = (lm.nodeKind == NodeKind::Pointer32
|| lm.nodeKind == NodeKind::Pointer64)
&& lm.pointerTargetName.isEmpty();
if ((isFP || isVoidPtr) && lm.nodeIdx >= 0
&& lm.nodeIdx < m_disasmTree->nodes.size()) {
// Check hover is over the address portion of the value column
QString lineText = getLineText(m_sci, h.line);
ColumnSpan vs = narrowPtrValueSpan(lm,
valueSpan(lm, lineText.size(), lm.effectiveTypeW, lm.effectiveNameW),
lineText);
if (vs.valid && h.col >= vs.start && h.col < vs.end) {
const Node& node = m_disasmTree->nodes[lm.nodeIdx];
// For void ptrs, only show hex dump if refId == 0
if (!isVoidPtr || node.refId == 0) {
bool is64 = (lm.nodeKind == NodeKind::FuncPtr64
|| lm.nodeKind == NodeKind::Pointer64);
// Use composed address (correct for pointer-expanded nodes)
// not node.offset (which is just offset within struct definition).
uint64_t provAddr = lm.offsetAddr >= m_disasmTree->baseAddress
? lm.offsetAddr - m_disasmTree->baseAddress
: static_cast<uint64_t>(node.offset);
uint64_t ptrVal = is64
? m_disasmProvider->readU64(provAddr)
: (uint64_t)m_disasmProvider->readU32(provAddr);
if (ptrVal != 0 && ptrVal != UINT64_MAX
&& !(is64 == false && ptrVal == 0xFFFFFFFF)) {
// Read code bytes from the function target address.
// Use the real provider (not snapshot) because function
// code lives at arbitrary process addresses that aren't
// in the snapshot page table. The provider reads from
// m_base + addr via ReadProcessMemory, so we convert
// the absolute ptrVal to provider-relative.
const Provider* codeProv = m_disasmRealProv
? m_disasmRealProv : m_disasmProvider;
constexpr int kMaxRead = 128;
uint64_t codeAddr = ptrVal - m_disasmTree->baseAddress;
QByteArray bytes(kMaxRead, Qt::Uninitialized);
bool readOk = codeProv->read(codeAddr, bytes.data(), kMaxRead);
if (readOk) {
QString title, body;
if (isFP) {
title = QStringLiteral("Disassembly");
body = disassemble(bytes, ptrVal,
is64 ? 64 : 32, kMaxRead);
} else {
title = QStringLiteral("Hex Dump");
body = hexDump(bytes, ptrVal, kMaxRead);
}
// Cap at 6 lines so the popup stays compact
{
const int kMaxLines = 6;
int nth = 0, idx = 0;
while (nth < kMaxLines && (idx = body.indexOf('\n', idx)) != -1)
{ ++nth; ++idx; }
if (nth == kMaxLines && idx < body.size()) {
body.truncate(idx);
body += QStringLiteral("...");
}
}
if (!body.isEmpty()) {
if (!m_disasmPopup)
m_disasmPopup = new DisasmPopup(this);
auto* popup = static_cast<DisasmPopup*>(
m_disasmPopup);
popup->populate(lm.nodeId, title, body,
editorFont());
long linePos = m_sci->SendScintilla(
QsciScintillaBase::SCI_POSITIONFROMLINE,
(unsigned long)h.line);
long byteOff = lineText.left(vs.start)
.toUtf8().size();
int px = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTXFROMPOSITION,
(unsigned long)0, linePos + byteOff);
int py = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_POINTYFROMPOSITION,
(unsigned long)0, linePos);
int lh = (int)m_sci->SendScintilla(
QsciScintillaBase::SCI_TEXTHEIGHT,
(unsigned long)h.line);
QPoint anchor = m_sci->viewport()->mapToGlobal(
QPoint(px, py + lh));
popup->showAt(anchor);
showDisasm = true;
// Dismiss value history popup to avoid fighting
if (m_historyPopup && m_historyPopup->isVisible())
static_cast<ValueHistoryPopup*>(m_historyPopup)->dismiss();
}
}
}
}
}
}
}
if (!showDisasm && m_disasmPopup && m_disasmPopup->isVisible())
static_cast<DisasmPopup*>(m_disasmPopup)->dismiss();
}
// Determine cursor shape based on interaction type
Qt::CursorShape desired = Qt::ArrowCursor;

View File

@@ -55,6 +55,9 @@ public:
QString textWithMargins() const;
void setCustomTypeNames(const QStringList& names);
void setValueHistoryRef(const QHash<uint64_t, ValueHistory>* ref) { m_valueHistory = ref; }
void setProviderRef(const Provider* prov, const Provider* realProv, const NodeTree* tree) {
m_disasmProvider = prov; m_disasmRealProv = realProv; m_disasmTree = tree;
}
// Saved sources for quick-switch in source picker
void setSavedSources(const QVector<SavedSourceDisplay>& sources) { m_savedSourceDisplay = sources; }
@@ -62,6 +65,7 @@ public:
signals:
void marginClicked(int margin, int line, Qt::KeyboardModifiers mods);
void contextMenuRequested(int line, int nodeIdx, int subLine, QPoint globalPos);
void keywordConvertRequested(const QString& newKeyword);
void nodeClicked(int line, uint64_t nodeId, Qt::KeyboardModifiers mods);
void inlineEditCommitted(int nodeIdx, int subLine,
EditTarget target, const QString& text);
@@ -133,6 +137,10 @@ private:
// ── Value history ref (owned by controller) ──
const QHash<uint64_t, ValueHistory>* m_valueHistory = nullptr;
QWidget* m_historyPopup = nullptr; // ValueHistoryPopup (file-local class in editor.cpp)
QWidget* m_disasmPopup = nullptr; // DisasmPopup (file-local class in editor.cpp)
const Provider* m_disasmProvider = nullptr; // snapshot or real — for reading tree data
const Provider* m_disasmRealProv = nullptr; // real process provider — for reading code at arbitrary addresses
const NodeTree* m_disasmTree = nullptr;
// ── Reentrancy guards ──
bool m_applyingDocument = false;
@@ -152,6 +160,7 @@ private:
void applyFoldLevels(const QVector<LineMeta>& meta);
void applyHexDimming(const QVector<LineMeta>& meta);
void applyHeatmapHighlight(const QVector<LineMeta>& meta);
void applySymbolColoring(const QVector<LineMeta>& meta);
void applyBaseAddressColoring(const QVector<LineMeta>& meta);
void applyCommandRowPills();

File diff suppressed because it is too large Load Diff

View File

@@ -262,7 +262,7 @@ static QString readValueImpl(const Node& node, const Provider& prov,
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Pointer64: {
@@ -270,7 +270,23 @@ static QString readValueImpl(const Node& node, const Provider& prov,
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" ") + sym;
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr32: {
uint32_t val = prov.readU32(addr);
if (!display) return rawHex(val, 8);
QString s = fmtPointer32(val);
QString sym = prov.getSymbol((uint64_t)val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::FuncPtr64: {
uint64_t val = prov.readU64(addr);
if (!display) return rawHex(val, 16);
QString s = fmtPointer64(val);
QString sym = prov.getSymbol(val);
if (!sym.isEmpty()) s += QStringLiteral(" // ") + sym;
return s;
}
case NodeKind::Vec2:
@@ -543,6 +559,14 @@ QByteArray parseValue(NodeKind kind, const QString& text, bool* ok) {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr32: {
uint val = stripHex(s).toUInt(ok, 16);
return *ok ? toBytes<uint32_t>(val) : QByteArray{};
}
case NodeKind::FuncPtr64: {
qulonglong val = stripHex(s).toULongLong(ok, 16);
return *ok ? toBytes<uint64_t>(val) : QByteArray{};
}
case NodeKind::UTF8: {
*ok = true;
if (s.startsWith('"') && s.endsWith('"'))
@@ -571,7 +595,8 @@ QString validateValue(NodeKind kind, const QString& text) {
// For integer/hex types, validate character set first
bool isHexKind = (kind >= NodeKind::Hex8 && kind <= NodeKind::Hex64)
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64;
|| kind == NodeKind::Pointer32 || kind == NodeKind::Pointer64
|| kind == NodeKind::FuncPtr32 || kind == NodeKind::FuncPtr64;
bool isIntKind = (kind >= NodeKind::Int8 && kind <= NodeKind::UInt64);
if (isHexKind || isIntKind) {

View File

@@ -44,6 +44,8 @@ static QString cTypeName(NodeKind kind) {
case NodeKind::Bool: return QStringLiteral("bool");
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
case NodeKind::FuncPtr32: return QStringLiteral("uint32_t");
case NodeKind::FuncPtr64: return QStringLiteral("uint64_t");
case NodeKind::Vec2: return QStringLiteral("float");
case NodeKind::Vec3: return QStringLiteral("float");
case NodeKind::Vec4: return QStringLiteral("float");
@@ -143,6 +145,10 @@ static QString emitField(GenContext& ctx, const Node& node) {
}
return QStringLiteral(" void* %1;").arg(name) + oc;
}
case NodeKind::FuncPtr32:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
case NodeKind::FuncPtr64:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
default:
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
}

View File

@@ -254,7 +254,7 @@ static void applyGlobalTheme(const rcx::Theme& theme) {
pal.setColor(QPalette::HighlightedText, theme.text);
pal.setColor(QPalette::ToolTipBase, theme.backgroundAlt);
pal.setColor(QPalette::ToolTipText, theme.text);
pal.setColor(QPalette::Mid, theme.border);
pal.setColor(QPalette::Mid, theme.hover);
pal.setColor(QPalette::Dark, theme.background);
pal.setColor(QPalette::Light, theme.textFaint);
pal.setColor(QPalette::Link, theme.indHoverSpan);
@@ -399,8 +399,9 @@ inline QAction* Qt5Qt6AddAction(QMenu* menu, const QString &text, const QKeySequ
void MainWindow::createMenus() {
// File
auto* file = m_titleBar->menuBar()->addMenu("&File");
Qt5Qt6AddAction(file, "&New", QKeySequence::New, QIcon(), this, &MainWindow::newDocument);
Qt5Qt6AddAction(file, "New &Tab", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newFile);
Qt5Qt6AddAction(file, "New &Class", QKeySequence::New, QIcon(), this, &MainWindow::newClass);
Qt5Qt6AddAction(file, "New &Struct", QKeySequence(Qt::CTRL | Qt::Key_T), QIcon(), this, &MainWindow::newStruct);
Qt5Qt6AddAction(file, "New &Enum", QKeySequence(Qt::CTRL | Qt::Key_E), QIcon(), this, &MainWindow::newEnum);
Qt5Qt6AddAction(file, "&Open...", QKeySequence::Open, makeIcon(":/vsicons/folder-opened.svg"), this, &MainWindow::openFile);
file->addSeparator();
Qt5Qt6AddAction(file, "&Save", QKeySequence::Save, makeIcon(":/vsicons/save.svg"), this, &MainWindow::saveFile);
@@ -745,11 +746,12 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
}
// Build a minimal empty struct for new documents
static void buildEmptyStruct(NodeTree& tree) {
static void buildEmptyStruct(NodeTree& tree, const QString& classKeyword = QString()) {
Node root;
root.kind = NodeKind::Struct;
root.name = "instance";
root.structTypeName = "Unnamed";
root.classKeyword = classKeyword;
root.parentId = 0;
root.offset = 0;
int ri = tree.addNode(root);
@@ -765,66 +767,111 @@ static void buildEmptyStruct(NodeTree& tree) {
}
}
void MainWindow::newFile() {
void MainWindow::newClass() {
project_new(QStringLiteral("class"));
}
void MainWindow::newStruct() {
project_new();
}
void MainWindow::newDocument() {
auto* tab = activeTab();
if (!tab) {
project_new();
return;
void MainWindow::newEnum() {
project_new(QStringLiteral("enum"));
}
static void buildEditorDemo(NodeTree& tree, uintptr_t editorAddr) {
tree.nodes.clear();
tree.invalidateIdCache();
tree.m_nextId = 1;
tree.baseAddress = static_cast<uint64_t>(editorAddr);
// ── Root struct: RcxEditor ──
Node root;
root.kind = NodeKind::Struct;
root.name = QStringLiteral("editor");
root.structTypeName = QStringLiteral("RcxEditor");
root.classKeyword = QStringLiteral("class");
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// ── VTable struct definition (separate root) ──
Node vtStruct;
vtStruct.kind = NodeKind::Struct;
vtStruct.name = QStringLiteral("VTable");
vtStruct.structTypeName = QStringLiteral("QWidgetVTable");
int vti = tree.addNode(vtStruct);
uint64_t vtId = tree.nodes[vti].id;
// VTable entries — these are real virtual function pointers from QObject/QWidget
static const char* vfNames[] = {
"deleting_dtor", "metaObject", "qt_metacast", "qt_metacall",
"event", "eventFilter", "timerEvent", "childEvent",
"customEvent", "connectNotify", "disconnectNotify", "devType",
"setVisible", "sizeHint", "minimumSizeHint", "heightForWidth",
};
for (int i = 0; i < 16; i++) {
Node fn;
fn.kind = NodeKind::FuncPtr64;
fn.name = QString::fromLatin1(vfNames[i]);
fn.parentId = vtId;
fn.offset = i * 8;
tree.addNode(fn);
}
auto* doc = tab->doc;
auto* ctrl = tab->ctrl;
// Clear everything
doc->undoStack.clear();
doc->tree = NodeTree();
doc->tree.baseAddress = 0x00400000;
doc->filePath.clear();
doc->typeAliases.clear();
doc->modified = false;
buildEmptyStruct(doc->tree);
QByteArray data(256, '\0');
doc->provider = std::make_shared<BufferProvider>(data);
// Focus on first struct
ctrl->setViewRootId(0);
for (const auto& n : doc->tree.nodes) {
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
ctrl->setViewRootId(n.id);
break;
}
// ── RcxEditor fields ──
// offset 0: vtable pointer → QWidgetVTable
{
Node n;
n.kind = NodeKind::Pointer64;
n.name = QStringLiteral("__vptr");
n.parentId = rootId;
n.offset = 0;
n.refId = vtId;
tree.addNode(n);
}
// offset 8: QObjectData* d_ptr (QObject internals)
{
Node n;
n.kind = NodeKind::Pointer64;
n.name = QStringLiteral("d_ptr");
n.parentId = rootId;
n.offset = 8;
tree.addNode(n);
}
// The rest of the object: raw memory visible as Hex64 fields
// QWidget base is large (~200+ bytes), then RcxEditor members follow.
// Lay out enough to cover the interesting editor state.
for (int off = 16; off < 512; off += 8) {
Node n;
n.kind = NodeKind::Hex64;
n.name = QStringLiteral("field_%1").arg(off, 3, 16, QLatin1Char('0'));
n.parentId = rootId;
n.offset = off;
tree.addNode(n);
}
ctrl->clearSelection();
emit doc->documentChanged();
auto* sub = m_mdiArea->activeSubWindow();
if (sub) sub->setWindowTitle(rootName(doc->tree, ctrl->viewRootId()));
updateWindowTitle();
rebuildWorkspaceModel();
}
void MainWindow::selfTest() {
// Auto-open KUSER_SHARED_DATA example if available
QString exPath = QCoreApplication::applicationDirPath()
+ "/examples/KUSER_SHARED_DATA.rcx";
if (QFile::exists(exPath)) {
project_open(exPath);
} else {
project_new();
}
#ifdef Q_OS_WIN
// Create a new project, then point it at the live editor object
project_new();
// Auto-attach process memory plugin to self
auto* ctrl = activeController();
if (ctrl) {
DWORD pid = GetCurrentProcessId();
QString target = QString("%1:Reclass.exe").arg(pid);
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
}
if (!ctrl || ctrl->editors().isEmpty()) return;
auto* editor = ctrl->editors().first();
auto* doc = ctrl->document();
// Build a tree describing RcxEditor, based at the real object address
buildEditorDemo(doc->tree, reinterpret_cast<uintptr_t>(editor));
// Attach process memory to self — provider base will be set to the editor address
DWORD pid = GetCurrentProcessId();
QString target = QString("%1:Reclass.exe").arg(pid);
ctrl->attachViaPlugin(QStringLiteral("processmemory"), target);
#else
project_new();
#endif
}
void MainWindow::openFile() {
@@ -1484,14 +1531,14 @@ void MainWindow::showTypeAliasesDialog() {
// ── Project Lifecycle API ──
QMdiSubWindow* MainWindow::project_new() {
QMdiSubWindow* MainWindow::project_new(const QString& classKeyword) {
auto* doc = new RcxDocument(this);
QByteArray data(256, '\0');
doc->loadData(data);
doc->tree.baseAddress = 0x00400000;
buildEmptyStruct(doc->tree);
buildEmptyStruct(doc->tree, classKeyword);
auto* sub = createTab(doc);
rebuildWorkspaceModel();
@@ -1605,22 +1652,52 @@ void MainWindow::createWorkspaceDock() {
auto structIdVar = index.data(Qt::UserRole + 1);
uint64_t structId = structIdVar.isValid() ? structIdVar.toULongLong() : 0;
if (structId == 0 || structId == rcx::kGroupSentinel) return;
// Right-click on "Project" group → New Class / New Struct / New Enum
if (structId == rcx::kGroupSentinel) {
QMenu menu;
auto* actClass = menu.addAction("New Class");
auto* actStruct = menu.addAction("New Struct");
auto* actEnum = menu.addAction("New Enum");
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
if (chosen == actClass) newClass();
else if (chosen == actStruct) newStruct();
else if (chosen == actEnum) newEnum();
return;
}
if (structId == 0) return;
auto subVar = index.data(Qt::UserRole);
if (!subVar.isValid()) return;
auto* sub = static_cast<QMdiSubWindow*>(subVar.value<void*>());
if (!sub || !m_tabs.contains(sub)) return;
auto& tab = m_tabs[sub];
int ni = tab.doc->tree.indexOfId(structId);
if (ni < 0) return;
QString kw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
QMenu menu;
auto* deleteAction = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete");
if (menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos)) == deleteAction) {
auto& tab = m_tabs[sub];
int ni = tab.doc->tree.indexOfId(structId);
if (ni >= 0) {
tab.ctrl->removeNode(ni);
rebuildWorkspaceModel();
}
QAction* actConvert = nullptr;
// class↔struct conversion only (no enum conversion)
if (kw == QStringLiteral("class"))
actConvert = menu.addAction("Convert to Struct");
else if (kw == QStringLiteral("struct"))
actConvert = menu.addAction("Convert to Class");
auto* actDelete = menu.addAction(QIcon(":/vsicons/remove.svg"), "Delete");
QAction* chosen = menu.exec(m_workspaceTree->viewport()->mapToGlobal(pos));
if (chosen == actDelete) {
tab.ctrl->removeNode(ni);
rebuildWorkspaceModel();
} else if (chosen && chosen == actConvert) {
QString newKw = kw == QStringLiteral("class")
? QStringLiteral("struct") : QStringLiteral("class");
QString oldKw = tab.doc->tree.nodes[ni].resolvedClassKeyword();
tab.doc->undoStack.push(new rcx::RcxCommand(tab.ctrl,
rcx::cmd::ChangeClassKeyword{structId, oldKw, newKw}));
rebuildWorkspaceModel();
}
});
@@ -1821,27 +1898,11 @@ int main(int argc, char* argv[]) {
rcx::MainWindow window;
window.setWindowIcon(QIcon(":/icons/class.png"));
bool screenshotMode = app.arguments().contains("--screenshot");
if (screenshotMode)
window.setWindowOpacity(0.0);
window.show();
// Auto-open demo project from saved .rcx file
QMetaObject::invokeMethod(&window, "selfTest");
if (screenshotMode) {
QString out = "screenshot.png";
int idx = app.arguments().indexOf("--screenshot");
if (idx + 1 < app.arguments().size())
out = app.arguments().at(idx + 1);
QTimer::singleShot(1000, [&window, out]() {
QDir().mkpath(QFileInfo(out).absolutePath());
window.grab().save(out);
::_Exit(0); // immediate exit — no need for clean shutdown in screenshot mode
});
}
return app.exec();
}

View File

@@ -25,8 +25,9 @@ public:
explicit MainWindow(QWidget* parent = nullptr);
private slots:
void newFile();
void newDocument();
void newClass();
void newStruct();
void newEnum();
void selfTest();
void openFile();
void saveFile();
@@ -56,7 +57,7 @@ private slots:
public:
// Project Lifecycle API
QMdiSubWindow* project_new();
QMdiSubWindow* project_new(const QString& classKeyword = QString());
QMdiSubWindow* project_open(const QString& path = {});
bool project_save(QMdiSubWindow* sub = nullptr, bool saveAs = false);
void project_close(QMdiSubWindow* sub = nullptr);

View File

@@ -0,0 +1,32 @@
{
"name": "Mid",
"background": "#0D1219",
"backgroundAlt": "#121720",
"surface": "#161C28",
"border": "#1E2636",
"borderFocused": "#485068",
"button": "#181E2C",
"text": "#B0B8CC",
"textDim": "#505C74",
"textMuted": "#384258",
"textFaint": "#2C3448",
"hover": "#121720",
"selected": "#121720",
"selection": "#1A2038",
"syntaxKeyword": "#5688C0",
"syntaxNumber": "#90B480",
"syntaxString": "#B88060",
"syntaxComment": "#385030",
"syntaxPreproc": "#9868A8",
"syntaxType": "#8FDBFE",
"indHoverSpan": "#C09038",
"indCmdPill": "#141A26",
"indDataChanged": "#608C54",
"indHeatCold": "#B09030",
"indHeatWarm": "#C09038",
"indHeatHot": "#C83838",
"indHintGreen": "#385830",
"markerPtr": "#C83838",
"markerCycle": "#B89028",
"markerError": "#481818"
}

View File

@@ -334,7 +334,12 @@ TypeSelectorPopup::TypeSelectorPopup(QWidget* parent)
this, [this](int id, bool checked) {
if (!checked) return;
m_arrayCountEdit->setVisible(id == 3);
if (id == 3) m_arrayCountEdit->setFocus();
if (id == 3) {
if (m_arrayCountEdit->text().trimmed().isEmpty())
m_arrayCountEdit->setText(QStringLiteral("1"));
m_arrayCountEdit->setFocus();
m_arrayCountEdit->selectAll();
}
updateModifierPreview();
applyFilter(m_filterEdit->text());
});

View File

@@ -1920,54 +1920,9 @@ private slots:
}
}
void testComputeStructAlignment() {
NodeTree tree;
tree.baseAddress = 0;
Node root;
root.kind = NodeKind::Struct;
root.name = "Root";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Int32 has alignment 4
Node f1;
f1.kind = NodeKind::Int32;
f1.name = "a";
f1.parentId = rootId;
f1.offset = 0;
tree.addNode(f1);
QCOMPARE(tree.computeStructAlignment(rootId), 4);
// Add Hex64 (alignment 8) — max should become 8
Node f2;
f2.kind = NodeKind::Hex64;
f2.name = "b";
f2.parentId = rootId;
f2.offset = 8;
tree.addNode(f2);
QCOMPARE(tree.computeStructAlignment(rootId), 8);
}
void testComputeStructAlignmentEmpty() {
NodeTree tree;
Node root;
root.kind = NodeKind::Struct;
root.name = "Empty";
root.parentId = 0;
int ri = tree.addNode(root);
uint64_t rootId = tree.nodes[ri].id;
// Empty struct → alignment 1
QCOMPARE(tree.computeStructAlignment(rootId), 1);
}
void testCommandRowRootNameSpan() {
// Name span should cover the class name in the merged command row
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct\u25BE MyClass {";
QString text = "source\u25BE \u00B7 0x0 \u00B7 struct MyClass {";
ColumnSpan nameSpan = commandRowRootNameSpan(text);
QVERIFY(nameSpan.valid);

View File

@@ -643,6 +643,36 @@ private slots:
QCOMPARE(vals.size(), ValueHistory::kCapacity);
QCOMPARE(vals.last(), vh.last());
}
// ── Test: inline edit "int32_t[4]" on primitive converts to array ──
void testInlineEditPrimitiveArray() {
// Find a primitive field to convert
int idx = -1;
for (int i = 0; i < m_doc->tree.nodes.size(); i++) {
if (m_doc->tree.nodes[i].name == "field_u32") { idx = i; break; }
}
QVERIFY(idx >= 0);
QCOMPARE(m_doc->tree.nodes[idx].kind, NodeKind::UInt32);
uint64_t nodeId = m_doc->tree.nodes[idx].id;
// Emit inlineEditCommitted with array syntax
emit m_editor->inlineEditCommitted(idx, 0, EditTarget::Type,
QStringLiteral("int32_t[4]"));
QApplication::processEvents();
// Node should now be an Array with elementKind=Int32, arrayLen=4
int newIdx = m_doc->tree.indexOfId(nodeId);
QVERIFY(newIdx >= 0);
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::Array);
QCOMPARE(m_doc->tree.nodes[newIdx].elementKind, NodeKind::Int32);
QCOMPARE(m_doc->tree.nodes[newIdx].arrayLen, 4);
// Undo should restore to UInt32
m_doc->undoStack.undo();
QApplication::processEvents();
newIdx = m_doc->tree.indexOfId(nodeId);
QVERIFY(newIdx >= 0);
QCOMPARE(m_doc->tree.nodes[newIdx].kind, NodeKind::UInt32);
}
};
QTEST_MAIN(TestController)

470
tests/test_disasm.cpp Normal file
View File

@@ -0,0 +1,470 @@
#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 (provider-relative, i.e. offset from baseAddress):
//
// [0x0000] Root "Obj" struct
// +0x00: Pointer64 __vptr => points to 0xBASE+0x100 (vtable)
//
// [0x0100] VTable (expanded via pointer deref)
// +0x00: func ptr 0 => value 0xBASE+0x200 (func0 code)
// +0x08: func ptr 1 => value 0xBASE+0x300 (func1 code)
//
// [0x0200] func0 code: push rbp; ret
// [0x0300] func1 code: xor eax, eax; ret
//
const uint64_t kBase = 0x7FF600000000ULL;
// 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 kBase + 0x100
w64(0x00, kBase + 0x100);
// VTable at offset 0x100: two function pointers
w64(0x100, kBase + 0x200); // slot 0 -> func0
w64(0x108, kBase + 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 = kBase;
// 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;
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 kBase+0x100)
if (lm.offsetAddr >= kBase + 0x100 && lm.offsetAddr < kBase + 0x200) {
int nodeIdx = lm.nodeIdx;
funcPtrs.append({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 kBase + 0x100 (vtable + 0)
QCOMPARE(funcPtrs[0].offsetAddr, kBase + 0x100);
// func1 should be at kBase + 0x108 (vtable + 8)
QCOMPARE(funcPtrs[1].offsetAddr, kBase + 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-relative address = offsetAddr - baseAddress
uint64_t provAddr = fp.offsetAddr - kBase;
// 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, kBase + 0x200);
} else {
QCOMPARE(ptrVal, kBase + 0x300);
}
// Convert pointer value to provider-relative for reading code bytes
uint64_t codeProvAddr = ptrVal - kBase;
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].startsWith("00007ff600000200"),
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].startsWith("00007ff600000300"),
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 = kBase + 0x100 (the vptr itself, NOT a function address)
QCOMPARE(wrongVal0, kBase + 0x100);
// This is the vtable address, not a function — disassembling it would be wrong
QVERIFY2(wrongVal0 != kBase + 0x200,
"node.offset reads the vptr, not the function pointer");
QVERIFY2(wrongVal1 != kBase + 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
const uint64_t kBase = 0x10000;
QByteArray mem(1024, '\0');
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
// Root at 0: vptr -> 0x80
w64(0x00, kBase + 0x80);
// VTable at 0x80: one func ptr -> 0x100
w64(0x80, kBase + 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, kBase + 0x80); // This is the vtable addr, not a function!
// RIGHT: read from composed address (vtable + 0)
uint64_t rightPtrVal = prov.readU64(0x80);
QCOMPARE(rightPtrVal, kBase + 0x100); // This IS the function address
// Disassemble the RIGHT target
QByteArray rightCode = prov.readBytes(0x100, 128);
QString rightAsm = disassemble(rightCode, kBase + 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, kBase + 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 snapshot/provider
// using lm.offsetAddr - baseAddress (composed address)
// 3. Read code bytes from the REAL provider using ptrVal - baseAddress
// (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).
const uint64_t kBase = 0x7FF600000000ULL;
QByteArray mem(8192, '\0');
auto w64 = [&](int off, uint64_t val) {
memcpy(mem.data() + off, &val, 8);
};
// Layout:
// [0x000] Root struct: __vptr -> vtable at kBase + 0x100
// [0x100] VTable: func0 -> kBase + 0x1000, func1 -> kBase + 0x1800
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
// [0x1800] func1 code: xor eax, eax; ret
w64(0x000, kBase + 0x100); // __vptr
w64(0x100, kBase + 0x1000); // vtable[0]
w64(0x108, kBase + 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.
// In production, this is the ProcessMemoryProvider that reads via
// ReadProcessMemory at m_base + addr.
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 = kBase;
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;
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 < kBase + 0x100 || lm.offsetAddr >= kBase + 0x200)
continue; // skip standalone VTable definition entries
// --- Hover step 1: read pointer value from snapshot ---
uint64_t provAddr = lm.offsetAddr - tree.baseAddress;
// 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 - tree.baseAddress;
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"

View File

@@ -941,19 +941,13 @@ private slots:
// Set CommandRow text with root class (simulates controller.updateCommandRow)
m_editor->setCommandRowText(
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"));
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
// RootClassName should be allowed on CommandRow (line 0)
bool ok = m_editor->beginInlineEdit(EditTarget::RootClassName, 0);
QVERIFY2(ok, "RootClassName edit should be allowed on CommandRow");
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
// RootClassType should be allowed on CommandRow (line 0)
ok = m_editor->beginInlineEdit(EditTarget::RootClassType, 0);
QVERIFY2(ok, "RootClassType edit should be allowed on CommandRow");
QVERIFY(m_editor->isEditing());
m_editor->cancelInlineEdit();
}
// ── Test: CommandRow root class name editable ──
@@ -962,7 +956,7 @@ private slots:
// Set CommandRow with root class
m_editor->setCommandRowText(
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {"));
QStringLiteral("source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {"));
// Line 0 is CommandRow
const LineMeta* lm = m_editor->metaForLine(0);
@@ -1008,7 +1002,7 @@ private slots:
// Set command row text (simulates controller.updateCommandRow)
QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
m_editor->setCommandRowText(cmdText);
QApplication::processEvents();
@@ -1086,7 +1080,7 @@ private slots:
m_editor->applyDocument(m_result);
QString cmdText = QStringLiteral(
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct\u25BE _PEB64 {");
"source\u25BE \u00B7 0xD87B5E5000 \u00B7 struct _PEB64 {");
m_editor->setCommandRowText(cmdText);
QApplication::processEvents();

View File

@@ -62,7 +62,7 @@ private slots:
// ── Chevron span detection ──
void testChevronSpanDetected() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
ColumnSpan span = commandRowChevronSpan(text);
QVERIFY(span.valid);
QCOMPARE(span.start, 0);
@@ -79,7 +79,7 @@ private slots:
// ── Existing spans unbroken by chevron prefix ──
void testSpansWithPrefix() {
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct\u25BE Alpha {");
QString text = QStringLiteral("[\u25B8] source\u25BE \u00B7 0x1000 \u00B7 struct Alpha {");
ColumnSpan src = commandRowSrcSpan(text);
QVERIFY(src.valid);
@@ -736,6 +736,63 @@ private slots:
QVERIFY(listView);
QVERIFY(listView->model()->rowCount() > 2);
}
// ── FieldType popup: primitive with [n] creates an array ──
void testFieldTypePrimitiveArrayCreation() {
auto* doc = new RcxDocument();
buildTwoRootTree(doc->tree);
doc->provider = std::make_unique<BufferProvider>(makeBuffer());
auto* splitter = new QSplitter();
auto* ctrl = new RcxController(doc, nullptr);
ctrl->addSplitEditor(splitter);
splitter->resize(800, 600);
splitter->show();
QVERIFY(QTest::qWaitForWindowExposed(splitter));
ctrl->refresh();
QApplication::processEvents();
// Find the "x" field (Int32)
int xIdx = -1;
for (int i = 0; i < doc->tree.nodes.size(); i++) {
if (doc->tree.nodes[i].name == "x") { xIdx = i; break; }
}
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
uint64_t xNodeId = doc->tree.nodes[xIdx].id;
// Simulate the primitive-array path of applyTypePopupResult:
// beginMacro → changeNodeKind(Array) → ChangeArrayMeta → endMacro
doc->undoStack.beginMacro(QStringLiteral("Change to primitive array"));
ctrl->changeNodeKind(xIdx, NodeKind::Array);
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
doc->undoStack.push(new RcxCommand(ctrl,
cmd::ChangeArrayMeta{xNodeId, doc->tree.nodes[xIdx].elementKind,
NodeKind::Int32,
doc->tree.nodes[xIdx].arrayLen, 4}));
doc->undoStack.endMacro();
QApplication::processEvents();
// Node should now be an Array
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Array);
QCOMPARE(doc->tree.nodes[xIdx].elementKind, NodeKind::Int32);
QCOMPARE(doc->tree.nodes[xIdx].arrayLen, 4);
// Single undo reverses the entire macro
doc->undoStack.undo();
QApplication::processEvents();
xIdx = doc->tree.indexOfId(xNodeId);
QVERIFY(xIdx >= 0);
QCOMPARE(doc->tree.nodes[xIdx].kind, NodeKind::Int32);
delete ctrl;
delete splitter;
delete doc;
}
};
QTEST_MAIN(TestTypeSelector)

16
third_party/fadec/.build.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
image: alpine/edge
sources:
- https://git.sr.ht/~aengelke/fadec
packages:
- meson
tasks:
- build: |
mkdir fadec-build1
meson fadec-build1 fadec
ninja -C fadec-build1
ninja -C fadec-build1 test
# Complete test with encode2 API.
mkdir fadec-build2
meson fadec-build2 fadec -Dwith_encode2=true
ninja -C fadec-build2
ninja -C fadec-build2 test

View File

@@ -0,0 +1,51 @@
name: CI
on: [push]
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt install -y ninja-build meson
- name: Configure
run: mkdir build; CC=clang CXX=clang++ meson -Dbuildtype=debugoptimized -Dwith_encode2=true build
- name: Build
run: ninja -v -C build
- name: Test
run: meson test -v -C build
build-linux-cmake:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt install -y ninja-build cmake
- name: Configure
run: CC=clang CXX=clang++ cmake -B build -G Ninja -DFADEC_ENCODE2=ON
- name: Build
run: cmake --build build -v
- name: Test
run: ctest --test-dir build -V
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install ninja meson
- name: Configure
run: mkdir build; meson setup --vsenv -Dbuildtype=debugoptimized -Dwith_encode2=true build
- name: Build
run: meson compile -v -C build
- name: Test
run: meson test -v -C build
build-windows-cmake:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build -DFADEC_ENCODE2=ON
- name: Build
run: cmake --build build -v
- name: Test
run: ctest --test-dir build -V -C Debug

1
third_party/fadec/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build/

109
third_party/fadec/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,109 @@
cmake_minimum_required(VERSION 3.23)
project(fadec LANGUAGES C)
enable_testing()
# TODO: make this actually optional
enable_language(CXX OPTIONAL)
# Options
set(FADEC_ARCHMODE "both" CACHE STRING "Support only 32-bit x86, 64-bit x86 or both")
set_property(CACHE FADEC_ARCHMODE PROPERTY STRINGS both only32 only64)
option(FADEC_UNDOC "Include undocumented instructions" FALSE)
option(FADEC_DECODE "Include support for decoding" TRUE)
option(FADEC_ENCODE "Include support for encoding" TRUE)
option(FADEC_ENCODE2 "Include support for new encoding API" FALSE)
set(CMAKE_C_STANDARD 11)
if (MSVC)
add_compile_options(/W4 -D_CRT_SECURE_NO_WARNINGS /wd4018 /wd4146 /wd4244 /wd4245 /wd4267 /wd4310)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Zc:preprocessor>)
else()
add_compile_options(-Wall -Wextra -Wpedantic -Wno-overlength-strings)
endif()
find_package(Python3 3.9 REQUIRED)
add_library(fadec)
add_library(fadec::fadec ALIAS fadec)
set_target_properties(fadec PROPERTIES
LINKER_LANGUAGE C
)
set(GEN_ARGS "")
if (NOT FADEC_ARCHMODE STREQUAL "only64")
list(APPEND GEN_ARGS "--32")
endif ()
if (NOT FADEC_ARCHMODE STREQUAL "only32")
list(APPEND GEN_ARGS "--64")
endif ()
if (FADEC_UNDOC)
list(APPEND GEN_ARGS "--with-undoc")
endif ()
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include")
function(fadec_component)
cmake_parse_arguments(ARG "" "NAME" "HEADERS;SOURCES" ${ARGN})
set(PRIV_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-private.inc)
set(PUB_INC ${CMAKE_CURRENT_BINARY_DIR}/include/fadec-${ARG_NAME}-public.inc)
add_custom_command(
OUTPUT ${PRIV_INC} ${PUB_INC}
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/parseinstrs.py ${ARG_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/instrs.txt
${PUB_INC} ${PRIV_INC} ${GEN_ARGS}
DEPENDS instrs.txt parseinstrs.py
COMMENT "Building table for ${ARG_NAME}"
)
list(APPEND FADEC_HEADERS ${PUB_INC})
target_sources(fadec PRIVATE
${ARG_SOURCES}
PUBLIC
FILE_SET HEADERS
BASE_DIRS .
FILES
${ARG_HEADERS}
PUBLIC
FILE_SET generated_public TYPE HEADERS
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
FILES
${PUB_INC}
PRIVATE
FILE_SET generated_private TYPE HEADERS
BASE_DIRS ${CMAKE_CURRENT_BINARY_DIR}/include
FILES
${PRIV_INC}
)
add_executable(fadec-${ARG_NAME}-test ${ARG_NAME}-test.c)
target_link_libraries(fadec-${ARG_NAME}-test PRIVATE fadec)
add_test(NAME ${ARG_NAME} COMMAND fadec-${ARG_NAME}-test)
if (CMAKE_CXX_COMPILER AND ${ARG_NAME} STREQUAL "encode2")
add_executable(fadec-${ARG_NAME}-test-cpp ${ARG_NAME}-test.cc)
target_link_libraries(fadec-${ARG_NAME}-test-cpp PRIVATE fadec)
add_test(NAME ${ARG_NAME}-cpp COMMAND fadec-${ARG_NAME}-test-cpp)
endif()
endfunction()
if (FADEC_DECODE)
fadec_component(NAME decode SOURCES decode.c format.c HEADERS fadec.h)
endif ()
if (FADEC_ENCODE)
fadec_component(NAME encode SOURCES encode.c HEADERS fadec-enc.h)
endif ()
if (FADEC_ENCODE2)
fadec_component(NAME encode2 SOURCES encode2.c HEADERS fadec-enc2.h)
endif ()
install(TARGETS fadec EXPORT fadec
LIBRARY
ARCHIVE
FILE_SET HEADERS FILE_SET generated_public)

28
third_party/fadec/LICENSE vendored Normal file
View File

@@ -0,0 +1,28 @@
Copyright (c) 2018, Alexis Engelke
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

184
third_party/fadec/README.md vendored Normal file
View File

@@ -0,0 +1,184 @@
# Fadec — Fast Decoder for x86-32 and x86-64 and Encoder for x86-64
Fadec is a fast and lightweight decoder for x86-32 and x86-64. To meet the goal of speed, lookup tables are used to map the opcode the (internal) description of the instruction encoding. This table currently has a size of roughly 37 kiB (for 32/64-bit combined).
Fadec-Enc (or Faenc) is a small, lightweight and easy-to-use encoder, currently for x86-64 only.
## Key features
> **Q: Why not just use any other decoding/encoding library available out there?**
>
> A: I needed to embed a small and fast decoder in a project for a freestanding environment (i.e., no libc). Further, only very few plain encoding libraries are available for x86-64; and most of them are large or make heavy use of external dependencies.
- **Small size:** the entire library with the x86-64/32 decoder and the x86-64 encoder are only 95 kiB; for specific use cases, the size can be reduced even further (e.g., by dropping AVX-512). The main decode/encode routines are only a few hundreds lines of code.
- **Performance:** Fadec is significantly faster than libopcodes, Capstone, or Zydis due to the absence of high-level abstractions and the small lookup table.
- **Zero dependencies:** the entire library has no dependencies, even on the standard library, making it suitable for freestanding environments without a full libc or `malloc`-style memory allocation.
- **Correctness:** even corner cases should be handled correctly (if not, that's a bug), e.g., the order of prefixes, immediate sizes of jump instructions, the presence of the `lock` prefix, or properly handling VEX.W in 32-bit mode.
All components of this library target the Intel 64 implementations of x86. While AMD64 is _mostly similar_, there are some minor differences (e.g. operand sizes for jump instructions, more instructions, `cr8` can be accessed with `lock` prefix, `f34190` is `xchg`, not `pause`) which are currently not handled.
## Decoder Usage
### Example
```c
uint8_t buffer[] = {0x49, 0x90};
FdInstr instr;
// Decode from buffer into instr in 64-bit mode.
int ret = fd_decode(buffer, sizeof(buffer), 64, 0, &instr);
// ret<0 indicates an error, ret>0 the number of decoded bytes
// Relevant properties of instructions can now be queried using the FD_* macros.
// Or, we can format the instruction to a string buffer:
char fmtbuf[64];
fd_format(&instr, fmtbuf, sizeof(fmtbuf));
// fmtbuf now reads: "xchg r8, rax"
```
### API
The API consists of two functions to decode and format instructions, as well as several accessor macros. A full documentation can be found in [fadec.h](fadec.h). Direct access of any structure fields is not recommended.
- `int fd_decode(const uint8_t* buf, size_t len, int mode, uintptr_t address, FdInstr* out_instr)`
- Decode a single instruction. For internal performance reasons, note that:
- The decoded operand sizes are not always exact. However, the exact size can be reconstructed in all cases.
- An implicit `fwait` in FPU instructions is decoded as a separate instruction (matching the opcode layout in machine code). For example, `finit` is decoded as `FD_FWAIT` + `FD_FINIT`
- Return value: number of bytes used, or a negative value in case of an error.
- `buf`/`len`: buffer containing instruction bytes. At most 15 bytes will be read. If the instruction is longer than `len`, an error value is returned.
- `mode`: architecture mode, either `32` or `64`.
- `address`: set to `0`. (Obsolete use: virtual address of the decoded instruction.)
- `out_instr`: Pointer to the instruction buffer, might get written partially in case of an error.
- `void fd_format(const FdInstr* instr, char* buf, size_t len)`
- Format a single instruction to a human-readable format.
- `instr`: decoded instruction.
- `buf`/`len`: buffer for formatted instruction string
- Various accessor macros: see [fadec.h](fadec.h).
## Encoder Usage
The encoder has two API variants: "v1" has a single entry point (`fe_enc64`) and the instruction is specified as integer parameter. "v2" has one entry point per instruction. v2 is currently about 3x faster than v1, but also has much larger code size (v1: <10 kiB; v2: ~3 MiB) and takes much longer to compile. It is therefore off by default and can be enabled by passing `-Dwith_encode2=true` to Meson. Both variants are supported.
### Example (API v1)
```c
int failed = 0;
uint8_t buf[64];
uint8_t* cur = buf;
// xor eax, eax
failed |= fe_enc64(&cur, FE_XOR32rr, FE_AX, FE_AX);
// movzx ecx, byte ptr [rdi + 1*rax + 0]
failed |= fe_enc64(&cur, FE_MOVZXr32m8, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
// test ecx, ecx
failed |= fe_enc64(&cur, FE_TEST32rr, FE_CX, FE_CX);
// jnz $
// This will be replaced later; FE_JMPL enforces use of longest offset
uint8_t* fwd_jmp = cur;
failed |= fe_enc64(&cur, FE_JNZ|FE_JMPL, (intptr_t) cur);
uint8_t* loop_tgt = cur;
// add rax, rcx
failed |= fe_enc64(&cur, FE_ADD64rr, FE_AX, FE_CX);
// sub ecx, 1
failed |= fe_enc64(&cur, FE_SUB32ri, FE_CX, 1);
// jnz loop_tgt
failed |= fe_enc64(&cur, FE_JNZ, (intptr_t) loop_tgt);
// (alternatively: fe_enc64(&cur, FE_Jcc|FE_CC_NZ, (intptr_t) loop_tgt).)
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
failed |= fe_enc64(&fwd_jmp, FE_JNZ|FE_JMPL, (intptr_t) cur);
// ret
failed |= fe_enc64(&cur, FE_RET);
// cur now points to the end of the buffer, failed indicates any failures.
```
### Example (API v2)
```c
uint8_t buf[64];
uint8_t* cur = buf;
// xor eax, eax
cur += fe64_XOR32rr(cur, 0, FE_AX, FE_AX);
// movzx ecx, byte ptr [rdi + 1*rax + 0]
cur += fe64_MOVZXr32m8(cur, 0, FE_CX, FE_MEM(FE_DI, 1, FE_AX, 0));
// test ecx, ecx
cur += fe64_TEST32rr(cur, 0, FE_CX, FE_CX);
// jnz $
// This will be replaced later; FE_JMPL enforces use of longest offset
uint8_t* fwd_jmp = cur;
cur += fe64_JNZ(cur, FE_JMPL, cur);
uint8_t* loop_tgt = cur;
// add rax, rcx
cur += fe64_ADD64rr(cur, 0, FE_AX, FE_CX);
// sub ecx, 1
cur += fe64_SUB32ri(cur, 0, FE_CX, 1);
// jnz loop_tgt
cur += fe64_JNZ(cur, 0, loop_tgt);
// (alternatively: fe64_Jcc(cur, FE_CC_NZ, loop_tgt).)
// Update previous jump to jump here. Note that we _must_ specify FE_JMPL too.
fe64_JNZ(fwd_jmp, FE_JMPL, cur);
// ret
cur += fe64_RET(cur, 0);
// cur now points to the end of the buffer
// errors are ignored, this example should not cause any :-)
```
### API v1
The API consists of one function to handle encode requests, as well as some macros. More information can be found in [fadec-enc.h](fadec-enc.h). Usage of internals like enum values is not recommended.
- `int fe_enc64(uint8_t** buf, uint64_t mnem, int64_t operands...)`
- Encodes an instruction for x86-64 into `*buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
- Return value: `0` on success, a negative value in error cases.
- `buf`: Pointer to the pointer to the instruction buffer. The pointer (`*buf`) will be advanced by the number of bytes written. The instruction buffer must have at least 15 bytes left.
- `mnem`: Instruction mnemonic to encode combined with extra flags:
- `FE_SEG(segreg)`: override segment to specified segment register.
- `FE_ADDR32`: override address size to 32-bit.
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
- `FE_MASK(maskreg)`: specify non-zero mask register (1--7) for instructions that support masking (suffixed with `_mask` or `_maskz`) or require a mask (AVX-512 gather/scatter).
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
- `operands...`: Up to 4 instruction operands. The operand kinds must match the requirements of the mnemonic.
- For register operands (`r`=non-mask register, `k`=mask register), use the register: `FE_AX`, `FE_AH`, `FE_XMM12`.
- For immediate operands (`i`=regular, `a`=absolute address), use the constant: `12`, `-0xbeef`.
- For memory operands (`m`=regular or `b`=broadcast), use: `FE_MEM(basereg,scale,indexreg,offset)`. Use `0` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
- For offset operands (`o`), specify the target address.
### API v2
The API consists of one function per instruction, as well as some macros. The API provides type safety for different register types as well as for memory operands (regular vs. VSIB). Besides a few details listed here, the usage is very similar to API v1. More information can be found in [fadec-enc2.h](fadec-enc2.h). Usage of internals like enum values is not recommended.
- `int fe64_<mnemonic>(uint8_t* buf, int flags, <operands...>)`
- Encodes the specified instruction for x86-64 into `buf`. EVEX-encoded instructions will transparently encode with the shorter VEX prefix where permitted.
- Return value: `0` on failure, otherwise the instruction length.
- `buf`: Pointer to the instruction buffer. The instruction buffer must have at least 15 bytes left. Bytes beyond the returned instruction length can be overwritten.
- `flags`: combination of extra flags, default to `0`:
- `FE_SEG(segreg)`: override segment to specified segment register.
- `FE_ADDR32`: override address size to 32-bit.
- `FE_JMPL`: use longest possible offset encoding, useful when jump target is not known.
- `FE_RC_RN/RD/RU/RZ`: set rounding mode for instructions with static rounding control (suffixed `_er`).
- `FE_CC_O/NO/E/NE/...`: set condition code for instructions with unspecified condition code (`Jcc`, `SETcc`, `CMOVcc`, `CMPccXADD`).
- `FeRegMASK opmask` (instructions with opmask only): specify non-zero mask register (1--7) for instructions suffixed with `_mask`/`_maskz` and AVX-512 gather/scatter.
- `operands...`: up to four instruction operands.
- Registers have types `FeRegGP`/`FeRegXMM`/`FeRegMASK`/etc.; byte registers accepting high-byte operands also accept `FeRegGPH`.
- Immediate operands have an appropriately sized integer type.
- Memory operands use a `FeMem` (VSIB: `FeMemV`) structure, use the macro `FE_MEM(basereg,scale,indexreg,offset)` (VSIB: `FE_MEMV(...)`). Use `FE_NOREG` to specify _no register_. For RIP-relative addressing, the size of the instruction is added automatically.
- For offset operands (`o`), specify the target address relative to `buf`.
- `int fe64_NOP(uint8_t* buf, unsigned size)`
- Encode a series of `nop`s of `size` bytes, but at least emit one byte. This will use larger the `nop` encodings to reduce the number of instructions and is intended for filling padding.
## Known issues
- Decoder/Encoder: register uniqueness constraints are not enforced. This affects:
- VSIB-encoded instructions: no vector register may be used more than once
- AMX instructions: no tile register may be used more than once
- AVX-512 complex FP16 multiplication: destination must be not be equal to a source register
- Prefixes for indirect jumps and calls are not properly decoded, e.g. `notrack`, `bnd`.
- Low test coverage. (Help needed.)
- No Python API.
Some ISA extensions are not supported, often because they are deprecated or unsupported by recent hardware. These are unlikely to be implemented in the near future:
- (Intel) MPX: Intel lists MPX as deprecated.
- (Intel) HLE prefixes `xacquire`/`xrelease`: Intel lists HLE as deprecated. The formatter for decoded instructions is able to reconstruct these in most cases, though.
- (Intel) Xeon Phi (KNC/KNL/KNM) extensions, including the MVEX prefix: the hardware is discontinued/no longer available.
- (AMD) XOP: unsupported by newer hardware.
- (AMD) FMA4: unsupported by newer hardware.
If you find any other issues, please report a bug. Or, even better, send a patch fixing the issue.

3248
third_party/fadec/decode-test.c vendored Normal file

File diff suppressed because it is too large Load Diff

791
third_party/fadec/decode.c vendored Normal file
View File

@@ -0,0 +1,791 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <fadec.h>
#ifdef __GNUC__
#define LIKELY(x) __builtin_expect((x), 1)
#define UNLIKELY(x) __builtin_expect((x), 0)
#define ASSUME(x) do { if (!(x)) __builtin_unreachable(); } while (0)
#else
#define LIKELY(x) (x)
#define UNLIKELY(x) (x)
#define ASSUME(x) ((void) 0)
#endif
// Defines FD_TABLE_OFFSET_32 and FD_TABLE_OFFSET_64, if available
#define FD_DECODE_TABLE_DEFINES
#include <fadec-decode-private.inc>
#undef FD_DECODE_TABLE_DEFINES
enum DecodeMode {
DECODE_64 = 0,
DECODE_32 = 1,
};
typedef enum DecodeMode DecodeMode;
#define ENTRY_NONE 0
#define ENTRY_INSTR 1
#define ENTRY_TABLE256 2
#define ENTRY_TABLE16 3
#define ENTRY_TABLE8E 4
#define ENTRY_TABLE_PREFIX 5
#define ENTRY_TABLE_VEX 6
#define ENTRY_TABLE_ROOT 8
#define ENTRY_MASK 7
static uint16_t
table_lookup(unsigned cur_idx, unsigned entry_idx) {
static _Alignas(16) const uint16_t _decode_table[] = {
#define FD_DECODE_TABLE_DATA
#include <fadec-decode-private.inc>
#undef FD_DECODE_TABLE_DATA
};
return _decode_table[cur_idx + entry_idx];
}
static unsigned
table_walk(unsigned table_entry, unsigned entry_idx) {
return table_lookup(table_entry & ~0x3, entry_idx);
}
#define LOAD_LE_1(buf) ((uint64_t) *(const uint8_t*) (buf))
#define LOAD_LE_2(buf) (LOAD_LE_1(buf) | LOAD_LE_1((const uint8_t*) (buf) + 1)<<8)
#define LOAD_LE_3(buf) (LOAD_LE_2(buf) | LOAD_LE_1((const uint8_t*) (buf) + 2)<<16)
#define LOAD_LE_4(buf) (LOAD_LE_2(buf) | LOAD_LE_2((const uint8_t*) (buf) + 2)<<16)
#define LOAD_LE_8(buf) (LOAD_LE_4(buf) | LOAD_LE_4((const uint8_t*) (buf) + 4)<<32)
enum
{
PREFIX_REXB = 0x01,
PREFIX_REXX = 0x02,
PREFIX_REXR = 0x04,
PREFIX_REXW = 0x08,
PREFIX_REX = 0x40,
PREFIX_REXRR = 0x10,
PREFIX_VEX = 0x20,
};
struct InstrDesc
{
uint16_t type;
uint16_t operand_indices;
uint16_t operand_sizes;
uint16_t reg_types;
};
#define DESC_HAS_MODRM(desc) (((desc)->operand_indices & (3 << 0)) != 0)
#define DESC_MODRM_IDX(desc) ((((desc)->operand_indices >> 0) & 3) ^ 3)
#define DESC_HAS_MODREG(desc) (((desc)->operand_indices & (3 << 2)) != 0)
#define DESC_MODREG_IDX(desc) ((((desc)->operand_indices >> 2) & 3) ^ 3)
#define DESC_HAS_VEXREG(desc) (((desc)->operand_indices & (3 << 4)) != 0)
#define DESC_VEXREG_IDX(desc) ((((desc)->operand_indices >> 4) & 3) ^ 3)
#define DESC_IMM_CONTROL(desc) (((desc)->operand_indices >> 12) & 0x7)
#define DESC_IMM_IDX(desc) ((((desc)->operand_indices >> 6) & 3) ^ 3)
#define DESC_EVEX_BCST(desc) (((desc)->operand_indices >> 8) & 1)
#define DESC_EVEX_MASK(desc) (((desc)->operand_indices >> 9) & 1)
#define DESC_ZEROREG_VAL(desc) (((desc)->operand_indices >> 10) & 1)
#define DESC_LOCK(desc) (((desc)->operand_indices >> 11) & 1)
#define DESC_VSIB(desc) (((desc)->operand_indices >> 15) & 1)
#define DESC_OPSIZE(desc) (((desc)->reg_types >> 11) & 7)
#define DESC_MODRM_SIZE(desc) (((desc)->operand_sizes >> 0) & 3)
#define DESC_MODREG_SIZE(desc) (((desc)->operand_sizes >> 2) & 3)
#define DESC_VEXREG_SIZE(desc) (((desc)->operand_sizes >> 4) & 3)
#define DESC_IMM_SIZE(desc) (((desc)->operand_sizes >> 6) & 3)
#define DESC_LEGACY(desc) (((desc)->operand_sizes >> 8) & 1)
#define DESC_SIZE_FIX1(desc) (((desc)->operand_sizes >> 10) & 7)
#define DESC_SIZE_FIX2(desc) (((desc)->operand_sizes >> 13) & 3)
#define DESC_INSTR_WIDTH(desc) (((desc)->operand_sizes >> 15) & 1)
#define DESC_MODRM(desc) (((desc)->reg_types >> 14) & 1)
#define DESC_IGN66(desc) (((desc)->reg_types >> 15) & 1)
#define DESC_EVEX_SAE(desc) (((desc)->reg_types >> 8) & 1)
#define DESC_EVEX_ER(desc) (((desc)->reg_types >> 9) & 1)
#define DESC_EVEX_BCST16(desc) (((desc)->reg_types >> 10) & 1)
#define DESC_REGTY_MODRM(desc) (((desc)->reg_types >> 0) & 7)
#define DESC_REGTY_MODREG(desc) (((desc)->reg_types >> 3) & 7)
#define DESC_REGTY_VEXREG(desc) (((desc)->reg_types >> 6) & 3)
int
fd_decode(const uint8_t* buffer, size_t len_sz, int mode_int, uintptr_t address,
FdInstr* instr)
{
int len = len_sz > 15 ? 15 : len_sz;
// Ensure that we can actually handle the decode request
DecodeMode mode;
unsigned table_root_idx;
switch (mode_int)
{
#if defined(FD_TABLE_OFFSET_32)
case 32: table_root_idx = FD_TABLE_OFFSET_32; mode = DECODE_32; break;
#endif
#if defined(FD_TABLE_OFFSET_64)
case 64: table_root_idx = FD_TABLE_OFFSET_64; mode = DECODE_64; break;
#endif
default: return FD_ERR_INTERNAL;
}
int off = 0;
uint8_t vex_operand = 0;
uint8_t addr_size = mode == DECODE_64 ? 3 : 2;
unsigned prefix_rex = 0;
uint8_t prefix_rep = 0;
unsigned vexl = 0;
unsigned prefix_evex = 0;
instr->segment = FD_REG_NONE;
// Values must match prefixes in parseinstrs.py.
enum {
PF_SEG1 = 0xfff8 - 0xfff8,
PF_SEG2 = 0xfff9 - 0xfff8,
PF_66 = 0xfffa - 0xfff8,
PF_67 = 0xfffb - 0xfff8,
PF_LOCK = 0xfffc - 0xfff8,
PF_REP = 0xfffd - 0xfff8,
PF_REX = 0xfffe - 0xfff8,
};
uint8_t prefixes[8] = {0};
unsigned table_entry = 0;
while (true) {
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
uint8_t prefix = buffer[off];
table_entry = table_lookup(table_root_idx, prefix);
if (LIKELY(table_entry - 0xfff8 >= 8))
break;
prefixes[PF_REX] = 0;
prefixes[table_entry - 0xfff8] = prefix;
off++;
}
if (off) {
if (UNLIKELY(prefixes[PF_SEG2])) {
if (prefixes[PF_SEG2] & 0x02)
instr->segment = prefixes[PF_SEG2] >> 3 & 3;
else
instr->segment = prefixes[PF_SEG2] & 7;
}
if (UNLIKELY(prefixes[PF_67]))
addr_size--;
prefix_rex = prefixes[PF_REX];
prefix_rep = prefixes[PF_REP];
}
// table_entry kinds: INSTR(0), T16(1), ESCAPE_A(2), ESCAPE_B(3)
if (LIKELY(!(table_entry & 2))) {
off++;
// Then, walk through ModR/M-encoded opcode extensions.
if (table_entry & 1) {
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
unsigned isreg = buffer[off] >= 0xc0;
table_entry = table_walk(table_entry, ((buffer[off] >> 2) & 0xe) | isreg);
// table_entry kinds: INSTR(0), T8E(1)
if (table_entry & 1)
table_entry = table_walk(table_entry, buffer[off] & 7);
}
// table_entry kinds: INSTR(0)
goto direct;
}
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
unsigned opcode_escape = 0;
uint8_t mandatory_prefix = 0; // without escape/VEX/EVEX, this is ignored.
if (buffer[off] == 0x0f)
{
if (UNLIKELY(off + 1 >= len))
return FD_ERR_PARTIAL;
if (buffer[off + 1] == 0x38)
opcode_escape = 2;
else if (buffer[off + 1] == 0x3a)
opcode_escape = 3;
else
opcode_escape = 1;
off += opcode_escape >= 2 ? 2 : 1;
// If there is no REP/REPNZ prefix offer 66h as mandatory prefix. If
// there is a REP prefix, then the 66h prefix is ignored here.
mandatory_prefix = prefix_rep ? prefix_rep ^ 0xf1 : !!prefixes[PF_66];
}
else if (UNLIKELY((unsigned) buffer[off] - 0xc4 < 2 || buffer[off] == 0x62))
{
unsigned vex_prefix = buffer[off];
// VEX (C4/C5) or EVEX (62)
if (UNLIKELY(off + 1 >= len))
return FD_ERR_PARTIAL;
if (UNLIKELY(mode == DECODE_32 && buffer[off + 1] < 0xc0)) {
off++;
table_entry = table_walk(table_entry, 0);
// table_entry kinds: INSTR(0)
goto direct;
}
// VEX/EVEX + 66/F3/F2/REX will #UD.
// Note: REX is also here only respected if it immediately precedes the
// opcode, in this case the VEX/EVEX "prefix".
if (prefixes[PF_66] || prefixes[PF_REP] || prefix_rex)
return FD_ERR_UD;
uint8_t byte = buffer[off + 1];
if (vex_prefix == 0xc5) // 2-byte VEX
{
opcode_escape = 1;
prefix_rex = byte & 0x80 ? 0 : PREFIX_REXR;
}
else // 3-byte VEX or EVEX
{
// SDM Vol 2A 2-15 (Dec. 2016): Ignored in 32-bit mode
if (mode == DECODE_64)
prefix_rex = byte >> 5 ^ 0x7;
if (vex_prefix == 0x62) // EVEX
{
if (byte & 0x08) // Bit 3 of opcode_escape must be clear.
return FD_ERR_UD;
_Static_assert(PREFIX_REXRR == 0x10, "wrong REXRR value");
if (mode == DECODE_64)
prefix_rex |= (byte & PREFIX_REXRR) ^ PREFIX_REXRR;
}
else // 3-byte VEX
{
if (byte & 0x18) // Bits 4:3 of opcode_escape must be clear.
return FD_ERR_UD;
}
opcode_escape = (byte & 0x07);
if (UNLIKELY(opcode_escape == 0)) {
int prefix_len = vex_prefix == 0x62 ? 4 : 3;
// Pretend to decode the prefix plus one opcode byte.
return off + prefix_len > len ? FD_ERR_PARTIAL : FD_ERR_UD;
}
// Load third byte of VEX prefix
if (UNLIKELY(off + 2 >= len))
return FD_ERR_PARTIAL;
byte = buffer[off + 2];
prefix_rex |= byte & 0x80 ? PREFIX_REXW : 0;
}
mandatory_prefix = byte & 3;
vex_operand = ((byte & 0x78) >> 3) ^ 0xf;
prefix_rex |= PREFIX_VEX;
if (vex_prefix == 0x62) // EVEX
{
if (!(byte & 0x04)) // Bit 10 must be 1.
return FD_ERR_UD;
if (UNLIKELY(off + 3 >= len))
return FD_ERR_PARTIAL;
byte = buffer[off + 3];
// prefix_evex is z:L'L/RC:b:V':aaa
vexl = (byte >> 5) & 3;
prefix_evex = byte | 0x100; // Ensure that prefix_evex is non-zero.
if (mode == DECODE_64) // V' causes UD in 32-bit mode
vex_operand |= byte & 0x08 ? 0 : 0x10; // V'
else if (!(byte & 0x08))
return FD_ERR_UD;
off += 4;
}
else // VEX
{
vexl = byte & 0x04 ? 1 : 0;
off += 0xc7 - vex_prefix; // 3 for c4, 2 for c5
}
}
table_entry = table_walk(table_entry, opcode_escape);
// table_entry kinds: INSTR(0) [only for invalid], T256(2)
if (UNLIKELY(!table_entry))
return FD_ERR_UD;
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
table_entry = table_walk(table_entry, buffer[off++]);
// table_entry kinds: INSTR(0), T16(1), TVEX(2), TPREFIX(3)
// Handle mandatory prefixes (which behave like an opcode ext.).
if ((table_entry & 3) == 3)
table_entry = table_walk(table_entry, mandatory_prefix);
// table_entry kinds: INSTR(0), T16(1), TVEX(2)
// Then, walk through ModR/M-encoded opcode extensions.
if (table_entry & 1) {
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
unsigned isreg = buffer[off] >= 0xc0;
table_entry = table_walk(table_entry, ((buffer[off] >> 2) & 0xe) | isreg);
// table_entry kinds: INSTR(0), T8E(1), TVEX(2)
if (table_entry & 1)
table_entry = table_walk(table_entry, buffer[off] & 7);
}
// table_entry kinds: INSTR(0), TVEX(2)
// For VEX prefix, we have to distinguish between VEX.W and VEX.L which may
// be part of the opcode.
if (UNLIKELY(table_entry & 2))
{
uint8_t index = 0;
index |= prefix_rex & PREFIX_REXW ? (1 << 0) : 0;
// When EVEX.L'L is the rounding mode, the instruction must not have
// L'L constraints.
index |= vexl << 1;
table_entry = table_walk(table_entry, index);
}
// table_entry kinds: INSTR(0)
direct:
// table_entry kinds: INSTR(0)
if (UNLIKELY(!table_entry))
return FD_ERR_UD;
static _Alignas(16) const struct InstrDesc descs[] = {
#define FD_DECODE_TABLE_DESCS
#include <fadec-decode-private.inc>
#undef FD_DECODE_TABLE_DESCS
};
const struct InstrDesc* desc = &descs[table_entry >> 2];
instr->type = desc->type;
instr->addrsz = addr_size;
instr->flags = ((prefix_rep + 1) & 6) + (mode == DECODE_64 ? FD_FLAG_64 : 0);
instr->address = address;
for (unsigned i = 0; i < sizeof(instr->operands) / sizeof(FdOp); i++)
instr->operands[i] = (FdOp) {0};
if (DESC_MODRM(desc) && UNLIKELY(off++ >= len))
return FD_ERR_PARTIAL;
unsigned op_byte = buffer[off - 1] | (!DESC_MODRM(desc) ? 0xc0 : 0);
if (UNLIKELY(prefix_evex)) {
// VSIB inst (gather/scatter) without mask register or w/EVEX.z is UD
if (DESC_VSIB(desc) && (!(prefix_evex & 0x07) || (prefix_evex & 0x80)))
return FD_ERR_UD;
// Inst doesn't support masking, so EVEX.z or EVEX.aaa is UD
if (!DESC_EVEX_MASK(desc) && (prefix_evex & 0x87))
return FD_ERR_UD;
// EVEX.z without EVEX.aaa is UD. The Intel SDM is rather unprecise
// about this, but real hardware doesn't accept this.
if ((prefix_evex & 0x87) == 0x80)
return FD_ERR_UD;
// Cases for SAE/RC (reg operands only):
// - ER supported -> all ok
// - SAE supported -> assume L'L is RC, but ignored (undocumented)
// - Neither supported -> b == 0
if ((prefix_evex & 0x10) && (op_byte & 0xc0) == 0xc0) { // EVEX.b+reg
if (!DESC_EVEX_SAE(desc))
return FD_ERR_UD;
vexl = 2;
if (DESC_EVEX_ER(desc))
instr->evex = prefix_evex;
else
instr->evex = (prefix_evex & 0x87) | 0x60; // set RC, clear B
} else {
if (UNLIKELY(vexl == 3)) // EVEX.L'L == 11b is UD
return FD_ERR_UD;
instr->evex = prefix_evex & 0x87; // clear RC, clear B
}
if (DESC_VSIB(desc))
vex_operand &= 0xf; // EVEX.V' is used as index extension instead.
} else {
instr->evex = 0;
}
unsigned op_size;
unsigned op_size_alt = 0;
if (!(DESC_OPSIZE(desc) & 4)) {
if (mode == DECODE_64)
op_size = ((prefix_rex & PREFIX_REXW) || DESC_OPSIZE(desc) == 3) ? 4 :
UNLIKELY(prefixes[PF_66] && !DESC_IGN66(desc)) ? 2 :
DESC_OPSIZE(desc) ? 4 :
3;
else
op_size = UNLIKELY(prefixes[PF_66] && !DESC_IGN66(desc)) ? 2 : 3;
} else {
op_size = 5 + vexl;
op_size_alt = op_size - (DESC_OPSIZE(desc) & 3);
}
uint8_t operand_sizes[4] = {
DESC_SIZE_FIX1(desc), DESC_SIZE_FIX2(desc) + 1, op_size, op_size_alt
};
if (UNLIKELY(instr->type == FDI_MOV_CR || instr->type == FDI_MOV_DR)) {
unsigned modreg = (op_byte >> 3) & 0x7;
unsigned modrm = op_byte & 0x7;
FdOp* op_modreg = &instr->operands[DESC_MODREG_IDX(desc)];
op_modreg->type = FD_OT_REG;
op_modreg->size = op_size;
op_modreg->reg = modreg | (prefix_rex & PREFIX_REXR ? 8 : 0);
op_modreg->misc = instr->type == FDI_MOV_CR ? FD_RT_CR : FD_RT_DR;
if (instr->type == FDI_MOV_CR && (~0x011d >> op_modreg->reg) & 1)
return FD_ERR_UD;
else if (instr->type == FDI_MOV_DR && prefix_rex & PREFIX_REXR)
return FD_ERR_UD;
FdOp* op_modrm = &instr->operands[DESC_MODRM_IDX(desc)];
op_modrm->type = FD_OT_REG;
op_modrm->size = op_size;
op_modrm->reg = modrm | (prefix_rex & PREFIX_REXB ? 8 : 0);
op_modrm->misc = FD_RT_GPL;
goto skip_modrm;
}
if (DESC_HAS_MODREG(desc))
{
FdOp* op_modreg = &instr->operands[DESC_MODREG_IDX(desc)];
unsigned reg_idx = (op_byte & 0x38) >> 3;
unsigned reg_ty = DESC_REGTY_MODREG(desc);
op_modreg->misc = reg_ty;
if (LIKELY(reg_ty < 2))
reg_idx += prefix_rex & PREFIX_REXR ? 8 : 0;
else if (reg_ty == 7 && (prefix_rex & PREFIX_REXR || prefix_evex & 0x80))
return FD_ERR_UD; // REXR in 64-bit mode or EVEX.z with mask as dest
if (UNLIKELY(reg_ty == FD_RT_VEC)) // REXRR ignored above in 32-bit mode
reg_idx += prefix_rex & PREFIX_REXRR ? 16 : 0;
else if (UNLIKELY(prefix_rex & PREFIX_REXRR))
return FD_ERR_UD;
op_modreg->type = FD_OT_REG;
op_modreg->size = operand_sizes[DESC_MODREG_SIZE(desc)];
op_modreg->reg = reg_idx;
}
if (DESC_HAS_MODRM(desc))
{
FdOp* op_modrm = &instr->operands[DESC_MODRM_IDX(desc)];
op_modrm->size = operand_sizes[DESC_MODRM_SIZE(desc)];
unsigned rm = op_byte & 0x07;
if (op_byte >= 0xc0)
{
uint8_t reg_idx = rm;
unsigned reg_ty = DESC_REGTY_MODRM(desc);
op_modrm->misc = reg_ty;
if (LIKELY(reg_ty < 2))
reg_idx += prefix_rex & PREFIX_REXB ? 8 : 0;
if (prefix_evex && reg_ty == 0) // vector registers only
reg_idx += prefix_rex & PREFIX_REXX ? 16 : 0;
op_modrm->type = FD_OT_REG;
op_modrm->reg = reg_idx;
}
else
{
unsigned dispscale = 0;
if (UNLIKELY(prefix_evex)) {
// EVEX.z for memory destination operand is UD.
if (UNLIKELY(prefix_evex & 0x80) && DESC_MODRM_IDX(desc) == 0)
return FD_ERR_UD;
// EVEX.b for memory-operand without broadcast support is UD.
if (UNLIKELY(prefix_evex & 0x10)) {
if (UNLIKELY(!DESC_EVEX_BCST(desc)))
return FD_ERR_UD;
if (UNLIKELY(DESC_EVEX_BCST16(desc)))
dispscale = 1;
else
dispscale = prefix_rex & PREFIX_REXW ? 3 : 2;
instr->segment |= dispscale << 6; // Store broadcast size
op_modrm->type = FD_OT_MEMBCST;
} else {
dispscale = op_modrm->size - 1;
op_modrm->type = FD_OT_MEM;
}
} else {
op_modrm->type = FD_OT_MEM;
}
// 16-bit address size implies different ModRM encoding
if (UNLIKELY(addr_size == 1)) {
ASSUME(mode == DECODE_32);
if (UNLIKELY(DESC_VSIB(desc))) // 16-bit addr size + VSIB is UD
return FD_ERR_UD;
if (rm < 6)
op_modrm->misc = rm & 1 ? FD_REG_DI : FD_REG_SI;
else
op_modrm->misc = FD_REG_NONE;
if (rm < 4)
op_modrm->reg = rm & 2 ? FD_REG_BP : FD_REG_BX;
else if (rm < 6 || (op_byte & 0xc7) == 0x06)
op_modrm->reg = FD_REG_NONE;
else
op_modrm->reg = rm == 6 ? FD_REG_BP : FD_REG_BX;
const uint8_t* dispbase = &buffer[off];
if (op_byte & 0x40) {
if (UNLIKELY((off += 1) > len))
return FD_ERR_PARTIAL;
instr->disp = (int8_t) LOAD_LE_1(dispbase) * (1 << dispscale);
} else if (op_byte & 0x80 || (op_byte & 0xc7) == 0x06) {
if (UNLIKELY((off += 2) > len))
return FD_ERR_PARTIAL;
instr->disp = (int16_t) LOAD_LE_2(dispbase);
} else {
instr->disp = 0;
}
goto end_modrm;
}
// SIB byte
uint8_t base = rm;
if (rm == 4) {
if (UNLIKELY(off >= len))
return FD_ERR_PARTIAL;
uint8_t sib = buffer[off++];
unsigned scale = sib & 0xc0;
unsigned idx = (sib & 0x38) >> 3;
idx += prefix_rex & PREFIX_REXX ? 8 : 0;
base = sib & 0x07;
if (idx == 4)
idx = FD_REG_NONE;
op_modrm->misc = scale | idx;
} else {
op_modrm->misc = FD_REG_NONE;
}
if (UNLIKELY(DESC_VSIB(desc))) {
// VSIB must have a memory operand with SIB byte.
if (rm != 4)
return FD_ERR_UD;
_Static_assert(FD_REG_NONE == 0x3f, "unexpected FD_REG_NONE");
// idx 4 is valid for VSIB
if ((op_modrm->misc & 0x3f) == FD_REG_NONE)
op_modrm->misc &= 0xc4;
if (prefix_evex) // EVEX.V':EVEX.X:SIB.idx
op_modrm->misc |= prefix_evex & 0x8 ? 0 : 0x10;
}
// RIP-relative addressing only if SIB-byte is absent
if (op_byte < 0x40 && rm == 5 && mode == DECODE_64)
op_modrm->reg = FD_REG_IP;
else if (op_byte < 0x40 && base == 5)
op_modrm->reg = FD_REG_NONE;
else
op_modrm->reg = base + (prefix_rex & PREFIX_REXB ? 8 : 0);
const uint8_t* dispbase = &buffer[off];
if (op_byte & 0x40) {
if (UNLIKELY((off += 1) > len))
return FD_ERR_PARTIAL;
instr->disp = (int8_t) LOAD_LE_1(dispbase) * (1 << dispscale);
} else if (op_byte & 0x80 || (op_byte < 0x40 && base == 5)) {
if (UNLIKELY((off += 4) > len))
return FD_ERR_PARTIAL;
instr->disp = (int32_t) LOAD_LE_4(dispbase);
} else {
instr->disp = 0;
}
end_modrm:;
}
}
if (UNLIKELY(DESC_HAS_VEXREG(desc)))
{
FdOp* operand = &instr->operands[DESC_VEXREG_IDX(desc)];
if (DESC_ZEROREG_VAL(desc)) {
operand->type = FD_OT_REG;
operand->size = 1;
operand->reg = FD_REG_CL;
operand->misc = FD_RT_GPL;
} else {
operand->type = FD_OT_REG;
// Without VEX prefix, this encodes an implicit register
operand->size = operand_sizes[DESC_VEXREG_SIZE(desc)];
if (mode == DECODE_32)
vex_operand &= 0x7;
// Note: 32-bit will never UD here. EVEX.V' is caught above already.
// Note: UD if > 16 for non-VEC. No EVEX-encoded instruction uses
// EVEX.vvvv to refer to non-vector registers. Verified in parseinstrs.
operand->reg = vex_operand;
unsigned reg_ty = DESC_REGTY_VEXREG(desc); // VEC GPL MSK FPU/TMM
if (prefix_rex & PREFIX_VEX) { // TMM with VEX, FPU otherwise
// In 64-bit mode: UD if FD_RT_MASK and vex_operand&8 != 0
if (reg_ty == 2 && vex_operand >= 8)
return FD_ERR_UD;
if (UNLIKELY(reg_ty == 3)) // TMM
operand->reg &= 0x7; // TODO: verify
operand->misc = (06710 >> (3 * reg_ty)) & 0x7;
} else {
operand->misc = (04710 >> (3 * reg_ty)) & 0x7;
}
}
}
else if (vex_operand != 0)
{
// TODO: bit 3 ignored in 32-bit mode? unverified
return FD_ERR_UD;
}
uint32_t imm_control = UNLIKELY(DESC_IMM_CONTROL(desc));
if (LIKELY(!imm_control)) {
} else if (UNLIKELY(imm_control == 1))
{
// 1 = immediate constant 1, used for shifts
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
operand->type = FD_OT_IMM;
operand->size = 1;
instr->imm = 1;
}
else if (UNLIKELY(imm_control == 2))
{
// 2 = memory, address-sized, used for mov with moffs operand
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
operand->type = FD_OT_MEM;
operand->size = operand_sizes[DESC_IMM_SIZE(desc)];
operand->reg = FD_REG_NONE;
operand->misc = FD_REG_NONE;
int moffsz = 1 << addr_size;
if (UNLIKELY(off + moffsz > len))
return FD_ERR_PARTIAL;
if (moffsz == 2)
instr->disp = LOAD_LE_2(&buffer[off]);
if (moffsz == 4)
instr->disp = LOAD_LE_4(&buffer[off]);
if (LIKELY(moffsz == 8))
instr->disp = LOAD_LE_8(&buffer[off]);
off += moffsz;
}
else if (UNLIKELY(imm_control == 3))
{
// 3 = register in imm8[7:4], used for RVMR encoding with VBLENDVP[SD]
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
operand->type = FD_OT_REG;
operand->size = op_size;
operand->misc = FD_RT_VEC;
if (UNLIKELY(off + 1 > len))
return FD_ERR_PARTIAL;
uint8_t reg = (uint8_t) LOAD_LE_1(&buffer[off]);
off += 1;
if (mode == DECODE_32)
reg &= 0x7f;
operand->reg = reg >> 4;
instr->imm = reg & 0x0f;
}
else if (imm_control != 0)
{
// 4/5 = immediate, operand-sized/8 bit
// 6/7 = offset, operand-sized/8 bit (used for jumps/calls)
int imm_byte = imm_control & 1;
int imm_offset = imm_control & 2;
FdOp* operand = &instr->operands[DESC_IMM_IDX(desc)];
operand->type = FD_OT_IMM;
if (imm_byte) {
if (UNLIKELY(off + 1 > len))
return FD_ERR_PARTIAL;
instr->imm = (int8_t) LOAD_LE_1(&buffer[off++]);
operand->size = DESC_IMM_SIZE(desc) & 1 ? 1 : op_size;
} else {
operand->size = operand_sizes[DESC_IMM_SIZE(desc)];
uint8_t imm_size;
if (UNLIKELY(instr->type == FDI_RET || instr->type == FDI_RETF ||
instr->type == FDI_SSE_EXTRQ ||
instr->type == FDI_SSE_INSERTQ))
imm_size = 2;
else if (UNLIKELY(instr->type == FDI_JMPF || instr->type == FDI_CALLF))
imm_size = (1 << op_size >> 1) + 2;
else if (UNLIKELY(instr->type == FDI_ENTER))
imm_size = 3;
else if (instr->type == FDI_MOVABS)
imm_size = (1 << op_size >> 1);
else
imm_size = op_size == 2 ? 2 : 4;
if (UNLIKELY(off + imm_size > len))
return FD_ERR_PARTIAL;
if (imm_size == 2)
instr->imm = (int16_t) LOAD_LE_2(&buffer[off]);
else if (imm_size == 3)
instr->imm = LOAD_LE_3(&buffer[off]);
else if (imm_size == 4)
instr->imm = (int32_t) LOAD_LE_4(&buffer[off]);
else if (imm_size == 6)
instr->imm = LOAD_LE_4(&buffer[off]) | LOAD_LE_2(&buffer[off+4]) << 32;
else if (imm_size == 8)
instr->imm = (int64_t) LOAD_LE_8(&buffer[off]);
off += imm_size;
}
if (imm_offset)
{
if (instr->address != 0)
instr->imm += instr->address + off;
else
operand->type = FD_OT_OFF;
}
}
skip_modrm:
if (UNLIKELY(prefixes[PF_LOCK])) {
if (!DESC_LOCK(desc) || instr->operands[0].type != FD_OT_MEM)
return FD_ERR_UD;
instr->flags |= FD_FLAG_LOCK;
}
if (UNLIKELY(DESC_LEGACY(desc))) {
// Without REX prefix, convert one-byte GP regs to high-byte regs
// This actually only applies to SZ8/MOVSX/MOVZX; but no VEX-encoded
// instructions have a byte-sized GP register in the first two operands.
if (!(prefix_rex & PREFIX_REX)) {
for (int i = 0; i < 2; i++) {
FdOp* operand = &instr->operands[i];
if (operand->type == FD_OT_NONE)
break;
if (operand->type == FD_OT_REG && operand->misc == FD_RT_GPL &&
operand->size == 1 && operand->reg >= 4)
operand->misc = FD_RT_GPH;
}
}
if (instr->type == FDI_XCHG_NOP) {
// Only 4890, 90, and 6690 are true NOPs.
if (instr->operands[0].reg == 0) {
instr->operands[0].type = FD_OT_NONE;
instr->operands[1].type = FD_OT_NONE;
instr->type = FD_HAS_REP(instr) ? FDI_PAUSE : FDI_NOP;
} else if ((instr->operands[0].reg & 7) == 0 && FD_HAS_REP(instr)) {
// On Intel, REX.B is ignored for F3.90.
instr->operands[0].type = FD_OT_NONE;
instr->operands[1].type = FD_OT_NONE;
instr->type = FDI_PAUSE;
} else {
instr->type = FDI_XCHG;
}
}
if (UNLIKELY(instr->type == FDI_3DNOW)) {
unsigned opc3dn = instr->imm;
if (opc3dn & 0x40)
return FD_ERR_UD;
uint64_t msk = opc3dn & 0x80 ? 0x88d144d144d14400 : 0x30003000;
if (!(msk >> (opc3dn & 0x3f) & 1))
return FD_ERR_UD;
}
instr->operandsz = UNLIKELY(DESC_INSTR_WIDTH(desc)) ? op_size - 1 : 0;
} else {
instr->operandsz = 0;
}
instr->size = off;
return off;
}

62
third_party/fadec/encode-test.c vendored Normal file
View File

@@ -0,0 +1,62 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <fadec-enc.h>
static
void
print_hex(const uint8_t* buf, size_t len)
{
for (size_t i = 0; i < len; i++)
printf("%02x", buf[i]);
}
static
int
test(uint8_t* buf, const char* name, uint64_t mnem, uint64_t op0, uint64_t op1, uint64_t op2, uint64_t op3, const void* exp, size_t exp_len)
{
memset(buf, 0, 16);
uint8_t* inst = buf;
int res = fe_enc64(&inst, mnem, op0, op1, op2, op3);
if ((res != 0) != (exp_len == 0)) goto fail;
if (inst - buf != (ptrdiff_t) exp_len) goto fail;
if (memcmp(buf, exp, exp_len)) goto fail;
return 0;
fail:
printf("Failed case %s:\n", name);
printf(" Exp (%2zu): ", exp_len);
print_hex(exp, exp_len);
printf("\n Got (%2zd): ", inst - buf);
print_hex(buf, inst - buf);
printf("\n");
return -1;
}
#define TEST2(str, exp, exp_len, mnem, flags, op0, op1, op2, op3, ...) test(buf, str, FE_ ## mnem|flags, op0, op1, op2, op3, exp, exp_len)
#define TEST1(str, exp, ...) TEST2(str, exp, sizeof(exp)-1, __VA_ARGS__, 0, 0, 0, 0, 0)
#define TEST(exp, ...) failed |= TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
int
main(int argc, char** argv)
{
(void) argc; (void) argv;
int failed = 0;
uint8_t buf[16];
// VSIB encoding doesn't differ for this API
#define FE_MEMV FE_MEM
#define FE_PTR(off) ((intptr_t) buf + (off))
#define FLAGMASK(flags, mask) (flags | FE_MASK(mask & 7))
#include "encode-test.inc"
puts(failed ? "Some tests FAILED" : "All tests PASSED");
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}

2192
third_party/fadec/encode-test.inc vendored Normal file

File diff suppressed because it is too large Load Diff

460
third_party/fadec/encode.c vendored Normal file
View File

@@ -0,0 +1,460 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <fadec-enc.h>
#ifdef __GNUC__
#define LIKELY(x) __builtin_expect((x), 1)
#define UNLIKELY(x) __builtin_expect((x), 0)
#else
#define LIKELY(x) (x)
#define UNLIKELY(x) (x)
#endif
#define OPC_66 0x80000
#define OPC_F2 0x100000
#define OPC_F3 0x200000
#define OPC_REXW 0x400000
#define OPC_LOCK 0x800000
#define OPC_VEXL0 0x1000000
#define OPC_VEXL1 0x1800000
#define OPC_EVEXL0 0x2000000
#define OPC_EVEXL1 0x2800000
#define OPC_EVEXL2 0x3000000
#define OPC_EVEXL3 0x3800000
#define OPC_EVEXB 0x4000000
#define OPC_VSIB 0x8000000
#define OPC_67 FE_ADDR32
#define OPC_SEG_MSK 0xe0000000
#define OPC_JMPL FE_JMPL
#define OPC_MASK_MSK 0xe00000000
#define OPC_EVEXZ 0x1000000000
#define OPC_USER_MSK (OPC_67|OPC_SEG_MSK|OPC_MASK_MSK)
#define OPC_FORCE_SIB 0x2000000000
#define OPC_DOWNGRADE_VEX 0x4000000000
#define OPC_DOWNGRADE_VEX_FLIPW 0x40000000000
#define OPC_EVEX_DISP8SCALE 0x38000000000
#define OPC_GPH_OP0 0x200000000000
#define OPC_GPH_OP1 0x400000000000
#define EPFX_REX_MSK 0x43f
#define EPFX_REX 0x20
#define EPFX_EVEX 0x40
#define EPFX_REXR 0x10
#define EPFX_REXX 0x08
#define EPFX_REXB 0x04
#define EPFX_REXR4 0x02
#define EPFX_REXB4 0x01
#define EPFX_REXX4 0x400
#define EPFX_VVVV_IDX 11
static bool op_mem(FeOp op) { return op < 0; }
static bool op_reg(FeOp op) { return op >= 0; }
static bool op_reg_gpl(FeOp op) { return (op & ~0x1f) == 0x100; }
static bool op_reg_gph(FeOp op) { return (op & ~0x3) == 0x204; }
static bool op_reg_xmm(FeOp op) { return (op & ~0x1f) == 0x600; }
static int64_t op_mem_offset(FeOp op) { return (int32_t) op; }
static unsigned op_mem_base(FeOp op) { return (op >> 32) & 0xfff; }
static unsigned op_mem_idx(FeOp op) { return (op >> 44) & 0xfff; }
static unsigned op_mem_scale(FeOp op) { return (op >> 56) & 0xf; }
static unsigned op_reg_idx(FeOp op) { return op & 0xff; }
static bool op_imm_n(FeOp imm, unsigned immsz) {
if (immsz == 0 && !imm) return true;
if (immsz == 1 && (int8_t) imm == imm) return true;
if (immsz == 2 && (int16_t) imm == imm) return true;
if (immsz == 3 && (imm&0xffffff) == imm) return true;
if (immsz == 4 && (int32_t) imm == imm) return true;
if (immsz == 8 && (int64_t) imm == imm) return true;
return false;
}
static
unsigned
opc_size(uint64_t opc, uint64_t epfx)
{
unsigned res = 1;
if (UNLIKELY(opc & OPC_EVEXL0)) {
res += 4;
} else if (UNLIKELY(opc & OPC_VEXL0)) {
if (opc & (OPC_REXW|0x20000) || epfx & (EPFX_REXX|EPFX_REXB))
res += 3;
else
res += 2;
} else {
if (opc & OPC_LOCK) res++;
if (opc & OPC_66) res++;
if (opc & (OPC_F2|OPC_F3)) res++;
if (opc & OPC_REXW || epfx & EPFX_REX_MSK) res++;
if (opc & 0x30000) res++;
if (opc & 0x20000) res++;
}
if (opc & OPC_SEG_MSK) res++;
if (opc & OPC_67) res++;
if (opc & 0x8000) res++;
return res;
}
static
int
enc_opc(uint8_t** restrict buf, uint64_t opc, uint64_t epfx)
{
if (opc & OPC_SEG_MSK)
*(*buf)++ = (0x65643e362e2600 >> (8 * ((opc >> 29) & 7))) & 0xff;
if (opc & OPC_67) *(*buf)++ = 0x67;
if (opc & OPC_EVEXL0) {
*(*buf)++ = 0x62;
unsigned b1 = opc >> 16 & 7;
if (!(epfx & EPFX_REXR)) b1 |= 0x80;
if (!(epfx & EPFX_REXX)) b1 |= 0x40;
if (!(epfx & EPFX_REXB)) b1 |= 0x20;
if (!(epfx & EPFX_REXR4)) b1 |= 0x10;
if ((epfx & EPFX_REXB4)) b1 |= 0x08;
*(*buf)++ = b1;
unsigned b2 = opc >> 20 & 3;
if (!(epfx & EPFX_REXX4)) b2 |= 0x04;
b2 |= (~(epfx >> EPFX_VVVV_IDX) & 0xf) << 3;
if (opc & OPC_REXW) b2 |= 0x80;
*(*buf)++ = b2;
unsigned b3 = opc >> 33 & 7;
b3 |= (~(epfx >> EPFX_VVVV_IDX) & 0x10) >> 1;
if (opc & OPC_EVEXB) b3 |= 0x10;
b3 |= (opc >> 23 & 3) << 5;
if (opc & OPC_EVEXZ) b3 |= 0x80;
*(*buf)++ = b3;
} else if (opc & OPC_VEXL0) {
if (epfx & (EPFX_REXR4|EPFX_REXX4|EPFX_REXB4|(0x10<<EPFX_VVVV_IDX))) return -1;
bool vex3 = opc & (OPC_REXW|0x20000) || epfx & (EPFX_REXX|EPFX_REXB);
unsigned pp = opc >> 20 & 3;
*(*buf)++ = 0xc4 | !vex3;
unsigned b2 = pp | (opc & 0x800000 ? 0x4 : 0);
if (vex3) {
unsigned b1 = opc >> 16 & 7;
if (!(epfx & EPFX_REXR)) b1 |= 0x80;
if (!(epfx & EPFX_REXX)) b1 |= 0x40;
if (!(epfx & EPFX_REXB)) b1 |= 0x20;
*(*buf)++ = b1;
if (opc & OPC_REXW) b2 |= 0x80;
} else {
if (!(epfx & EPFX_REXR)) b2 |= 0x80;
}
b2 |= (~(epfx >> EPFX_VVVV_IDX) & 0xf) << 3;
*(*buf)++ = b2;
} else {
if (opc & OPC_LOCK) *(*buf)++ = 0xF0;
if (opc & OPC_66) *(*buf)++ = 0x66;
if (opc & OPC_F2) *(*buf)++ = 0xF2;
if (opc & OPC_F3) *(*buf)++ = 0xF3;
if (opc & OPC_REXW || epfx & (EPFX_REX_MSK)) {
unsigned rex = 0x40;
if (opc & OPC_REXW) rex |= 8;
if (epfx & EPFX_REXR) rex |= 4;
if (epfx & EPFX_REXX) rex |= 2;
if (epfx & EPFX_REXB) rex |= 1;
*(*buf)++ = rex;
}
if (opc & 0x30000) *(*buf)++ = 0x0F;
if ((opc & 0x30000) == 0x20000) *(*buf)++ = 0x38;
if ((opc & 0x30000) == 0x30000) *(*buf)++ = 0x3A;
}
*(*buf)++ = opc & 0xff;
if (opc & 0x8000) *(*buf)++ = (opc >> 8) & 0xff;
return 0;
}
static
int
enc_imm(uint8_t** restrict buf, uint64_t imm, unsigned immsz)
{
if (!op_imm_n(imm, immsz)) return -1;
for (unsigned i = 0; i < immsz; i++)
*(*buf)++ = imm >> 8 * i;
return 0;
}
static
int
enc_o(uint8_t** restrict buf, uint64_t opc, uint64_t epfx, uint64_t op0)
{
if (op_reg_idx(op0) & 0x8) epfx |= EPFX_REXB;
// NB: this cannot happen. There is only one O-encoded instruction which
// accepts high-byte registers (b0+/MOVABS Rb,Ib), which will never have a
// REx prefix if the operand is a high-byte register.
// bool has_rex = opc & OPC_REXW || epfx & EPFX_REX_MSK;
// if (has_rex && op_reg_gph(op0)) return -1;
if (enc_opc(buf, opc, epfx)) return -1;
*(*buf - 1) = (*(*buf - 1) & 0xf8) | (op_reg_idx(op0) & 0x7);
return 0;
}
static
int
enc_mr(uint8_t** restrict buf, uint64_t opc, uint64_t epfx, uint64_t op0,
uint64_t op1, unsigned immsz)
{
// If !op_reg(op1), it is a constant value for ModRM.reg
if (op_reg(op0) && (op_reg_idx(op0) & 0x8)) epfx |= EPFX_REXB;
if (op_reg(op0) && (op_reg_idx(op0) & 0x10))
epfx |= 0 ? EPFX_REXB4 : EPFX_REXX|EPFX_EVEX;
if (op_mem(op0) && (op_mem_base(op0) & 0x8)) epfx |= EPFX_REXB;
if (op_mem(op0) && (op_mem_base(op0) & 0x10)) epfx |= EPFX_REXB4;
if (op_mem(op0) && (op_mem_idx(op0) & 0x8)) epfx |= EPFX_REXX;
if (op_mem(op0) && (op_mem_idx(op0) & 0x10))
epfx |= opc & OPC_VSIB ? 0x10<<EPFX_VVVV_IDX : EPFX_REXX4;
if (op_reg(op1) && (op_reg_idx(op1) & 0x8)) epfx |= EPFX_REXR;
if (op_reg(op1) && (op_reg_idx(op1) & 0x10)) epfx |= EPFX_REXR4;
bool has_rex = opc & (OPC_REXW|OPC_VEXL0|OPC_EVEXL0) || (epfx & EPFX_REX_MSK);
if (has_rex && (op_reg_gph(op0) || op_reg_gph(op1))) return -1;
if (epfx & (EPFX_EVEX|EPFX_REXB4|EPFX_REXX4|EPFX_REXR4|(0x10<<EPFX_VVVV_IDX))) {
if (!(opc & OPC_EVEXL0)) return -1;
} else if (opc & OPC_DOWNGRADE_VEX) { // downgrade EVEX to VEX
// clear EVEX and disp8scale, set VEX
opc = (opc & ~(uint64_t) (OPC_EVEXL0|OPC_EVEX_DISP8SCALE)) | OPC_VEXL0;
if (opc & OPC_DOWNGRADE_VEX_FLIPW)
opc ^= OPC_REXW;
}
if (LIKELY(op_reg(op0))) {
if (enc_opc(buf, opc, epfx)) return -1;
*(*buf)++ = 0xc0 | ((op_reg_idx(op1) & 7) << 3) | (op_reg_idx(op0) & 7);
return 0;
}
unsigned opcsz = opc_size(opc, epfx);
int mod = 0, reg = op1 & 7, rm;
int scale = 0, idx = 4, base = 0;
int32_t off = op_mem_offset(op0);
bool withsib = opc & OPC_FORCE_SIB;
if (!!op_mem_idx(op0) != !!op_mem_scale(op0)) return -1;
if (!op_mem_idx(op0) && (opc & OPC_VSIB)) return -1;
if (op_mem_idx(op0))
{
if (opc & OPC_VSIB)
{
if (!op_reg_xmm(op_mem_idx(op0))) return -1;
// EVEX VSIB requires non-zero opmask
if ((opc & OPC_EVEXL0) && !(opc & OPC_MASK_MSK)) return -1;
}
else
{
if (!op_reg_gpl(op_mem_idx(op0))) return -1;
if (op_reg_idx(op_mem_idx(op0)) == 4) return -1;
}
idx = op_mem_idx(op0) & 7;
int scalabs = op_mem_scale(op0);
if (scalabs & (scalabs - 1)) return -1;
scale = (scalabs & 0xA ? 1 : 0) | (scalabs & 0xC ? 2 : 0);
withsib = true;
}
unsigned dispsz = 0;
if (!op_mem_base(op0))
{
base = 5;
rm = 4;
dispsz = 4;
}
else if (op_mem_base(op0) == FE_IP)
{
rm = 5;
dispsz = 4;
// Adjust offset, caller doesn't know instruction length.
off -= opcsz + 5 + immsz;
if (withsib) return -1;
}
else
{
if (!op_reg_gpl(op_mem_base(op0))) return -1;
rm = op_reg_idx(op_mem_base(op0)) & 7;
if (withsib || rm == 4) {
base = rm;
rm = 4;
}
if (off) {
unsigned disp8scale = (opc & OPC_EVEX_DISP8SCALE) >> 39;
if (!(off & ((1 << disp8scale) - 1)) && op_imm_n(off >> disp8scale, 1)) {
mod = 0x40;
dispsz = 1;
off >>= disp8scale;
} else {
mod = 0x80;
dispsz = 4;
}
} else if (rm == 5) {
mod = 0x40;
dispsz = 1;
}
}
if (opcsz + 1 + (rm == 4) + dispsz + immsz > 15) return -1;
if (enc_opc(buf, opc, epfx)) return -1;
*(*buf)++ = mod | (reg << 3) | rm;
if (UNLIKELY(rm == 4))
*(*buf)++ = (scale << 6) | (idx << 3) | base;
return enc_imm(buf, off, dispsz);
}
typedef enum {
ENC_NP, ENC_M, ENC_R, ENC_M1, ENC_MC, ENC_MR, ENC_RM, ENC_RMA, ENC_MRC,
ENC_AM, ENC_MA, ENC_I, ENC_O, ENC_OA, ENC_S, ENC_A, ENC_D, ENC_FD, ENC_TD,
ENC_IM,
ENC_RVM, ENC_RVMR, ENC_RMV, ENC_VM, ENC_MVR, ENC_MRV,
ENC_MAX
} Encoding;
struct EncodingInfo {
uint8_t modrm : 2;
uint8_t modreg : 2;
uint8_t vexreg : 2;
uint8_t immidx : 2;
// 0 = normal or jump, 1 = constant 1, 2 = address-size, 3 = RVMR
uint8_t immctl : 3;
uint8_t zregidx : 2;
uint8_t zregval : 1;
};
const struct EncodingInfo encoding_infos[ENC_MAX] = {
[ENC_NP] = { 0 },
[ENC_M] = { .modrm = 0x0^3, .immidx = 1 },
[ENC_R] = { .modreg = 0x0^3 },
[ENC_M1] = { .modrm = 0x0^3, .immctl = 1, .immidx = 1 },
[ENC_MC] = { .modrm = 0x0^3, .zregidx = 0x1^3, .zregval = 1 },
[ENC_MR] = { .modrm = 0x0^3, .modreg = 0x1^3, .immidx = 2 },
[ENC_RM] = { .modrm = 0x1^3, .modreg = 0x0^3, .immidx = 2 },
[ENC_RMA] = { .modrm = 0x1^3, .modreg = 0x0^3, .zregidx = 0x2^3, .zregval = 0 },
[ENC_MRC] = { .modrm = 0x0^3, .modreg = 0x1^3, .zregidx = 0x2^3, .zregval = 1 },
[ENC_AM] = { .modrm = 0x1^3, .zregidx = 0x0^3, .zregval = 0 },
[ENC_MA] = { .modrm = 0x0^3, .zregidx = 0x1^3, .zregval = 0 },
[ENC_I] = { .immidx = 0 },
[ENC_O] = { .modreg = 0x0^3, .immidx = 1 },
[ENC_OA] = { .modreg = 0x0^3, .zregidx = 0x1^3, .zregval = 0 },
[ENC_S] = { 0 },
[ENC_A] = { .zregidx = 0x0^3, .zregval = 0, .immidx = 1 },
[ENC_D] = { .immidx = 0 },
[ENC_FD] = { .zregidx = 0x0^3, .zregval = 0, .immctl = 2, .immidx = 1 },
[ENC_TD] = { .zregidx = 0x1^3, .zregval = 0, .immctl = 2, .immidx = 0 },
[ENC_IM] = { .modrm = 0x1^3, .immidx = 0 },
[ENC_RVM] = { .modrm = 0x2^3, .modreg = 0x0^3, .vexreg = 0x1^3, .immidx = 3 },
[ENC_RVMR] = { .modrm = 0x2^3, .modreg = 0x0^3, .vexreg = 0x1^3, .immctl = 3, .immidx = 3 },
[ENC_RMV] = { .modrm = 0x1^3, .modreg = 0x0^3, .vexreg = 0x2^3 },
[ENC_VM] = { .modrm = 0x1^3, .vexreg = 0x0^3, .immidx = 2 },
[ENC_MVR] = { .modrm = 0x0^3, .modreg = 0x2^3, .vexreg = 0x1^3 },
[ENC_MRV] = { .modrm = 0x0^3, .modreg = 0x1^3, .vexreg = 0x2^3 },
};
static const uint64_t alt_tab[] = {
#include <fadec-encode-private.inc>
};
int
fe_enc64_impl(uint8_t** restrict buf, uint64_t opc, FeOp op0, FeOp op1,
FeOp op2, FeOp op3)
{
uint8_t* buf_start = *buf;
uint64_t ops[4] = {op0, op1, op2, op3};
uint64_t epfx = 0;
// Doesn't change between variants
if ((opc & OPC_GPH_OP0) && op_reg_gpl(op0) && op0 >= FE_SP)
epfx |= EPFX_REX;
else if (!(opc & OPC_GPH_OP0) && op_reg_gph(op0))
goto fail;
if ((opc & OPC_GPH_OP1) && op_reg_gpl(op1) && op1 >= FE_SP)
epfx |= EPFX_REX;
else if (!(opc & OPC_GPH_OP1) && op_reg_gph(op1))
goto fail;
try_encode:;
unsigned enc = (opc >> 51) & 0x1f;
const struct EncodingInfo* ei = &encoding_infos[enc];
int64_t imm = 0xcc;
unsigned immsz = (opc >> 47) & 0xf;
if (UNLIKELY(ei->zregidx && op_reg_idx(ops[ei->zregidx^3]) != ei->zregval))
goto next;
if (UNLIKELY(enc == ENC_S)) {
if ((op_reg_idx(op0) << 3 & 0x20) != (opc & 0x20)) goto next;
opc |= op_reg_idx(op0) << 3;
}
if (immsz) {
imm = ops[ei->immidx];
if (UNLIKELY(ei->immctl)) {
if (ei->immctl == 2) {
immsz = UNLIKELY(opc & OPC_67) ? 4 : 8;
if (immsz == 4) imm = (int32_t) imm; // address are zero-extended
} else if (ei->immctl == 3) {
if (!op_reg_xmm(imm)) goto fail;
imm = op_reg_idx(imm) << 4;
if (!op_imm_n(imm, 1)) goto fail;
} else if (ei->immctl == 1) {
if (imm != 1) goto next;
immsz = 0;
}
} else if (enc == ENC_D) {
imm -= (int64_t) *buf + opc_size(opc, epfx) + immsz;
bool has_alt = opc >> 56 != 0;
bool skip_to_alt = has_alt && UNLIKELY(opc & FE_JMPL);
if (skip_to_alt || !op_imm_n(imm, immsz)) {
if (!has_alt) goto fail;
// JMP/Jcc special case
immsz = 4;
if (opc & 0x80) { // JMP
opc -= 2; // Convert opcode 0xeb to 0xe9
imm -= 3; // 3 extra immediate bytes
} else { // Jcc
opc += 0x10010; // Add 0f escape + 0x10 to opcode
imm -= 4; // 0f escape + 3 extra immediate bytes
}
if (!op_imm_n(imm, immsz)) goto fail;
}
} else {
if (!op_imm_n(imm, immsz)) goto next;
}
}
// NOP has no operands, so this must be the 32-bit OA XCHG
if ((opc & 0xfffffff) == 0x90 && ops[0] == FE_AX) goto next;
if (UNLIKELY(enc == ENC_R)) {
if (enc_mr(buf, opc, epfx, 0, ops[0], immsz)) goto fail;
} else if (ei->modrm) {
FeOp modreg = ei->modreg ? ops[ei->modreg^3] : (opc & 0xff00) >> 8;
if (ei->vexreg)
epfx |= ((uint64_t) op_reg_idx(ops[ei->vexreg^3])) << EPFX_VVVV_IDX;
// Can fail for upgrade to EVEX due to high register numbers
if (enc_mr(buf, opc, epfx, ops[ei->modrm^3], modreg, immsz)) goto next;
} else if (ei->modreg) {
if (enc_o(buf, opc, epfx, ops[ei->modreg^3])) goto fail;
} else {
if (enc_opc(buf, opc, epfx)) goto fail;
}
if (immsz)
if (enc_imm(buf, imm, immsz)) goto fail;
return 0;
next:;
uint64_t alt = opc >> 56;
if (alt) { // try alternative encoding, if available
opc = alt_tab[alt] | (opc & OPC_USER_MSK);
goto try_encode;
}
fail:
// Don't advance buffer on error; though we shouldn't write anything.
*buf = buf_start;
return -1;
}

64
third_party/fadec/encode2-test.c vendored Normal file
View File

@@ -0,0 +1,64 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <fadec-enc2.h>
static
void print_hex(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++)
printf("%02x", buf[i]);
}
static int
check(const uint8_t* buf, const void* exp, size_t exp_len, unsigned res, const char* name) {
if (res == exp_len && !memcmp(buf, exp, exp_len))
return 0;
printf("Failed case (new) %s:\n", name);
printf(" Exp (%2zu): ", exp_len);
print_hex((const uint8_t*)exp, exp_len);
printf("\n Got (%2u): ", res);
print_hex(buf, res);
printf("\n");
return -1;
}
#define TEST1(str, exp, name, ...) do { \
memset(buf, 0, sizeof buf); \
unsigned res = fe64_ ## name(buf, __VA_ARGS__); \
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
} while (0)
#define TEST(exp, ...) TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
int
main(void) {
int failed = 0;
uint8_t buf[16];
// This API is type safe and prohibits compilation of reg-type mismatches
#define ENC_TEST_TYPESAFE
// Silence -Warray-bounds with double cast
#define FE_PTR(off) (const void*) ((uintptr_t) buf + (off))
#define FLAGMASK(flags, mask) flags, mask
#include "encode-test.inc"
TEST("\x90", NOP, 0);
TEST("\x90", NOP, 1);
TEST("\x66\x90", NOP, 2);
TEST("\x0f\x1f\x00", NOP, 3);
TEST("\x0f\x1f\x40\x00", NOP, 4);
TEST("\x0f\x1f\x44\x00\x00", NOP, 5);
TEST("\x66\x0f\x1f\x44\x00\x00", NOP, 6);
TEST("\x0f\x1f\x80\x00\x00\x00\x00", NOP, 7);
TEST("\x0f\x1f\x84\x00\x00\x00\x00\x00", NOP, 8);
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00", NOP, 9);
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x90", NOP, 10);
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x66\x90", NOP, 11);
TEST("\x66\x0f\x1f\x84\x00\x00\x00\x00\x00\x0f\x1f\x00", NOP, 12);
puts(failed ? "Some tests FAILED" : "All tests PASSED");
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}

64
third_party/fadec/encode2-test.cc vendored Normal file
View File

@@ -0,0 +1,64 @@
#include <array>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <fadec-enc2.h>
using Buffer = std::array<uint8_t, 16>;
static
void print_hex(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++)
std::printf("%02x", buf[i]);
}
static int
check(const Buffer& buf, const char* exp, size_t exp_len, unsigned res, const char* name) {
if (res == exp_len && !std::memcmp(buf.data(), exp, exp_len))
return 0;
std::printf("Failed case (new) %s:\n", name);
std::printf(" Exp (%2zu): ", exp_len);
print_hex(reinterpret_cast<const uint8_t*>(exp), exp_len);
std::printf("\n Got (%2u): ", res);
print_hex(buf.data(), res);
std::printf("\n");
return -1;
}
#define TEST1(str, exp, name, ...) do { \
buf.fill(0); \
unsigned res = fe64_ ## name(buf.data(), __VA_ARGS__); \
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
} while (0)
#define TEST(exp, ...) TEST1(#__VA_ARGS__, exp, __VA_ARGS__)
#define TEST_CPP1(str, exp, expr) do { \
buf.fill(0); \
unsigned res = (expr); \
failed |= check(buf, exp, sizeof(exp) - 1, res, str); \
} while (0)
#define TEST_CPP(exp, ...) TEST_CPP1(#__VA_ARGS__, exp, __VA_ARGS__)
int main() {
int failed = 0;
Buffer buf{};
// This API is type safe and prohibits compilation of reg-type mismatches
#define ENC_TEST_TYPESAFE
// Silence -Warray-bounds with double cast
#define FE_PTR(off) (const void*) ((uintptr_t) buf.data() + (off))
#define FLAGMASK(flags, mask) flags, mask
#include "encode-test.inc"
// Test implicit conversion of parameters also on the actual functions
TEST_CPP("\x0f\x90\xc0", fe64_SETO8r(buf.data(), 0, FE_AX));
TEST_CPP("\x0f\x90\xc0", (fe64_SETO8r)(buf.data(), 0, FE_AX));
TEST_CPP("\x0f\x90\xc4", fe64_SETO8r(buf.data(), 0, FE_AH));
TEST_CPP("\x0f\x90\xc4", (fe64_SETO8r)(buf.data(), 0, FE_AH));
std::puts(failed ? "Some tests FAILED" : "All tests PASSED");
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}

345
third_party/fadec/encode2.c vendored Normal file
View File

@@ -0,0 +1,345 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <fadec-enc2.h>
#ifdef __GNUC__
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#if __has_attribute(cold) && __has_attribute(preserve_most)
#define HINT_COLD __attribute__((cold,preserve_most,noinline))
#elif __has_attribute(cold)
#define HINT_COLD __attribute__((cold,noinline))
#else
#define HINT_COLD
#endif
#else
#define LIKELY(x) (x)
#define UNLIKELY(x) (x)
#define HINT_COLD
#endif
#define op_reg_idx(op) (op).idx
#define op_reg_gph(op) (((op).idx & ~0x3) == 0x24)
#define op_mem_base(mem) op_reg_idx((mem).base)
#define op_mem_idx(mem) op_reg_idx((mem).idx)
static bool
op_imm_n(int64_t imm, unsigned immsz) {
if (immsz == 0 && !imm) return true;
if (immsz == 1 && (int8_t) imm == imm) return true;
if (immsz == 2 && (int16_t) imm == imm) return true;
if (immsz == 3 && (imm&0xffffff) == imm) return true;
if (immsz == 4 && (int32_t) imm == imm) return true;
if (immsz == 8 && (int64_t) imm == imm) return true;
return false;
}
HINT_COLD static unsigned
enc_seg67(uint8_t* buf, unsigned flags) {
unsigned idx = 0;
if (UNLIKELY(flags & FE_SEG_MASK)) {
unsigned seg = (0x65643e362e2600 >> (8 * (flags & FE_SEG_MASK))) & 0xff;
buf[idx++] = seg;
}
if (UNLIKELY(flags & FE_ADDR32)) buf[idx++] = 0x67;
return idx;
}
static unsigned
enc_rex_mem(FeMem op0, uint64_t op1) {
// Essentially just an and+or due to struct layout.
uint32_t val = op1 | op0.flags | (op_mem_base(op0) << 8) |
((uint32_t)op_mem_idx(op0) << 24);
// Combine REX.RXB using multiplication for branch-less code.
uint32_t masked = val & 0x08000808;
return masked ? (uint8_t) (masked * (1|(1<<15)|(1<<25)) >> 26) + 0x40 : 0;
}
static void
enc_imm(uint8_t* buf, uint64_t imm, unsigned immsz) {
#ifdef __GNUC__
// Clang doesn't fold the loop into a single store.
// See: https://github.com/llvm/llvm-project/issues/154696
if (__builtin_constant_p(immsz)) {
__builtin_memcpy(buf, &imm, immsz);
return;
}
#endif
for (unsigned i = 0; i < immsz; i++)
*buf++ = imm >> 8 * i;
}
static int
enc_mem_common(uint8_t* buf, unsigned ripoff, FeMem op0, uint64_t op1,
unsigned disp8scale) {
int mod = 0, reg = op1 & 7, rm;
unsigned sib = 0x20;
bool withsib = false;
unsigned dispsz = 0;
int32_t off = op0.off;
if (op_reg_idx(op0.idx) < 0x80) {
int scalabs = op0.scale;
if (UNLIKELY((unsigned) (op0.scale - 1) >= 8 ||
(op0.scale & (op0.scale - 1))))
return 0;
unsigned scale = (scalabs & 0xA ? 1 : 0) | (scalabs & 0xC ? 2 : 0);
sib = scale << 6 | (op_reg_idx(op0.idx) & 7) << 3;
withsib = true;
} else if (UNLIKELY(op0.scale != 0)) {
return 0;
}
if (UNLIKELY(op0.base.idx >= 0x20)) {
if (UNLIKELY(op0.base.idx >= op_reg_idx(FE_NOREG))) {
*buf++ = (reg << 3) | 4;
*buf++ = sib | 5;
enc_imm(buf, off, 4);
return ripoff + 6;
} else if (LIKELY(op0.base.idx == FE_IP.idx)) {
if (withsib)
return 0;
*buf++ = (reg << 3) | 5;
// Adjust offset, caller doesn't know instruction length.
enc_imm(buf, off - ripoff - 5, 4);
return ripoff + 5;
} else {
return 0;
}
}
rm = op_reg_idx(op0.base) & 7;
if (off) {
if (LIKELY(!disp8scale)) {
mod = (int8_t) off == off ? 0x40 : 0x80;
dispsz = (int8_t) off == off ? 1 : 4;
} else {
if (!(off & ((1 << disp8scale) - 1)) && op_imm_n(off >> disp8scale, 1))
off >>= disp8scale, mod = 0x40, dispsz = 1;
else
mod = 0x80, dispsz = 4;
}
} else if (rm == 5) {
dispsz = 1;
mod = 0x40;
}
// Always write four bytes of displacement. The buffer is always large
// enough, and we truncate by returning a smaller "written bytes" count.
if (withsib || rm == 4) {
*buf++ = mod | (reg << 3) | 4;
*buf++ = sib | rm;
enc_imm(buf, off, 4);
return ripoff + 2 + dispsz;
} else {
*buf++ = mod | (reg << 3) | rm;
enc_imm(buf, off, 4);
return ripoff + 1 + dispsz;
}
}
static int
enc_mem(uint8_t* buf, unsigned ripoff, FeMem op0, uint64_t op1, bool forcesib,
unsigned disp8scale) {
if (UNLIKELY(op_reg_idx(op0.idx) == 4))
return 0;
if (forcesib && op_reg_idx(op0.idx) == op_reg_idx(FE_NOREG)) {
op0.scale = 1;
op0.idx = FE_GP(4);
}
return enc_mem_common(buf, ripoff, op0, op1, disp8scale);
}
static int
enc_mem_vsib(uint8_t* buf, unsigned ripoff, FeMemV op0, uint64_t op1,
bool forcesib, unsigned disp8scale) {
(void) forcesib;
FeMem mem = FE_MEM(op0.base, op0.scale, FE_GP(op_reg_idx(op0.idx)), op0.off);
return enc_mem_common(buf, ripoff, mem, op1, disp8scale);
}
// EVEX/VEX "Opcode" format:
//
// | EVEX byte 4 | P P M M M - - W | Opcode byte | VEX-D VEX-D-FLIPW
// 0 8 16 24
enum {
FE_OPC_VEX_WPP_SHIFT = 8,
FE_OPC_VEX_WPP_MASK = 0x83 << FE_OPC_VEX_WPP_SHIFT,
FE_OPC_VEX_MMM_SHIFT = 10,
FE_OPC_VEX_MMM_MASK = 0x1f << FE_OPC_VEX_MMM_SHIFT,
FE_OPC_VEX_DOWNGRADE_VEX = 1 << 24,
FE_OPC_VEX_DOWNGRADE_VEX_FLIPW = 1 << 25,
};
static int
enc_vex_common(uint8_t* buf, unsigned opcode, unsigned base,
unsigned idx, unsigned reg, unsigned vvvv) {
if ((base | idx | reg | vvvv) & 0x10) return 0;
bool vex3 = ((base | idx) & 0x08) || (opcode & 0xfc00) != 0x0400;
if (vex3) {
*buf++ = 0xc4;
unsigned b1 = (opcode & FE_OPC_VEX_MMM_MASK) >> FE_OPC_VEX_MMM_SHIFT;
if (!(reg & 0x08)) b1 |= 0x80;
if (!(idx & 0x08)) b1 |= 0x40;
if (!(base & 0x08)) b1 |= 0x20;
*buf++ = b1;
unsigned b2 = (opcode & FE_OPC_VEX_WPP_MASK) >> FE_OPC_VEX_WPP_SHIFT;
if (opcode & 0x20) b2 |= 0x04;
b2 |= (vvvv ^ 0xf) << 3;
*buf++ = b2;
} else {
*buf++ = 0xc5;
unsigned b2 = opcode >> FE_OPC_VEX_WPP_SHIFT & 3;
if (opcode & 0x20) b2 |= 0x04;
if (!(reg & 0x08)) b2 |= 0x80;
b2 |= (vvvv ^ 0xf) << 3;
*buf++ = b2;
}
*buf++ = (opcode & 0xff0000) >> 16;
return 3 + vex3;
}
static int
enc_vex_reg(uint8_t* buf, unsigned opcode, uint64_t rm, uint64_t reg,
uint64_t vvvv) {
unsigned off = enc_vex_common(buf, opcode, rm, 0, reg, vvvv);
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
return off ? off + 1 : 0;
}
static int
enc_vex_mem(uint8_t* buf, unsigned opcode, FeMem rm, uint64_t reg,
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
unsigned off = enc_vex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
unsigned memoff = enc_mem(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
return off && memoff ? memoff : 0;
}
static int
enc_vex_vsib(uint8_t* buf, unsigned opcode, FeMemV rm, uint64_t reg,
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
unsigned off = enc_vex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
unsigned memoff = enc_mem_vsib(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
return off && memoff ? memoff : 0;
}
static int
enc_evex_common(uint8_t* buf, unsigned opcode, unsigned base,
unsigned idx, unsigned reg, unsigned vvvv) {
*buf++ = 0x62;
bool evexr3 = reg & 0x08;
bool evexr4 = reg & 0x10;
bool evexb3 = base & 0x08;
bool evexb4 = base & 0x10; // evexb4 is unused in AVX-512 encoding
bool evexx3 = idx & 0x08;
bool evexx4 = idx & 0x10;
bool evexv4 = vvvv & 0x10;
unsigned b1 = (opcode & FE_OPC_VEX_MMM_MASK) >> FE_OPC_VEX_MMM_SHIFT;
if (!evexr3) b1 |= 0x80;
if (!evexx3) b1 |= 0x40;
if (!evexb3) b1 |= 0x20;
if (!evexr4) b1 |= 0x10;
if (evexb4) b1 |= 0x08;
*buf++ = b1;
unsigned b2 = (opcode & FE_OPC_VEX_WPP_MASK) >> FE_OPC_VEX_WPP_SHIFT;
if (!evexx4) b2 |= 0x04;
b2 |= (~vvvv & 0xf) << 3;
*buf++ = b2;
unsigned b3 = opcode & 0xff;
if (!evexv4) b3 |= 0x08;
*buf++ = b3;
*buf++ = (opcode & 0xff0000) >> 16;
return 5;
}
static unsigned
enc_evex_to_vex(unsigned opcode) {
return opcode & FE_OPC_VEX_DOWNGRADE_VEX_FLIPW ? opcode ^ 0x8000 : opcode;
}
// Encode AVX-512 EVEX r/m-reg, non-xmm reg, vvvv, prefer vex
static int
enc_evex_reg(uint8_t* buf, unsigned opcode, unsigned rm,
unsigned reg, unsigned vvvv) {
unsigned off;
if (!((rm | reg | vvvv) & 0x10) && (opcode & FE_OPC_VEX_DOWNGRADE_VEX))
off = enc_vex_common(buf, enc_evex_to_vex(opcode), rm, 0, reg, vvvv);
else
off = enc_evex_common(buf, opcode, rm, 0, reg, vvvv);
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
return off + 1;
}
// Encode AVX-512 EVEX r/m-reg, xmm reg, vvvv, prefer vex
static int
enc_evex_xmm(uint8_t* buf, unsigned opcode, unsigned rm,
unsigned reg, unsigned vvvv) {
unsigned off;
if (!((rm | reg | vvvv) & 0x10) && (opcode & FE_OPC_VEX_DOWNGRADE_VEX))
off = enc_vex_common(buf, enc_evex_to_vex(opcode), rm, 0, reg, vvvv);
else
// AVX-512 XMM reg encoding uses X3 instead of B4.
off = enc_evex_common(buf, opcode, rm & 0x0f, rm >> 1, reg, vvvv);
buf[off] = 0xc0 | (reg << 3 & 0x38) | (rm & 7);
return off + 1;
}
static int
enc_evex_mem(uint8_t* buf, unsigned opcode, FeMem rm, uint64_t reg,
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
unsigned off;
if (!((op_reg_idx(rm.base) | op_reg_idx(rm.idx) | reg | vvvv) & 0x10) &&
(opcode & FE_OPC_VEX_DOWNGRADE_VEX)) {
disp8scale = 0; // Only AVX-512 EVEX compresses displacement
off = enc_vex_common(buf, enc_evex_to_vex(opcode), op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
} else {
off = enc_evex_common(buf, opcode, op_reg_idx(rm.base), op_reg_idx(rm.idx), reg, vvvv);
}
unsigned memoff = enc_mem(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
return off && memoff ? memoff : 0;
}
static int
enc_evex_vsib(uint8_t* buf, unsigned opcode, FeMemV rm, uint64_t reg,
uint64_t vvvv, unsigned ripoff, bool forcesib, unsigned disp8scale) {
(void) vvvv;
// EVEX VSIB requires non-zero mask operand
if (!(opcode & 0x7)) return 0;
// EVEX.X4 is encoded in EVEX.V4
unsigned idx = op_reg_idx(rm.idx);
unsigned off = enc_evex_common(buf, opcode, op_reg_idx(rm.base), idx & 0x0f, reg, idx & 0x10);
unsigned memoff = enc_mem_vsib(buf + off, ripoff + off, rm, reg, forcesib, disp8scale);
return off && memoff ? memoff : 0;
}
unsigned fe64_NOP(uint8_t* buf, unsigned flags) {
unsigned len = flags ? flags : 1;
// Taken from Intel SDM
static const uint8_t tbl[] = {
0x90,
0x66, 0x90,
0x0f, 0x1f, 0x00,
0x0f, 0x1f, 0x40, 0x00,
0x0f, 0x1f, 0x44, 0x00, 0x00,
0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00,
0x0f, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00,
0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00,
};
unsigned remain = len;
for (; remain > 9; remain -= 9)
for (unsigned i = 0; i < 9; i++)
*(buf++) = tbl[36 + i];
const uint8_t* src = tbl + (remain * (remain - 1)) / 2;
for (unsigned i = 0; i < remain; i++)
*(buf++) = src[i];
return len;
}
#include <fadec-encode2-private.inc>

File diff suppressed because one or more lines are too long

1888
third_party/fadec/fadec-decode-public.inc vendored Normal file

File diff suppressed because it is too large Load Diff

113
third_party/fadec/fadec-enc.h vendored Normal file
View File

@@ -0,0 +1,113 @@
#ifndef FD_FADEC_ENC_H_
#define FD_FADEC_ENC_H_
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
FE_AX = 0x100, FE_CX, FE_DX, FE_BX, FE_SP, FE_BP, FE_SI, FE_DI,
FE_R8, FE_R9, FE_R10, FE_R11, FE_R12, FE_R13, FE_R14, FE_R15,
FE_IP = 0x120,
FE_AH = 0x204, FE_CH, FE_DH, FE_BH,
FE_ES = 0x300, FE_CS, FE_SS, FE_DS, FE_FS, FE_GS,
FE_ST0 = 0x400, FE_ST1, FE_ST2, FE_ST3, FE_ST4, FE_ST5, FE_ST6, FE_ST7,
FE_MM0 = 0x500, FE_MM1, FE_MM2, FE_MM3, FE_MM4, FE_MM5, FE_MM6, FE_MM7,
FE_XMM0 = 0x600, FE_XMM1, FE_XMM2, FE_XMM3, FE_XMM4, FE_XMM5, FE_XMM6, FE_XMM7,
FE_XMM8, FE_XMM9, FE_XMM10, FE_XMM11, FE_XMM12, FE_XMM13, FE_XMM14, FE_XMM15,
FE_XMM16, FE_XMM17, FE_XMM18, FE_XMM19, FE_XMM20, FE_XMM21, FE_XMM22, FE_XMM23,
FE_XMM24, FE_XMM25, FE_XMM26, FE_XMM27, FE_XMM28, FE_XMM29, FE_XMM30, FE_XMM31,
FE_K0 = 0x700, FE_K1, FE_K2, FE_K3, FE_K4, FE_K5, FE_K6, FE_K7,
FE_TMM0 = 0x800, FE_TMM1, FE_TMM2, FE_TMM3, FE_TMM4, FE_TMM5, FE_TMM6, FE_TMM7,
} FeReg;
typedef int64_t FeOp;
/** Construct a memory operand. Unused parts can be set to 0 and will be
* ignored. FE_IP can be used as base register, in which case the offset is
* interpreted as the offset from the /current/ position -- the size of the
* encoded instruction will be subtracted during encoding. scale must be 1, 2,
* 4, or 8; but is ignored if idx == 0. **/
#define FE_MEM(base,sc,idx,off) (INT64_MIN | ((int64_t) ((base) & 0xfff) << 32) | ((int64_t) ((idx) & 0xfff) << 44) | ((int64_t) ((sc) & 0xf) << 56) | ((off) & 0xffffffff))
#define FE_NOREG ((FeReg) 0)
/** Add segment override prefix. This may or may not generate prefixes for the
* ignored prefixes ES/CS/DS/SS in 64-bit mode. **/
#define FE_SEG(seg) ((uint64_t) (((seg) & 0x7) + 1) << 29)
/** Do not use. **/
#define FE_SEG_MASK 0xe0000000
/** Overrides address size. **/
#define FE_ADDR32 0x10000000
/** Used together with a RIP-relative (conditional) jump, this will force the
* use of the encoding with the largest distance. Useful for reserving a jump
* when the target offset is still unknown; if the jump is re-encoded later on,
* FE_JMPL must be specified there, too, so that the encoding lengths match. **/
#define FE_JMPL 0x100000000
#define FE_MASK(kreg) ((uint64_t) ((kreg) & 0x7) << 33)
#define FE_RC_RN 0x0000000
#define FE_RC_RD 0x0800000
#define FE_RC_RU 0x1000000
#define FE_RC_RZ 0x1800000
enum {
FE_CC_O = 0x0,
FE_CC_NO = 0x1,
FE_CC_C = 0x2,
FE_CC_B = FE_CC_C,
FE_CC_NAE = FE_CC_C,
FE_CC_NC = 0x3,
FE_CC_AE = FE_CC_NC,
FE_CC_NB = FE_CC_NC,
FE_CC_Z = 0x4,
FE_CC_E = FE_CC_Z,
FE_CC_NZ = 0x5,
FE_CC_NE = FE_CC_NZ,
FE_CC_BE = 0x6,
FE_CC_NA = FE_CC_BE,
FE_CC_A = 0x7,
FE_CC_NBE = FE_CC_A,
FE_CC_S = 0x8,
FE_CC_NS = 0x9,
FE_CC_P = 0xa,
FE_CC_PE = FE_CC_P,
FE_CC_NP = 0xb,
FE_CC_PO = FE_CC_NP,
FE_CC_L = 0xc,
FE_CC_NGE = FE_CC_L,
FE_CC_GE = 0xd,
FE_CC_NL = FE_CC_GE,
FE_CC_LE = 0xe,
FE_CC_NG = FE_CC_LE,
FE_CC_G = 0xf,
FE_CC_NLE = FE_CC_G,
};
#include <fadec-encode-public.inc>
/** Do not use. **/
#define fe_enc64_1(buf, mnem, op0, op1, op2, op3, ...) fe_enc64_impl(buf, mnem, op0, op1, op2, op3)
/** Encode a single instruction for 64-bit mode.
* \param buf Pointer to the buffer for instruction bytes, must have a size of
* 15 bytes. The pointer is advanced by the number of bytes used for
* encoding the specified instruction.
* \param mnem Mnemonic, optionally or-ed with FE_SEG(), FE_ADDR32, or FE_JMPL.
* \param operands... Instruction operands. Immediate operands are passed as
* plain value; register operands using the FeReg enum; memory operands
* using FE_MEM(); and offset operands for RIP-relative jumps/calls are
* specified as _address in buf_, e.g. (intptr_t) jmptgt, the address of
* buf and the size of the encoded instruction are subtracted internally.
* \return Zero for success or a negative value in case of an error.
**/
#define fe_enc64(buf, ...) fe_enc64_1(buf, __VA_ARGS__, 0, 0, 0, 0, 0)
/** Do not use. **/
int fe_enc64_impl(uint8_t** buf, uint64_t mnem, FeOp op0, FeOp op1, FeOp op2, FeOp op3);
#ifdef __cplusplus
}
#endif
#endif

226
third_party/fadec/fadec-enc2.h vendored Normal file
View File

@@ -0,0 +1,226 @@
#ifndef FD_FADEC_ENC2_H_
#define FD_FADEC_ENC2_H_
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
#define FE_STRUCT(name) name
#else
#define FE_STRUCT(name) (name)
#endif
// Flags
#define FE_JMPL 0x8
#define FE_ADDR32 0x10
#define FE_SEG_MASK 0x7
#define FE_SEG(seg) (((seg).idx + 1) & FE_SEG_MASK)
#define FE_RC_MASK 0x60
#define FE_RC_RN 0x00
#define FE_RC_RD 0x20
#define FE_RC_RU 0x40
#define FE_RC_RZ 0x60
// Condition codes
typedef enum FeCond {
FE_CC_O = 0x00000,
FE_CC_NO = 0x10000,
FE_CC_C = 0x20000,
FE_CC_B = FE_CC_C,
FE_CC_NAE = FE_CC_C,
FE_CC_NC = 0x30000,
FE_CC_AE = FE_CC_NC,
FE_CC_NB = FE_CC_NC,
FE_CC_Z = 0x40000,
FE_CC_E = FE_CC_Z,
FE_CC_NZ = 0x50000,
FE_CC_NE = FE_CC_NZ,
FE_CC_BE = 0x60000,
FE_CC_NA = FE_CC_BE,
FE_CC_A = 0x70000,
FE_CC_NBE = FE_CC_A,
FE_CC_S = 0x80000,
FE_CC_NS = 0x90000,
FE_CC_P = 0xa0000,
FE_CC_PE = FE_CC_P,
FE_CC_NP = 0xb0000,
FE_CC_PO = FE_CC_NP,
FE_CC_L = 0xc0000,
FE_CC_NGE = FE_CC_L,
FE_CC_GE = 0xd0000,
FE_CC_NL = FE_CC_GE,
FE_CC_LE = 0xe0000,
FE_CC_NG = FE_CC_LE,
FE_CC_G = 0xf0000,
FE_CC_NLE = FE_CC_G,
FE_CC_MASK = 0xf0000
} FeCond;
typedef struct FeRegGP { unsigned char idx; } FeRegGP;
#define FE_GP(idx) (FE_STRUCT(FeRegGP) { idx })
#define FE_AX FE_GP(0)
#define FE_CX FE_GP(1)
#define FE_DX FE_GP(2)
#define FE_BX FE_GP(3)
#define FE_SP FE_GP(4)
#define FE_BP FE_GP(5)
#define FE_SI FE_GP(6)
#define FE_DI FE_GP(7)
#define FE_R8 FE_GP(8)
#define FE_R9 FE_GP(9)
#define FE_R10 FE_GP(10)
#define FE_R11 FE_GP(11)
#define FE_R12 FE_GP(12)
#define FE_R13 FE_GP(13)
#define FE_R14 FE_GP(14)
#define FE_R15 FE_GP(15)
#define FE_IP FE_GP(0x20)
#define FE_NOREG FE_GP(0x80)
typedef struct FeRegGPH { unsigned char idx; } FeRegGPH;
#define FE_GPH(idx) (FE_STRUCT(FeRegGPH) { idx })
#define FE_AH FE_GPH(4)
#define FE_CH FE_GPH(5)
#define FE_DH FE_GPH(6)
#define FE_BH FE_GPH(7)
typedef struct FeRegSREG { unsigned char idx; } FeRegSREG;
#define FE_SREG(idx) (FE_STRUCT(FeRegSREG) { idx })
#define FE_ES FE_SREG(0)
#define FE_CS FE_SREG(1)
#define FE_SS FE_SREG(2)
#define FE_DS FE_SREG(3)
#define FE_FS FE_SREG(4)
#define FE_GS FE_SREG(5)
typedef struct FeRegST { unsigned char idx; } FeRegST;
#define FE_ST(idx) (FE_STRUCT(FeRegST) { idx })
#define FE_ST0 FE_ST(0)
#define FE_ST1 FE_ST(1)
#define FE_ST2 FE_ST(2)
#define FE_ST3 FE_ST(3)
#define FE_ST4 FE_ST(4)
#define FE_ST5 FE_ST(5)
#define FE_ST6 FE_ST(6)
#define FE_ST7 FE_ST(7)
typedef struct FeRegMM { unsigned char idx; } FeRegMM;
#define FE_MM(idx) (FE_STRUCT(FeRegMM) { idx })
#define FE_MM0 FE_MM(0)
#define FE_MM1 FE_MM(1)
#define FE_MM2 FE_MM(2)
#define FE_MM3 FE_MM(3)
#define FE_MM4 FE_MM(4)
#define FE_MM5 FE_MM(5)
#define FE_MM6 FE_MM(6)
#define FE_MM7 FE_MM(7)
typedef struct FeRegXMM { unsigned char idx; } FeRegXMM;
#define FE_XMM(idx) (FE_STRUCT(FeRegXMM) { idx })
#define FE_XMM0 FE_XMM(0)
#define FE_XMM1 FE_XMM(1)
#define FE_XMM2 FE_XMM(2)
#define FE_XMM3 FE_XMM(3)
#define FE_XMM4 FE_XMM(4)
#define FE_XMM5 FE_XMM(5)
#define FE_XMM6 FE_XMM(6)
#define FE_XMM7 FE_XMM(7)
#define FE_XMM8 FE_XMM(8)
#define FE_XMM9 FE_XMM(9)
#define FE_XMM10 FE_XMM(10)
#define FE_XMM11 FE_XMM(11)
#define FE_XMM12 FE_XMM(12)
#define FE_XMM13 FE_XMM(13)
#define FE_XMM14 FE_XMM(14)
#define FE_XMM15 FE_XMM(15)
#define FE_XMM16 FE_XMM(16)
#define FE_XMM17 FE_XMM(17)
#define FE_XMM18 FE_XMM(18)
#define FE_XMM19 FE_XMM(19)
#define FE_XMM20 FE_XMM(20)
#define FE_XMM21 FE_XMM(21)
#define FE_XMM22 FE_XMM(22)
#define FE_XMM23 FE_XMM(23)
#define FE_XMM24 FE_XMM(24)
#define FE_XMM25 FE_XMM(25)
#define FE_XMM26 FE_XMM(26)
#define FE_XMM27 FE_XMM(27)
#define FE_XMM28 FE_XMM(28)
#define FE_XMM29 FE_XMM(29)
#define FE_XMM30 FE_XMM(30)
#define FE_XMM31 FE_XMM(31)
typedef struct FeRegMASK { unsigned char idx; } FeRegMASK;
#define FE_K(idx) (FE_STRUCT(FeRegMASK) { idx })
#define FE_K0 FE_K(0)
#define FE_K1 FE_K(1)
#define FE_K2 FE_K(2)
#define FE_K3 FE_K(3)
#define FE_K4 FE_K(4)
#define FE_K5 FE_K(5)
#define FE_K6 FE_K(6)
#define FE_K7 FE_K(7)
typedef struct FeRegTMM { unsigned char idx; } FeRegTMM;
#define FE_TMM(idx) (FE_STRUCT(FeRegTMM) { idx })
#define FE_TMM0 FE_TMM(0)
#define FE_TMM1 FE_TMM(1)
#define FE_TMM2 FE_TMM(2)
#define FE_TMM3 FE_TMM(3)
#define FE_TMM4 FE_TMM(4)
#define FE_TMM5 FE_TMM(5)
#define FE_TMM6 FE_TMM(6)
#define FE_TMM7 FE_TMM(7)
typedef struct FeRegCR { unsigned char idx; } FeRegCR;
#define FE_CR(idx) (FE_STRUCT(FeRegCR) { idx })
typedef struct FeRegDR { unsigned char idx; } FeRegDR;
#define FE_DR(idx) (FE_STRUCT(FeRegDR) { idx })
// Internal only
// Disambiguate GP and GPH -- C++ uses conversion constructors; C uses _Generic.
#ifdef __cplusplus
}
namespace {
struct FeRegGPLH {
unsigned char idx;
FeRegGPLH(FeRegGP gp) : idx(gp.idx) {}
FeRegGPLH(FeRegGPH gp) : idx(gp.idx | 0x20) {}
};
}
extern "C" {
#define FE_MAKE_GPLH(reg) reg
#else
typedef struct FeRegGPLH { unsigned char idx; } FeRegGPLH;
#define FE_GPLH(idx) (FE_STRUCT(FeRegGPLH) { idx })
#define FE_MAKE_GPLH(reg) FE_GPLH(_Generic((reg), FeRegGPH: 0x20, FeRegGP: 0) | (reg).idx)
#endif
typedef struct FeMem {
uint8_t flags;
FeRegGP base;
unsigned char scale;
// union {
FeRegGP idx;
// FeRegXMM idx_xmm;
// };
int32_t off;
} FeMem;
#define FE_MEM(base,sc,idx,off) (FE_STRUCT(FeMem) { 0, base, sc, idx, off })
typedef struct FeMemV {
uint8_t flags;
FeRegGP base;
unsigned char scale;
FeRegXMM idx;
int32_t off;
} FeMemV;
#define FE_MEMV(base,sc,idx,off) (FE_STRUCT(FeMemV) { 0, base, sc, idx, off })
// NOP is special: flags is interpreted as the length in bytes, 0 = 1 byte, too.
unsigned fe64_NOP(uint8_t* buf, unsigned flags);
#include <fadec-encode2-public.inc>
#ifdef __cplusplus
}
#endif
#endif

286
third_party/fadec/fadec.h vendored Normal file
View File

@@ -0,0 +1,286 @@
#ifndef FD_FADEC_H_
#define FD_FADEC_H_
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
FD_REG_R0 = 0, FD_REG_R1, FD_REG_R2, FD_REG_R3,
FD_REG_R4, FD_REG_R5, FD_REG_R6, FD_REG_R7,
FD_REG_R8, FD_REG_R9, FD_REG_R10, FD_REG_R11,
FD_REG_R12, FD_REG_R13, FD_REG_R14, FD_REG_R15,
// Alternative names for byte registers
FD_REG_AL = 0, FD_REG_CL, FD_REG_DL, FD_REG_BL,
FD_REG_AH, FD_REG_CH, FD_REG_DH, FD_REG_BH,
// Alternative names for general purpose registers
FD_REG_AX = 0, FD_REG_CX, FD_REG_DX, FD_REG_BX,
FD_REG_SP, FD_REG_BP, FD_REG_SI, FD_REG_DI,
// FD_REG_IP can only be accessed in long mode (64-bit)
FD_REG_IP = 0x10,
// Segment register values
FD_REG_ES = 0, FD_REG_CS, FD_REG_SS, FD_REG_DS, FD_REG_FS, FD_REG_GS,
// No register specified
FD_REG_NONE = 0x3f
} FdReg;
typedef enum {
#define FD_MNEMONIC(name,value) FDI_ ## name = value,
#include <fadec-decode-public.inc>
#undef FD_MNEMONIC
} FdInstrType;
/** Internal use only. **/
enum {
FD_FLAG_LOCK = 1 << 0,
FD_FLAG_REP = 1 << 2,
FD_FLAG_REPNZ = 1 << 1,
FD_FLAG_64 = 1 << 7,
};
/** Operand types. **/
typedef enum {
FD_OT_NONE = 0,
FD_OT_REG = 1,
FD_OT_IMM = 2,
FD_OT_MEM = 3,
FD_OT_OFF = 4,
FD_OT_MEMBCST = 5,
} FdOpType;
typedef enum {
/** Vector (SSE/AVX) register XMMn/YMMn/ZMMn **/
FD_RT_VEC = 0,
/** Low general purpose register **/
FD_RT_GPL = 1,
/** High-byte general purpose register **/
FD_RT_GPH = 2,
/** Segment register **/
FD_RT_SEG = 3,
/** FPU register ST(n) **/
FD_RT_FPU = 4,
/** MMX register MMn **/
FD_RT_MMX = 5,
/** TMM register TMMn **/
FD_RT_TMM = 6,
/** Vector mask (AVX-512) register Kn **/
FD_RT_MASK = 7,
/** Bound register BNDn **/
FD_RT_BND = 8,
/** Control Register CRn **/
FD_RT_CR = 9,
/** Debug Register DRn **/
FD_RT_DR = 10,
/** Must be a memory operand **/
FD_RT_MEM = 15,
} FdRegType;
/** Do not depend on the actual enum values. **/
typedef enum {
/** Round to nearest (even) **/
FD_RC_RN = 1,
/** Round down **/
FD_RC_RD = 3,
/** Round up **/
FD_RC_RU = 5,
/** Round to zero (truncate) **/
FD_RC_RZ = 7,
/** Rounding mode as specified in MXCSR **/
FD_RC_MXCSR = 0,
/** Rounding mode irrelevant, but SAE **/
FD_RC_SAE = 6,
} FdRoundControl;
/** Internal use only. **/
typedef struct {
uint8_t type;
uint8_t size;
uint8_t reg;
uint8_t misc;
} FdOp;
/** Never(!) access struct fields directly. Use the macros defined below. **/
typedef struct {
uint16_t type;
uint8_t flags;
uint8_t segment;
uint8_t addrsz;
uint8_t operandsz;
uint8_t size;
uint8_t evex;
FdOp operands[4];
int64_t disp;
int64_t imm;
uint64_t address;
} FdInstr;
typedef enum {
FD_ERR_UD = -1,
FD_ERR_INTERNAL = -2,
FD_ERR_PARTIAL = -3,
} FdErr;
/** Decode an instruction.
* \param buf Buffer for instruction bytes.
* \param len Length of the buffer (in bytes). An instruction is not longer than
* 15 bytes on all x86 architectures.
* \param mode Decoding mode, either 32 for protected/compatibility mode or 64
* for long mode. 16-bit mode is not supported.
* \param address Virtual address where the decoded instruction. This is used
* for computing jump targets. If "0" is passed, operands which require
* adding EIP/RIP will be stored as FD_OT_OFF operands.
* DEPRECATED: Strongly prefer passing 0 and using FD_OT_OFF operands.
* \param out_instr Pointer to the instruction buffer. Note that this may get
* partially written even if an error is returned.
* \return The number of bytes consumed by the instruction, or a negative number
* indicating an error.
**/
int fd_decode(const uint8_t* buf, size_t len, int mode, uintptr_t address,
FdInstr* out_instr);
/** Format an instruction to a string.
* \param instr The instruction.
* \param buf The buffer to hold the formatted string.
* \param len The length of the buffer.
**/
void fd_format(const FdInstr* instr, char* buf, size_t len);
/** Format an instruction to a string.
* NOTE: API stability is currently not guaranteed for this function; its name
* and/or signature may change in future.
*
* \param instr The instruction.
* \param addr The base address to use for printing FD_OT_OFF operands.
* \param buf The buffer to hold the formatted string.
* \param len The length of the buffer.
**/
void fd_format_abs(const FdInstr* instr, uint64_t addr, char* buf, size_t len);
/** Get the stringified name of an instruction type.
* NOTE: API stability is currently not guaranteed for this function; changes
* to the signature and/or the returned string can be expected. E.g., a future
* version may take an extra parameter for the instruction operand size; or may
* take a complete decoded instruction as first parameter and return the
* mnemonic returned by fd_format.
*
* \param ty An instruction type
* \return The instruction type as string, or "(invalid)".
**/
const char* fdi_name(FdInstrType ty);
/** Gets the type/mnemonic of the instruction.
* ABI STABILITY NOTE: different versions or builds of the library may use
* different values. When linking as shared library, any interpretation of this
* value is meaningless; in such cases use fdi_name.
*
* API STABILITY NOTE: a future version of this library may decode string
* instructions prefixed with REP/REPNZ and instructions prefixed with LOCK as
* separate instruction types. **/
#define FD_TYPE(instr) ((FdInstrType) (instr)->type)
/** DEPRECATED: This functionality is obsolete in favor of FD_OT_OFF.
* Gets the address of the instruction. Invalid if decoded address == 0. **/
#define FD_ADDRESS(instr) ((instr)->address)
/** Gets the size of the instruction in bytes. **/
#define FD_SIZE(instr) ((instr)->size)
/** Gets the specified segment override, or FD_REG_NONE for default segment. **/
#define FD_SEGMENT(instr) ((FdReg) (instr)->segment & 0x3f)
/** Gets the address size attribute of the instruction in bytes. **/
#define FD_ADDRSIZE(instr) (1 << (instr)->addrsz)
/** Get the logarithmic address size; FD_ADDRSIZE == 1 << FD_ADDRSIZELG **/
#define FD_ADDRSIZELG(instr) ((instr)->addrsz)
/** Gets the operation width in bytes of the instruction if this is not encoded
* in the operands, for example for the string instruction (e.g. MOVS). **/
#define FD_OPSIZE(instr) (1 << (instr)->operandsz)
/** Get the logarithmic operand size; FD_OPSIZE == 1 << FD_OPSIZELG iff
* FD_OPSIZE is valid. **/
#define FD_OPSIZELG(instr) ((instr)->operandsz)
/** Indicates whether the instruction was encoded with a REP prefix. Needed for:
* (1) Handling the instructions MOVS, STOS, LODS, INS and OUTS properly.
* (2) Handling the instructions SCAS and CMPS, for which this means REPZ. **/
#define FD_HAS_REP(instr) ((instr)->flags & FD_FLAG_REP)
/** Indicates whether the instruction was encoded with a REPNZ prefix. **/
#define FD_HAS_REPNZ(instr) ((instr)->flags & FD_FLAG_REPNZ)
/** Indicates whether the instruction was encoded with a LOCK prefix. **/
#define FD_HAS_LOCK(instr) ((instr)->flags & FD_FLAG_LOCK)
/** Do not use. **/
#define FD_IS64(instr) ((instr)->flags & FD_FLAG_64)
/** Gets the type of an operand at the given index. **/
#define FD_OP_TYPE(instr,idx) ((FdOpType) (instr)->operands[idx].type)
/** Gets the size in bytes of an operand. However, there are a few exceptions:
* (1) For some register types, e.g., segment registers, or x87 registers, the
* size is zero. (This allows some simplifications internally.)
* (2) On some vector instructions this may be only an approximation of the
* actually needed operand size (that is, an instruction may/must only use
* a smaller part than specified here). The real operand size is always
* fully recoverable in combination with the instruction type. **/
#define FD_OP_SIZE(instr,idx) (1 << (instr)->operands[idx].size >> 1)
/** Get the logarithmic size of an operand; see FD_OP_SIZE for special cases.
* The following equality holds: FD_OP_SIZE == 1 << (FD_OP_SIZELG + 1) >> 1
* Note that typically FD_OP_SIZE == 1 << FD_OP_SIZELG unless a zero-sized
* memory operand, FPU register, or mask register is involved. **/
#define FD_OP_SIZELG(instr,idx) ((instr)->operands[idx].size - 1)
/** Gets the accessed register index of a register operand. Note that /only/ the
* index is returned, no further interpretation of the index (which depends on
* the instruction type) is done. The register type can be fetched using
* FD_OP_REG_TYPE, e.g. for distinguishing high-byte registers.
* Only valid if FD_OP_TYPE == FD_OT_REG **/
#define FD_OP_REG(instr,idx) ((FdReg) (instr)->operands[idx].reg)
/** Gets the type of the accessed register.
* Only valid if FD_OP_TYPE == FD_OT_REG **/
#define FD_OP_REG_TYPE(instr,idx) ((FdRegType) (instr)->operands[idx].misc)
/** DEPRECATED: use FD_OP_REG_TYPE() == FD_RT_GPH instead.
* Returns whether the accessed register is a high-byte register. In that case,
* the register index has to be decreased by 4.
* Only valid if FD_OP_TYPE == FD_OT_REG **/
#define FD_OP_REG_HIGH(instr,idx) (FD_OP_REG_TYPE(instr,idx) == FD_RT_GPH)
/** Gets the index of the base register from a memory operand, or FD_REG_NONE,
* if the memory operand has no base register. This is the only case where the
* 64-bit register RIP can be returned, in which case the operand also has no
* scaled index register.
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
#define FD_OP_BASE(instr,idx) ((FdReg) (instr)->operands[idx].reg)
/** Gets the index of the index register from a memory operand, or FD_REG_NONE,
* if the memory operand has no scaled index register.
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
#define FD_OP_INDEX(instr,idx) ((FdReg) (instr)->operands[idx].misc & 0x3f)
/** Gets the scale of the index register from a memory operand when existent.
* This does /not/ return the scale in an absolute value but returns the amount
* of bits the index register is shifted to the left (i.e. the value in in the
* range 0-3). The actual scale can be computed easily using 1<<FD_OP_SCALE.
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST and FD_OP_INDEX != NONE **/
#define FD_OP_SCALE(instr,idx) ((instr)->operands[idx].misc >> 6)
/** Gets the sign-extended displacement of a memory operand.
* Only valid if FD_OP_TYPE == FD_OT_MEM/MEMBCST **/
#define FD_OP_DISP(instr,idx) ((int64_t) (instr)->disp)
/** Get memory broadcast size in bytes.
* Only valid if FD_OP_TYPE == FD_OT_MEMBCST **/
#define FD_OP_BCSTSZ(instr,idx) (1 << FD_OP_BCSTSZLG(instr,idx))
/** Get logarithmic memory broadcast size (1 = 2-byte; 2=4-byte; 3=8-byte).
* Only valid if FD_OP_TYPE == FD_OT_MEMBCST **/
#define FD_OP_BCSTSZLG(instr,idx) ((instr)->segment >> 6)
/** Gets the (sign-extended) encoded constant for an immediate operand.
* Only valid if FD_OP_TYPE == FD_OT_IMM or FD_OP_TYPE == FD_OT_OFF **/
#define FD_OP_IMM(instr,idx) ((instr)->imm)
/** Get the opmask register for EVEX-encoded instructions; 0 for no mask. **/
#define FD_MASKREG(instr) ((instr)->evex & 0x07)
/** Get whether zero masking shall be used. Only valid if FD_MASKREG != 0. **/
#define FD_MASKZERO(instr) ((instr)->evex & 0x80)
/** Get rounding mode for EVEX-encoded instructions. See FdRoundControl. **/
#define FD_ROUNDCONTROL(instr) ((FdRoundControl) (((instr)->evex & 0x70) >> 4))
#ifdef __cplusplus
}
#endif
#endif

563
third_party/fadec/format.c vendored Normal file
View File

@@ -0,0 +1,563 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include <fadec.h>
#ifdef __GNUC__
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#define DECLARE_ARRAY_SIZE(n) static n
#define DECLARE_RESTRICTED_ARRAY_SIZE(n) restrict static n
#else
#define LIKELY(x) (x)
#define UNLIKELY(x) (x)
#define DECLARE_ARRAY_SIZE(n) n
#define DECLARE_RESTRICTED_ARRAY_SIZE(n) n
#endif
#if defined(__has_attribute)
#if __has_attribute(fallthrough)
#define FALLTHROUGH() __attribute__((fallthrough))
#endif
#endif
#if !defined(FALLTHROUGH)
#define FALLTHROUGH() ((void)0)
#endif
struct FdStr {
const char* s;
unsigned sz;
};
#define fd_stre(s) ((struct FdStr) { (s "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"), sizeof (s)-1 })
static char*
fd_strpcat(char* restrict dst, struct FdStr src) {
#ifdef __GNUC__
unsigned lim = __builtin_constant_p(src.sz) && src.sz <= 8 ? 8 : 16;
#else
unsigned lim = 16;
#endif
for (unsigned i = 0; i < lim; i++)
dst[i] = src.s[i];
// __builtin_memcpy(dst, src.s, 16);
return dst + src.sz;
}
static unsigned
fd_clz64(uint64_t v) {
#if defined(__GNUC__)
return __builtin_clzll(v);
#elif defined(_MSC_VER)
unsigned long index;
// 32-bit MSVC doesn't support _BitScanReverse64. This is an attempt to
// identify this case.
#if INTPTR_MAX == INT64_MAX
_BitScanReverse64(&index, v);
#else
if (_BitScanReverse(&index, v >> 32))
return 31 - index;
_BitScanReverse(&index, v & 0xffffffff);
#endif
return 63 - index;
#else
#error Unsupported compiler.
#endif
}
#if defined(__SSE2__)
#include <immintrin.h>
#endif
static char*
fd_strpcatnum(char dst[DECLARE_ARRAY_SIZE(18)], uint64_t val) {
unsigned lz = fd_clz64(val|1);
unsigned numbytes = 16 - (lz / 4);
#if defined(__SSE2__)
__m128i mv = _mm_set_epi64x(0, val << (lz & -4));
__m128i mvp = _mm_unpacklo_epi8(mv, mv);
__m128i mva = _mm_srli_epi16(mvp, 12);
__m128i mvb = _mm_and_si128(mvp, _mm_set1_epi16(0x0f00u));
__m128i ml = _mm_or_si128(mva, mvb);
__m128i mn = _mm_or_si128(ml, _mm_set1_epi8(0x30));
__m128i mgt = _mm_cmpgt_epi8(ml, _mm_set1_epi8(9));
__m128i mgtm = _mm_and_si128(mgt, _mm_set1_epi8(0x61 - 0x3a));
__m128i ma = _mm_add_epi8(mn, mgtm);
__m128i msw = _mm_shufflehi_epi16(_mm_shufflelo_epi16(ma, 0x1b), 0x1b);
__m128i ms = _mm_shuffle_epi32(msw, 0x4e);
_mm_storeu_si128((__m128i_u*) (dst + 2), ms);
#else
unsigned idx = numbytes + 2;
do {
dst[--idx] = "0123456789abcdef"[val % 16];
val /= 16;
} while (val);
#endif
dst[0] = '0';
dst[1] = 'x';
return dst + numbytes + 2;
}
static char*
fd_strpcatreg(char* restrict dst, size_t rt, size_t ri, unsigned size) {
const char* nametab =
"\2al\4bnd0\2cl\4bnd1\2dl\4bnd2\2bl\4bnd3"
"\3spl\0 \3bpl\0 \3sil\0 \3dil\0 "
"\3r8b\0 \3r9b\0 \4r10b\0 \4r11b\0 "
"\4r12b\2ah\4r13b\2ch\4r14b\2dh\4r15b\2bh\0\0 "
"\2ax\4tmm0\2cx\4tmm1\2dx\4tmm2\2bx\4tmm3"
"\2sp\4tmm4\2bp\4tmm5\2si\4tmm6\2di\4tmm7"
"\3r8w \2es\3r9w \2cs\4r10w\2ss\4r11w\2ds"
"\4r12w\2fs\4r13w\2gs\4r14w\0 \4r15w\0 \2ip\0 "
"\3eax\3mm0\3ecx\3mm1\3edx\3mm2\3ebx\3mm3"
"\3esp\3mm4\3ebp\3mm5\3esi\3mm6\3edi\3mm7"
"\3r8d \2k0\3r9d \2k1\4r10d\2k2\4r11d\2k3"
"\4r12d\2k4\4r13d\2k5\4r14d\2k6\4r15d\2k7\3eip\0 "
"\3rax\3cr0\3rcx\0 \3rdx\3cr2\3rbx\3cr3"
"\3rsp\3cr4\3rbp\0 \3rsi\0 \3rdi\0 "
"\2r8 \3cr8\2r9 \3dr0\3r10\3dr1\3r11\3dr2"
"\3r12\3dr3\3r13\3dr4\3r14\3dr5\3r15\3dr6\3rip\3dr7"
"\5st(0)\0 \5st(1)\0 \5st(2)\0 \5st(3)\0 "
"\5st(4)\0 \5st(5)\0 \5st(6)\0 \5st(7)\0 "
"\4xmm0\0 \4xmm1\0 \4xmm2\0 \4xmm3\0 "
"\4xmm4\0 \4xmm5\0 \4xmm6\0 \4xmm7\0 "
"\4xmm8\0 \4xmm9\0 \5xmm10\0 \5xmm11\0 "
"\5xmm12\0 \5xmm13\0 \5xmm14\0 \5xmm15\0 "
"\5xmm16\0 \5xmm17\0 \5xmm18\0 \5xmm19\0 "
"\5xmm20\0 \5xmm21\0 \5xmm22\0 \5xmm23\0 "
"\5xmm24\0 \5xmm25\0 \5xmm26\0 \5xmm27\0 "
"\5xmm28\0 \5xmm29\0 \5xmm30\0 \5xmm31\0 ";
static const uint16_t nametabidx[] = {
[FD_RT_GPL] = 0 * 17*8 + 0 * 8 + 0,
[FD_RT_GPH] = 0 * 17*8 + 8 * 8 + 5,
[FD_RT_SEG] = 1 * 17*8 + 8 * 8 + 5,
[FD_RT_FPU] = 4 * 17*8 + 0 * 8 + 0,
[FD_RT_MMX] = 2 * 17*8 + 0 * 8 + 4,
[FD_RT_VEC] = 4 * 17*8 + 8 * 8 + 0,
[FD_RT_MASK]= 2 * 17*8 + 8 * 8 + 5,
[FD_RT_BND] = 0 * 17*8 + 0 * 8 + 3,
[FD_RT_CR] = 3 * 17*8 + 0 * 8 + 4,
[FD_RT_DR] = 3 * 17*8 + 9 * 8 + 4,
[FD_RT_TMM] = 1 * 17*8 + 0 * 8 + 3,
};
unsigned idx = rt == FD_RT_GPL ? size * 17*8 : nametabidx[rt];
const char* name = nametab + idx + 8*ri;
for (unsigned i = 0; i < 8; i++)
dst[i] = name[i+1];
if (UNLIKELY(rt == FD_RT_VEC && size > 4))
dst[0] += size - 4;
return dst + *name;
}
const char*
fdi_name(FdInstrType ty) {
(void) ty;
return "(invalid)";
}
static char*
fd_mnemonic(char buf[DECLARE_RESTRICTED_ARRAY_SIZE(48)], const FdInstr* instr) {
#define FD_DECODE_TABLE_STRTAB1
static const char* mnemonic_str =
#include <fadec-decode-private.inc>
// 20 NULL Bytes to prevent out-of-bounds reads
"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
#undef FD_DECODE_TABLE_STRTAB1
#define FD_DECODE_TABLE_STRTAB2
static const uint16_t mnemonic_offs[] = {
#include <fadec-decode-private.inc>
};
#undef FD_DECODE_TABLE_STRTAB2
#define FD_DECODE_TABLE_STRTAB3
static const uint8_t mnemonic_lens[] = {
#include <fadec-decode-private.inc>
};
#undef FD_DECODE_TABLE_STRTAB3
const char* mnem = &mnemonic_str[mnemonic_offs[FD_TYPE(instr)]];
unsigned mnemlen = mnemonic_lens[FD_TYPE(instr)];
bool prefix_xacq_xrel = false;
bool prefix_segment = false;
char sizesuffix[4] = {0};
unsigned sizesuffixlen = 0;
if (UNLIKELY(FD_OP_TYPE(instr, 0) == FD_OT_OFF && FD_OP_SIZELG(instr, 0) == 1))
sizesuffix[0] = 'w', sizesuffixlen = 1;
switch (FD_TYPE(instr)) {
case FDI_C_SEP:
mnem += FD_OPSIZE(instr) & 0xc;
mnemlen = 3;
break;
case FDI_C_EX:
mnem += FD_OPSIZE(instr) & 0xc;
mnemlen = FD_OPSIZE(instr) < 4 ? 3 : 4;
break;
case FDI_CMPXCHGD:
switch (FD_OPSIZELG(instr)) {
default: break;
case 2: sizesuffix[0] = '8', sizesuffix[1] = 'b', sizesuffixlen = 2; break;
case 3: sizesuffix[0] = '1', sizesuffix[1] = '6', sizesuffix[2] = 'b', sizesuffixlen = 3; break;
}
break;
case FDI_JCXZ:
mnemlen = FD_ADDRSIZELG(instr) == 1 ? 4 : 5;
mnem += 5 * (FD_ADDRSIZELG(instr) - 1);
break;
case FDI_PUSH:
if (FD_OP_SIZELG(instr, 0) == 1 && FD_OP_TYPE(instr, 0) == FD_OT_IMM)
sizesuffix[0] = 'w', sizesuffixlen = 1;
FALLTHROUGH();
case FDI_POP:
if (FD_OP_SIZELG(instr, 0) == 1 && FD_OP_TYPE(instr, 0) == FD_OT_REG &&
FD_OP_REG_TYPE(instr, 0) == FD_RT_SEG)
sizesuffix[0] = 'w', sizesuffixlen = 1;
break;
case FDI_XCHG:
if (FD_OP_TYPE(instr, 0) == FD_OT_MEM)
prefix_xacq_xrel = true;
break;
case FDI_MOV:
// MOV C6h/C7h can have XRELEASE prefix.
if (FD_HAS_REP(instr) && FD_OP_TYPE(instr, 0) == FD_OT_MEM &&
FD_OP_TYPE(instr, 1) == FD_OT_IMM)
prefix_xacq_xrel = true;
break;
case FDI_FXSAVE:
case FDI_FXRSTOR:
case FDI_XSAVE:
case FDI_XSAVEC:
case FDI_XSAVEOPT:
case FDI_XSAVES:
case FDI_XRSTOR:
case FDI_XRSTORS:
if (FD_OPSIZELG(instr) == 3)
sizesuffix[0] = '6', sizesuffix[1] = '4', sizesuffixlen = 2;
break;
case FDI_EVX_MOV_G2X:
case FDI_EVX_MOV_X2G:
case FDI_EVX_PEXTR:
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 0)];
sizesuffixlen = 1;
break;
case FDI_EVX_PBROADCAST:
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 1)];
sizesuffixlen = 1;
break;
case FDI_EVX_PINSR:
sizesuffix[0] = "bwdq"[FD_OP_SIZELG(instr, 2)];
sizesuffixlen = 1;
break;
case FDI_RET:
case FDI_ENTER:
case FDI_LEAVE:
if (FD_OPSIZELG(instr) == 1)
sizesuffix[0] = 'w', sizesuffixlen = 1;
break;
case FDI_LODS:
case FDI_MOVS:
case FDI_CMPS:
case FDI_OUTS:
prefix_segment = true;
FALLTHROUGH();
case FDI_STOS:
case FDI_SCAS:
case FDI_INS:
if (FD_HAS_REP(instr))
buf = fd_strpcat(buf, fd_stre("rep "));
if (FD_HAS_REPNZ(instr))
buf = fd_strpcat(buf, fd_stre("repnz "));
if (FD_IS64(instr) && FD_ADDRSIZELG(instr) == 2)
buf = fd_strpcat(buf, fd_stre("addr32 "));
if (!FD_IS64(instr) && FD_ADDRSIZELG(instr) == 1)
buf = fd_strpcat(buf, fd_stre("addr16 "));
FALLTHROUGH();
case FDI_IN:
case FDI_OUT:
if (FD_OP_TYPE(instr, 0) != FD_OT_NONE)
break;
FALLTHROUGH();
case FDI_PUSHA:
case FDI_POPA:
case FDI_PUSHF:
case FDI_POPF:
case FDI_RETF:
case FDI_IRET:
sizesuffix[0] = "bwdq"[FD_OPSIZELG(instr)];
sizesuffixlen = 1;
break;
default: break;
}
if (UNLIKELY(prefix_xacq_xrel || FD_HAS_LOCK(instr))) {
if (FD_HAS_REP(instr))
buf = fd_strpcat(buf, fd_stre("xrelease "));
if (FD_HAS_REPNZ(instr))
buf = fd_strpcat(buf, fd_stre("xacquire "));
}
if (UNLIKELY(FD_HAS_LOCK(instr)))
buf = fd_strpcat(buf, fd_stre("lock "));
if (UNLIKELY(prefix_segment && FD_SEGMENT(instr) != FD_REG_NONE)) {
*buf++ = "ecsdfg\0"[FD_SEGMENT(instr) & 7];
*buf++ = 's';
*buf++ = ' ';
}
for (unsigned i = 0; i < 20; i++)
buf[i] = mnem[i];
buf += mnemlen;
for (unsigned i = 0; i < 4; i++)
buf[i] = sizesuffix[i];
buf += sizesuffixlen;
return buf;
}
static char*
fd_format_impl(char buf[DECLARE_RESTRICTED_ARRAY_SIZE(128)], const FdInstr* instr, uint64_t addr) {
buf = fd_mnemonic(buf, instr);
for (int i = 0; i < 4; i++)
{
FdOpType op_type = FD_OP_TYPE(instr, i);
if (op_type == FD_OT_NONE)
break;
if (i > 0)
*buf++ = ',';
*buf++ = ' ';
int size = FD_OP_SIZELG(instr, i);
if (op_type == FD_OT_REG) {
unsigned type = FD_OP_REG_TYPE(instr, i);
unsigned idx = FD_OP_REG(instr, i);
buf = fd_strpcatreg(buf, type, idx, size);
} else if (op_type == FD_OT_MEM || op_type == FD_OT_MEMBCST) {
unsigned idx_rt = FD_RT_GPL;
unsigned idx_sz = FD_ADDRSIZELG(instr);
switch (FD_TYPE(instr)) {
case FDI_CMPXCHGD: size = FD_OPSIZELG(instr) + 1; break;
case FDI_BOUND: size += 1; break;
case FDI_JMPF:
case FDI_CALLF:
case FDI_LDS:
case FDI_LES:
case FDI_LFS:
case FDI_LGS:
case FDI_LSS:
size += 6;
break;
case FDI_FLD:
case FDI_FSTP:
case FDI_FBLD:
case FDI_FBSTP:
size = size >= 0 ? size : 9;
break;
case FDI_VPGATHERQD:
case FDI_VGATHERQPS:
case FDI_EVX_PGATHERQD:
case FDI_EVX_GATHERQPS:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 0) + 1;
break;
case FDI_EVX_PSCATTERQD:
case FDI_EVX_SCATTERQPS:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 1) + 1;
break;
case FDI_VPGATHERDQ:
case FDI_VGATHERDPD:
case FDI_EVX_PGATHERDQ:
case FDI_EVX_GATHERDPD:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 0) - 1;
break;
case FDI_EVX_PSCATTERDQ:
case FDI_EVX_SCATTERDPD:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 1) - 1;
break;
case FDI_VPGATHERDD:
case FDI_VPGATHERQQ:
case FDI_VGATHERDPS:
case FDI_VGATHERQPD:
case FDI_EVX_PGATHERDD:
case FDI_EVX_PGATHERQQ:
case FDI_EVX_GATHERDPS:
case FDI_EVX_GATHERQPD:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 0);
break;
case FDI_EVX_PSCATTERDD:
case FDI_EVX_PSCATTERQQ:
case FDI_EVX_SCATTERDPS:
case FDI_EVX_SCATTERQPD:
idx_rt = FD_RT_VEC;
idx_sz = FD_OP_SIZELG(instr, 1);
break;
default: break;
}
if (op_type == FD_OT_MEMBCST)
size = FD_OP_BCSTSZLG(instr, i);
const char* ptrsizes =
"\00 "
"\11byte ptr "
"\11word ptr "
"\12dword ptr "
"\12qword ptr "
"\14xmmword ptr "
"\14ymmword ptr "
"\14zmmword ptr "
"\12dword ptr " // far ptr; word + 2
"\12fword ptr " // far ptr; dword + 2
"\12tbyte ptr "; // far ptr/FPU; qword + 2
const char* ptrsize = ptrsizes + 16 * (size + 1);
buf = fd_strpcat(buf, (struct FdStr) { ptrsize+1, *ptrsize });
unsigned seg = FD_SEGMENT(instr);
if (seg != FD_REG_NONE) {
*buf++ = "ecsdfg\0"[seg & 7];
*buf++ = 's';
*buf++ = ':';
}
*buf++ = '[';
bool has_base = FD_OP_BASE(instr, i) != FD_REG_NONE;
bool has_idx = FD_OP_INDEX(instr, i) != FD_REG_NONE;
if (has_base)
buf = fd_strpcatreg(buf, FD_RT_GPL, FD_OP_BASE(instr, i), FD_ADDRSIZELG(instr));
if (has_idx) {
if (has_base)
*buf++ = '+';
*buf++ = '0' + (1 << FD_OP_SCALE(instr, i));
*buf++ = '*';
buf = fd_strpcatreg(buf, idx_rt, FD_OP_INDEX(instr, i), idx_sz);
}
uint64_t disp = FD_OP_DISP(instr, i);
if (disp && (has_base || has_idx)) {
*buf++ = (int64_t) disp < 0 ? '-' : '+';
if ((int64_t) disp < 0)
disp = -disp;
}
if (FD_ADDRSIZELG(instr) == 1)
disp &= 0xffff;
else if (FD_ADDRSIZELG(instr) == 2)
disp &= 0xffffffff;
if (disp || (!has_base && !has_idx))
buf = fd_strpcatnum(buf, disp);
*buf++ = ']';
if (UNLIKELY(op_type == FD_OT_MEMBCST)) {
// {1toX}, X = FD_OP_SIZE(instr, i) / BCSTSZ (=> 2/4/8/16/32)
unsigned bcstszidx = FD_OP_SIZELG(instr, i) - FD_OP_BCSTSZLG(instr, i) - 1;
const char* bcstsizes = "\6{1to2} \6{1to4} \6{1to8} \7{1to16}\7{1to32} ";
const char* bcstsize = bcstsizes + bcstszidx * 8;
buf = fd_strpcat(buf, (struct FdStr) { bcstsize+1, *bcstsize });
}
} else if (op_type == FD_OT_IMM || op_type == FD_OT_OFF) {
uint64_t immediate = FD_OP_IMM(instr, i);
// Some instructions have actually two immediate operands which are
// decoded as a single operand. Split them here appropriately.
switch (FD_TYPE(instr)) {
default:
goto nosplitimm;
case FDI_SSE_EXTRQ:
case FDI_SSE_INSERTQ:
buf = fd_strpcatnum(buf, immediate & 0xff);
buf = fd_strpcat(buf, fd_stre(", "));
immediate = (immediate >> 8) & 0xff;
break;
case FDI_ENTER:
buf = fd_strpcatnum(buf, immediate & 0xffff);
buf = fd_strpcat(buf, fd_stre(", "));
immediate = (immediate >> 16) & 0xff;
break;
case FDI_JMPF:
case FDI_CALLF:
buf = fd_strpcatnum(buf, (immediate >> (8 << size)) & 0xffff);
*buf++ = ':';
// immediate is masked below.
break;
}
nosplitimm:
if (op_type == FD_OT_OFF)
immediate += addr + FD_SIZE(instr);
if (size == 0)
immediate &= 0xff;
else if (size == 1)
immediate &= 0xffff;
else if (size == 2)
immediate &= 0xffffffff;
buf = fd_strpcatnum(buf, immediate);
}
if (i == 0 && FD_MASKREG(instr)) {
*buf++ = '{';
buf = fd_strpcatreg(buf, FD_RT_MASK, FD_MASKREG(instr), 0);
*buf++ = '}';
if (FD_MASKZERO(instr))
buf = fd_strpcat(buf, fd_stre("{z}"));
}
}
if (UNLIKELY(FD_ROUNDCONTROL(instr) != FD_RC_MXCSR)) {
switch (FD_ROUNDCONTROL(instr)) {
case FD_RC_RN: buf = fd_strpcat(buf, fd_stre(", {rn-sae}")); break;
case FD_RC_RD: buf = fd_strpcat(buf, fd_stre(", {rd-sae}")); break;
case FD_RC_RU: buf = fd_strpcat(buf, fd_stre(", {ru-sae}")); break;
case FD_RC_RZ: buf = fd_strpcat(buf, fd_stre(", {rz-sae}")); break;
case FD_RC_SAE: buf = fd_strpcat(buf, fd_stre(", {sae}")); break;
default: break; // should not happen
}
}
*buf++ = '\0';
return buf;
}
void
fd_format(const FdInstr* instr, char* buffer, size_t len)
{
fd_format_abs(instr, 0, buffer, len);
}
void
fd_format_abs(const FdInstr* instr, uint64_t addr, char* restrict buffer, size_t len) {
char tmp[128];
char* buf = buffer;
if (UNLIKELY(len < 128)) {
if (!len)
return;
buf = tmp;
}
char* end = fd_format_impl(buf, instr, addr);
if (buf != buffer) {
unsigned i;
for (i = 0; i < (end - tmp) && i < len-1; i++)
buffer[i] = tmp[i];
buffer[i] = '\0';
}
}

2596
third_party/fadec/instrs.txt vendored Normal file

File diff suppressed because it is too large Load Diff

126
third_party/fadec/meson.build vendored Normal file
View File

@@ -0,0 +1,126 @@
project('fadec', ['c'], default_options: ['warning_level=3', 'c_std=c11'],
meson_version: '>=0.49')
python3 = find_program('python3')
# Check Python version
py_version_res = run_command(python3, ['--version'], check: true)
py_version = py_version_res.stdout().split(' ')[1]
if not py_version.version_compare('>=3.9')
error('Python 3.9 required, got @0@'.format(py_version))
endif
has_cpp = add_languages('cpp', required: false)
cc = meson.get_compiler('c')
if cc.has_argument('-fstrict-aliasing')
add_project_arguments('-fstrict-aliasing', language: 'c')
endif
if get_option('warning_level').to_int() >= 3
extra_warnings = [
'-Wmissing-prototypes', '-Wshadow', '-Wwrite-strings', '-Wswitch-default',
'-Winline', '-Wstrict-prototypes', '-Wundef',
# We have strings longer than 4095 characters
'-Wno-overlength-strings',
# GCC 8 requires an extra option for strict cast alignment checks, Clang
# always warns, even on architectures without alignment requirements.
'-Wcast-align', '-Wcast-align=strict',
]
add_project_arguments(cc.get_supported_arguments(extra_warnings), language: 'c')
endif
if cc.get_argument_syntax() == 'msvc'
# Disable some warnings to align warnings with GCC and Clang:
add_project_arguments('-D_CRT_SECURE_NO_WARNINGS',
'/wd4018', # - Signed/unsigned comparison
'/wd4146', # - Unary minus operator applied to unsigned
# type, result still unsigned
'/wd4244', # - Possible loss of data in conversion
# from integer type to smaller integer type
'/wd4245', # - Signed/unsigned assignment
'/wd4267', # - Possible loss of data in conversion
# from size_t to smaller type
'/wd4310', # - Possible loss of data in conversion
# of constant value to smaller type
language: 'c')
endif
if cc.get_id() == 'msvc' and has_cpp
cxx = meson.get_compiler('cpp')
if cxx.get_id() == 'msvc'
# Enable standard conformant preprocessor
add_project_arguments(cxx.get_supported_arguments(['-Zc:preprocessor']), language: 'cpp')
endif
endif
sources = []
headers = []
components = []
if get_option('with_decode')
components += 'decode'
headers += files('fadec.h')
sources += files('decode.c', 'format.c')
endif
if get_option('with_encode')
components += 'encode'
headers += files('fadec-enc.h')
sources += files('encode.c')
endif
if get_option('with_encode2')
components += 'encode2'
headers += files('fadec-enc2.h')
sources += files('encode2.c')
endif
generate_args = []
if get_option('archmode') != 'only64'
generate_args += ['--32']
endif
if get_option('archmode') != 'only32'
generate_args += ['--64']
endif
if get_option('with_undoc')
generate_args += ['--with-undoc']
endif
if not meson.is_subproject()
generate_args += ['--stats']
endif
tables = []
foreach component : components
tables += custom_target('@0@_table'.format(component),
command: [python3, '@INPUT0@', component,
'@INPUT1@', '@OUTPUT@'] + generate_args,
input: files('parseinstrs.py', 'instrs.txt'),
output: ['fadec-@0@-public.inc'.format(component),
'fadec-@0@-private.inc'.format(component)],
install: true,
install_dir: [get_option('includedir'), false])
endforeach
libfadec = static_library('fadec', sources, tables, install: true)
fadec = declare_dependency(link_with: libfadec,
include_directories: include_directories('.'),
sources: tables)
install_headers(headers)
foreach component : components
test(component, executable('@0@-test'.format(component),
'@0@-test.c'.format(component),
dependencies: fadec))
if component == 'encode2' and has_cpp
test(component + '-cpp', executable('@0@-test-cpp'.format(component),
'@0@-test.cc'.format(component),
dependencies: fadec))
endif
endforeach
if meson.version().version_compare('>=0.54.0')
meson.override_dependency('fadec', fadec)
endif
pkg = import('pkgconfig')
pkg.generate(libraries: libfadec,
version: '0.1',
name: 'fadec',
filebase: 'fadec',
description: 'Fast Decoder for x86-32 and x86-64')

6
third_party/fadec/meson_options.txt vendored Normal file
View File

@@ -0,0 +1,6 @@
option('archmode', type: 'combo', choices: ['both', 'only32', 'only64'])
option('with_undoc', type: 'boolean', value: false)
option('with_decode', type: 'boolean', value: true)
option('with_encode', type: 'boolean', value: true)
# encode2 is off-by-default to reduce size and compile-time
option('with_encode2', type: 'boolean', value: false)

1403
third_party/fadec/parseinstrs.py vendored Normal file

File diff suppressed because it is too large Load Diff