mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 01:46:38 +03:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7cff67afce
|
|||
|
244511b66e
|
|||
|
51feb8d39c
|
|||
|
1b06206043
|
|||
|
8ca84a5861
|
|||
|
91e5e05de4
|
|||
|
32440c7104
|
|||
|
d26f6c0bf0
|
|||
|
0c3aa5ef25
|
|||
|
3028a4eb11
|
|||
|
02e63f2ff0
|
|||
|
5ea9a3785f
|
@@ -70,3 +70,59 @@ jobs:
|
||||
curl -fsSL -X POST -H "$auth" \
|
||||
-F "attachment=@dist/${asset};type=application/zip" \
|
||||
"${api}/releases/${rid}/assets?name=${asset}"
|
||||
|
||||
deb:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bookworm-slim
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install toolchain
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
cmake make gcc libc6-dev dpkg-dev file \
|
||||
libzydis-dev \
|
||||
gcc-mingw-w64-x86-64 ca-certificates curl
|
||||
|
||||
- name: Configure
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v} -DVMIE_DISASM=zydis
|
||||
|
||||
- name: Build shared library
|
||||
run: cmake --build build -j --target vmie_shared
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
set -euo pipefail
|
||||
(cd build && cpack -G DEB)
|
||||
ls -l build/*.deb
|
||||
|
||||
- name: Publish to Debian registry
|
||||
env:
|
||||
TOKEN: ${{ secrets.PUBLISH_TOKEN }} # requires scope: package:write
|
||||
SERVER: ${{ github.server_url }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
DISTRIBUTION: stable
|
||||
COMPONENT: main
|
||||
run: |
|
||||
set -euo pipefail
|
||||
url="${SERVER}/api/packages/${OWNER}/debian/pool/${DISTRIBUTION}/${COMPONENT}/upload"
|
||||
auth="Authorization: token ${TOKEN}"
|
||||
for deb in build/*.deb; do
|
||||
# 201 Created = uploaded; 409 Conflict = this version already present (re-run).
|
||||
# Both are success; anything else fails. Do not use curl -f (it would fail 409).
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
-X PUT -H "$auth" -T "$deb" "$url")
|
||||
echo "$deb -> HTTP $code"
|
||||
if [ "$code" != 201 ] && [ "$code" != 409 ]; then
|
||||
echo "upload failed: $deb (HTTP $code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
+147
-7
@@ -1,15 +1,31 @@
|
||||
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
|
||||
project(vmi-engine C)
|
||||
|
||||
set(VMIE_VERSION "0.2.1" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
|
||||
project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C)
|
||||
|
||||
set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_EXTENSIONS ON) # deliberate: strnlen (POSIX) + void* arithmetic (GNU)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON) # -fPIC on all objects: static lib stays linkable into a shared object
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO
|
||||
|
||||
# ---- host: VMI core as a static library ---------------------------------
|
||||
add_library(vmie STATIC
|
||||
# ---- optional semantic-signature backend (operand disassembler) ----------
|
||||
# VMIE_DISASM selects the OPTIONAL operand-decode backend for the semantic
|
||||
# signature feature (semsig.h). OFF (default) ships the 0.1.6 behaviour: the
|
||||
# generic symbol exists via semsig_stub.c, returns 0, VMIE_HAVE_DISASM=0, and no
|
||||
# external library is pulled - CI and the .deb packages are unchanged. A backend
|
||||
# (zydis|capstone) is NEVER vendored: it is brought from OUTSIDE the tree, either
|
||||
# by VMIE_DISASM_SRC (a path to the backend's own source tree, add_subdirectory'd)
|
||||
# or, failing that, by find_package of an already-installed library.
|
||||
set(VMIE_DISASM "OFF" CACHE STRING "Semantic-signature backend: OFF | zydis | capstone")
|
||||
set_property(CACHE VMIE_DISASM PROPERTY STRINGS OFF zydis capstone)
|
||||
set(VMIE_DISASM_SRC "" CACHE PATH "Path to the disassembler backend source tree (external; add_subdirectory'd)")
|
||||
|
||||
# ---- host: VMI core objects (single owner of the source list) -----------
|
||||
add_library(vmie_obj OBJECT
|
||||
src/core/gpa.c
|
||||
src/engine/gva.c
|
||||
src/engine/sigphys.c
|
||||
@@ -27,15 +43,98 @@ add_library(vmie STATIC
|
||||
src/handlers/x86dec.c
|
||||
src/handlers/pmap.c
|
||||
src/handlers/snapdiff.c)
|
||||
target_include_directories(vmie
|
||||
|
||||
# semantic-signature TUs: OFF compiles ONLY the stub (which carries the whole
|
||||
# generic symbol and returns 0); a backend compiles the core (semsig.c) + the one
|
||||
# backend TU that talks to the disassembler. semsig.c is never built in OFF (it
|
||||
# calls the backend decode, absent there) - so there is no dead/unresolved call.
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig_stub.c
|
||||
src/handlers/insndec_stub.c)
|
||||
elseif(VMIE_DISASM STREQUAL "zydis")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||
src/handlers/insndec.c
|
||||
src/handlers/codetrace.c
|
||||
src/handlers/semsig_backend_zydis.c)
|
||||
elseif(VMIE_DISASM STREQUAL "capstone")
|
||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||
src/handlers/insndec.c
|
||||
src/handlers/codetrace.c
|
||||
src/handlers/semsig_backend_capstone.c)
|
||||
else()
|
||||
message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')")
|
||||
endif()
|
||||
|
||||
target_include_directories(vmie_obj
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/engine/include # private: engine-arch.h, pe.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/engine/win32) # private: engine-win32.h, contract.h
|
||||
target_compile_options(vmie PRIVATE -O2 -Wall -Wextra)
|
||||
target_compile_options(vmie_obj PRIVATE -O2 -Wall -Wextra)
|
||||
if(VMIE_LTO)
|
||||
target_compile_options(vmie PRIVATE -flto)
|
||||
target_link_options(vmie PRIVATE -flto)
|
||||
target_compile_options(vmie_obj PRIVATE -flto)
|
||||
target_link_options(vmie_obj PRIVATE -flto)
|
||||
endif()
|
||||
|
||||
# Resolve the backend target/package name (the actual link/define is applied to
|
||||
# vmie_obj here AND re-applied to the static/shared libs below, because
|
||||
# $<TARGET_OBJECTS:vmie_obj> carries object files only - not link/define usage
|
||||
# requirements). The backend is PRIVATE (an implementation detail), while
|
||||
# VMIE_HAVE_DISASM is PUBLIC (visible through semsig.h). The backend comes from
|
||||
# VMIE_DISASM_SRC (external sources) or find_package - never from in-tree sources.
|
||||
if(NOT VMIE_DISASM STREQUAL "OFF")
|
||||
if(VMIE_DISASM STREQUAL "zydis")
|
||||
set(VMIE_DISASM_LINK "Zydis::Zydis")
|
||||
set(_vmie_disasm_pkg "Zydis")
|
||||
else()
|
||||
set(VMIE_DISASM_LINK "capstone::capstone")
|
||||
set(_vmie_disasm_pkg "capstone")
|
||||
endif()
|
||||
|
||||
if(VMIE_DISASM_SRC)
|
||||
# Build the backend from its OWN external source tree (kept out of this
|
||||
# repo). Its CMake exports the imported target linked below.
|
||||
add_subdirectory(${VMIE_DISASM_SRC} ${CMAKE_CURRENT_BINARY_DIR}/_disasm_backend)
|
||||
else()
|
||||
find_package(${_vmie_disasm_pkg} REQUIRED)
|
||||
endif()
|
||||
|
||||
target_link_libraries(vmie_obj PRIVATE ${VMIE_DISASM_LINK})
|
||||
target_compile_definitions(vmie_obj PUBLIC VMIE_HAVE_DISASM=1)
|
||||
else()
|
||||
set(VMIE_DISASM_LINK "")
|
||||
target_compile_definitions(vmie_obj PUBLIC VMIE_HAVE_DISASM=0)
|
||||
endif()
|
||||
|
||||
# ---- host: static library (in-tree consumers: CLIs + test) --------------
|
||||
# $<TARGET_OBJECTS:> carries object files only, not usage requirements:
|
||||
# re-declare the PUBLIC include scope so vmie_cli/vmie_scan still see include/.
|
||||
add_library(vmie STATIC $<TARGET_OBJECTS:vmie_obj>)
|
||||
target_include_directories(vmie PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
# Re-apply the feature macro (PUBLIC: seen by in-tree consumers + the test) and
|
||||
# the backend link (PRIVATE), which $<TARGET_OBJECTS> does not carry over.
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_compile_definitions(vmie PUBLIC VMIE_HAVE_DISASM=0)
|
||||
else()
|
||||
target_compile_definitions(vmie PUBLIC VMIE_HAVE_DISASM=1)
|
||||
target_link_libraries(vmie PRIVATE ${VMIE_DISASM_LINK})
|
||||
endif()
|
||||
|
||||
# ---- host: shared library (external delivery: libvmie.so.*) -------------
|
||||
add_library(vmie_shared SHARED $<TARGET_OBJECTS:vmie_obj>)
|
||||
target_include_directories(vmie_shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
set_target_properties(vmie_shared PROPERTIES
|
||||
OUTPUT_NAME vmie
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
VERSION ${PROJECT_VERSION})
|
||||
if(VMIE_LTO)
|
||||
target_link_options(vmie_shared PRIVATE -flto)
|
||||
endif()
|
||||
if(VMIE_DISASM STREQUAL "OFF")
|
||||
target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=0)
|
||||
else()
|
||||
target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=1)
|
||||
target_link_libraries(vmie_shared PRIVATE ${VMIE_DISASM_LINK})
|
||||
endif()
|
||||
|
||||
# ---- host: CLI demonstrator over the library ----------------------------
|
||||
@@ -61,3 +160,44 @@ add_custom_command(
|
||||
COMMENT "Cross-compiling vmie-startup.exe (mingw-w64, x86-64)"
|
||||
VERBATIM)
|
||||
add_custom_target(vmie-startup ALL DEPENDS ${VMIE_STARTUP})
|
||||
|
||||
# ---- install: shared library + public headers ---------------------------
|
||||
# Versioned file + SONAME symlink -> runtime; unversioned linker symlink -> dev.
|
||||
install(TARGETS vmie_shared
|
||||
LIBRARY
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
COMPONENT runtime
|
||||
NAMELINK_COMPONENT dev)
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/vmie
|
||||
COMPONENT dev
|
||||
FILES_MATCHING PATTERN "*.h")
|
||||
|
||||
# ---- package: two Debian packages (runtime + dev) -----------------------
|
||||
set(CPACK_GENERATOR "DEB")
|
||||
set(CPACK_DEB_COMPONENT_INSTALL ON)
|
||||
set(CPACK_COMPONENTS_ALL runtime dev)
|
||||
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
|
||||
|
||||
# component -> Debian package name (Debian shared-library convention)
|
||||
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "libvmie${PROJECT_VERSION_MAJOR}")
|
||||
set(CPACK_DEBIAN_DEV_PACKAGE_NAME "libvmie-dev")
|
||||
|
||||
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
|
||||
set(VMIE_DEB_MAINTAINER "Gregory Lirent <opensource@lirent.ru>"
|
||||
CACHE STRING "Debian package Maintainer field")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${VMIE_DEB_MAINTAINER}")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_FILE_NAME "DEB-DEFAULT")
|
||||
|
||||
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION
|
||||
"VM introspection engine (Windows-guest memory introspection) - shared library")
|
||||
set(CPACK_COMPONENT_DEV_DESCRIPTION
|
||||
"VM introspection engine (Windows-guest memory introspection) - development files")
|
||||
|
||||
# dev depends on the exact runtime version; runtime libc dependency via shlibdeps
|
||||
set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS "libvmie${PROJECT_VERSION_MAJOR} (= ${PROJECT_VERSION})")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
|
||||
include(CPack)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* codetrace.h - generic (OS-agnostic) register def-use over one function.
|
||||
*
|
||||
* Handler layer, gated like semsig.h: a process-scoped, STATIC register def-use
|
||||
* analysis built on insn_decode (insndec.h) and the basic-block partition of
|
||||
* cfg_blocks (codeanalysis.h, REUSED - this header writes no second splitter).
|
||||
* For one function view it computes per-instruction register read (use) / write
|
||||
* (def) sets - the building material a caller needs for reaching-definitions.
|
||||
*
|
||||
* It returns DATA. It does NOT execute or model values, does NOT unroll loops,
|
||||
* does NOT build use-def CHAINS, and carries no dataflow solver: those are the
|
||||
* caller's analysis. v1 deliberately stops at per-instruction use/def so the
|
||||
* surface does not predetermine the caller's algorithm.
|
||||
*
|
||||
* Family placement. codeanalysis.h is PURE / always available; semsig.h is gated
|
||||
* on the disassembler backend. codetrace.h sits in the second row with semsig: it
|
||||
* needs the backend decode (via insndec.h) plus cfg_blocks. With no backend the
|
||||
* symbol still exists (ABI stable) but returns the sentinel, and VMIE_HAVE_DISASM
|
||||
* is 0. The chosen split keeps codeanalysis.h's "zero-dep, works always" contract
|
||||
* literally true.
|
||||
*
|
||||
* GENERIC / OS-agnostic. The analysis names no OS. A calling convention enters
|
||||
* ONLY through the explicit trace_abi argument, which selects a static caller-
|
||||
* saved (volatile) clobber table applied at call sites. Win64 is just one enum
|
||||
* value, not a hardcoded dependency; TRACE_ABI_NONE reproduces conservative
|
||||
* behavior for any non-Windows consumer. The OS-specific CHOICE of Win64 is made
|
||||
* by the win32 wrapper (vmie_win32_func_usedef, win32.h), not by this header.
|
||||
*/
|
||||
#ifndef VMIE_CODETRACE_H
|
||||
#define VMIE_CODETRACE_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
|
||||
|
||||
/* Shared feature gate (see semsig.h / insndec.h). Default keeps the header valid
|
||||
* when compiled outside the project build. */
|
||||
#ifndef VMIE_HAVE_DISASM
|
||||
#define VMIE_HAVE_DISASM 0
|
||||
#endif
|
||||
|
||||
/* Calling convention applied to call sites when computing def-use. The analysis
|
||||
* itself stays OS-agnostic: an ABI is just a parameter selecting a static
|
||||
* caller-saved (volatile) clobber table. An unknown value is treated as
|
||||
* TRACE_ABI_NONE (conservative), not an error. */
|
||||
typedef enum {
|
||||
TRACE_ABI_NONE = 0, /* conservative: a call clobbers only what insn_decode
|
||||
* reports architecturally (rsp/rip write, indirect
|
||||
* target read); no volatile registers are killed. */
|
||||
TRACE_ABI_WIN64 /* standard Win64: volatile GPR/vec/FLAGS are killed
|
||||
* (full-kill) across every call site (see codetrace.c
|
||||
* for the exact set and its honest limits). */
|
||||
} trace_abi;
|
||||
|
||||
/* A compact register set over canonical reg_id roots (a bitset). The field split
|
||||
* is an implementation detail of the struct; consumers should treat it opaquely
|
||||
* and compare/inspect via the documented def/use semantics, not the raw words.
|
||||
* gpr - the 16 GPR roots (REGID_RAX..REGID_R15)
|
||||
* vec_lo - vector roots REGID_VEC0..REGID_VEC31
|
||||
* misc - mask roots (k0..k7), REGID_FLAGS, REGID_OTHER */
|
||||
typedef struct {
|
||||
uint16_t gpr; /* bit i set => REGID_RAX + i present (16 GPR roots) */
|
||||
uint32_t vec_lo; /* bit i set => REGID_VEC0 + i present (32 vector roots) */
|
||||
uint16_t misc; /* bit i set => (REGID_K0 + i) for i<8, then FLAGS, OTHER */
|
||||
} regset;
|
||||
|
||||
/* Per-instruction use/def in the function view's coordinate space.
|
||||
* off - byte offset of the instruction within fn
|
||||
* len - instruction length
|
||||
* use - roots READ (incl. implicit, incl. address regs of mem operands)
|
||||
* def - roots WRITTEN as a FULL/zero-extending write (these KILL the
|
||||
* enclosing root); for a call site under a non-NONE trace_abi the
|
||||
* ABI volatile clobber is merged here as a full kill too
|
||||
* def_partial - roots written PARTIALLY (8/16-bit GPR, legacy-SSE): they do NOT
|
||||
* kill the enclosing root, so a wider live root survives
|
||||
* cond_def - roots written CONDITIONALLY (cmovcc/setcc): do not kill either */
|
||||
typedef struct {
|
||||
uint32_t off;
|
||||
uint8_t len;
|
||||
regset use, def, def_partial, cond_def;
|
||||
} insn_usedef;
|
||||
|
||||
/* Compute per-instruction use/def for one function view under calling convention
|
||||
* `abi`. `fn` is a view spanning EXACTLY one function (as for cfg_blocks /
|
||||
* semsig_hash). REUSES cfg_blocks only to validate the partition and iterate in
|
||||
* block order; the use/def itself comes from insn_decode per instruction.
|
||||
*
|
||||
* `abi` selects the call-site volatile clobber: TRACE_ABI_NONE = conservative (no
|
||||
* volatile kill, only the architectural rsp/rip write insn_decode reports);
|
||||
* TRACE_ABI_WIN64 = full-kill of the Win64 volatile roots at every call site.
|
||||
* The analysis stays OS-agnostic: `abi` is just a parameter, not a hardcoded OS.
|
||||
*
|
||||
* Writes up to `max` records in ascending off order (out=NULL to count). Returns
|
||||
* the TOTAL instruction count, 0 if `fn` is empty, or -1 on a decode desync
|
||||
* (cfg_blocks could not split, or the backend could not decode an instruction) /
|
||||
* a build with no backend (VMIE_HAVE_DISASM==0). The sentinel convention mirrors
|
||||
* cfg_blocks: -1 = desync/no-backend, 0 = empty.
|
||||
*
|
||||
* PURE: only the view and the decoder, no vmie_mem / no I/O (a stack buffer with
|
||||
* a heap spill only when the block count overflows, like semsig_hash).
|
||||
*
|
||||
* Example - registers killed by each instruction of a function:
|
||||
* insn_usedef ud[512];
|
||||
* int n = func_usedef(fn, TRACE_ABI_WIN64, ud, 512);
|
||||
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
|
||||
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max);
|
||||
|
||||
#endif /* VMIE_CODETRACE_H */
|
||||
@@ -0,0 +1,194 @@
|
||||
/* insndec.h - readable per-instruction operand decode (generic, ABI-agnostic).
|
||||
*
|
||||
* Handler layer: a RICH decode of ONE x86-64 instruction into concrete operands
|
||||
* (concrete registers as a backend-neutral stable id, memory base/index/scale/
|
||||
* disp, immediate VALUES, sizes), per-operand read/write, AND the full aggregate
|
||||
* register read/write sets INCLUDING IMPLICIT operands/effects (rsp on push/pop,
|
||||
* rdx:rax on mul/div, FLAGS on cmp/jcc, ...). This is the "perception" primitive
|
||||
* the register def-use analysis (codetrace.h) is built on.
|
||||
*
|
||||
* Relationship to semsig.h (shared backend, DIFFERENT purpose). semsig_hash
|
||||
* folds a NORMALIZED, LOSSY token stream: it erases register identity (keeps only
|
||||
* a class), erases immediate/displacement values, and carries no read/write - by
|
||||
* design, so the hash is register-/value-/reorder-stable. That makes semsig
|
||||
* irreversible: you cannot trace registers through it. insn_decode is the
|
||||
* opposite: it KEEPS register identity, values, and read/write, so an analysis
|
||||
* can reason about WHICH physical register an instruction touches. Both ride the
|
||||
* SAME optional external disassembler (Zydis/Capstone) behind the same private
|
||||
* seam, but insn_decode is NOT a hash and NOT lossy.
|
||||
*
|
||||
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
|
||||
* length-only) and NOT a second hand-rolled operand decoder: it is supplied by
|
||||
* the optional backend, so this primitive is FEATURE-GATED exactly like semsig.
|
||||
* With no backend the symbol still exists (ABI stable) but returns 0, and
|
||||
* VMIE_HAVE_DISASM is 0.
|
||||
*
|
||||
* ABI-AGNOSTIC. insn_decode reports ONLY what an instruction does
|
||||
* ARCHITECTURALLY. In particular a `call` writes rsp and rip and (for an
|
||||
* indirect call) reads its target register/memory - that is ALL. It does NOT
|
||||
* mark caller-saved (volatile) registers as written: that is a property of the
|
||||
* CALLED function / the calling convention, not of the instruction. The Win64
|
||||
* volatile clobber lives in the def-use layer (codetrace.h/.c) behind an explicit
|
||||
* trace_abi, never here.
|
||||
*/
|
||||
#ifndef VMIE_INSNDEC_H
|
||||
#define VMIE_INSNDEC_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Compile-time feature availability, shared with semsig (one backend, one gate).
|
||||
* Pushed from CMake as a PUBLIC compile definition so consumers see the SAME
|
||||
* value the library was built with:
|
||||
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; insn_decode is
|
||||
* live, insndec_backend_name() names the backend.
|
||||
* VMIE_HAVE_DISASM == 0 built without a backend (the default); insn_decode
|
||||
* always returns 0 and insndec_backend_name() == "none".
|
||||
* The default here keeps the header valid when compiled outside the project. */
|
||||
#ifndef VMIE_HAVE_DISASM
|
||||
#define VMIE_HAVE_DISASM 0
|
||||
#endif
|
||||
|
||||
/* Canonical full-register root id. al/ah/ax/eax/rax all map to REGID_RAX;
|
||||
* xmm0/ymm0/zmm0 -> REGID_VEC0; etc. Identity is KEPT here (unlike sem_insn,
|
||||
* which erases it) so def-use can reason about WHICH physical register. The
|
||||
* concrete backend register enum is NEVER carried across the seam - the backend
|
||||
* TU normalizes into THIS stable id, exactly as it does for mnemonic classes. */
|
||||
typedef enum {
|
||||
REGID_NONE = 0,
|
||||
/* 16 GPR roots; al/ax/eax/rax -> REGID_RAX, etc. */
|
||||
REGID_RAX, REGID_RCX, REGID_RDX, REGID_RBX,
|
||||
REGID_RSP, REGID_RBP, REGID_RSI, REGID_RDI,
|
||||
REGID_R8, REGID_R9, REGID_R10, REGID_R11,
|
||||
REGID_R12, REGID_R13, REGID_R14, REGID_R15,
|
||||
/* 32 vector roots (zmm0..zmm31; xmm/ymm fold into the zmm root). */
|
||||
REGID_VEC0,
|
||||
REGID_VEC31 = REGID_VEC0 + 31,
|
||||
/* AVX-512 mask k0..k7. */
|
||||
REGID_K0,
|
||||
REGID_K7 = REGID_K0 + 7,
|
||||
/* FLAGS as ONE pseudo-register root (NOT per-bit CF/ZF/...): enough for
|
||||
* def-use "cmp writes flags, jcc/cmov/setcc read flags"; per-bit is not v1. */
|
||||
REGID_FLAGS,
|
||||
/* rip / segment / other roots kept coarse (def-use rarely tracks them). */
|
||||
REGID_OTHER,
|
||||
REGID_COUNT
|
||||
} reg_id;
|
||||
|
||||
/* Per-operand / per-register access, normalized (raw backend enums never leak). */
|
||||
typedef enum {
|
||||
ACC_NONE = 0,
|
||||
ACC_READ, /* read only */
|
||||
ACC_WRITE, /* write only */
|
||||
ACC_READWRITE, /* read-modify-write (add, sub, ...) */
|
||||
ACC_COND_READ, /* conditionally read (cmovcc src, ...) */
|
||||
ACC_COND_WRITE /* conditionally written (cmovcc dst, setcc) */
|
||||
} reg_access;
|
||||
|
||||
/* Explicit-operand kind (the readable, perception side of the decode). */
|
||||
typedef enum {
|
||||
OPND_NONE = 0,
|
||||
OPND_REG, /* register operand */
|
||||
OPND_MEM, /* memory operand (base/index/scale/disp) */
|
||||
OPND_IMM /* immediate operand (value kept) */
|
||||
} operand_kind;
|
||||
|
||||
/* One register reference, used both for explicit register operands and for the
|
||||
* aggregate implicit-or-explicit read/write sets. Sub-register overlap is modeled
|
||||
* EXPLICITLY and backend-neutrally:
|
||||
* root - canonical full-register root (al/ax/eax/rax -> REGID_RAX)
|
||||
* width_log2 - ACCESS width in log2 bytes (0=1B .. 4=16B), as
|
||||
* width_log2_of already computes for semsig
|
||||
* writes_zero_extend- on a WRITE, 1 if the upper part of the root is zeroed
|
||||
* (a 32-bit GPR write, e.g. `eax`, zeroes the upper 32 bits
|
||||
* of `rax`; a VEX/EVEX vector write zeroes the upper YMM/ZMM)
|
||||
* and 0 if it is a PARTIAL write that preserves the upper
|
||||
* part (8/16-bit GPR write, legacy-SSE vector write). This
|
||||
* ONE flag is what def-use needs to decide overlap without
|
||||
* carrying a raw bit mask.
|
||||
* access - ACC_* for this reference */
|
||||
typedef struct {
|
||||
uint8_t root; /* reg_id */
|
||||
uint8_t width_log2; /* access width, log2 bytes (0..4+) */
|
||||
uint8_t writes_zero_extend;
|
||||
uint8_t access; /* reg_access */
|
||||
} reg_ref;
|
||||
|
||||
/* One explicit operand (readable / perception view). */
|
||||
typedef struct {
|
||||
uint8_t kind; /* operand_kind */
|
||||
uint8_t access; /* reg_access for the operand as a whole */
|
||||
uint8_t width_log2; /* access width, log2 bytes */
|
||||
uint8_t mem_scale; /* MEM: index scale (1/2/4/8), 0 if no index */
|
||||
uint8_t mem_is_riprel; /* MEM: 1 if RIP-relative */
|
||||
uint8_t reg_root; /* REG: reg_id of the operand register */
|
||||
uint8_t reg_zero_extend; /* REG: writes_zero_extend for a write operand */
|
||||
uint8_t imm_width_log2; /* IMM: value width, log2 bytes */
|
||||
uint8_t mem_base; /* MEM: reg_id of the base register (read) */
|
||||
uint8_t mem_index; /* MEM: reg_id of the index register (read) */
|
||||
int64_t mem_disp; /* MEM: displacement value (signed) */
|
||||
int64_t imm_value; /* IMM: immediate value (sign-extended) */
|
||||
} operand;
|
||||
|
||||
/* Mnemonic-class group, backend-neutral. Mirrors the semsig SEM_MN_* groups so a
|
||||
* consumer can switch on the instruction kind without backend enums; def-use uses
|
||||
* only INSN_CALL (call sites) but the rest aids readability of the decode. */
|
||||
typedef enum {
|
||||
INSN_OTHER = 0,
|
||||
INSN_MOV, INSN_ARITH, INSN_LOGIC, INSN_CMP, INSN_TEST,
|
||||
INSN_BRANCH, /* conditional jcc / setcc / cmovcc */
|
||||
INSN_JMP, /* unconditional jmp */
|
||||
INSN_CALL, INSN_RET, INSN_PUSH, INSN_POP, INSN_LEA, INSN_NOP,
|
||||
INSN_FLOAT, INSN_VEC
|
||||
} insn_class;
|
||||
|
||||
/* Caps on the rich decode. Four explicit operands cover x86-64; eight register
|
||||
* refs per side cover the widest implicit sets (e.g. mul touching rax/rdx plus an
|
||||
* explicit source) with margin. Anything beyond is truncated (count still grows
|
||||
* past the cap so a caller can detect it), which never happens for real code. */
|
||||
#define INSN_MAX_OPERANDS 4
|
||||
#define INSN_MAX_REGREFS 8
|
||||
|
||||
/* A fully decoded instruction. Two complementary representations:
|
||||
* op[] - explicit operands, in instruction operand order (readability);
|
||||
* reads[] / writes[] - aggregate register sets over canonical roots, UNIONING
|
||||
* explicit AND implicit effects (this is what def-use consumes). A
|
||||
* memory operand's base/index registers appear in reads[] as address
|
||||
* reads; FLAGS appears as REGID_FLAGS; rsp/rdx:rax appear via the
|
||||
* backend's implicit operands. */
|
||||
typedef struct {
|
||||
uint8_t length; /* full instruction length in bytes (>= 1) */
|
||||
uint8_t mnemonic_class; /* insn_class */
|
||||
uint8_t is_control_flow; /* 1 if branch/call/ret */
|
||||
uint8_t noperands; /* explicit operands in op[] (<= INSN_MAX_OPERANDS)*/
|
||||
uint8_t nreads; /* register refs in reads[] */
|
||||
uint8_t nwrites; /* register refs in writes[] */
|
||||
operand op[INSN_MAX_OPERANDS];
|
||||
reg_ref reads[INSN_MAX_REGREFS];
|
||||
reg_ref writes[INSN_MAX_REGREFS];
|
||||
} insn_decoded;
|
||||
|
||||
/* Decode ONE instruction at view[off .. ]. `view` is a flat byte span and `off`
|
||||
* is the byte offset of the instruction within it; at most (view_size - off)
|
||||
* bytes are available to the backend. Returns 1 on success (fills *out), 0 on
|
||||
* undecodable / not enough bytes / a build with no backend (VMIE_HAVE_DISASM==0).
|
||||
* out->length == 0 is also treated as a desync by callers. PURE: only the byte
|
||||
* span and the decoder, no I/O, no allocation, reentrant.
|
||||
*
|
||||
* `view` / `view_size` are a flat buffer (not mem_view_t) so this primitive
|
||||
* stays dependency-light; the caller owns the buffer.
|
||||
*
|
||||
* Example - read the registers an instruction writes:
|
||||
* insn_decoded d;
|
||||
* if (insn_decode(code, n, 0, &d))
|
||||
* for (int i = 0; i < d.nwrites; i++)
|
||||
* printf("writes root %u (zx=%u)\n",
|
||||
* d.writes[i].root, d.writes[i].writes_zero_extend); */
|
||||
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||
insn_decoded* out);
|
||||
|
||||
/* Stable identity of the compiled disassembler backend ("zydis"/"capstone", or
|
||||
* "none" without a backend). Shares the backend with semsig; this name is the
|
||||
* same backend. Static, never-NULL. */
|
||||
const char* insndec_backend_name(void);
|
||||
|
||||
#endif /* VMIE_INSNDEC_H */
|
||||
+46
-2
@@ -62,7 +62,47 @@ vmie_mem* vmie_mem_open(const char* path, uint64_t low);
|
||||
* Returns a heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */
|
||||
vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg);
|
||||
|
||||
/* Unmap, close, and free a handle from vmie_mem_open*. Safe on NULL. */
|
||||
/* Build a source from an already-open file descriptor `fd` instead of a path,
|
||||
* with the single-`low` map (as vmie_mem_open). `fd` is BORROWED: it is dup()'d
|
||||
* internally, so the source owns its own copy and the caller's `fd` stays open
|
||||
* and valid both after this call AND after vmie_mem_close(). `fd` must reference
|
||||
* an mmap-able, read-write object (a regular file, memfd_create, or POSIX shm) -
|
||||
* a pipe/socket/tty is not mmap-able and yields NULL. The fd's file position is
|
||||
* not read or changed (the mapping starts at offset 0). Returns a heap-owned
|
||||
* handle, or NULL on dup/fstat/mmap failure. Release with vmie_mem_close(). */
|
||||
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low);
|
||||
|
||||
/* As vmie_mem_from_fd, but with an explicit segment map (`nseg` entries; see
|
||||
* gpa_seg), well-formed against the file size. Same dup-borrow semantics: the
|
||||
* caller's `fd` stays valid after this call and after vmie_mem_close(); `fd`
|
||||
* must be an mmap-able RW object and its position is untouched. Returns a
|
||||
* heap-owned handle, or NULL on failure. Release with vmie_mem_close(). */
|
||||
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg);
|
||||
|
||||
/* Read-only twin of vmie_mem_from_fd: build a source from `fd` with the
|
||||
* single-`low` map, but map it PROT_READ and mark it read-only. On the returned
|
||||
* handle gpa_write/gva_write (and any write through the engine) return -1; all
|
||||
* reads - gpa_read/gva_read, gpa_ptr/gva_ptr, gva_regions, sig_scan_mem - behave
|
||||
* exactly as on an RW source. Because only read access is needed, `fd` may be
|
||||
* opened O_RDONLY (an O_RDWR fd is also accepted). Same dup-borrow semantics: the
|
||||
* fd is dup()'d internally, so the caller's `fd` stays valid after this call AND
|
||||
* after vmie_mem_close(); its file position is not read or changed. NOTE: writing
|
||||
* THROUGH a pointer returned by gpa_ptr/gva_ptr on a read-only source is a
|
||||
* contract violation (the pages are read-only and the store will fault) - use
|
||||
* such pointers for reading only. Returns a heap-owned handle, or NULL on
|
||||
* dup/fstat/mmap failure. Release with vmie_mem_close(). */
|
||||
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low);
|
||||
|
||||
/* As vmie_mem_from_ro_fd, but with an explicit segment map (`nseg` entries; see
|
||||
* gpa_seg), well-formed against the file size. Same read-only contract
|
||||
* (writes -> -1; no writes through gpa_ptr/gva_ptr) and dup-borrow semantics
|
||||
* (caller's `fd` stays valid after this call and after vmie_mem_close(); accepts
|
||||
* an O_RDONLY or O_RDWR fd; position untouched). Returns a heap-owned handle, or
|
||||
* NULL on failure. Release with vmie_mem_close(). */
|
||||
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg);
|
||||
|
||||
/* Unmap, close, and free a handle from vmie_mem_open*, vmie_mem_from_fd*, or
|
||||
* vmie_mem_from_ro_fd*. Safe on NULL. */
|
||||
void vmie_mem_close(vmie_mem* m);
|
||||
|
||||
/* ---- flat memory view (single owner) ------------------------------------- *
|
||||
@@ -85,7 +125,11 @@ typedef struct {
|
||||
* x86-64 has no read bit: a present page is readable, so VR_R is always set on a
|
||||
* returned region. Write/execute/user are the EFFECTIVE rights along the whole
|
||||
* page-table path (RW & US are AND-ed across levels, NX is OR-ed), not just the
|
||||
* leaf entry, so they reflect what the guest CPU actually enforces. */
|
||||
* leaf entry, so they reflect what the guest CPU actually enforces. One
|
||||
* exception keeps VR_X honest under KVA shadow (KPTI): the kernel CR3 marks
|
||||
* user-half PML4Es NX as hardening, but ring 3 executes those pages via the
|
||||
* user/shadow CR3 (same shared lower tables, NX-clear PML4E), so the user-half
|
||||
* top-level NX bit is ignored when deriving VR_X. */
|
||||
#ifndef VMIE_VREGION_DEFINED
|
||||
#define VMIE_VREGION_DEFINED
|
||||
#define VR_R 0x1u /* readable (present => always set) */
|
||||
|
||||
+12
-9
@@ -70,18 +70,21 @@ int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint32_t prot_any, const sigset* s,
|
||||
sig_multi_hit* out, int max);
|
||||
|
||||
/* code-xref: every instruction in the X-regions of [lo,hi] whose near rel
|
||||
* branch or RIP-relative memory operand resolves to `target_va`. Brute-scans
|
||||
* each byte offset with the light x86-64 decoder (x86dec.h, NOT a full
|
||||
* disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
|
||||
/* code-xref: every instruction in the regions of [lo,hi] kept by `prot_any`
|
||||
* whose near rel branch or RIP-relative memory operand resolves to `target_va`.
|
||||
* Brute-scans each byte offset with the light x86-64 decoder (x86dec.h, NOT a
|
||||
* full disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
|
||||
* target_va, and any RIP-relative operand (ModRM mod=00, rm=101) matches when
|
||||
* next_rip + disp32 == target_va (this covers lea/mov and any other rip-rel
|
||||
* form). Records each matching instruction-start VA. The sweep forces VR_X and
|
||||
* carries a >=15-byte overlap (max x86 instruction length) so no instruction is
|
||||
* cut at a window seam. Writes up to `max` VAs to `out` (NULL to count only) and
|
||||
* returns the TOTAL number of matches, or -1 on bad input. */
|
||||
* form). Records each matching instruction-start VA. The sweep is scoped by
|
||||
* `prot_any` (pass VR_X to restrict to code; pass VR_R when the target's
|
||||
* page-table X bit is unavailable - the decoder and exact target match keep the
|
||||
* result correct, only more bytes are decoded) and carries a >=15-byte overlap
|
||||
* (max x86 instruction length) so no instruction is cut at a window seam. Writes
|
||||
* up to `max` VAs to `out` (NULL to count only) and returns the TOTAL number of
|
||||
* matches, or -1 on bad input. */
|
||||
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint64_t target_va, uint64_t* out, int max);
|
||||
uint32_t prot_any, uint64_t target_va, uint64_t* out, int max);
|
||||
|
||||
/* immediate / constant xref: every instruction in [lo,hi] (kept by the
|
||||
* protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/* semsig.h - generic (OS-agnostic) semantic / normalized code signature.
|
||||
*
|
||||
* Handler layer: a NORMALIZED function fingerprint that survives what the byte
|
||||
* world cannot. func_hash (codeanalysis.h) and sig_generate (siggen.h) are
|
||||
* BYTE fingerprints - both only neutralize the rel/RIP-relative displacement
|
||||
* bytes; they still change when the compiler shuffles registers, picks a
|
||||
* different equivalent register allocation, alters an immediate/displacement
|
||||
* VALUE, or reschedules independent instructions. semsig_hash instead decodes
|
||||
* OPERANDS (registers, operand classes, sizes, mnemonic group) and folds a
|
||||
* canonical token stream, so it answers "semantically the same function" rather
|
||||
* than "byte-for-byte the same function" - a FLIRT-like recognition primitive.
|
||||
*
|
||||
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
|
||||
* length-only). It is supplied by an OPTIONAL external disassembler (Zydis or
|
||||
* Capstone) selected at configure time, behind a private adapter seam. So this
|
||||
* primitive is FEATURE-GATED: with no backend the symbol still exists (ABI
|
||||
* stable) but returns the "no hash" sentinel 0, and VMIE_HAVE_DISASM is 0 (see
|
||||
* below). The byte world (func_hash / sig_generate) is untouched either way.
|
||||
*
|
||||
* NOT COMPARABLE TO func_hash. semsig_hash uses a different algorithm over a
|
||||
* different (normalized) input, so its values live in their OWN domain: never
|
||||
* compare a semsig_hash to a func_hash. Compare semsig_hash to semsig_hash only
|
||||
* (equality / a known-hash table), exactly as func_hash is used for library-ID.
|
||||
*/
|
||||
#ifndef VMIE_SEMSIG_H
|
||||
#define VMIE_SEMSIG_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
|
||||
|
||||
/* Compile-time feature availability. Pushed from CMake as a PUBLIC compile
|
||||
* definition so consumers of this header see the SAME value the library was
|
||||
* built with:
|
||||
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; semsig_hash is
|
||||
* live, semsig_backend_name() names the backend.
|
||||
* VMIE_HAVE_DISASM == 0 built without a backend (the default); semsig_hash
|
||||
* always returns 0 and semsig_backend_name() == "none".
|
||||
* Test it BEFORE calling to tell "feature off" apart from "empty/undecodable
|
||||
* function" - both return 0, but only the latter is a real input. The default
|
||||
* here keeps the header valid when compiled outside the project build. */
|
||||
#ifndef VMIE_HAVE_DISASM
|
||||
#define VMIE_HAVE_DISASM 0
|
||||
#endif
|
||||
|
||||
/* Reorder-/register-canonical semantic hash of one function view. `fn` is a view
|
||||
* spanning EXACTLY one function (e.g. a section-view sub-range covering a
|
||||
* func_range from vmie_win32_functions): fn.data[0] is the function's first
|
||||
* byte, fn.size its length. Reported in the view's own coordinate space, but the
|
||||
* hash is position-INDEPENDENT (like func_hash), so SECTION_LOCAL is enough for
|
||||
* a stable value.
|
||||
*
|
||||
* What it normalizes (the canonicalization contract):
|
||||
* - mnemonic CLASS is kept (the semantic backbone of each instruction);
|
||||
* - the concrete REGISTER identity is erased to its register CLASS, so a
|
||||
* rax<->rcx reshuffle does not change the hash;
|
||||
* - operand WIDTH is kept (mov al,_ differs from mov rax,_);
|
||||
* - immediate and displacement VALUES are erased (only "an imm of this width
|
||||
* is present" survives), as func_hash/sig_generate already do for rel/RIP;
|
||||
* - memory-operand SHAPE is kept (has_base / has_index / is_riprel flags, not
|
||||
* the register ids);
|
||||
* - operand ORDER is kept (mov dst,src differs from mov src,dst).
|
||||
*
|
||||
* REORDER INVARIANCE (v1 default behavior) AND ITS COST. The fold is two-level
|
||||
* and tied to the CFG. The function is split into basic blocks (via cfg_blocks),
|
||||
* and:
|
||||
* - WITHIN a block the per-instruction token hashes are folded ORDER-
|
||||
* INSENSITIVELY (sorted, then folded), so the compiler's intra-block
|
||||
* instruction scheduling - reordering independent instructions - does not
|
||||
* change the hash;
|
||||
* - BETWEEN blocks the order is PRESERVED (blocks are folded in cfg_blocks'
|
||||
* ascending-start order), because block order is the control-flow structure
|
||||
* and must stay significant.
|
||||
* The cost, stated plainly (this is a PROPERTY of the hash, not a free win): the
|
||||
* within-block order-insensitive fold LOWERS discrimination - two blocks with
|
||||
* the same MULTISET of normalized instructions but a different internal order
|
||||
* yield the SAME block hash, even when that order is semantically meaningful
|
||||
* (a data dependency this primitive does not model). That RAISES the false-match
|
||||
* (collision) probability versus an order-sensitive hash. It is a deliberate
|
||||
* trade for robustness against the scheduler; zero collisions are NOT promised.
|
||||
*
|
||||
* Returns the 64-bit semantic hash, or 0 on any of: `fn` empty (no data / size
|
||||
* 0), a decode desync (cfg_blocks could not split the bytes, or the backend
|
||||
* could not decode an instruction inside a block), or a build with no
|
||||
* disassembler backend (VMIE_HAVE_DISASM == 0). 0 is therefore "no hash", never
|
||||
* a valid fingerprint - the SAME sentinel as func_hash, so callers handle it the
|
||||
* same way.
|
||||
*
|
||||
* Token layout (stability contract). The per-instruction token folded by this
|
||||
* function is a fixed little-endian tuple; its layout is part of the hash's
|
||||
* stability contract (changing it changes every value). Per instruction:
|
||||
* byte 0 : mnemonic_class (the stable SEM_MN_* class id, low 8 bits)
|
||||
* byte 1 : mnemonic_class high (the SEM_MN_* class id, high 8 bits)
|
||||
* byte 2 : noperands (0..4)
|
||||
* byte 3 : flags (bit0 = is_control_flow)
|
||||
* bytes 4.. : per operand i in [0, noperands), 2 bytes each:
|
||||
* +0 : (kind & 0x0f) | ((reg_class & 0x0f) << 4)
|
||||
* +1 : (width_log2 & 0x07)
|
||||
* | (mem_has_base ? 0x08 : 0)
|
||||
* | (mem_has_index ? 0x10 : 0)
|
||||
* | (mem_is_riprel ? 0x20 : 0)
|
||||
* trailing : for the OTHER mnemonic class only, the raw opcode byte(s) so
|
||||
* unclassified instructions still differ from one another.
|
||||
* Operand order in the tuple follows the instruction's operand order (kept).
|
||||
*
|
||||
* Example - recognize a library function semantically (register-shuffle stable):
|
||||
* mem_view_t fn; // a SECTION_LOCAL sub-view of one function body
|
||||
* uint64_t h = semsig_hash(fn);
|
||||
* if (h && h == known_crt_memcpy_semsig) puts("looks like memcpy");
|
||||
*
|
||||
* Example - guard on availability before relying on the feature:
|
||||
* #if VMIE_HAVE_DISASM
|
||||
* uint64_t h = semsig_hash(fn); // live; 0 only on empty/desync
|
||||
* #else
|
||||
* // feature not built (semsig_hash would return 0)
|
||||
* #endif */
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
|
||||
/* Stable identity of the compiled disassembler backend, for diagnostics and for
|
||||
* tagging which decoder produced a stored hash. Returns a static, never-NULL
|
||||
* string: the backend name (e.g. "zydis", "capstone") when VMIE_HAVE_DISASM==1,
|
||||
* or "none" when built without a backend. The normalized hash is designed to be
|
||||
* domain-stable ACROSS backends (the mnemonic classes are backend-neutral), so
|
||||
* this name is informational, not a hash-comparison key. */
|
||||
const char* semsig_backend_name(void);
|
||||
|
||||
#endif /* VMIE_SEMSIG_H */
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
||||
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
||||
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
|
||||
#include "codetrace.h" /* insn_usedef, trace_abi (register def-use surface) */
|
||||
|
||||
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
|
||||
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
|
||||
@@ -83,6 +84,29 @@ typedef struct {
|
||||
* failure. Free with vmie_win32_close(). */
|
||||
vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low);
|
||||
|
||||
/* As vmie_win32_open, but over an already-open file descriptor `fd` for the RAM
|
||||
* backing file instead of a path. `fd` is BORROWED: it is dup()'d internally, so
|
||||
* the context owns its own copy and the caller's `fd` stays open and valid after
|
||||
* this call AND after vmie_win32_close(). `fd` must reference an mmap-able,
|
||||
* read-write object (regular file / memfd_create / POSIX shm); its file position
|
||||
* is not read or changed. Returns a new context (call host_bootstrap() next), or
|
||||
* NULL on dup/fstat/mmap failure. Free with vmie_win32_close(). */
|
||||
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low);
|
||||
|
||||
/* Open a READ-ONLY context over the RAM backing file `fd`, for a consumer that
|
||||
* only reads. It takes a caller-supplied `kcr3` (kernel cr3 / System DirectoryTableBase)
|
||||
* and builds the struct-offset profile read-only from the image (no bootstrap
|
||||
* handshake - beacon and ACK are not used). The result has the FULL read
|
||||
* capability: proc_list/proc_modules AND the section / function / import / export
|
||||
* / callgraph / hook surfaces, gva_read_text, and the generic gva_* and scanner
|
||||
* surfaces via vmie_win32_mem(). Any write (gva_write) returns -1 because the
|
||||
* mapping is PROT_READ. `fd` is BORROWED (dup()'d internally; the caller's fd
|
||||
* stays valid after this call AND after vmie_win32_close()); an O_RDONLY fd is
|
||||
* accepted. `low` is the below-4G split, as in vmie_win32_open_fd. Returns a new
|
||||
* context, or NULL if `kcr3` does not resolve a kernel (ntoskrnl not found under
|
||||
* it) or on dup/fstat/mmap failure. Free with vmie_win32_close(). */
|
||||
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3);
|
||||
|
||||
/* Unmap, close, and free a context. Safe on NULL. After this, every pointer
|
||||
* into guest memory obtained through this context is invalid. */
|
||||
void vmie_win32_close(vmie_win32* v);
|
||||
@@ -438,6 +462,71 @@ int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, uint32_t* iat_rvas, int max);
|
||||
|
||||
/* Reorder-/register-canonical SEMANTIC hash of ONE function, addressed by its
|
||||
* func_rva (a .pdata function start, as from vmie_win32_functions or an export).
|
||||
* The win32 convenience entry point over the generic semsig_hash (semsig.h): it
|
||||
* locates the function extent from .pdata (like vmie_win32_func_imports),
|
||||
* gathers its body with gva_read, builds a SECTION_LOCAL mem_view_t over those
|
||||
* bytes, and DELEGATES to semsig_hash - it does NOT reimplement normalization or
|
||||
* folding, and it pulls in no disassembler backend itself (the feature is
|
||||
* inherited transitively via the same VMIE_HAVE_DISASM as the generic).
|
||||
*
|
||||
* v - engine handle.
|
||||
* cr3 - the process address space the module is mapped in.
|
||||
* module_base - image base VA (its PE headers must be resident).
|
||||
* func_rva - function start RVA (must match a .pdata function start).
|
||||
*
|
||||
* Returns the 64-bit semantic hash, or 0 on any of: func_rva is not a known
|
||||
* function start, the body is unreadable (paged out), a decode desync, or the
|
||||
* build has no disassembler backend (VMIE_HAVE_DISASM == 0) - the SAME "no hash"
|
||||
* sentinel as semsig_hash / func_hash. NOT COMPARABLE to func_hash: it is a
|
||||
* different (normalized) domain; compare it only to other vmie_win32_func_semsig
|
||||
* / semsig_hash values. semsig_hash is position-independent, so the SECTION_LOCAL
|
||||
* gather gives a stable value regardless of where the module is based.
|
||||
*
|
||||
* Reorder invariance and its cost (lowered intra-block discrimination => higher
|
||||
* collision risk) are properties of the underlying semsig_hash - see semsig.h.
|
||||
*
|
||||
* Example - name a function semantically against a known-hash table:
|
||||
* uint64_t h = vmie_win32_func_semsig(v, pr->cr3, m.base, fn_rva);
|
||||
* if (h && h == known_crt_strlen_semsig) puts("strlen"); */
|
||||
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||
uint64_t module_base, uint32_t func_rva);
|
||||
|
||||
/* Register def-use of ONE function, addressed by its func_rva (a .pdata function
|
||||
* start, as from vmie_win32_functions or an export). The win32 convenience entry
|
||||
* point over the generic func_usedef (codetrace.h): it locates the function
|
||||
* extent from .pdata (like vmie_win32_func_semsig), gathers its body with
|
||||
* gva_read, builds a SECTION_LOCAL mem_view_t, and DELEGATES to func_usedef -
|
||||
* PASSING TRACE_ABI_WIN64, because choosing the Win64 calling convention is the
|
||||
* OS-specific decision that belongs in this layer (which already owns .pdata /
|
||||
* cr3 / module_base). It does NOT reimplement the analysis or apply the volatile
|
||||
* clobber itself (func_usedef does), and pulls in no disassembler backend (the
|
||||
* feature is inherited transitively via the same VMIE_HAVE_DISASM as the generic).
|
||||
*
|
||||
* v - engine handle.
|
||||
* cr3 - the process address space the module is mapped in.
|
||||
* module_base - image base VA (its PE headers must be resident).
|
||||
* func_rva - function start RVA (must match a .pdata function start).
|
||||
* out, max - caller array receiving up to `max` per-instruction insn_usedef
|
||||
* records in ascending function-local offset order (NULL to count).
|
||||
*
|
||||
* Returns the TOTAL instruction count, 0 if the function is empty, or -1 on any
|
||||
* of: func_rva is not a known function start, the body is unreadable (paged out),
|
||||
* a decode desync, or a build with no disassembler backend (VMIE_HAVE_DISASM==0).
|
||||
* The off field of each record is a function-local byte offset (SECTION_LOCAL).
|
||||
*
|
||||
* The Win64 volatile clobber applied at call sites - and its honest limits (only
|
||||
* the standard Win64 convention; not __vectorcall/custom; the return value is not
|
||||
* a separate def) - are documented at the clobber table in codetrace.c.
|
||||
*
|
||||
* Example - registers each instruction of a function kills:
|
||||
* insn_usedef ud[512];
|
||||
* int n = vmie_win32_func_usedef(v, pr->cr3, m.base, fn_rva, ud, 512);
|
||||
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
|
||||
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, insn_usedef* out, int max);
|
||||
|
||||
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
||||
* COMPOSITION of primitives the engine already exposes:
|
||||
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
||||
|
||||
+154
-18
@@ -52,6 +52,9 @@ int gpa_read(vmie_mem* m, uintptr_t offs, void* buf, const size_t nmemb) {
|
||||
}
|
||||
|
||||
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, const size_t nmemb) {
|
||||
if (m->ro) {
|
||||
return -1;
|
||||
}
|
||||
if (out_of_bounds(m, &offs, nmemb)) {
|
||||
return -1;
|
||||
}
|
||||
@@ -88,37 +91,64 @@ static int segs_valid(const gpa_seg* segs, int nseg, uint64_t fsize) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Finish a vmie_mem over an ALREADY-OWNED fd (from open() or dup()): fstat it,
|
||||
* validate the seg map against its real size, mmap it shared (PROT_READ when
|
||||
* `ro`, else PROT_RW), and fill `m`. On ANY failure close `fd` and clean `m`
|
||||
* (no partial state); on success store the fd in `m->fd`, record `ro`, and
|
||||
* return 0. The fd's lifetime is now owned by `m` (closed in gpa_close). The
|
||||
* single mmap/validate site shared by every constructor. */
|
||||
__attribute__((cold))
|
||||
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
|
||||
static int gpa_map_owned_fd(vmie_mem* m, int fd, const gpa_seg* segs, int nseg, int ro) {
|
||||
struct stat st;
|
||||
|
||||
if ((m->fd = open(path, O_RDWR)) < 0) {
|
||||
goto ret_;
|
||||
}
|
||||
|
||||
if (fstat(m->fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
|
||||
if (fstat(fd, &st) || !segs_valid(segs, nseg, (uint64_t)st.st_size)) {
|
||||
goto close_;
|
||||
}
|
||||
|
||||
if ((m->pa = mmap(NULL, st.st_size, PROT_RW, MAP_SHARED, m->fd, 0)) == MAP_FAILED) {
|
||||
const int prot = ro ? PROT_READ : PROT_RW;
|
||||
if ((m->pa = mmap(NULL, st.st_size, prot, MAP_SHARED, fd, 0)) == MAP_FAILED) {
|
||||
close_:
|
||||
close(m->fd);
|
||||
ret_:
|
||||
close(fd);
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
|
||||
m->fd = fd;
|
||||
m->fsize = st.st_size;
|
||||
m->nseg = nseg;
|
||||
m->ro = ro;
|
||||
memcpy(m->seg, segs, (size_t)nseg * sizeof *segs);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Convenience: the classic single-`low` QEMU map. Below the 4 GiB PCI hole the
|
||||
* file maps 1:1 ([0,low)->file[0,low)); at and above 4 GiB it resumes at file
|
||||
* offset low. When low >= fsize the hole is never reached, so one inert identity
|
||||
* seg covering the whole image suffices. */
|
||||
/* Build the classic single-`low` QEMU map into out[0..nseg) and return nseg.
|
||||
* Below the 4 GiB PCI hole the file maps 1:1 ([0,low)->file[0,low)); at and
|
||||
* above 4 GiB it resumes at file offset low. When low >= fsize the hole is never
|
||||
* reached, so one inert identity seg covering the whole image suffices (nseg=1).
|
||||
* The single low-map site shared by gpa_open (fsize from stat) and gpa_from_fd
|
||||
* (fsize from fstat). */
|
||||
__attribute__((cold))
|
||||
static int low_segs(uint64_t fsize, uint64_t low, gpa_seg out[2]) {
|
||||
if (low >= fsize) {
|
||||
out[0] = (gpa_seg){ 0, fsize, 0 };
|
||||
return 1;
|
||||
}
|
||||
out[0] = (gpa_seg){ 0, low, 0 };
|
||||
out[1] = (gpa_seg){ RAM_H, fsize - low, low };
|
||||
return 2;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg) {
|
||||
const int fd = open(path, O_RDWR);
|
||||
if (fd < 0) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
return gpa_map_owned_fd(m, fd, segs, nseg, 0);
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
|
||||
struct stat st;
|
||||
@@ -126,16 +156,70 @@ int gpa_open(vmie_mem* m, const char* path, uintptr_t low) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
const uint64_t fsize = (uint64_t)st.st_size;
|
||||
|
||||
const gpa_seg one[1] = { { 0, fsize, 0 } };
|
||||
const gpa_seg two[2] = { { 0, low, 0 }, { RAM_H, fsize > low ? fsize - low : 0, low } };
|
||||
const gpa_seg* segs = low >= fsize ? one : two;
|
||||
const int nseg = low >= fsize ? 1 : 2;
|
||||
gpa_seg segs[2];
|
||||
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
|
||||
|
||||
return gpa_open_segs(m, path, segs, nseg);
|
||||
}
|
||||
|
||||
/* fd-backed constructors: build a vmie_mem from an already-open file descriptor
|
||||
* instead of a path. The fd is BORROWED via dup() - core owns the copy (closed
|
||||
* in gpa_close); the caller's fd stays valid. The fd must reference an mmap-able
|
||||
* RW object (regular file / memfd / shm); its file position is not used. */
|
||||
__attribute__((cold))
|
||||
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
|
||||
const int dupfd = dup(fd);
|
||||
if (dupfd < 0) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
return gpa_map_owned_fd(m, dupfd, segs, nseg, 0);
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
int gpa_from_fd(vmie_mem* m, int fd, uintptr_t low) {
|
||||
struct stat st;
|
||||
if (fstat(fd, &st)) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
|
||||
gpa_seg segs[2];
|
||||
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
|
||||
|
||||
return gpa_from_fd_segs(m, fd, segs, nseg);
|
||||
}
|
||||
|
||||
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
|
||||
* source read-only (m->ro), so gpa_write/gva_write return -1. Same dup-borrow
|
||||
* semantics (core owns the copy, caller's fd stays valid). Accept O_RDONLY as
|
||||
* well as O_RDWR fds - only read access is required for a PROT_READ mapping.
|
||||
* Reads/gpa_ptr/scan behave exactly as on an RW source. */
|
||||
__attribute__((cold))
|
||||
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg) {
|
||||
const int dupfd = dup(fd);
|
||||
if (dupfd < 0) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
return gpa_map_owned_fd(m, dupfd, segs, nseg, 1);
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
int gpa_from_ro_fd(vmie_mem* m, int fd, uintptr_t low) {
|
||||
struct stat st;
|
||||
if (fstat(fd, &st)) {
|
||||
clean_ctx(m);
|
||||
return -1;
|
||||
}
|
||||
|
||||
gpa_seg segs[2];
|
||||
const int nseg = low_segs((uint64_t)st.st_size, low, segs);
|
||||
|
||||
return gpa_from_ro_fd_segs(m, fd, segs, nseg);
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
void gpa_close(vmie_mem* m) {
|
||||
if (m->pa) {
|
||||
@@ -180,6 +264,58 @@ vmie_mem* vmie_mem_open_segs(const char* path, const gpa_seg* segs, int nseg) {
|
||||
return m;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
vmie_mem* vmie_mem_from_fd(int fd, uint64_t low) {
|
||||
vmie_mem* m = calloc(1, sizeof *m);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_fd(m, fd, (uintptr_t)low)) {
|
||||
free(m);
|
||||
return NULL;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
vmie_mem* vmie_mem_from_fd_segs(int fd, const gpa_seg* segs, int nseg) {
|
||||
vmie_mem* m = calloc(1, sizeof *m);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_fd_segs(m, fd, segs, nseg)) {
|
||||
free(m);
|
||||
return NULL;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
vmie_mem* vmie_mem_from_ro_fd(int fd, uint64_t low) {
|
||||
vmie_mem* m = calloc(1, sizeof *m);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_ro_fd(m, fd, (uintptr_t)low)) {
|
||||
free(m);
|
||||
return NULL;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
vmie_mem* vmie_mem_from_ro_fd_segs(int fd, const gpa_seg* segs, int nseg) {
|
||||
vmie_mem* m = calloc(1, sizeof *m);
|
||||
if (!m) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_ro_fd_segs(m, fd, segs, nseg)) {
|
||||
free(m);
|
||||
return NULL;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
void vmie_mem_close(vmie_mem* m) {
|
||||
if (!m) {
|
||||
|
||||
@@ -20,6 +20,7 @@ typedef struct vmie_mem {
|
||||
int fd;
|
||||
int nseg;
|
||||
gpa_seg seg[VMIE_MAX_SEGS];
|
||||
int ro; /* source is read-only (gpa_write/gva_write -> -1); 0 = RW */
|
||||
} vmie_mem;
|
||||
|
||||
/* GPA <-> file-offset converters over the segment map.
|
||||
@@ -40,6 +41,15 @@ static inline uintptr_t offset_gpa(const vmie_mem* m, uintptr_t off) {
|
||||
/* guest-physical lifecycle + primitives (gpa.c) */
|
||||
int gpa_open_segs(vmie_mem* m, const char* path, const gpa_seg* segs, int nseg);
|
||||
int gpa_open (vmie_mem* m, const char* path, uintptr_t low);
|
||||
/* fd-backed twins of gpa_open*: build from a borrowed fd (dup'd internally,
|
||||
* caller's fd stays valid) instead of a path. fd must be an mmap-able RW object. */
|
||||
int gpa_from_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
|
||||
int gpa_from_fd (vmie_mem* m, int fd, uintptr_t low);
|
||||
/* read-only twins of gpa_from_fd*: map the borrowed fd PROT_READ and mark the
|
||||
* source read-only (gpa_write/gva_write -> -1). Accept O_RDONLY as well as
|
||||
* O_RDWR fds (only read access is required). Reads/gpa_ptr/scan behave as RW. */
|
||||
int gpa_from_ro_fd_segs(vmie_mem* m, int fd, const gpa_seg* segs, int nseg);
|
||||
int gpa_from_ro_fd (vmie_mem* m, int fd, uintptr_t low);
|
||||
void gpa_close(vmie_mem* m);
|
||||
int gpa_read (vmie_mem* m, uintptr_t offs, void* dst, size_t nmemb);
|
||||
int gpa_write(vmie_mem* m, uintptr_t offs, const void* src, size_t nmemb);
|
||||
|
||||
+10
-1
@@ -158,7 +158,16 @@ int gva_regions(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
if (!(e4 & PG_P)) continue;
|
||||
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
|
||||
if (!rgn_hit(b4, 1ull << 39, lo, hi)) continue;
|
||||
const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1, nx4 = (int)(e4 >> 63) & 1;
|
||||
const int rw4 = (e4 >> 1) & 1, us4 = (e4 >> 2) & 1;
|
||||
/* On a KVA-shadow (KPTI) Windows the kernel CR3 marks user-half PML4Es
|
||||
* NX as hardening: the kernel must not execute user pages through its own
|
||||
* mapping. Ring 3 runs under the user/shadow CR3, which reaches the SAME
|
||||
* lower tables (PDPT/PD/PT are shared) through a PML4E with NX clear, so
|
||||
* that top-level NX is not what executes user code. Drop it on the user
|
||||
* half; sub-PML4E NX stays authoritative (it is shared between the two
|
||||
* CR3s). On a non-KPTI guest a code-covering user PML4E already has NX
|
||||
* clear, so this only ever discards a zero. i4 < 256 == canonical user. */
|
||||
const int nx4 = (i4 < 256) ? 0 : ((int)(e4 >> 63) & 1);
|
||||
|
||||
const uint64_t* t3 = gpa_ptr(m, e4 & PFN_MASK, 4096);
|
||||
if (!t3) continue;
|
||||
|
||||
+65
-22
@@ -6,6 +6,9 @@
|
||||
|
||||
#define MZ 0x5A4Du
|
||||
|
||||
__attribute__((cold))
|
||||
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3);
|
||||
|
||||
/* ---- lifecycle (cold) ---------------------------------------------------- */
|
||||
|
||||
__attribute__((cold))
|
||||
@@ -21,6 +24,40 @@ vmie_win32* vmie_win32_open(const char* ram_path, uint64_t low) {
|
||||
return v;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
vmie_win32* vmie_win32_open_fd(int fd, uint64_t low) {
|
||||
vmie_win32* v = calloc(1, sizeof *v);
|
||||
if (!v) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_fd(&v->mem, fd, low)) {
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Read-only consumer context: maps the backing file PROT_READ and builds the
|
||||
* struct-offset profile from the image under the caller-supplied kcr3 (no
|
||||
* beacon, no cr3 recovery, no ACK write). All read APIs work, incl.
|
||||
* proc_list/proc_modules; writes return -1. */
|
||||
__attribute__((cold))
|
||||
vmie_win32* vmie_win32_open_ro_fd(int fd, uint64_t low, uint64_t kcr3) {
|
||||
vmie_win32* v = calloc(1, sizeof *v);
|
||||
if (!v) {
|
||||
return NULL;
|
||||
}
|
||||
if (gpa_from_ro_fd(&v->mem, fd, low)) {
|
||||
free(v);
|
||||
return NULL;
|
||||
}
|
||||
if (host_profile_from_cr3(v, (uintptr_t)kcr3)) {
|
||||
vmie_win32_close(v);
|
||||
return NULL;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
void vmie_win32_close(vmie_win32* v) {
|
||||
if (!v) {
|
||||
@@ -153,6 +190,31 @@ static uint32_t ko_export_rva(vmie_mem* m, uintptr_t cr3, uint64_t kbase, const
|
||||
return 0;
|
||||
}
|
||||
|
||||
__attribute__((cold))
|
||||
static int host_profile_from_cr3(vmie_win32* v, uintptr_t cr3) {
|
||||
vmie_mem* m = &v->mem;
|
||||
uint8_t guid[16];
|
||||
uint32_t age, rva;
|
||||
uint64_t sys_ep, dtb;
|
||||
|
||||
if (find_ntoskrnl(m, cr3, &v->kbase, guid, &age)) {
|
||||
return -3;
|
||||
}
|
||||
rva = ko_export_rva(m, cr3, v->kbase, "PsInitialSystemProcess");
|
||||
if (!rva || gva_read(m, cr3, v->kbase + rva, &sys_ep, 8)) {
|
||||
return -4;
|
||||
}
|
||||
if (profile_build(v, cr3, sys_ep, guid, age)) {
|
||||
return -5;
|
||||
}
|
||||
if (gva_read(m, cr3, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
|
||||
return -6;
|
||||
}
|
||||
v->kcr3 = dtb & PFN_MASK;
|
||||
v->sysproc = sys_ep;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void beacon_ack(vmie_mem* m, uint64_t anchor_pa) {
|
||||
uint64_t ack = CONTRACT_MAGIC_R;
|
||||
gpa_write(m, anchor_pa + offsetof(contract, ack), &ack, 8);
|
||||
@@ -167,10 +229,6 @@ int host_bootstrap(vmie_win32* v) {
|
||||
vmie_mem* m = &v->mem;
|
||||
uint64_t anchor_pa, va_self;
|
||||
uintptr_t cr3boot;
|
||||
uint32_t rva;
|
||||
uint8_t guid[16];
|
||||
uint32_t age;
|
||||
uint64_t sys_ep;
|
||||
|
||||
if (beacon_find(m, &anchor_pa, &va_self)) {
|
||||
return -1;
|
||||
@@ -180,26 +238,11 @@ int host_bootstrap(vmie_win32* v) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (find_ntoskrnl(m, cr3boot, &v->kbase, guid, &age)) {
|
||||
return -3;
|
||||
const int rc = host_profile_from_cr3(v, cr3boot);
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
rva = ko_export_rva(m, cr3boot, v->kbase, "PsInitialSystemProcess");
|
||||
if (!rva || gva_read(m, cr3boot, v->kbase + rva, &sys_ep, 8)) {
|
||||
return -4;
|
||||
}
|
||||
|
||||
if (profile_build(v, cr3boot, sys_ep, guid, age)) {
|
||||
return -5;
|
||||
}
|
||||
|
||||
uint64_t dtb;
|
||||
if (gva_read(m, cr3boot, sys_ep + v->prof.ep_dtb, &dtb, 8)) {
|
||||
return -6;
|
||||
}
|
||||
v->kcr3 = dtb & PFN_MASK;
|
||||
v->sysproc = sys_ep;
|
||||
|
||||
beacon_ack(m, anchor_pa);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */
|
||||
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
|
||||
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
|
||||
#include "semsig.h" /* semsig_hash (generic; no backend header pulled here) */
|
||||
#include "codetrace.h" /* func_usedef (generic def-use; no backend header here) */
|
||||
|
||||
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
|
||||
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
|
||||
@@ -775,3 +777,103 @@ int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
free(fb);
|
||||
return total;
|
||||
}
|
||||
|
||||
/* Semantic hash of one function by func_rva: reuse the func_imports gather (find
|
||||
* the extent from .pdata, read the body), then delegate to the generic
|
||||
* semsig_hash over a SECTION_LOCAL view. No normalization/fold is duplicated
|
||||
* here, and no backend header is included - the feature is inherited via the
|
||||
* generic (a stub returning 0 in an OFF build). Cold: one-shot per function. */
|
||||
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||
uint64_t module_base, uint32_t func_rva)
|
||||
__attribute__((cold));
|
||||
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||
uint64_t module_base, uint32_t func_rva) {
|
||||
vmie_mem* m = vmie_win32_mem(v);
|
||||
if (!m) { return 0; }
|
||||
|
||||
/* locate the function's extent from .pdata (count then gather). */
|
||||
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||
if (nfn < 0) { return 0; }
|
||||
func_range stack_fr[256];
|
||||
func_range* fr = stack_fr;
|
||||
func_range* heap_fr = NULL;
|
||||
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||
if (!heap_fr) { return 0; }
|
||||
fr = heap_fr;
|
||||
}
|
||||
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||
if (got < 0) { free(heap_fr); return 0; }
|
||||
uint32_t fn_size = 0;
|
||||
for (int f = 0; f < got; f++) {
|
||||
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
|
||||
}
|
||||
free(heap_fr);
|
||||
if (fn_size == 0) { return 0; } /* not a known function start */
|
||||
|
||||
/* gather the body and view it SECTION_LOCAL (base_va = 0): semsig_hash is
|
||||
* position-independent, so the local base is enough for a stable value. */
|
||||
uint8_t* fb = malloc(fn_size);
|
||||
if (!fb) { return 0; }
|
||||
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
|
||||
free(fb);
|
||||
return 0;
|
||||
}
|
||||
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
|
||||
const uint64_t h = semsig_hash(view);
|
||||
|
||||
free(fb);
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Register def-use of one function by func_rva: reuse the same .pdata extent
|
||||
* gather as vmie_win32_func_semsig (locate the function, read the body), then
|
||||
* delegate to the generic func_usedef over a SECTION_LOCAL view, PASSING
|
||||
* TRACE_ABI_WIN64 - the OS-specific "choose the Win64 convention" decision lives
|
||||
* here, in the layer that already owns .pdata / cr3 / module_base, exactly as the
|
||||
* extent lookup does. The analysis (block walk, the volatile clobber merge) is
|
||||
* NOT reimplemented or applied here; that lives once in func_usedef. No backend
|
||||
* header is included - the feature is inherited via the generic (a stub returning
|
||||
* -1 in an OFF build). Cold: one-shot per function. */
|
||||
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, insn_usedef* out, int max)
|
||||
__attribute__((cold));
|
||||
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||
uint32_t func_rva, insn_usedef* out, int max) {
|
||||
vmie_mem* m = vmie_win32_mem(v);
|
||||
if (!m) { return -1; }
|
||||
|
||||
/* locate the function's extent from .pdata (count then gather). */
|
||||
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||
if (nfn < 0) { return -1; }
|
||||
func_range stack_fr[256];
|
||||
func_range* fr = stack_fr;
|
||||
func_range* heap_fr = NULL;
|
||||
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||
if (!heap_fr) { return -1; }
|
||||
fr = heap_fr;
|
||||
}
|
||||
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||
if (got < 0) { free(heap_fr); return -1; }
|
||||
uint32_t fn_size = 0;
|
||||
for (int f = 0; f < got; f++) {
|
||||
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
|
||||
}
|
||||
free(heap_fr);
|
||||
if (fn_size == 0) { return -1; } /* not a known function start */
|
||||
|
||||
/* gather the body and view it SECTION_LOCAL (base_va = 0): func_usedef reports
|
||||
* in the view's own coordinate space, so off is a function-local byte offset. */
|
||||
uint8_t* fb = malloc(fn_size);
|
||||
if (!fb) { return -1; }
|
||||
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
|
||||
free(fb);
|
||||
return -1;
|
||||
}
|
||||
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
|
||||
const int n = func_usedef(view, TRACE_ABI_WIN64, out, max);
|
||||
|
||||
free(fb);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ static int xref_sweep_cb(void* u, const uint8_t* data, size_t len,
|
||||
}
|
||||
|
||||
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
||||
uint64_t target_va, uint64_t* out, int max) {
|
||||
uint32_t prot_any, uint64_t target_va, uint64_t* out, int max) {
|
||||
struct xref_cb c; memset(&c, 0, sizeof c);
|
||||
c.target = target_va; c.out = out; c.max = max;
|
||||
if (gva_sweep(m, cr3, lo, hi, VR_X, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
|
||||
if (gva_sweep(m, cr3, lo, hi, prot_any, X86_MAX_INSN, xref_sweep_cb, &c) < 0) {
|
||||
return -1;
|
||||
}
|
||||
return c.n;
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/* codetrace.c - generic register def-use over one function (see codetrace.h).
|
||||
*
|
||||
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||
* OFF build compiles insndec_stub.c (which also carries func_usedef's sentinel).
|
||||
* It turns one function view into per-instruction register use/def:
|
||||
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||
* not a second splitter - to validate the partition and iterate in order;
|
||||
* 2. step each block with insn_decode (insndec.h), and from each instruction's
|
||||
* aggregate reads[]/writes[] build the use/def/def_partial/cond_def sets;
|
||||
* 3. at a call site, under a non-NONE trace_abi, merge the static ABI volatile
|
||||
* clobber into `def` as a FULL kill.
|
||||
*
|
||||
* Boundary: includes ONLY insndec.h, codeanalysis.h (cfg_blocks/code_block) and
|
||||
* codetrace.h plus standard headers. It NEVER includes a backend's own headers -
|
||||
* the decode rides insn_decode, which is itself behind the one backend seam.
|
||||
*
|
||||
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h> /* malloc/free (block-list overflow only) */
|
||||
#include "insndec.h"
|
||||
#include "codetrace.h"
|
||||
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||
|
||||
/* Cap on the stack block array; denser functions spill the list to the heap
|
||||
* (same scheme as semsig.c). */
|
||||
#define CFG_MAX_BLOCKS 512
|
||||
|
||||
/* ---- regset: a bitset over canonical reg_id roots ------------------------ */
|
||||
|
||||
/* Set the bit for one reg_id root in a regset. The category fold mirrors the
|
||||
* regset field layout documented in codetrace.h:
|
||||
* GPR roots REGID_RAX..REGID_R15 -> gpr bit (root - REGID_RAX)
|
||||
* vec roots REGID_VEC0..REGID_VEC31 -> vec_lo bit (root - REGID_VEC0)
|
||||
* mask roots REGID_K0..REGID_K7 -> misc bit (root - REGID_K0)
|
||||
* FLAGS -> misc bit 8
|
||||
* OTHER -> misc bit 9
|
||||
* REGID_NONE and out-of-range ids are ignored (no overlap to track). */
|
||||
static void rs_set(regset* s, uint8_t root) {
|
||||
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||
s->gpr |= (uint16_t)(1u << (root - REGID_RAX));
|
||||
} else if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||
s->vec_lo |= (uint32_t)1u << (root - REGID_VEC0);
|
||||
} else if (root >= REGID_K0 && root <= REGID_K7) {
|
||||
s->misc |= (uint16_t)(1u << (root - REGID_K0));
|
||||
} else if (root == REGID_FLAGS) {
|
||||
s->misc |= (uint16_t)(1u << 8);
|
||||
} else if (root == REGID_OTHER) {
|
||||
s->misc |= (uint16_t)(1u << 9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Union b into a (pure value op; no branching on a concrete register). */
|
||||
static void rs_or(regset* a, const regset* b) {
|
||||
a->gpr |= b->gpr;
|
||||
a->vec_lo |= b->vec_lo;
|
||||
a->misc |= b->misc;
|
||||
}
|
||||
|
||||
/* ---- Win64 caller-saved (volatile) clobber table ------------------------- */
|
||||
|
||||
/* Caller-saved (volatile) roots clobbered across a Win64 call site. Applied as a
|
||||
* FULL kill in `def` (NOT def_partial/cond_def): after a standard Win64 call
|
||||
* these roots are dead from the caller's view. RAX additionally carries the
|
||||
* return value, but RAX is already in this set, so a return value needs no
|
||||
* separate def. Callee-saved roots are deliberately ABSENT (live across a call):
|
||||
* GPR saved: RBX RBP RDI RSI RSP R12 R13 R14 R15
|
||||
* vec saved: XMM6..XMM15 (VEC6..VEC15 roots)
|
||||
* RSP changes architecturally via call/ret itself (already reported by
|
||||
* insn_decode) and is callee-saved by VALUE, so it is NOT a clobber here.
|
||||
*
|
||||
* Selected by trace_abi: TRACE_ABI_WIN64 -> this set; TRACE_ABI_NONE -> the empty
|
||||
* set, reproducing the prior conservative behavior (no volatile kill on a call).
|
||||
*
|
||||
* HONEST LIMITS (named, not hidden): this models the STANDARD Win64 convention
|
||||
* ONLY - it does NOT catch __vectorcall / custom conventions; the return value is
|
||||
* not a separate def (RAX already covers it); the indirect-call target register
|
||||
* read is supplied by insn_decode, not by this table. */
|
||||
static regset win64_clobber(void) {
|
||||
regset s = (regset){ 0, 0, 0 };
|
||||
/* volatile GPR: RAX RCX RDX R8 R9 R10 R11 */
|
||||
rs_set(&s, REGID_RAX); rs_set(&s, REGID_RCX); rs_set(&s, REGID_RDX);
|
||||
rs_set(&s, REGID_R8); rs_set(&s, REGID_R9);
|
||||
rs_set(&s, REGID_R10); rs_set(&s, REGID_R11);
|
||||
/* volatile vector: XMM0..XMM5 (VEC0..VEC5 and their YMM/ZMM upper) */
|
||||
rs_set(&s, REGID_VEC0); rs_set(&s, REGID_VEC0 + 1); rs_set(&s, REGID_VEC0 + 2);
|
||||
rs_set(&s, REGID_VEC0 + 3); rs_set(&s, REGID_VEC0 + 4); rs_set(&s, REGID_VEC0 + 5);
|
||||
/* FLAGS */
|
||||
rs_set(&s, REGID_FLAGS);
|
||||
return s;
|
||||
}
|
||||
|
||||
/* The clobber selected by trace_abi (table ABI -> regset, by value). An unknown
|
||||
* abi falls to the conservative empty set. */
|
||||
static regset abi_clobber(trace_abi abi) {
|
||||
switch (abi) {
|
||||
case TRACE_ABI_WIN64: return win64_clobber();
|
||||
case TRACE_ABI_NONE:
|
||||
default: return (regset){ 0, 0, 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- per-instruction use/def -------------------------------------------- */
|
||||
|
||||
/* Build one insn_usedef from a decoded instruction. A write that zero-extends
|
||||
* (full/64-bit GPR or VEX/EVEX vector) goes to `def` (it KILLS the root); an
|
||||
* 8/16-bit GPR or legacy-SSE write is PARTIAL (-> def_partial, no kill); a
|
||||
* conditional write (cmovcc/setcc) goes to cond_def (no kill). Reads always go to
|
||||
* `use`. The aggregate sets from insn_decode already include implicit operands
|
||||
* (rsp/rdx:rax/FLAGS) and the address registers of memory operands. */
|
||||
static void usedef_of(const insn_decoded* d, insn_usedef* ud) {
|
||||
for (uint8_t i = 0; i < d->nreads && i < INSN_MAX_REGREFS; i++) {
|
||||
rs_set(&ud->use, d->reads[i].root);
|
||||
}
|
||||
for (uint8_t i = 0; i < d->nwrites && i < INSN_MAX_REGREFS; i++) {
|
||||
const reg_ref* w = &d->writes[i];
|
||||
if (w->access == ACC_COND_WRITE) {
|
||||
rs_set(&ud->cond_def, w->root);
|
||||
} else if (w->writes_zero_extend ||
|
||||
(w->root >= REGID_RAX && w->root <= REGID_R15 &&
|
||||
w->width_log2 >= 3)) {
|
||||
/* zero-extending (32-bit) OR a full 64-bit GPR write: a full kill. */
|
||||
rs_set(&ud->def, w->root);
|
||||
} else if (w->root >= REGID_VEC0 && w->root <= REGID_VEC31 &&
|
||||
w->writes_zero_extend) {
|
||||
rs_set(&ud->def, w->root);
|
||||
} else if (w->root == REGID_FLAGS || w->root >= REGID_K0) {
|
||||
/* FLAGS / mask / other roots: treat a write as a full def (no narrow
|
||||
* sub-register concept to preserve). */
|
||||
rs_set(&ud->def, w->root);
|
||||
} else {
|
||||
/* 8/16-bit GPR or legacy-SSE write: partial, does not kill the root. */
|
||||
rs_set(&ud->def_partial, w->root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
|
||||
if (!fn.data || fn.size == 0) { return 0; }
|
||||
|
||||
/* Validate the partition with cfg_blocks (count then gather); -1 is a desync. */
|
||||
const int nb = cfg_blocks(fn, NULL, 0);
|
||||
if (nb < 0) { return -1; }
|
||||
if (nb == 0) { return 0; }
|
||||
|
||||
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||
code_block* bb = stack_bb;
|
||||
code_block* heap_bb = NULL;
|
||||
if (nb > CFG_MAX_BLOCKS) {
|
||||
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||
if (!heap_bb) { return -1; }
|
||||
bb = heap_bb;
|
||||
}
|
||||
const int got = cfg_blocks(fn, bb, nb);
|
||||
if (got < 0) { free(heap_bb); return -1; }
|
||||
const int use = got < nb ? got : nb;
|
||||
|
||||
const regset clob = abi_clobber(abi);
|
||||
|
||||
/* Step instructions in ascending block order; cfg_blocks already partitions
|
||||
* [0, fn.size) with no gaps, so a linear walk per block covers the function. */
|
||||
int total = 0;
|
||||
for (int b = 0; b < use; b++) {
|
||||
const size_t start = bb[b].start;
|
||||
const size_t end = bb[b].end <= fn.size ? bb[b].end : fn.size;
|
||||
for (size_t off = start; off < end; ) {
|
||||
insn_decoded d;
|
||||
if (!insn_decode(fn.data, end, off, &d) || d.length == 0) {
|
||||
free(heap_bb);
|
||||
return -1; /* backend desync */
|
||||
}
|
||||
insn_usedef ud = (insn_usedef){ 0 };
|
||||
ud.off = (uint32_t)off;
|
||||
ud.len = d.length;
|
||||
usedef_of(&d, &ud);
|
||||
/* Win64 (or any non-NONE abi) call site: merge the volatile clobber
|
||||
* into `def` as a full kill. TRACE_ABI_NONE -> clob is empty (no-op). */
|
||||
if (d.mnemonic_class == INSN_CALL) {
|
||||
rs_or(&ud.def, &clob);
|
||||
}
|
||||
if (out && total < max) { out[total] = ud; }
|
||||
total++;
|
||||
off += d.length;
|
||||
}
|
||||
}
|
||||
|
||||
free(heap_bb);
|
||||
return total;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* insndec.c - generic delegate for the readable per-instruction decode.
|
||||
*
|
||||
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||
* OFF build compiles insndec_stub.c instead. It is a thin, ABI-agnostic wrapper
|
||||
* over the ONE backend seam (semsig_backend.h: insn_decode_full): it validates
|
||||
* the byte range and forwards to the backend, which fills the public insn_decoded.
|
||||
*
|
||||
* Boundary: includes ONLY insndec.h and semsig_backend.h (the seam) plus standard
|
||||
* headers. It NEVER includes a backend's own headers (Zydis/Capstone) - the rich
|
||||
* decode and the lossy semsig share that single seam. The backend name is the
|
||||
* same backend as semsig, so insndec_backend_name delegates to it.
|
||||
*
|
||||
* ABI-agnostic: nothing here knows about any calling convention. The Win64
|
||||
* volatile clobber lives in codetrace.c, never here.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "insndec.h"
|
||||
#include "semsig_backend.h" /* insn_decode_full, semsig_backend_name (seam) */
|
||||
|
||||
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||
insn_decoded* out) {
|
||||
if (!view || !out || off >= view_size) { return 0; }
|
||||
return insn_decode_full(view + off, view_size - off, out);
|
||||
}
|
||||
|
||||
const char* insndec_backend_name(void) {
|
||||
/* One backend, one identity: insndec rides the same decoder as semsig. */
|
||||
return semsig_backend_name();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/* insndec_stub.c - the OFF (no-backend) path of the readable decode + def-use.
|
||||
*
|
||||
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides the
|
||||
* full public generic surface of BOTH features so the ABI is stable - linking
|
||||
* never fails for a missing symbol - while the features are not present:
|
||||
* insn_decode -> 0 ("not decoded": same idea as the semsig sentinel)
|
||||
* insndec_backend_name -> "none"
|
||||
* func_usedef -> -1 (the cfg_blocks/desync sentinel; no backend)
|
||||
* Callers tell "feature off" from a real empty/undecodable input via the compile-
|
||||
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime sentinel.
|
||||
*
|
||||
* One stub carries both surfaces (fewer TUs). The pure types trace_abi / regset /
|
||||
* insn_decoded are visible from the headers even in OFF. In the OFF build
|
||||
* insndec.c / codetrace.c are NOT compiled (they call the backend, absent there);
|
||||
* this stub is the whole generic symbol. See CMakeLists.txt for the selection.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "insndec.h"
|
||||
#include "codetrace.h"
|
||||
|
||||
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||
insn_decoded* out) {
|
||||
(void)view; (void)view_size; (void)off; (void)out;
|
||||
return 0; /* feature not built: nothing decoded */
|
||||
}
|
||||
|
||||
const char* insndec_backend_name(void) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
|
||||
(void)fn; (void)abi; (void)out; (void)max;
|
||||
return -1; /* feature not built: same desync/no-backend sentinel as cfg */
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/* semsig.c - normalized (semantic) code-signature core (see semsig.h).
|
||||
*
|
||||
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||
* OFF build compiles semsig_stub.c instead. It turns one function view into a
|
||||
* register-/value-/reorder-canonical 64-bit hash:
|
||||
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||
* not a second splitter;
|
||||
* 2. step each block with the BACKEND decoder (semsig_backend.h), normalize
|
||||
* each instruction into a fixed token tuple (the layout documented in
|
||||
* semsig.h), and FNV-1a-fold that tuple into a per-instruction hash;
|
||||
* 3. fold a block's per-instruction hashes ORDER-INSENSITIVELY (sort, then
|
||||
* fold) so intra-block instruction scheduling does not change the result;
|
||||
* 4. fold the block hashes into the final hash IN cfg_blocks' order (block
|
||||
* order is the CFG structure and stays significant).
|
||||
*
|
||||
* Boundary: includes ONLY semsig.h, semsig_backend.h, codeanalysis.h (for
|
||||
* cfg_blocks / code_block) and the standard headers. It NEVER includes a
|
||||
* backend's own headers (Zydis/Capstone) - the seam is semsig_backend.h. The
|
||||
* pure core (x86dec, codeanalysis, ...) is reused, never modified.
|
||||
*
|
||||
* FNV-1a uses the SAME constants and scheme as codeanalysis.c so the hash world
|
||||
* stays stylistically consistent. They are NOT factored into a shared header for
|
||||
* a single second consumer (Rule of three: a tolerated 2nd appearance of two
|
||||
* short #defines); a 3rd consumer would justify a private util.
|
||||
*
|
||||
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h> /* malloc/free (per-block hash buffer overflow only) */
|
||||
#include "semsig.h"
|
||||
#include "semsig_backend.h"
|
||||
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||
|
||||
#define FNV64_OFFSET 0xcbf29ce484222325ull
|
||||
#define FNV64_PRIME 0x00000100000001b3ull
|
||||
|
||||
/* Caps. CFG_MAX_BLOCKS bounds the stack block array; functions with more blocks
|
||||
* spill the block list to the heap. INSN_STACK bounds the per-block per-insn
|
||||
* hash array on the stack; a denser block spills that one buffer to the heap. */
|
||||
#define CFG_MAX_BLOCKS 512
|
||||
#define INSN_STACK 256
|
||||
|
||||
/* Fold one byte into an FNV-1a accumulator. */
|
||||
static inline uint64_t fnv_byte(uint64_t h, uint8_t b) {
|
||||
h ^= b;
|
||||
h *= FNV64_PRIME;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Normalize ONE decoded instruction into its token tuple and return the
|
||||
* per-instruction FNV-1a hash of that tuple. The tuple layout is the stability
|
||||
* contract documented in semsig.h: mnemonic class, noperands, a flags byte, then
|
||||
* 2 bytes per operand (kind|reg_class, then width_log2|mem-shape flags), plus
|
||||
* the raw opcode for the OTHER class so unclassified instructions still differ.
|
||||
* Pure data->data: no branching on a concrete register, only on its class. */
|
||||
static uint64_t token_hash(const sem_insn* in) {
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
h = fnv_byte(h, (uint8_t)(in->mnemonic_class & 0xff));
|
||||
h = fnv_byte(h, (uint8_t)((in->mnemonic_class >> 8) & 0xff));
|
||||
const uint8_t nop = in->noperands <= 4 ? in->noperands : 4;
|
||||
h = fnv_byte(h, nop);
|
||||
h = fnv_byte(h, (uint8_t)(in->is_control_flow ? 0x01u : 0x00u));
|
||||
for (uint8_t i = 0; i < nop; i++) {
|
||||
const uint8_t b0 = (uint8_t)((in->op[i].kind & 0x0f) |
|
||||
((in->op[i].reg_class & 0x0f) << 4));
|
||||
const uint8_t b1 = (uint8_t)((in->op[i].width_log2 & 0x07) |
|
||||
(in->op[i].mem_has_base ? 0x08u : 0u) |
|
||||
(in->op[i].mem_has_index ? 0x10u : 0u) |
|
||||
(in->op[i].mem_is_riprel ? 0x20u : 0u));
|
||||
h = fnv_byte(h, b0);
|
||||
h = fnv_byte(h, b1);
|
||||
}
|
||||
if (in->mnemonic_class == SEM_MN_OTHER) {
|
||||
h = fnv_byte(h, in->raw_opcode); /* keep unclassified ops distinct */
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Ascending uint64_t comparator for the deterministic per-block sort. The sort
|
||||
* is what makes the within-block fold order-INSENSITIVE while keeping the
|
||||
* MULTISET (duplicates survive, their count is significant). A plain xor/sum was
|
||||
* REJECTED: xor cancels identical-instruction pairs (a block "loses" two equal
|
||||
* moves) and sum collides easily on multiset permutations; sort-then-fold avoids
|
||||
* both. */
|
||||
static int cmp_u64(const void* a, const void* b) {
|
||||
const uint64_t x = *(const uint64_t*)a;
|
||||
const uint64_t y = *(const uint64_t*)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
/* Fold one basic block [start, end) of `fn` into a block hash, stepping the
|
||||
* BACKEND decoder from start to end and order-insensitively folding the per-insn
|
||||
* hashes. Returns 1 on success and writes *out; returns 0 on backend desync
|
||||
* (an undecodable instruction inside the block) - the caller turns that into the
|
||||
* 0 "no hash" sentinel. */
|
||||
static int fold_block(mem_view_t fn, const code_block* blk, uint64_t* out) {
|
||||
const size_t start = blk->start;
|
||||
const size_t end = blk->end <= fn.size ? blk->end : fn.size;
|
||||
if (end <= start) { *out = FNV64_OFFSET; return 1; } /* empty block */
|
||||
|
||||
uint64_t stack_h[INSN_STACK];
|
||||
uint64_t* hh = stack_h;
|
||||
uint64_t* heap_h = NULL;
|
||||
const size_t span = end - start; /* >= 1 insn per byte */
|
||||
if (span > INSN_STACK) {
|
||||
heap_h = malloc(span * sizeof *heap_h);
|
||||
if (!heap_h) { return 0; }
|
||||
hh = heap_h;
|
||||
}
|
||||
|
||||
size_t n = 0;
|
||||
int ok = 1;
|
||||
for (size_t off = start; off < end; ) {
|
||||
sem_insn in;
|
||||
if (!semsig_backend_decode(fn.data + off, end - off, &in) ||
|
||||
in.length == 0) {
|
||||
ok = 0;
|
||||
break; /* backend desync */
|
||||
}
|
||||
hh[n++] = token_hash(&in);
|
||||
off += in.length;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
qsort(hh, n, sizeof *hh, cmp_u64); /* order-insensitive */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const uint64_t v = hh[i];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((v >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
*out = h;
|
||||
}
|
||||
|
||||
free(heap_h);
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
if (!fn.data || fn.size == 0) { return 0; }
|
||||
|
||||
/* Block partition (count then gather), REUSING cfg_blocks - -1 is a desync
|
||||
* and maps to the 0 sentinel. cfg_blocks gives ORDER-of-blocks boundaries;
|
||||
* the within-block step uses the backend, so a backend/x86_decode length
|
||||
* disagreement only ever turns into the 0 sentinel, never a wrong hash. */
|
||||
const int nb = cfg_blocks(fn, NULL, 0);
|
||||
if (nb <= 0) { return 0; }
|
||||
|
||||
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||
code_block* bb = stack_bb;
|
||||
code_block* heap_bb = NULL;
|
||||
if (nb > CFG_MAX_BLOCKS) {
|
||||
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||
if (!heap_bb) { return 0; }
|
||||
bb = heap_bb;
|
||||
}
|
||||
const int got = cfg_blocks(fn, bb, nb);
|
||||
if (got <= 0) { free(heap_bb); return 0; }
|
||||
const int use = got < nb ? got : nb;
|
||||
|
||||
/* Fold block hashes into the final hash IN cfg_blocks' ascending-start order:
|
||||
* block order is the CFG structure and stays significant (only WITHIN a
|
||||
* block is the fold order-insensitive). */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (int i = 0; i < use; i++) {
|
||||
uint64_t bh;
|
||||
if (!fold_block(fn, &bb[i], &bh)) { /* backend desync */
|
||||
free(heap_bb);
|
||||
return 0;
|
||||
}
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((bh >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
free(heap_bb);
|
||||
return h;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* semsig_backend.h - backend-agnostic operand-decode boundary (PRIVATE).
|
||||
*
|
||||
* The ONE place semsig.c talks to an external disassembler. Each concrete
|
||||
* backend (Zydis, Capstone, ...) provides exactly the entities below; semsig.c
|
||||
* includes THIS header and NEVER a backend's own headers. That keeps the
|
||||
* operand-decode dependency behind a single seam so the feature core stays
|
||||
* backend-neutral and the resulting hash is domain-stable across backends.
|
||||
*
|
||||
* This header is intentionally NOT in include/ - it is an implementation detail
|
||||
* of the feature, not public API. The public surface is semsig.h.
|
||||
*/
|
||||
#ifndef VMIE_SEMSIG_BACKEND_H
|
||||
#define VMIE_SEMSIG_BACKEND_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "insndec.h" /* insn_decoded - the rich decode type shared with the
|
||||
* public insndec.h surface; backend TUs fill it here so
|
||||
* the type is defined ONCE, not duplicated at the seam.
|
||||
* Raw backend enums still never leak (normalized in TU). */
|
||||
|
||||
/* Operand kind, normalized. Carried in sem_insn.op[].kind. */
|
||||
enum {
|
||||
SEMOP_NONE = 0, /* no operand in this slot */
|
||||
SEMOP_REG, /* register operand (identity erased, class kept) */
|
||||
SEMOP_MEM, /* memory operand (shape kept: base/index/riprel flags) */
|
||||
SEMOP_IMM /* immediate operand (value erased, width kept) */
|
||||
};
|
||||
|
||||
/* Canonical register class. Carried in sem_insn.op[].reg_class. The concrete
|
||||
* register id is NEVER carried across the seam - only its class - so a compiler
|
||||
* register reshuffle (rax<->rcx) does not change the normalized token. */
|
||||
enum {
|
||||
SEM_RC_NONE = 0, /* not a register / unknown */
|
||||
SEM_RC_GPR, /* general-purpose integer register (al..r15) */
|
||||
SEM_RC_XMM, /* vector register (xmm/ymm/zmm) */
|
||||
SEM_RC_MASK, /* AVX-512 mask register (k0..k7) */
|
||||
SEM_RC_SEG, /* segment register */
|
||||
SEM_RC_FLAGS, /* flags / control / other special register */
|
||||
SEM_RC_OTHER /* any register class not distinguished above */
|
||||
};
|
||||
|
||||
/* Backend-neutral mnemonic CLASS ids. NOT a 1:1 map of every mnemonic: only the
|
||||
* groups that matter for normalization. Each backend TU maps its own mnemonic
|
||||
* enum into these with a static table; unclassified mnemonics fall to
|
||||
* SEM_MN_OTHER, for which semsig.c additionally folds the raw opcode byte(s) so
|
||||
* they still differ from one another. These ids are part of the hash's
|
||||
* stability contract (see the token layout in semsig.h). */
|
||||
enum {
|
||||
SEM_MN_OTHER = 0, /* unclassified - raw opcode folded in (see semsig.h) */
|
||||
SEM_MN_MOV, /* register/memory moves (mov, movzx, movsx, lea, ...) */
|
||||
SEM_MN_ARITH, /* add/sub/inc/dec/neg/adc/sbb/mul/imul/div/idiv */
|
||||
SEM_MN_LOGIC, /* and/or/xor/not/shl/shr/sar/rol/ror */
|
||||
SEM_MN_CMP, /* cmp */
|
||||
SEM_MN_TEST, /* test */
|
||||
SEM_MN_BRANCH, /* conditional jump (jcc) / setcc / cmovcc */
|
||||
SEM_MN_JMP, /* unconditional jmp */
|
||||
SEM_MN_CALL, /* call */
|
||||
SEM_MN_RET, /* ret / retf / leave epilogue return */
|
||||
SEM_MN_PUSH, /* push */
|
||||
SEM_MN_POP, /* pop */
|
||||
SEM_MN_LEA, /* lea (address computation; kept distinct from mov) */
|
||||
SEM_MN_NOP, /* nop / padding */
|
||||
SEM_MN_FLOAT, /* x87/SSE/AVX float arithmetic */
|
||||
SEM_MN_VEC /* integer/other vector ops */
|
||||
};
|
||||
|
||||
/* A decoded instruction reduced to ONLY what normalization needs - no full
|
||||
* operand model leaks across the seam. */
|
||||
typedef struct {
|
||||
uint8_t length; /* full instruction length in bytes (>= 1) */
|
||||
uint16_t mnemonic_class; /* one of SEM_MN_* (backend-neutral group id) */
|
||||
uint8_t is_control_flow; /* 1 if branch/call/ret (affects normalization) */
|
||||
uint8_t noperands; /* number of meaningful operands (0..4) */
|
||||
uint8_t raw_opcode; /* for SEM_MN_OTHER: a raw opcode byte to fold,
|
||||
* so unclassified instructions still differ; 0
|
||||
* (and unused) for classified instructions */
|
||||
struct {
|
||||
uint8_t kind; /* SEMOP_NONE / SEMOP_REG / SEMOP_MEM / SEMOP_IMM */
|
||||
uint8_t reg_class; /* SEM_RC_* canonical register class (not an id) */
|
||||
uint8_t width_log2; /* operand size as log2 bytes (0=1B..4=16B), or 0 */
|
||||
uint8_t mem_has_base; /* memory operand has a base register */
|
||||
uint8_t mem_has_index;/* memory operand has an index register */
|
||||
uint8_t mem_is_riprel;/* memory operand is RIP-relative */
|
||||
} op[4];
|
||||
} sem_insn;
|
||||
|
||||
/* Decode ONE instruction at code[0..avail). Returns 1 on success (fills *out),
|
||||
* 0 on undecodable / not enough bytes. Stateless: no allocation, no I/O, no
|
||||
* shared mutable state - reentrant. */
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out);
|
||||
|
||||
/* The SECOND projection of the same backend parse: a full, lossless decode into
|
||||
* the public insn_decoded (insndec.h) - concrete register roots, values, sizes,
|
||||
* per-operand access, and the aggregate implicit-or-explicit read/write sets
|
||||
* (incl. rsp/rdx:rax/FLAGS). ABI-AGNOSTIC: `call` reports only architectural
|
||||
* effects (rsp/rip write, indirect target read), NEVER a volatile clobber. Each
|
||||
* backend TU implements BOTH this and semsig_backend_decode behind the ONE seam.
|
||||
* Returns 1 on success (fills *out), 0 on undecodable / not enough bytes.
|
||||
* Stateless / reentrant - no allocation, no I/O, no shared mutable state. */
|
||||
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out);
|
||||
|
||||
/* Stable backend identity for diagnostics / hash-domain tagging. Static,
|
||||
* never-NULL. This declaration is shared with semsig.h's public symbol of the
|
||||
* same name; the concrete backend TU (or the stub) defines it once. */
|
||||
const char* semsig_backend_name(void);
|
||||
|
||||
#endif /* VMIE_SEMSIG_BACKEND_H */
|
||||
@@ -0,0 +1,440 @@
|
||||
/* semsig_backend_capstone.c - Capstone implementation of the operand-decode seam.
|
||||
*
|
||||
* The ONE TU that includes Capstone headers and links its library. It implements
|
||||
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||
* backend name. Built ONLY when VMIE_DISASM=capstone (see CMakeLists.txt).
|
||||
*
|
||||
* Stateless: a Capstone handle is opened/closed on the call stack per decode, no
|
||||
* global mutable state - reentrant. Capstone allocates internally for cs_disasm;
|
||||
* the buffer is freed before return so there is no leak across the seam.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <capstone/capstone.h>
|
||||
#include "semsig_backend.h"
|
||||
|
||||
/* Map a Capstone x86 instruction id to a backend-neutral SEM_MN_* class. STATIC
|
||||
* table, not a switch forest; unlisted ids fall through to SEM_MN_OTHER (where
|
||||
* the core folds the raw opcode). Conditional branches (Jcc) are a family folded
|
||||
* by Capstone group, handled before the table lookup. */
|
||||
typedef struct { unsigned int id; uint16_t cls; } mn_map;
|
||||
|
||||
static const mn_map MN_TABLE[] = {
|
||||
/* moves */
|
||||
{ X86_INS_MOV, SEM_MN_MOV },
|
||||
{ X86_INS_MOVZX, SEM_MN_MOV },
|
||||
{ X86_INS_MOVSX, SEM_MN_MOV },
|
||||
{ X86_INS_MOVSXD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVAPS, SEM_MN_MOV },
|
||||
{ X86_INS_MOVAPD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVDQA, SEM_MN_MOV },
|
||||
{ X86_INS_MOVDQU, SEM_MN_MOV },
|
||||
{ X86_INS_MOVD, SEM_MN_MOV },
|
||||
{ X86_INS_MOVQ, SEM_MN_MOV },
|
||||
{ X86_INS_XCHG, SEM_MN_MOV },
|
||||
{ X86_INS_LEA, SEM_MN_LEA },
|
||||
/* arithmetic */
|
||||
{ X86_INS_ADD, SEM_MN_ARITH },
|
||||
{ X86_INS_ADC, SEM_MN_ARITH },
|
||||
{ X86_INS_SUB, SEM_MN_ARITH },
|
||||
{ X86_INS_SBB, SEM_MN_ARITH },
|
||||
{ X86_INS_INC, SEM_MN_ARITH },
|
||||
{ X86_INS_DEC, SEM_MN_ARITH },
|
||||
{ X86_INS_NEG, SEM_MN_ARITH },
|
||||
{ X86_INS_MUL, SEM_MN_ARITH },
|
||||
{ X86_INS_IMUL, SEM_MN_ARITH },
|
||||
{ X86_INS_DIV, SEM_MN_ARITH },
|
||||
{ X86_INS_IDIV, SEM_MN_ARITH },
|
||||
/* logic / shifts */
|
||||
{ X86_INS_AND, SEM_MN_LOGIC },
|
||||
{ X86_INS_OR, SEM_MN_LOGIC },
|
||||
{ X86_INS_XOR, SEM_MN_LOGIC },
|
||||
{ X86_INS_NOT, SEM_MN_LOGIC },
|
||||
{ X86_INS_SHL, SEM_MN_LOGIC },
|
||||
{ X86_INS_SHR, SEM_MN_LOGIC },
|
||||
{ X86_INS_SAR, SEM_MN_LOGIC },
|
||||
{ X86_INS_ROL, SEM_MN_LOGIC },
|
||||
{ X86_INS_ROR, SEM_MN_LOGIC },
|
||||
/* compare / test */
|
||||
{ X86_INS_CMP, SEM_MN_CMP },
|
||||
{ X86_INS_TEST, SEM_MN_TEST },
|
||||
/* control flow */
|
||||
{ X86_INS_JMP, SEM_MN_JMP },
|
||||
{ X86_INS_CALL, SEM_MN_CALL },
|
||||
{ X86_INS_RET, SEM_MN_RET },
|
||||
{ X86_INS_RETF, SEM_MN_RET },
|
||||
{ X86_INS_RETFQ, SEM_MN_RET },
|
||||
{ X86_INS_PUSH, SEM_MN_PUSH },
|
||||
{ X86_INS_POP, SEM_MN_POP },
|
||||
{ X86_INS_NOP, SEM_MN_NOP },
|
||||
};
|
||||
|
||||
/* True if `g` is among the instruction's Capstone groups. */
|
||||
static int has_group(const cs_insn* in, uint8_t g) {
|
||||
const cs_detail* d = in->detail;
|
||||
if (!d) { return 0; }
|
||||
for (uint8_t i = 0; i < d->groups_count; i++) {
|
||||
if (d->groups[i] == g) { return 1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint16_t classify_mnemonic(const cs_insn* in) {
|
||||
if (has_group(in, X86_GRP_CALL)) { return SEM_MN_CALL; }
|
||||
if (has_group(in, X86_GRP_RET)) { return SEM_MN_RET; }
|
||||
if (has_group(in, X86_GRP_JUMP)) {
|
||||
/* unconditional jmp vs conditional jcc: JMP id is unconditional. */
|
||||
return (in->id == X86_INS_JMP) ? SEM_MN_JMP : SEM_MN_BRANCH;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||
if (MN_TABLE[i].id == in->id) { return MN_TABLE[i].cls; }
|
||||
}
|
||||
if (has_group(in, X86_GRP_FPU)) { return SEM_MN_FLOAT; }
|
||||
if (has_group(in, X86_GRP_SSE) || has_group(in, X86_GRP_SSE1) ||
|
||||
has_group(in, X86_GRP_SSE2)) { return SEM_MN_VEC; }
|
||||
return SEM_MN_OTHER;
|
||||
}
|
||||
|
||||
/* Map a Capstone x86 register id to a canonical SEM_RC_* class. Capstone has no
|
||||
* single "register class" accessor; classify by the documented id ranges. */
|
||||
static uint8_t reg_class_of(unsigned int reg) {
|
||||
if (reg == X86_REG_INVALID) { return SEM_RC_NONE; }
|
||||
if ((reg >= X86_REG_AH && reg <= X86_REG_BH) ||
|
||||
(reg >= X86_REG_AL && reg <= X86_REG_R15D) ||
|
||||
(reg >= X86_REG_RAX && reg <= X86_REG_RBP) ||
|
||||
(reg >= X86_REG_RSP && reg <= X86_REG_RDI) ||
|
||||
(reg >= X86_REG_R8 && reg <= X86_REG_R15)) { return SEM_RC_GPR; }
|
||||
if (reg >= X86_REG_XMM0 && reg <= X86_REG_ZMM31) { return SEM_RC_XMM; }
|
||||
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) { return SEM_RC_MASK; }
|
||||
if (reg >= X86_REG_CS && reg <= X86_REG_SS) { return SEM_RC_SEG; }
|
||||
if (reg >= X86_REG_EFLAGS && reg <= X86_REG_RIP) { return SEM_RC_FLAGS; }
|
||||
return SEM_RC_OTHER;
|
||||
}
|
||||
|
||||
static uint8_t width_log2_of(uint8_t size_bytes) {
|
||||
uint8_t l = 0;
|
||||
uint8_t v = size_bytes;
|
||||
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||
return l;
|
||||
}
|
||||
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
csh h;
|
||||
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
|
||||
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
|
||||
|
||||
cs_insn* insn = NULL;
|
||||
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
|
||||
if (n < 1 || !insn) {
|
||||
if (insn) { cs_free(insn, n); }
|
||||
cs_close(&h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn->size;
|
||||
out->mnemonic_class = classify_mnemonic(insn);
|
||||
out->is_control_flow =
|
||||
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
|
||||
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
|
||||
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER && insn->detail)
|
||||
? insn->detail->x86.opcode[0] : 0u;
|
||||
|
||||
uint8_t no = 0;
|
||||
if (insn->detail) {
|
||||
const cs_x86* x = &insn->detail->x86;
|
||||
for (uint8_t i = 0; i < x->op_count && no < 4; i++) {
|
||||
const cs_x86_op* o = &x->operands[i];
|
||||
switch (o->type) {
|
||||
case X86_OP_REG:
|
||||
out->op[no].kind = SEMOP_REG;
|
||||
out->op[no].reg_class = reg_class_of(o->reg);
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
case X86_OP_MEM:
|
||||
out->op[no].kind = SEMOP_MEM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
out->op[no].mem_has_base =
|
||||
(o->mem.base != X86_REG_INVALID &&
|
||||
o->mem.base != X86_REG_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_has_index =
|
||||
(o->mem.index != X86_REG_INVALID) ? 1u : 0u;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
|
||||
no++;
|
||||
break;
|
||||
case X86_OP_IMM:
|
||||
out->op[no].kind = SEMOP_IMM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
|
||||
cs_free(insn, n);
|
||||
cs_close(&h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
|
||||
|
||||
/* Map a Capstone x86 register id to a canonical reg_id ROOT (al/ax/eax/rax ->
|
||||
* REGID_RAX, xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). Capstone has
|
||||
* no "largest enclosing" helper, so fold by the documented id ranges (the same
|
||||
* approach reg_class_of already uses). The raw x86_reg never leaves this TU. */
|
||||
static uint8_t reg_root_of(unsigned int reg) {
|
||||
switch (reg) {
|
||||
case X86_REG_AL: case X86_REG_AH: case X86_REG_AX:
|
||||
case X86_REG_EAX: case X86_REG_RAX: return REGID_RAX;
|
||||
case X86_REG_CL: case X86_REG_CH: case X86_REG_CX:
|
||||
case X86_REG_ECX: case X86_REG_RCX: return REGID_RCX;
|
||||
case X86_REG_DL: case X86_REG_DH: case X86_REG_DX:
|
||||
case X86_REG_EDX: case X86_REG_RDX: return REGID_RDX;
|
||||
case X86_REG_BL: case X86_REG_BH: case X86_REG_BX:
|
||||
case X86_REG_EBX: case X86_REG_RBX: return REGID_RBX;
|
||||
case X86_REG_SPL: case X86_REG_SP: case X86_REG_ESP:
|
||||
case X86_REG_RSP: return REGID_RSP;
|
||||
case X86_REG_BPL: case X86_REG_BP: case X86_REG_EBP:
|
||||
case X86_REG_RBP: return REGID_RBP;
|
||||
case X86_REG_SIL: case X86_REG_SI: case X86_REG_ESI:
|
||||
case X86_REG_RSI: return REGID_RSI;
|
||||
case X86_REG_DIL: case X86_REG_DI: case X86_REG_EDI:
|
||||
case X86_REG_RDI: return REGID_RDI;
|
||||
case X86_REG_R8B: case X86_REG_R8W: case X86_REG_R8D:
|
||||
case X86_REG_R8: return REGID_R8;
|
||||
case X86_REG_R9B: case X86_REG_R9W: case X86_REG_R9D:
|
||||
case X86_REG_R9: return REGID_R9;
|
||||
case X86_REG_R10B: case X86_REG_R10W: case X86_REG_R10D:
|
||||
case X86_REG_R10: return REGID_R10;
|
||||
case X86_REG_R11B: case X86_REG_R11W: case X86_REG_R11D:
|
||||
case X86_REG_R11: return REGID_R11;
|
||||
case X86_REG_R12B: case X86_REG_R12W: case X86_REG_R12D:
|
||||
case X86_REG_R12: return REGID_R12;
|
||||
case X86_REG_R13B: case X86_REG_R13W: case X86_REG_R13D:
|
||||
case X86_REG_R13: return REGID_R13;
|
||||
case X86_REG_R14B: case X86_REG_R14W: case X86_REG_R14D:
|
||||
case X86_REG_R14: return REGID_R14;
|
||||
case X86_REG_R15B: case X86_REG_R15W: case X86_REG_R15D:
|
||||
case X86_REG_R15: return REGID_R15;
|
||||
default: break;
|
||||
}
|
||||
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) {
|
||||
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_XMM0));
|
||||
}
|
||||
if (reg >= X86_REG_YMM0 && reg <= X86_REG_YMM31) {
|
||||
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_YMM0));
|
||||
}
|
||||
if (reg >= X86_REG_ZMM0 && reg <= X86_REG_ZMM31) {
|
||||
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_ZMM0));
|
||||
}
|
||||
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) {
|
||||
return (uint8_t)(REGID_K0 + (reg - X86_REG_K0));
|
||||
}
|
||||
if (reg == X86_REG_EFLAGS) { return REGID_FLAGS; }
|
||||
if (reg == X86_REG_INVALID) { return REGID_NONE; }
|
||||
return REGID_OTHER;
|
||||
}
|
||||
|
||||
/* Approximate the ACCESS width of a register in log2 bytes from its sub-register
|
||||
* id (Capstone register ids encode the width); used when only an id is known
|
||||
* (cs_regs_access returns ids without sizes). Coarse but enough for overlap. */
|
||||
static uint8_t reg_width_log2(unsigned int reg) {
|
||||
switch (reg) {
|
||||
case X86_REG_AL: case X86_REG_AH: case X86_REG_CL: case X86_REG_CH:
|
||||
case X86_REG_DL: case X86_REG_DH: case X86_REG_BL: case X86_REG_BH:
|
||||
case X86_REG_SPL: case X86_REG_BPL: case X86_REG_SIL: case X86_REG_DIL:
|
||||
case X86_REG_R8B: case X86_REG_R9B: case X86_REG_R10B:
|
||||
case X86_REG_R11B: case X86_REG_R12B: case X86_REG_R13B:
|
||||
case X86_REG_R14B: case X86_REG_R15B: return 0;
|
||||
case X86_REG_AX: case X86_REG_CX: case X86_REG_DX: case X86_REG_BX:
|
||||
case X86_REG_SP: case X86_REG_BP: case X86_REG_SI: case X86_REG_DI:
|
||||
case X86_REG_R8W: case X86_REG_R9W: case X86_REG_R10W:
|
||||
case X86_REG_R11W: case X86_REG_R12W: case X86_REG_R13W:
|
||||
case X86_REG_R14W: case X86_REG_R15W: return 1;
|
||||
case X86_REG_EAX: case X86_REG_ECX: case X86_REG_EDX: case X86_REG_EBX:
|
||||
case X86_REG_ESP: case X86_REG_EBP: case X86_REG_ESI: case X86_REG_EDI:
|
||||
case X86_REG_R8D: case X86_REG_R9D: case X86_REG_R10D:
|
||||
case X86_REG_R11D: case X86_REG_R12D: case X86_REG_R13D:
|
||||
case X86_REG_R14D: case X86_REG_R15D: return 2;
|
||||
default: break;
|
||||
}
|
||||
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) { return 4; }
|
||||
return 3; /* 64-bit GPR / vector-as-root / other: default qword */
|
||||
}
|
||||
|
||||
/* Backend-neutral mnemonic CLASS for the rich decode (insn_class enum), by
|
||||
* translating the existing classify_mnemonic SEM_MN_* result. */
|
||||
static uint8_t insn_class_of(const cs_insn* in) {
|
||||
switch (classify_mnemonic(in)) {
|
||||
case SEM_MN_MOV: return INSN_MOV;
|
||||
case SEM_MN_ARITH: return INSN_ARITH;
|
||||
case SEM_MN_LOGIC: return INSN_LOGIC;
|
||||
case SEM_MN_CMP: return INSN_CMP;
|
||||
case SEM_MN_TEST: return INSN_TEST;
|
||||
case SEM_MN_BRANCH: return INSN_BRANCH;
|
||||
case SEM_MN_JMP: return INSN_JMP;
|
||||
case SEM_MN_CALL: return INSN_CALL;
|
||||
case SEM_MN_RET: return INSN_RET;
|
||||
case SEM_MN_PUSH: return INSN_PUSH;
|
||||
case SEM_MN_POP: return INSN_POP;
|
||||
case SEM_MN_LEA: return INSN_LEA;
|
||||
case SEM_MN_NOP: return INSN_NOP;
|
||||
case SEM_MN_FLOAT: return INSN_FLOAT;
|
||||
case SEM_MN_VEC: return INSN_VEC;
|
||||
default: return INSN_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* Normalize a Capstone cs_ac_type access mask to a reg_access. */
|
||||
static uint8_t access_of(uint8_t a) {
|
||||
const int r = (a & CS_AC_READ) != 0;
|
||||
const int w = (a & CS_AC_WRITE) != 0;
|
||||
if (r && w) { return ACC_READWRITE; }
|
||||
if (r) { return ACC_READ; }
|
||||
if (w) { return ACC_WRITE; }
|
||||
return ACC_NONE;
|
||||
}
|
||||
|
||||
/* writes_zero_extend rule (see insndec.h): 32-bit GPR write zero-extends; EVEX/
|
||||
* VEX vector write zero-extends the upper part; 8/16-bit and legacy-SSE are
|
||||
* partial. */
|
||||
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
|
||||
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||
return (width_log2 == 2) ? 1u : 0u;
|
||||
}
|
||||
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||
return vector_evex_vex ? 1u : 0u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
|
||||
uint8_t width_log2, uint8_t zx, uint8_t access) {
|
||||
if (root == REGID_NONE) { return; }
|
||||
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
|
||||
if (set[i].root == root) {
|
||||
if (width_log2 > set[i].width_log2) {
|
||||
set[i].width_log2 = width_log2;
|
||||
set[i].writes_zero_extend = zx;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (*n < INSN_MAX_REGREFS) {
|
||||
set[*n].root = root;
|
||||
set[*n].width_log2 = width_log2;
|
||||
set[*n].writes_zero_extend = zx;
|
||||
set[*n].access = access;
|
||||
}
|
||||
(*n)++;
|
||||
}
|
||||
|
||||
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
csh h;
|
||||
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
|
||||
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
|
||||
|
||||
cs_insn* insn = NULL;
|
||||
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
|
||||
if (n < 1 || !insn) {
|
||||
if (insn) { cs_free(insn, n); }
|
||||
cs_close(&h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn->size;
|
||||
out->mnemonic_class = insn_class_of(insn);
|
||||
out->is_control_flow =
|
||||
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
|
||||
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
|
||||
|
||||
if (insn->detail) {
|
||||
const cs_x86* x = &insn->detail->x86;
|
||||
/* A VEX/EVEX form zero-extends the upper part of a written vector root;
|
||||
* legacy-SSE preserves it. Detect by the VEX/EVEX leading opcode byte. */
|
||||
const int is_vex_evex =
|
||||
(x->opcode[0] == 0xC4 || x->opcode[0] == 0xC5 || x->opcode[0] == 0x62);
|
||||
|
||||
/* Explicit operands (readable view): op[]. */
|
||||
uint8_t no = 0;
|
||||
for (uint8_t i = 0; i < x->op_count && no < INSN_MAX_OPERANDS; i++) {
|
||||
const cs_x86_op* o = &x->operands[i];
|
||||
const uint8_t acc = access_of(o->access);
|
||||
const uint8_t wl = width_log2_of(o->size);
|
||||
switch (o->type) {
|
||||
case X86_OP_REG: {
|
||||
const uint8_t root = reg_root_of(o->reg);
|
||||
out->op[no].kind = OPND_REG;
|
||||
out->op[no].access = acc;
|
||||
out->op[no].width_log2 = wl;
|
||||
out->op[no].reg_root = root;
|
||||
out->op[no].reg_zero_extend = zx_of(root, wl, is_vex_evex);
|
||||
no++;
|
||||
break;
|
||||
}
|
||||
case X86_OP_MEM:
|
||||
out->op[no].kind = OPND_MEM;
|
||||
out->op[no].access = acc;
|
||||
out->op[no].width_log2 = wl;
|
||||
out->op[no].mem_base =
|
||||
(o->mem.base == X86_REG_RIP) ? REGID_NONE
|
||||
: reg_root_of(o->mem.base);
|
||||
out->op[no].mem_index = reg_root_of(o->mem.index);
|
||||
out->op[no].mem_scale = (uint8_t)o->mem.scale;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_disp = o->mem.disp;
|
||||
no++;
|
||||
break;
|
||||
case X86_OP_IMM:
|
||||
out->op[no].kind = OPND_IMM;
|
||||
out->op[no].access = ACC_READ;
|
||||
out->op[no].imm_width_log2 = wl;
|
||||
out->op[no].imm_value = o->imm;
|
||||
no++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
|
||||
/* Aggregate implicit-or-explicit register sets via cs_regs_access: the
|
||||
* FULL read/write sets including rsp/rdx:rax/EFLAGS. EFLAGS -> REGID_FLAGS;
|
||||
* vector writes carry zero-extend by form. */
|
||||
cs_regs rd, wr;
|
||||
uint8_t nrd = 0, nwr = 0;
|
||||
if (cs_regs_access(h, insn, rd, &nrd, wr, &nwr) == CS_ERR_OK) {
|
||||
for (uint8_t i = 0; i < nrd; i++) {
|
||||
const uint8_t root = reg_root_of(rd[i]);
|
||||
regset_add(out->reads, &out->nreads, root,
|
||||
reg_width_log2(rd[i]), 0, ACC_READ);
|
||||
}
|
||||
for (uint8_t i = 0; i < nwr; i++) {
|
||||
const uint8_t root = reg_root_of(wr[i]);
|
||||
const uint8_t wl = reg_width_log2(wr[i]);
|
||||
regset_add(out->writes, &out->nwrites, root, wl,
|
||||
zx_of(root, wl, is_vex_evex), ACC_WRITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cs_free(insn, n);
|
||||
cs_close(&h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "capstone";
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
/* semsig_backend_zydis.c - Zydis implementation of the operand-decode seam.
|
||||
*
|
||||
* The ONE TU that includes Zydis headers and links its library. It implements
|
||||
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||
* backend name. Built ONLY when VMIE_DISASM=zydis (see CMakeLists.txt).
|
||||
*
|
||||
* Stateless: a ZydisDecoder is initialized on the call stack per decode, no
|
||||
* global mutable state - reentrant, no allocation, no I/O.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <Zydis/Zydis.h>
|
||||
#include "semsig_backend.h"
|
||||
|
||||
/* Map a Zydis mnemonic to a backend-neutral SEM_MN_* class. A STATIC table, not
|
||||
* a switch forest: only the groups that matter for normalization are listed;
|
||||
* everything else falls through to SEM_MN_OTHER (where the core folds the raw
|
||||
* opcode so unclassified instructions still differ). Keep entries sorted by
|
||||
* mnemonic for readability; lookup is a linear scan (cold path, one per insn). */
|
||||
typedef struct { ZydisMnemonic mn; uint16_t cls; } mn_map;
|
||||
|
||||
static const mn_map MN_TABLE[] = {
|
||||
/* moves */
|
||||
{ ZYDIS_MNEMONIC_MOV, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVZX, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVSX, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVSXD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVAPS, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVAPD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVDQA, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVDQU, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVD, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_MOVQ, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_XCHG, SEM_MN_MOV },
|
||||
{ ZYDIS_MNEMONIC_LEA, SEM_MN_LEA },
|
||||
/* arithmetic */
|
||||
{ ZYDIS_MNEMONIC_ADD, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_ADC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_SUB, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_SBB, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_INC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_DEC, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_NEG, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_MUL, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_IMUL, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_DIV, SEM_MN_ARITH },
|
||||
{ ZYDIS_MNEMONIC_IDIV, SEM_MN_ARITH },
|
||||
/* logic / shifts */
|
||||
{ ZYDIS_MNEMONIC_AND, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_OR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_XOR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_NOT, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SHL, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SHR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_SAR, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_ROL, SEM_MN_LOGIC },
|
||||
{ ZYDIS_MNEMONIC_ROR, SEM_MN_LOGIC },
|
||||
/* compare / test */
|
||||
{ ZYDIS_MNEMONIC_CMP, SEM_MN_CMP },
|
||||
{ ZYDIS_MNEMONIC_TEST, SEM_MN_TEST },
|
||||
/* control flow */
|
||||
{ ZYDIS_MNEMONIC_JMP, SEM_MN_JMP },
|
||||
{ ZYDIS_MNEMONIC_CALL, SEM_MN_CALL },
|
||||
{ ZYDIS_MNEMONIC_RET, SEM_MN_RET },
|
||||
{ ZYDIS_MNEMONIC_PUSH, SEM_MN_PUSH },
|
||||
{ ZYDIS_MNEMONIC_POP, SEM_MN_POP },
|
||||
{ ZYDIS_MNEMONIC_NOP, SEM_MN_NOP },
|
||||
};
|
||||
|
||||
/* jcc / setcc / cmovcc are a wide family; classify them by Zydis meta category
|
||||
* rather than enumerating every conditional mnemonic. */
|
||||
static uint16_t classify_mnemonic(const ZydisDecodedInstruction* insn) {
|
||||
/* conditional branch family -> BRANCH */
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_COND_BR) { return SEM_MN_BRANCH; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_UNCOND_BR) { return SEM_MN_JMP; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_CALL) { return SEM_MN_CALL; }
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_RET) { return SEM_MN_RET; }
|
||||
|
||||
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||
if (MN_TABLE[i].mn == insn->mnemonic) { return MN_TABLE[i].cls; }
|
||||
}
|
||||
|
||||
/* float / vector fall-backs by meta category, else OTHER. */
|
||||
if (insn->meta.category == ZYDIS_CATEGORY_X87_ALU) { return SEM_MN_FLOAT; }
|
||||
if (insn->meta.isa_set >= ZYDIS_ISA_SET_SSE &&
|
||||
insn->meta.isa_set <= ZYDIS_ISA_SET_SSE4A) { return SEM_MN_VEC; }
|
||||
return SEM_MN_OTHER;
|
||||
}
|
||||
|
||||
/* Map a Zydis register to a canonical SEM_RC_* class (identity erased). */
|
||||
static uint8_t reg_class_of(ZydisRegister reg) {
|
||||
const ZydisRegisterClass rc = ZydisRegisterGetClass(reg);
|
||||
switch (rc) {
|
||||
case ZYDIS_REGCLASS_GPR8:
|
||||
case ZYDIS_REGCLASS_GPR16:
|
||||
case ZYDIS_REGCLASS_GPR32:
|
||||
case ZYDIS_REGCLASS_GPR64: return SEM_RC_GPR;
|
||||
case ZYDIS_REGCLASS_XMM:
|
||||
case ZYDIS_REGCLASS_YMM:
|
||||
case ZYDIS_REGCLASS_ZMM: return SEM_RC_XMM;
|
||||
case ZYDIS_REGCLASS_MASK: return SEM_RC_MASK;
|
||||
case ZYDIS_REGCLASS_SEGMENT: return SEM_RC_SEG;
|
||||
case ZYDIS_REGCLASS_FLAGS:
|
||||
case ZYDIS_REGCLASS_IP: return SEM_RC_FLAGS;
|
||||
case ZYDIS_REGCLASS_INVALID: return SEM_RC_NONE;
|
||||
default: return SEM_RC_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* width in bytes -> log2 (0..4), saturating. */
|
||||
static uint8_t width_log2_of(uint16_t size_bits) {
|
||||
const uint32_t bytes = (uint32_t)size_bits / 8u;
|
||||
uint8_t l = 0;
|
||||
uint32_t v = bytes;
|
||||
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||
return l;
|
||||
}
|
||||
|
||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
ZydisDecoder dec;
|
||||
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
|
||||
ZYDIS_STACK_WIDTH_64))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ZydisDecodedInstruction insn;
|
||||
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
|
||||
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn.length;
|
||||
out->mnemonic_class = classify_mnemonic(&insn);
|
||||
out->is_control_flow =
|
||||
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_CALL ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
|
||||
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER) ? insn.opcode : 0u;
|
||||
|
||||
/* Reduce only EXPLICIT operands (implicit ones - e.g. flags, rsp on push -
|
||||
* are decoder noise that would differ pointlessly across forms). */
|
||||
uint8_t no = 0;
|
||||
for (uint8_t i = 0; i < insn.operand_count && no < 4; i++) {
|
||||
const ZydisDecodedOperand* o = &ops[i];
|
||||
if (o->visibility != ZYDIS_OPERAND_VISIBILITY_EXPLICIT) { continue; }
|
||||
switch (o->type) {
|
||||
case ZYDIS_OPERAND_TYPE_REGISTER:
|
||||
out->op[no].kind = SEMOP_REG;
|
||||
out->op[no].reg_class = reg_class_of(o->reg.value);
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
case ZYDIS_OPERAND_TYPE_MEMORY:
|
||||
out->op[no].kind = SEMOP_MEM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
out->op[no].mem_has_base =
|
||||
(o->mem.base != ZYDIS_REGISTER_NONE &&
|
||||
o->mem.base != ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_has_index =
|
||||
(o->mem.index != ZYDIS_REGISTER_NONE) ? 1u : 0u;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||
no++;
|
||||
break;
|
||||
case ZYDIS_OPERAND_TYPE_IMMEDIATE:
|
||||
out->op[no].kind = SEMOP_IMM;
|
||||
out->op[no].width_log2 = width_log2_of(o->size);
|
||||
no++;
|
||||
break;
|
||||
default:
|
||||
break; /* pointer/unused: ignored */
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
|
||||
|
||||
/* Map a Zydis register to a canonical reg_id ROOT (al/ax/eax/rax -> REGID_RAX,
|
||||
* xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). The enclosing fold is
|
||||
* done with ZydisRegisterGetLargestEnclosing first (GPR -> RAX..R15, vector ->
|
||||
* ZMM0..ZMM31); then a class-keyed offset turns the backend enum into the stable
|
||||
* reg_id. The raw ZydisRegister never leaves this TU. */
|
||||
static uint8_t reg_root_of(ZydisRegister reg) {
|
||||
if (reg == ZYDIS_REGISTER_NONE) { return REGID_NONE; }
|
||||
|
||||
const ZydisRegister enc =
|
||||
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg);
|
||||
const ZydisRegister r = (enc == ZYDIS_REGISTER_NONE) ? reg : enc;
|
||||
|
||||
if (r >= ZYDIS_REGISTER_RAX && r <= ZYDIS_REGISTER_R15) {
|
||||
return (uint8_t)(REGID_RAX + (r - ZYDIS_REGISTER_RAX));
|
||||
}
|
||||
if (r >= ZYDIS_REGISTER_ZMM0 && r <= ZYDIS_REGISTER_ZMM31) {
|
||||
return (uint8_t)(REGID_VEC0 + (r - ZYDIS_REGISTER_ZMM0));
|
||||
}
|
||||
if (r >= ZYDIS_REGISTER_K0 && r <= ZYDIS_REGISTER_K7) {
|
||||
return (uint8_t)(REGID_K0 + (r - ZYDIS_REGISTER_K0));
|
||||
}
|
||||
if (r == ZYDIS_REGISTER_FLAGS || r == ZYDIS_REGISTER_EFLAGS ||
|
||||
r == ZYDIS_REGISTER_RFLAGS) {
|
||||
return REGID_FLAGS;
|
||||
}
|
||||
return REGID_OTHER;
|
||||
}
|
||||
|
||||
/* Backend-neutral mnemonic CLASS for the rich decode (the insn_class enum). The
|
||||
* groups mirror SEM_MN_* but the values are the PUBLIC insn_class ids; reuse the
|
||||
* existing classify_mnemonic by translating its SEM_MN_* result. */
|
||||
static uint8_t insn_class_of(const ZydisDecodedInstruction* insn) {
|
||||
switch (classify_mnemonic(insn)) {
|
||||
case SEM_MN_MOV: return INSN_MOV;
|
||||
case SEM_MN_ARITH: return INSN_ARITH;
|
||||
case SEM_MN_LOGIC: return INSN_LOGIC;
|
||||
case SEM_MN_CMP: return INSN_CMP;
|
||||
case SEM_MN_TEST: return INSN_TEST;
|
||||
case SEM_MN_BRANCH: return INSN_BRANCH;
|
||||
case SEM_MN_JMP: return INSN_JMP;
|
||||
case SEM_MN_CALL: return INSN_CALL;
|
||||
case SEM_MN_RET: return INSN_RET;
|
||||
case SEM_MN_PUSH: return INSN_PUSH;
|
||||
case SEM_MN_POP: return INSN_POP;
|
||||
case SEM_MN_LEA: return INSN_LEA;
|
||||
case SEM_MN_NOP: return INSN_NOP;
|
||||
case SEM_MN_FLOAT: return INSN_FLOAT;
|
||||
case SEM_MN_VEC: return INSN_VEC;
|
||||
default: return INSN_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* Normalize ZydisOperandActions to a reg_access. */
|
||||
static uint8_t access_of(ZydisOperandActions a) {
|
||||
const int r = (a & ZYDIS_OPERAND_ACTION_READ) != 0;
|
||||
const int w = (a & ZYDIS_OPERAND_ACTION_WRITE) != 0;
|
||||
const int cr = (a & ZYDIS_OPERAND_ACTION_CONDREAD) != 0;
|
||||
const int cw = (a & ZYDIS_OPERAND_ACTION_CONDWRITE) != 0;
|
||||
if (r && w) { return ACC_READWRITE; }
|
||||
if (r) { return ACC_READ; }
|
||||
if (w) { return ACC_WRITE; }
|
||||
if (cw) { return ACC_COND_WRITE; }
|
||||
if (cr) { return ACC_COND_READ; }
|
||||
return ACC_NONE;
|
||||
}
|
||||
|
||||
/* writes_zero_extend rule (see insndec.h): a 32-bit GPR write zero-extends into
|
||||
* its 64-bit root; an EVEX/VEX vector write zero-extends the upper YMM/ZMM. 8/16-
|
||||
* bit GPR and legacy-SSE writes are PARTIAL. Computed from width + form, never by
|
||||
* branching on a concrete register. */
|
||||
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
|
||||
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||
return (width_log2 == 2) ? 1u : 0u; /* 32-bit GPR write zero-extends */
|
||||
}
|
||||
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||
return vector_evex_vex ? 1u : 0u; /* VEX/EVEX zeroes the upper part */
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/* Append a reg_ref to a set (dedup by root: keep the WIDEST width / strongest
|
||||
* access so a root appears once). Count grows past the cap so truncation is
|
||||
* detectable; writes stop at INSN_MAX_REGREFS. */
|
||||
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
|
||||
uint8_t width_log2, uint8_t zx, uint8_t access) {
|
||||
if (root == REGID_NONE) { return; }
|
||||
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
|
||||
if (set[i].root == root) {
|
||||
if (width_log2 > set[i].width_log2) {
|
||||
set[i].width_log2 = width_log2;
|
||||
set[i].writes_zero_extend = zx;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (*n < INSN_MAX_REGREFS) {
|
||||
set[*n].root = root;
|
||||
set[*n].width_log2 = width_log2;
|
||||
set[*n].writes_zero_extend = zx;
|
||||
set[*n].access = access;
|
||||
}
|
||||
(*n)++;
|
||||
}
|
||||
|
||||
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
|
||||
if (!code || !out || avail == 0) { return 0; }
|
||||
|
||||
ZydisDecoder dec;
|
||||
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
|
||||
ZYDIS_STACK_WIDTH_64))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ZydisDecodedInstruction insn;
|
||||
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
|
||||
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||
out->length = (uint8_t)insn.length;
|
||||
out->mnemonic_class = insn_class_of(&insn);
|
||||
out->is_control_flow =
|
||||
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_CALL ||
|
||||
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
|
||||
|
||||
/* A VEX/EVEX-encoded vector form zero-extends the upper part of a written
|
||||
* vector root; legacy-SSE preserves it. Approximate from the encoding. */
|
||||
const int vec_zx = (insn.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX ||
|
||||
insn.encoding == ZYDIS_INSTRUCTION_ENCODING_EVEX);
|
||||
|
||||
/* Pass over ALL operands. EXPLICIT ones populate op[] (readable view); EVERY
|
||||
* operand (explicit AND implicit/hidden: rsp on push/pop, rdx:rax on mul, the
|
||||
* memory address registers) feeds the aggregate reads[]/writes[] sets. */
|
||||
uint8_t no = 0;
|
||||
for (uint8_t i = 0; i < insn.operand_count && i < ZYDIS_MAX_OPERAND_COUNT;
|
||||
i++) {
|
||||
const ZydisDecodedOperand* o = &ops[i];
|
||||
const uint8_t acc = access_of(o->actions);
|
||||
const uint8_t wl = width_log2_of(o->size);
|
||||
|
||||
if (o->type == ZYDIS_OPERAND_TYPE_REGISTER) {
|
||||
const uint8_t root = reg_root_of(o->reg.value);
|
||||
const uint8_t zx = zx_of(root, wl, vec_zx);
|
||||
if (acc == ACC_READ || acc == ACC_READWRITE || acc == ACC_COND_READ) {
|
||||
regset_add(out->reads, &out->nreads, root, wl, 0, acc);
|
||||
}
|
||||
if (acc == ACC_WRITE || acc == ACC_READWRITE ||
|
||||
acc == ACC_COND_WRITE) {
|
||||
regset_add(out->writes, &out->nwrites, root, wl, zx, acc);
|
||||
}
|
||||
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||
no < INSN_MAX_OPERANDS) {
|
||||
out->op[no].kind = OPND_REG;
|
||||
out->op[no].access = acc;
|
||||
out->op[no].width_log2 = wl;
|
||||
out->op[no].reg_root = root;
|
||||
out->op[no].reg_zero_extend = zx;
|
||||
no++;
|
||||
}
|
||||
} else if (o->type == ZYDIS_OPERAND_TYPE_MEMORY) {
|
||||
/* Address registers are READ regardless of the operand's own access. */
|
||||
const uint8_t base = (o->mem.base == ZYDIS_REGISTER_RIP)
|
||||
? REGID_NONE : reg_root_of(o->mem.base);
|
||||
const uint8_t index = reg_root_of(o->mem.index);
|
||||
if (base != REGID_NONE) {
|
||||
regset_add(out->reads, &out->nreads, base, 3, 0, ACC_READ);
|
||||
}
|
||||
if (index != REGID_NONE) {
|
||||
regset_add(out->reads, &out->nreads, index, 3, 0, ACC_READ);
|
||||
}
|
||||
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||
no < INSN_MAX_OPERANDS) {
|
||||
out->op[no].kind = OPND_MEM;
|
||||
out->op[no].access = acc;
|
||||
out->op[no].width_log2 = wl;
|
||||
out->op[no].mem_base = base;
|
||||
out->op[no].mem_index = index;
|
||||
out->op[no].mem_scale = o->mem.scale;
|
||||
out->op[no].mem_is_riprel =
|
||||
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||
out->op[no].mem_disp =
|
||||
o->mem.disp.has_displacement ? o->mem.disp.value : 0;
|
||||
no++;
|
||||
}
|
||||
} else if (o->type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
|
||||
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||
no < INSN_MAX_OPERANDS) {
|
||||
out->op[no].kind = OPND_IMM;
|
||||
out->op[no].access = ACC_READ;
|
||||
out->op[no].imm_width_log2 = wl;
|
||||
out->op[no].imm_value = o->imm.is_signed
|
||||
? o->imm.value.s
|
||||
: (int64_t)o->imm.value.u;
|
||||
no++;
|
||||
}
|
||||
}
|
||||
}
|
||||
out->noperands = no;
|
||||
|
||||
/* FLAGS as ONE pseudo-register: read = TESTED, write = anything the
|
||||
* instruction MODIFIES / SET_0 / SET_1 / leaves UNDEFINED. */
|
||||
if (insn.cpu_flags) {
|
||||
if (insn.cpu_flags->tested) {
|
||||
regset_add(out->reads, &out->nreads, REGID_FLAGS, 2, 0, ACC_READ);
|
||||
}
|
||||
if (insn.cpu_flags->modified || insn.cpu_flags->set_0 ||
|
||||
insn.cpu_flags->set_1 || insn.cpu_flags->undefined) {
|
||||
regset_add(out->writes, &out->nwrites, REGID_FLAGS, 2, 0, ACC_WRITE);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "zydis";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* semsig_stub.c - the OFF (no-backend) path of the semantic-signature feature.
|
||||
*
|
||||
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides
|
||||
* the full public generic surface so the ABI is stable - linking never fails
|
||||
* for a missing symbol - while the feature is not present:
|
||||
* semsig_hash -> 0 (the same "no hash" sentinel as func_hash)
|
||||
* semsig_backend_name -> "none"
|
||||
* Callers tell "feature off" from "empty/undecodable function" via the compile-
|
||||
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime 0.
|
||||
*
|
||||
* In the OFF build semsig.c is NOT compiled (it calls the backend decode, which
|
||||
* does not exist without a backend); this stub is the whole generic symbol. See
|
||||
* CMakeLists.txt for the source-selection logic.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "semsig.h"
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
(void)fn;
|
||||
return 0; /* feature not built: same sentinel as func_hash */
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "none";
|
||||
}
|
||||
Reference in New Issue
Block a user