7 Commits

Author SHA1 Message Date
lirent 7cff67afce Bump version to 0.2.1 2026-06-28 23:03:02 +03:00
lirent 244511b66e Add readable operand decode and register def-use analysis
insndec.h: insn_decode() decodes one instruction into a readable form -
concrete registers as canonical roots (al/ah/ax/eax/rax -> one root), memory
base/index/scale/disp, immediate values, operand sizes, per-operand read/write,
and implicit operands/effects (rsp on push/pop, rdx:rax on mul/div, FLAGS). It
is ABI-agnostic: a call reports only its architectural effects.

codetrace.h: func_usedef(fn, abi, ...) computes per-instruction register
use/def over a function view (reusing cfg_blocks), splitting writes into
full-kill / partial / conditional so a reaching-definitions pass can be built on
top. With TRACE_ABI_WIN64 it marks the Win64 caller-saved set
(rax,rcx,rdx,r8-r11,xmm0-5,flags) clobbered at each call site; the clobber table
is confined to codetrace.c and the generic layer stays OS-agnostic
(TRACE_ABI_NONE keeps the conservative behaviour).

Both ride the existing optional disassembler seam (a second insn_decode_full
is added to semsig_backend.h; sem_insn and semsig_hash are untouched), gated
behind VMIE_HAVE_DISASM; OFF builds a stub. Adds the vmie_win32_func_usedef
wrapper. No new build option - the feature shares VMIE_DISASM with semsig.
2026-06-28 23:02:40 +03:00
lirent 51feb8d39c Bump version to 0.2.0 2026-06-28 21:52:22 +03:00
lirent 1b06206043 Add operand-normalized semantic code signatures
New semsig_hash(mem_view_t) primitive (include/semsig.h): a position-,
register-, immediate- and instruction-order-invariant function fingerprint.
It folds per-instruction operand-canonical tokens, splitting the body into
basic blocks via cfg_blocks and using a within-block order-insensitive
sort-then-fold (between-block order preserved), so it survives compiler
register reallocation and instruction scheduling that the byte-mask
func_hash/sig_generate cannot. Distinct hash domain from func_hash.

The operand decode is delegated to an OPTIONAL external disassembler behind
a private adapter seam (src/handlers/semsig_backend.h); semsig.c stays
backend-neutral and includes no backend header. VMIE_DISASM={OFF|zydis|
capstone} gates the build: OFF (default) compiles only semsig_stub.c and
keeps the zero-dependency build (VMIE_HAVE_DISASM=0, packages unchanged).
The backend is brought from VMIE_DISASM_SRC or find_package - never
vendored. Adds the vmie_win32_func_semsig wrapper.

The CI deb job builds with the Zydis backend (libzydis-dev); the runtime
package dependency on libzydis is derived automatically by dpkg-shlibdeps.
2026-06-28 21:51:47 +03:00
lirent 8ca84a5861 Bump version to 0.1.6 2026-06-26 07:57:36 +03:00
lirent 91e5e05de4 Scope gva_code_xref by an explicit prot_any argument
It hard-coded VR_X, so a target whose page tables expose no executable
region silently yielded zero xrefs. Callers now pass the scope (VR_X to
restrict to code, VR_R when the X bit is unavailable); the decoder and
exact target match remain the correctness gate. Mirrors the prot_any
parameter gva_imm_xref already carries.
2026-06-26 07:57:35 +03:00
lirent 32440c7104 Drop the user-half top-level NX bit when deriving VR_X
Under KVA shadow (KPTI) the kernel CR3 marks user-half PML4Es NX as
hardening, so OR-ing that top-level NX down the walk made every user
region non-executable - even live code (ntdll .text read back r--u).
Ring 3 executes those pages via the user/shadow CR3, which reaches the
same shared lower tables through an NX-clear PML4E, so the kernel
table's user-half PML4E NX is not what enforces user execution. Ignore
it on the user half; sub-PML4E NX (shared between both CR3s) stays
authoritative.
2026-06-26 07:57:26 +03:00
19 changed files with 2117 additions and 15 deletions
+2 -1
View File
@@ -87,12 +87,13 @@ jobs:
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}
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
+79 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
set(VMIE_VERSION "0.1.5" CACHE STRING "Library version (MAJOR.MINOR.PATCH); CI passes the tag")
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
@@ -12,6 +12,18 @@ include(GNUInstallDirs)
option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO
# ---- 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
@@ -31,6 +43,28 @@ add_library(vmie_obj OBJECT
src/handlers/x86dec.c
src/handlers/pmap.c
src/handlers/snapdiff.c)
# 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
@@ -42,11 +76,49 @@ if(VMIE_LTO)
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>)
@@ -58,6 +130,12 @@ set_target_properties(vmie_shared PROPERTIES
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 ----------------------------
add_executable(vmie_cli src/cli.c)
+106
View File
@@ -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 */
+194
View File
@@ -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 */
+5 -1
View File
@@ -125,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
View File
@@ -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
+126
View File
@@ -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 */
+66
View File
@@ -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
@@ -461,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
+10 -1
View File
@@ -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;
+102
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+190
View File
@@ -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;
}
+30
View File
@@ -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();
}
+35
View File
@@ -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 */
}
+181
View File
@@ -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;
}
+107
View File
@@ -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 */
+440
View File
@@ -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";
}
+404
View File
@@ -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";
}
+26
View File
@@ -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";
}