mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 00:46:36 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7cff67afce
|
|||
|
244511b66e
|
|||
|
51feb8d39c
|
|||
|
1b06206043
|
|||
|
8ca84a5861
|
|||
|
91e5e05de4
|
|||
|
32440c7104
|
@@ -87,12 +87,13 @@ jobs:
|
|||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
cmake make gcc libc6-dev dpkg-dev file \
|
cmake make gcc libc6-dev dpkg-dev file \
|
||||||
|
libzydis-dev \
|
||||||
gcc-mingw-w64-x86-64 ca-certificates curl
|
gcc-mingw-w64-x86-64 ca-certificates curl
|
||||||
|
|
||||||
- name: Configure
|
- name: Configure
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
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
|
- name: Build shared library
|
||||||
run: cmake --build build -j --target vmie_shared
|
run: cmake --build build -j --target vmie_shared
|
||||||
|
|||||||
+79
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.18) # find_program(... REQUIRED)
|
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)
|
project(vmi-engine VERSION ${VMIE_VERSION} LANGUAGES C)
|
||||||
|
|
||||||
set(CMAKE_C_STANDARD 17) # generation B uses no C23 feature
|
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
|
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) -----------
|
# ---- host: VMI core objects (single owner of the source list) -----------
|
||||||
add_library(vmie_obj OBJECT
|
add_library(vmie_obj OBJECT
|
||||||
src/core/gpa.c
|
src/core/gpa.c
|
||||||
@@ -31,6 +43,28 @@ add_library(vmie_obj OBJECT
|
|||||||
src/handlers/x86dec.c
|
src/handlers/x86dec.c
|
||||||
src/handlers/pmap.c
|
src/handlers/pmap.c
|
||||||
src/handlers/snapdiff.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
|
target_include_directories(vmie_obj
|
||||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h
|
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h
|
||||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.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)
|
target_link_options(vmie_obj PRIVATE -flto)
|
||||||
endif()
|
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) --------------
|
# ---- host: static library (in-tree consumers: CLIs + test) --------------
|
||||||
# $<TARGET_OBJECTS:> carries object files only, not usage requirements:
|
# $<TARGET_OBJECTS:> carries object files only, not usage requirements:
|
||||||
# re-declare the PUBLIC include scope so vmie_cli/vmie_scan still see include/.
|
# re-declare the PUBLIC include scope so vmie_cli/vmie_scan still see include/.
|
||||||
add_library(vmie STATIC $<TARGET_OBJECTS:vmie_obj>)
|
add_library(vmie STATIC $<TARGET_OBJECTS:vmie_obj>)
|
||||||
target_include_directories(vmie PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
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.*) -------------
|
# ---- host: shared library (external delivery: libvmie.so.*) -------------
|
||||||
add_library(vmie_shared SHARED $<TARGET_OBJECTS:vmie_obj>)
|
add_library(vmie_shared SHARED $<TARGET_OBJECTS:vmie_obj>)
|
||||||
@@ -58,6 +130,12 @@ set_target_properties(vmie_shared PROPERTIES
|
|||||||
if(VMIE_LTO)
|
if(VMIE_LTO)
|
||||||
target_link_options(vmie_shared PRIVATE -flto)
|
target_link_options(vmie_shared PRIVATE -flto)
|
||||||
endif()
|
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 ----------------------------
|
# ---- host: CLI demonstrator over the library ----------------------------
|
||||||
add_executable(vmie_cli src/cli.c)
|
add_executable(vmie_cli src/cli.c)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
/* codetrace.h - generic (OS-agnostic) register def-use over one function.
|
||||||
|
*
|
||||||
|
* Handler layer, gated like semsig.h: a process-scoped, STATIC register def-use
|
||||||
|
* analysis built on insn_decode (insndec.h) and the basic-block partition of
|
||||||
|
* cfg_blocks (codeanalysis.h, REUSED - this header writes no second splitter).
|
||||||
|
* For one function view it computes per-instruction register read (use) / write
|
||||||
|
* (def) sets - the building material a caller needs for reaching-definitions.
|
||||||
|
*
|
||||||
|
* It returns DATA. It does NOT execute or model values, does NOT unroll loops,
|
||||||
|
* does NOT build use-def CHAINS, and carries no dataflow solver: those are the
|
||||||
|
* caller's analysis. v1 deliberately stops at per-instruction use/def so the
|
||||||
|
* surface does not predetermine the caller's algorithm.
|
||||||
|
*
|
||||||
|
* Family placement. codeanalysis.h is PURE / always available; semsig.h is gated
|
||||||
|
* on the disassembler backend. codetrace.h sits in the second row with semsig: it
|
||||||
|
* needs the backend decode (via insndec.h) plus cfg_blocks. With no backend the
|
||||||
|
* symbol still exists (ABI stable) but returns the sentinel, and VMIE_HAVE_DISASM
|
||||||
|
* is 0. The chosen split keeps codeanalysis.h's "zero-dep, works always" contract
|
||||||
|
* literally true.
|
||||||
|
*
|
||||||
|
* GENERIC / OS-agnostic. The analysis names no OS. A calling convention enters
|
||||||
|
* ONLY through the explicit trace_abi argument, which selects a static caller-
|
||||||
|
* saved (volatile) clobber table applied at call sites. Win64 is just one enum
|
||||||
|
* value, not a hardcoded dependency; TRACE_ABI_NONE reproduces conservative
|
||||||
|
* behavior for any non-Windows consumer. The OS-specific CHOICE of Win64 is made
|
||||||
|
* by the win32 wrapper (vmie_win32_func_usedef, win32.h), not by this header.
|
||||||
|
*/
|
||||||
|
#ifndef VMIE_CODETRACE_H
|
||||||
|
#define VMIE_CODETRACE_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
|
||||||
|
|
||||||
|
/* Shared feature gate (see semsig.h / insndec.h). Default keeps the header valid
|
||||||
|
* when compiled outside the project build. */
|
||||||
|
#ifndef VMIE_HAVE_DISASM
|
||||||
|
#define VMIE_HAVE_DISASM 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Calling convention applied to call sites when computing def-use. The analysis
|
||||||
|
* itself stays OS-agnostic: an ABI is just a parameter selecting a static
|
||||||
|
* caller-saved (volatile) clobber table. An unknown value is treated as
|
||||||
|
* TRACE_ABI_NONE (conservative), not an error. */
|
||||||
|
typedef enum {
|
||||||
|
TRACE_ABI_NONE = 0, /* conservative: a call clobbers only what insn_decode
|
||||||
|
* reports architecturally (rsp/rip write, indirect
|
||||||
|
* target read); no volatile registers are killed. */
|
||||||
|
TRACE_ABI_WIN64 /* standard Win64: volatile GPR/vec/FLAGS are killed
|
||||||
|
* (full-kill) across every call site (see codetrace.c
|
||||||
|
* for the exact set and its honest limits). */
|
||||||
|
} trace_abi;
|
||||||
|
|
||||||
|
/* A compact register set over canonical reg_id roots (a bitset). The field split
|
||||||
|
* is an implementation detail of the struct; consumers should treat it opaquely
|
||||||
|
* and compare/inspect via the documented def/use semantics, not the raw words.
|
||||||
|
* gpr - the 16 GPR roots (REGID_RAX..REGID_R15)
|
||||||
|
* vec_lo - vector roots REGID_VEC0..REGID_VEC31
|
||||||
|
* misc - mask roots (k0..k7), REGID_FLAGS, REGID_OTHER */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t gpr; /* bit i set => REGID_RAX + i present (16 GPR roots) */
|
||||||
|
uint32_t vec_lo; /* bit i set => REGID_VEC0 + i present (32 vector roots) */
|
||||||
|
uint16_t misc; /* bit i set => (REGID_K0 + i) for i<8, then FLAGS, OTHER */
|
||||||
|
} regset;
|
||||||
|
|
||||||
|
/* Per-instruction use/def in the function view's coordinate space.
|
||||||
|
* off - byte offset of the instruction within fn
|
||||||
|
* len - instruction length
|
||||||
|
* use - roots READ (incl. implicit, incl. address regs of mem operands)
|
||||||
|
* def - roots WRITTEN as a FULL/zero-extending write (these KILL the
|
||||||
|
* enclosing root); for a call site under a non-NONE trace_abi the
|
||||||
|
* ABI volatile clobber is merged here as a full kill too
|
||||||
|
* def_partial - roots written PARTIALLY (8/16-bit GPR, legacy-SSE): they do NOT
|
||||||
|
* kill the enclosing root, so a wider live root survives
|
||||||
|
* cond_def - roots written CONDITIONALLY (cmovcc/setcc): do not kill either */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t off;
|
||||||
|
uint8_t len;
|
||||||
|
regset use, def, def_partial, cond_def;
|
||||||
|
} insn_usedef;
|
||||||
|
|
||||||
|
/* Compute per-instruction use/def for one function view under calling convention
|
||||||
|
* `abi`. `fn` is a view spanning EXACTLY one function (as for cfg_blocks /
|
||||||
|
* semsig_hash). REUSES cfg_blocks only to validate the partition and iterate in
|
||||||
|
* block order; the use/def itself comes from insn_decode per instruction.
|
||||||
|
*
|
||||||
|
* `abi` selects the call-site volatile clobber: TRACE_ABI_NONE = conservative (no
|
||||||
|
* volatile kill, only the architectural rsp/rip write insn_decode reports);
|
||||||
|
* TRACE_ABI_WIN64 = full-kill of the Win64 volatile roots at every call site.
|
||||||
|
* The analysis stays OS-agnostic: `abi` is just a parameter, not a hardcoded OS.
|
||||||
|
*
|
||||||
|
* Writes up to `max` records in ascending off order (out=NULL to count). Returns
|
||||||
|
* the TOTAL instruction count, 0 if `fn` is empty, or -1 on a decode desync
|
||||||
|
* (cfg_blocks could not split, or the backend could not decode an instruction) /
|
||||||
|
* a build with no backend (VMIE_HAVE_DISASM==0). The sentinel convention mirrors
|
||||||
|
* cfg_blocks: -1 = desync/no-backend, 0 = empty.
|
||||||
|
*
|
||||||
|
* PURE: only the view and the decoder, no vmie_mem / no I/O (a stack buffer with
|
||||||
|
* a heap spill only when the block count overflows, like semsig_hash).
|
||||||
|
*
|
||||||
|
* Example - registers killed by each instruction of a function:
|
||||||
|
* insn_usedef ud[512];
|
||||||
|
* int n = func_usedef(fn, TRACE_ABI_WIN64, ud, 512);
|
||||||
|
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
|
||||||
|
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max);
|
||||||
|
|
||||||
|
#endif /* VMIE_CODETRACE_H */
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
/* insndec.h - readable per-instruction operand decode (generic, ABI-agnostic).
|
||||||
|
*
|
||||||
|
* Handler layer: a RICH decode of ONE x86-64 instruction into concrete operands
|
||||||
|
* (concrete registers as a backend-neutral stable id, memory base/index/scale/
|
||||||
|
* disp, immediate VALUES, sizes), per-operand read/write, AND the full aggregate
|
||||||
|
* register read/write sets INCLUDING IMPLICIT operands/effects (rsp on push/pop,
|
||||||
|
* rdx:rax on mul/div, FLAGS on cmp/jcc, ...). This is the "perception" primitive
|
||||||
|
* the register def-use analysis (codetrace.h) is built on.
|
||||||
|
*
|
||||||
|
* Relationship to semsig.h (shared backend, DIFFERENT purpose). semsig_hash
|
||||||
|
* folds a NORMALIZED, LOSSY token stream: it erases register identity (keeps only
|
||||||
|
* a class), erases immediate/displacement values, and carries no read/write - by
|
||||||
|
* design, so the hash is register-/value-/reorder-stable. That makes semsig
|
||||||
|
* irreversible: you cannot trace registers through it. insn_decode is the
|
||||||
|
* opposite: it KEEPS register identity, values, and read/write, so an analysis
|
||||||
|
* can reason about WHICH physical register an instruction touches. Both ride the
|
||||||
|
* SAME optional external disassembler (Zydis/Capstone) behind the same private
|
||||||
|
* seam, but insn_decode is NOT a hash and NOT lossy.
|
||||||
|
*
|
||||||
|
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
|
||||||
|
* length-only) and NOT a second hand-rolled operand decoder: it is supplied by
|
||||||
|
* the optional backend, so this primitive is FEATURE-GATED exactly like semsig.
|
||||||
|
* With no backend the symbol still exists (ABI stable) but returns 0, and
|
||||||
|
* VMIE_HAVE_DISASM is 0.
|
||||||
|
*
|
||||||
|
* ABI-AGNOSTIC. insn_decode reports ONLY what an instruction does
|
||||||
|
* ARCHITECTURALLY. In particular a `call` writes rsp and rip and (for an
|
||||||
|
* indirect call) reads its target register/memory - that is ALL. It does NOT
|
||||||
|
* mark caller-saved (volatile) registers as written: that is a property of the
|
||||||
|
* CALLED function / the calling convention, not of the instruction. The Win64
|
||||||
|
* volatile clobber lives in the def-use layer (codetrace.h/.c) behind an explicit
|
||||||
|
* trace_abi, never here.
|
||||||
|
*/
|
||||||
|
#ifndef VMIE_INSNDEC_H
|
||||||
|
#define VMIE_INSNDEC_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Compile-time feature availability, shared with semsig (one backend, one gate).
|
||||||
|
* Pushed from CMake as a PUBLIC compile definition so consumers see the SAME
|
||||||
|
* value the library was built with:
|
||||||
|
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; insn_decode is
|
||||||
|
* live, insndec_backend_name() names the backend.
|
||||||
|
* VMIE_HAVE_DISASM == 0 built without a backend (the default); insn_decode
|
||||||
|
* always returns 0 and insndec_backend_name() == "none".
|
||||||
|
* The default here keeps the header valid when compiled outside the project. */
|
||||||
|
#ifndef VMIE_HAVE_DISASM
|
||||||
|
#define VMIE_HAVE_DISASM 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Canonical full-register root id. al/ah/ax/eax/rax all map to REGID_RAX;
|
||||||
|
* xmm0/ymm0/zmm0 -> REGID_VEC0; etc. Identity is KEPT here (unlike sem_insn,
|
||||||
|
* which erases it) so def-use can reason about WHICH physical register. The
|
||||||
|
* concrete backend register enum is NEVER carried across the seam - the backend
|
||||||
|
* TU normalizes into THIS stable id, exactly as it does for mnemonic classes. */
|
||||||
|
typedef enum {
|
||||||
|
REGID_NONE = 0,
|
||||||
|
/* 16 GPR roots; al/ax/eax/rax -> REGID_RAX, etc. */
|
||||||
|
REGID_RAX, REGID_RCX, REGID_RDX, REGID_RBX,
|
||||||
|
REGID_RSP, REGID_RBP, REGID_RSI, REGID_RDI,
|
||||||
|
REGID_R8, REGID_R9, REGID_R10, REGID_R11,
|
||||||
|
REGID_R12, REGID_R13, REGID_R14, REGID_R15,
|
||||||
|
/* 32 vector roots (zmm0..zmm31; xmm/ymm fold into the zmm root). */
|
||||||
|
REGID_VEC0,
|
||||||
|
REGID_VEC31 = REGID_VEC0 + 31,
|
||||||
|
/* AVX-512 mask k0..k7. */
|
||||||
|
REGID_K0,
|
||||||
|
REGID_K7 = REGID_K0 + 7,
|
||||||
|
/* FLAGS as ONE pseudo-register root (NOT per-bit CF/ZF/...): enough for
|
||||||
|
* def-use "cmp writes flags, jcc/cmov/setcc read flags"; per-bit is not v1. */
|
||||||
|
REGID_FLAGS,
|
||||||
|
/* rip / segment / other roots kept coarse (def-use rarely tracks them). */
|
||||||
|
REGID_OTHER,
|
||||||
|
REGID_COUNT
|
||||||
|
} reg_id;
|
||||||
|
|
||||||
|
/* Per-operand / per-register access, normalized (raw backend enums never leak). */
|
||||||
|
typedef enum {
|
||||||
|
ACC_NONE = 0,
|
||||||
|
ACC_READ, /* read only */
|
||||||
|
ACC_WRITE, /* write only */
|
||||||
|
ACC_READWRITE, /* read-modify-write (add, sub, ...) */
|
||||||
|
ACC_COND_READ, /* conditionally read (cmovcc src, ...) */
|
||||||
|
ACC_COND_WRITE /* conditionally written (cmovcc dst, setcc) */
|
||||||
|
} reg_access;
|
||||||
|
|
||||||
|
/* Explicit-operand kind (the readable, perception side of the decode). */
|
||||||
|
typedef enum {
|
||||||
|
OPND_NONE = 0,
|
||||||
|
OPND_REG, /* register operand */
|
||||||
|
OPND_MEM, /* memory operand (base/index/scale/disp) */
|
||||||
|
OPND_IMM /* immediate operand (value kept) */
|
||||||
|
} operand_kind;
|
||||||
|
|
||||||
|
/* One register reference, used both for explicit register operands and for the
|
||||||
|
* aggregate implicit-or-explicit read/write sets. Sub-register overlap is modeled
|
||||||
|
* EXPLICITLY and backend-neutrally:
|
||||||
|
* root - canonical full-register root (al/ax/eax/rax -> REGID_RAX)
|
||||||
|
* width_log2 - ACCESS width in log2 bytes (0=1B .. 4=16B), as
|
||||||
|
* width_log2_of already computes for semsig
|
||||||
|
* writes_zero_extend- on a WRITE, 1 if the upper part of the root is zeroed
|
||||||
|
* (a 32-bit GPR write, e.g. `eax`, zeroes the upper 32 bits
|
||||||
|
* of `rax`; a VEX/EVEX vector write zeroes the upper YMM/ZMM)
|
||||||
|
* and 0 if it is a PARTIAL write that preserves the upper
|
||||||
|
* part (8/16-bit GPR write, legacy-SSE vector write). This
|
||||||
|
* ONE flag is what def-use needs to decide overlap without
|
||||||
|
* carrying a raw bit mask.
|
||||||
|
* access - ACC_* for this reference */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t root; /* reg_id */
|
||||||
|
uint8_t width_log2; /* access width, log2 bytes (0..4+) */
|
||||||
|
uint8_t writes_zero_extend;
|
||||||
|
uint8_t access; /* reg_access */
|
||||||
|
} reg_ref;
|
||||||
|
|
||||||
|
/* One explicit operand (readable / perception view). */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t kind; /* operand_kind */
|
||||||
|
uint8_t access; /* reg_access for the operand as a whole */
|
||||||
|
uint8_t width_log2; /* access width, log2 bytes */
|
||||||
|
uint8_t mem_scale; /* MEM: index scale (1/2/4/8), 0 if no index */
|
||||||
|
uint8_t mem_is_riprel; /* MEM: 1 if RIP-relative */
|
||||||
|
uint8_t reg_root; /* REG: reg_id of the operand register */
|
||||||
|
uint8_t reg_zero_extend; /* REG: writes_zero_extend for a write operand */
|
||||||
|
uint8_t imm_width_log2; /* IMM: value width, log2 bytes */
|
||||||
|
uint8_t mem_base; /* MEM: reg_id of the base register (read) */
|
||||||
|
uint8_t mem_index; /* MEM: reg_id of the index register (read) */
|
||||||
|
int64_t mem_disp; /* MEM: displacement value (signed) */
|
||||||
|
int64_t imm_value; /* IMM: immediate value (sign-extended) */
|
||||||
|
} operand;
|
||||||
|
|
||||||
|
/* Mnemonic-class group, backend-neutral. Mirrors the semsig SEM_MN_* groups so a
|
||||||
|
* consumer can switch on the instruction kind without backend enums; def-use uses
|
||||||
|
* only INSN_CALL (call sites) but the rest aids readability of the decode. */
|
||||||
|
typedef enum {
|
||||||
|
INSN_OTHER = 0,
|
||||||
|
INSN_MOV, INSN_ARITH, INSN_LOGIC, INSN_CMP, INSN_TEST,
|
||||||
|
INSN_BRANCH, /* conditional jcc / setcc / cmovcc */
|
||||||
|
INSN_JMP, /* unconditional jmp */
|
||||||
|
INSN_CALL, INSN_RET, INSN_PUSH, INSN_POP, INSN_LEA, INSN_NOP,
|
||||||
|
INSN_FLOAT, INSN_VEC
|
||||||
|
} insn_class;
|
||||||
|
|
||||||
|
/* Caps on the rich decode. Four explicit operands cover x86-64; eight register
|
||||||
|
* refs per side cover the widest implicit sets (e.g. mul touching rax/rdx plus an
|
||||||
|
* explicit source) with margin. Anything beyond is truncated (count still grows
|
||||||
|
* past the cap so a caller can detect it), which never happens for real code. */
|
||||||
|
#define INSN_MAX_OPERANDS 4
|
||||||
|
#define INSN_MAX_REGREFS 8
|
||||||
|
|
||||||
|
/* A fully decoded instruction. Two complementary representations:
|
||||||
|
* op[] - explicit operands, in instruction operand order (readability);
|
||||||
|
* reads[] / writes[] - aggregate register sets over canonical roots, UNIONING
|
||||||
|
* explicit AND implicit effects (this is what def-use consumes). A
|
||||||
|
* memory operand's base/index registers appear in reads[] as address
|
||||||
|
* reads; FLAGS appears as REGID_FLAGS; rsp/rdx:rax appear via the
|
||||||
|
* backend's implicit operands. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t length; /* full instruction length in bytes (>= 1) */
|
||||||
|
uint8_t mnemonic_class; /* insn_class */
|
||||||
|
uint8_t is_control_flow; /* 1 if branch/call/ret */
|
||||||
|
uint8_t noperands; /* explicit operands in op[] (<= INSN_MAX_OPERANDS)*/
|
||||||
|
uint8_t nreads; /* register refs in reads[] */
|
||||||
|
uint8_t nwrites; /* register refs in writes[] */
|
||||||
|
operand op[INSN_MAX_OPERANDS];
|
||||||
|
reg_ref reads[INSN_MAX_REGREFS];
|
||||||
|
reg_ref writes[INSN_MAX_REGREFS];
|
||||||
|
} insn_decoded;
|
||||||
|
|
||||||
|
/* Decode ONE instruction at view[off .. ]. `view` is a flat byte span and `off`
|
||||||
|
* is the byte offset of the instruction within it; at most (view_size - off)
|
||||||
|
* bytes are available to the backend. Returns 1 on success (fills *out), 0 on
|
||||||
|
* undecodable / not enough bytes / a build with no backend (VMIE_HAVE_DISASM==0).
|
||||||
|
* out->length == 0 is also treated as a desync by callers. PURE: only the byte
|
||||||
|
* span and the decoder, no I/O, no allocation, reentrant.
|
||||||
|
*
|
||||||
|
* `view` / `view_size` are a flat buffer (not mem_view_t) so this primitive
|
||||||
|
* stays dependency-light; the caller owns the buffer.
|
||||||
|
*
|
||||||
|
* Example - read the registers an instruction writes:
|
||||||
|
* insn_decoded d;
|
||||||
|
* if (insn_decode(code, n, 0, &d))
|
||||||
|
* for (int i = 0; i < d.nwrites; i++)
|
||||||
|
* printf("writes root %u (zx=%u)\n",
|
||||||
|
* d.writes[i].root, d.writes[i].writes_zero_extend); */
|
||||||
|
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||||
|
insn_decoded* out);
|
||||||
|
|
||||||
|
/* Stable identity of the compiled disassembler backend ("zydis"/"capstone", or
|
||||||
|
* "none" without a backend). Shares the backend with semsig; this name is the
|
||||||
|
* same backend. Static, never-NULL. */
|
||||||
|
const char* insndec_backend_name(void);
|
||||||
|
|
||||||
|
#endif /* VMIE_INSNDEC_H */
|
||||||
+5
-1
@@ -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
|
* 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
|
* 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
|
* 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
|
#ifndef VMIE_VREGION_DEFINED
|
||||||
#define VMIE_VREGION_DEFINED
|
#define VMIE_VREGION_DEFINED
|
||||||
#define VR_R 0x1u /* readable (present => always set) */
|
#define VR_R 0x1u /* readable (present => always set) */
|
||||||
|
|||||||
+12
-9
@@ -70,18 +70,21 @@ int gva_sig_scan_multi(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
|||||||
uint32_t prot_any, const sigset* s,
|
uint32_t prot_any, const sigset* s,
|
||||||
sig_multi_hit* out, int max);
|
sig_multi_hit* out, int max);
|
||||||
|
|
||||||
/* code-xref: every instruction in the X-regions of [lo,hi] whose near rel
|
/* code-xref: every instruction in the regions of [lo,hi] kept by `prot_any`
|
||||||
* branch or RIP-relative memory operand resolves to `target_va`. Brute-scans
|
* whose near rel branch or RIP-relative memory operand resolves to `target_va`.
|
||||||
* each byte offset with the light x86-64 decoder (x86dec.h, NOT a full
|
* Brute-scans each byte offset with the light x86-64 decoder (x86dec.h, NOT a
|
||||||
* disassembler): an E8/E9/EB/Jcc rel branch matches when next_rip + rel ==
|
* 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
|
* 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
|
* 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
|
* form). Records each matching instruction-start VA. The sweep is scoped by
|
||||||
* carries a >=15-byte overlap (max x86 instruction length) so no instruction is
|
* `prot_any` (pass VR_X to restrict to code; pass VR_R when the target's
|
||||||
* cut at a window seam. Writes up to `max` VAs to `out` (NULL to count only) and
|
* page-table X bit is unavailable - the decoder and exact target match keep the
|
||||||
* returns the TOTAL number of matches, or -1 on bad input. */
|
* 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,
|
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
|
/* immediate / constant xref: every instruction in [lo,hi] (kept by the
|
||||||
* protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE
|
* protection filter `prot_any`; pass VR_X to restrict to code) whose IMMEDIATE
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/* semsig.h - generic (OS-agnostic) semantic / normalized code signature.
|
||||||
|
*
|
||||||
|
* Handler layer: a NORMALIZED function fingerprint that survives what the byte
|
||||||
|
* world cannot. func_hash (codeanalysis.h) and sig_generate (siggen.h) are
|
||||||
|
* BYTE fingerprints - both only neutralize the rel/RIP-relative displacement
|
||||||
|
* bytes; they still change when the compiler shuffles registers, picks a
|
||||||
|
* different equivalent register allocation, alters an immediate/displacement
|
||||||
|
* VALUE, or reschedules independent instructions. semsig_hash instead decodes
|
||||||
|
* OPERANDS (registers, operand classes, sizes, mnemonic group) and folds a
|
||||||
|
* canonical token stream, so it answers "semantically the same function" rather
|
||||||
|
* than "byte-for-byte the same function" - a FLIRT-like recognition primitive.
|
||||||
|
*
|
||||||
|
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
|
||||||
|
* length-only). It is supplied by an OPTIONAL external disassembler (Zydis or
|
||||||
|
* Capstone) selected at configure time, behind a private adapter seam. So this
|
||||||
|
* primitive is FEATURE-GATED: with no backend the symbol still exists (ABI
|
||||||
|
* stable) but returns the "no hash" sentinel 0, and VMIE_HAVE_DISASM is 0 (see
|
||||||
|
* below). The byte world (func_hash / sig_generate) is untouched either way.
|
||||||
|
*
|
||||||
|
* NOT COMPARABLE TO func_hash. semsig_hash uses a different algorithm over a
|
||||||
|
* different (normalized) input, so its values live in their OWN domain: never
|
||||||
|
* compare a semsig_hash to a func_hash. Compare semsig_hash to semsig_hash only
|
||||||
|
* (equality / a known-hash table), exactly as func_hash is used for library-ID.
|
||||||
|
*/
|
||||||
|
#ifndef VMIE_SEMSIG_H
|
||||||
|
#define VMIE_SEMSIG_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
|
||||||
|
|
||||||
|
/* Compile-time feature availability. Pushed from CMake as a PUBLIC compile
|
||||||
|
* definition so consumers of this header see the SAME value the library was
|
||||||
|
* built with:
|
||||||
|
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; semsig_hash is
|
||||||
|
* live, semsig_backend_name() names the backend.
|
||||||
|
* VMIE_HAVE_DISASM == 0 built without a backend (the default); semsig_hash
|
||||||
|
* always returns 0 and semsig_backend_name() == "none".
|
||||||
|
* Test it BEFORE calling to tell "feature off" apart from "empty/undecodable
|
||||||
|
* function" - both return 0, but only the latter is a real input. The default
|
||||||
|
* here keeps the header valid when compiled outside the project build. */
|
||||||
|
#ifndef VMIE_HAVE_DISASM
|
||||||
|
#define VMIE_HAVE_DISASM 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Reorder-/register-canonical semantic hash of one function view. `fn` is a view
|
||||||
|
* spanning EXACTLY one function (e.g. a section-view sub-range covering a
|
||||||
|
* func_range from vmie_win32_functions): fn.data[0] is the function's first
|
||||||
|
* byte, fn.size its length. Reported in the view's own coordinate space, but the
|
||||||
|
* hash is position-INDEPENDENT (like func_hash), so SECTION_LOCAL is enough for
|
||||||
|
* a stable value.
|
||||||
|
*
|
||||||
|
* What it normalizes (the canonicalization contract):
|
||||||
|
* - mnemonic CLASS is kept (the semantic backbone of each instruction);
|
||||||
|
* - the concrete REGISTER identity is erased to its register CLASS, so a
|
||||||
|
* rax<->rcx reshuffle does not change the hash;
|
||||||
|
* - operand WIDTH is kept (mov al,_ differs from mov rax,_);
|
||||||
|
* - immediate and displacement VALUES are erased (only "an imm of this width
|
||||||
|
* is present" survives), as func_hash/sig_generate already do for rel/RIP;
|
||||||
|
* - memory-operand SHAPE is kept (has_base / has_index / is_riprel flags, not
|
||||||
|
* the register ids);
|
||||||
|
* - operand ORDER is kept (mov dst,src differs from mov src,dst).
|
||||||
|
*
|
||||||
|
* REORDER INVARIANCE (v1 default behavior) AND ITS COST. The fold is two-level
|
||||||
|
* and tied to the CFG. The function is split into basic blocks (via cfg_blocks),
|
||||||
|
* and:
|
||||||
|
* - WITHIN a block the per-instruction token hashes are folded ORDER-
|
||||||
|
* INSENSITIVELY (sorted, then folded), so the compiler's intra-block
|
||||||
|
* instruction scheduling - reordering independent instructions - does not
|
||||||
|
* change the hash;
|
||||||
|
* - BETWEEN blocks the order is PRESERVED (blocks are folded in cfg_blocks'
|
||||||
|
* ascending-start order), because block order is the control-flow structure
|
||||||
|
* and must stay significant.
|
||||||
|
* The cost, stated plainly (this is a PROPERTY of the hash, not a free win): the
|
||||||
|
* within-block order-insensitive fold LOWERS discrimination - two blocks with
|
||||||
|
* the same MULTISET of normalized instructions but a different internal order
|
||||||
|
* yield the SAME block hash, even when that order is semantically meaningful
|
||||||
|
* (a data dependency this primitive does not model). That RAISES the false-match
|
||||||
|
* (collision) probability versus an order-sensitive hash. It is a deliberate
|
||||||
|
* trade for robustness against the scheduler; zero collisions are NOT promised.
|
||||||
|
*
|
||||||
|
* Returns the 64-bit semantic hash, or 0 on any of: `fn` empty (no data / size
|
||||||
|
* 0), a decode desync (cfg_blocks could not split the bytes, or the backend
|
||||||
|
* could not decode an instruction inside a block), or a build with no
|
||||||
|
* disassembler backend (VMIE_HAVE_DISASM == 0). 0 is therefore "no hash", never
|
||||||
|
* a valid fingerprint - the SAME sentinel as func_hash, so callers handle it the
|
||||||
|
* same way.
|
||||||
|
*
|
||||||
|
* Token layout (stability contract). The per-instruction token folded by this
|
||||||
|
* function is a fixed little-endian tuple; its layout is part of the hash's
|
||||||
|
* stability contract (changing it changes every value). Per instruction:
|
||||||
|
* byte 0 : mnemonic_class (the stable SEM_MN_* class id, low 8 bits)
|
||||||
|
* byte 1 : mnemonic_class high (the SEM_MN_* class id, high 8 bits)
|
||||||
|
* byte 2 : noperands (0..4)
|
||||||
|
* byte 3 : flags (bit0 = is_control_flow)
|
||||||
|
* bytes 4.. : per operand i in [0, noperands), 2 bytes each:
|
||||||
|
* +0 : (kind & 0x0f) | ((reg_class & 0x0f) << 4)
|
||||||
|
* +1 : (width_log2 & 0x07)
|
||||||
|
* | (mem_has_base ? 0x08 : 0)
|
||||||
|
* | (mem_has_index ? 0x10 : 0)
|
||||||
|
* | (mem_is_riprel ? 0x20 : 0)
|
||||||
|
* trailing : for the OTHER mnemonic class only, the raw opcode byte(s) so
|
||||||
|
* unclassified instructions still differ from one another.
|
||||||
|
* Operand order in the tuple follows the instruction's operand order (kept).
|
||||||
|
*
|
||||||
|
* Example - recognize a library function semantically (register-shuffle stable):
|
||||||
|
* mem_view_t fn; // a SECTION_LOCAL sub-view of one function body
|
||||||
|
* uint64_t h = semsig_hash(fn);
|
||||||
|
* if (h && h == known_crt_memcpy_semsig) puts("looks like memcpy");
|
||||||
|
*
|
||||||
|
* Example - guard on availability before relying on the feature:
|
||||||
|
* #if VMIE_HAVE_DISASM
|
||||||
|
* uint64_t h = semsig_hash(fn); // live; 0 only on empty/desync
|
||||||
|
* #else
|
||||||
|
* // feature not built (semsig_hash would return 0)
|
||||||
|
* #endif */
|
||||||
|
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||||
|
|
||||||
|
/* Stable identity of the compiled disassembler backend, for diagnostics and for
|
||||||
|
* tagging which decoder produced a stored hash. Returns a static, never-NULL
|
||||||
|
* string: the backend name (e.g. "zydis", "capstone") when VMIE_HAVE_DISASM==1,
|
||||||
|
* or "none" when built without a backend. The normalized hash is designed to be
|
||||||
|
* domain-stable ACROSS backends (the mnemonic classes are backend-neutral), so
|
||||||
|
* this name is informational, not a hash-comparison key. */
|
||||||
|
const char* semsig_backend_name(void);
|
||||||
|
|
||||||
|
#endif /* VMIE_SEMSIG_H */
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
||||||
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
||||||
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
|
#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;
|
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
|
||||||
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
|
* 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,
|
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);
|
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
|
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
||||||
* COMPOSITION of primitives the engine already exposes:
|
* COMPOSITION of primitives the engine already exposes:
|
||||||
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
||||||
|
|||||||
+10
-1
@@ -158,7 +158,16 @@ int gva_regions(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
|||||||
if (!(e4 & PG_P)) continue;
|
if (!(e4 & PG_P)) continue;
|
||||||
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
|
const uint64_t b4 = VA_CANON((uint64_t)i4 << 39);
|
||||||
if (!rgn_hit(b4, 1ull << 39, lo, hi)) continue;
|
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);
|
const uint64_t* t3 = gpa_ptr(m, e4 & PFN_MASK, 4096);
|
||||||
if (!t3) continue;
|
if (!t3) continue;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */
|
#include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */
|
||||||
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
|
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
|
||||||
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
|
#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
|
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
|
||||||
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
|
* (+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);
|
free(fb);
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Semantic hash of one function by func_rva: reuse the func_imports gather (find
|
||||||
|
* the extent from .pdata, read the body), then delegate to the generic
|
||||||
|
* semsig_hash over a SECTION_LOCAL view. No normalization/fold is duplicated
|
||||||
|
* here, and no backend header is included - the feature is inherited via the
|
||||||
|
* generic (a stub returning 0 in an OFF build). Cold: one-shot per function. */
|
||||||
|
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||||
|
uint64_t module_base, uint32_t func_rva)
|
||||||
|
__attribute__((cold));
|
||||||
|
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||||
|
uint64_t module_base, uint32_t func_rva) {
|
||||||
|
vmie_mem* m = vmie_win32_mem(v);
|
||||||
|
if (!m) { return 0; }
|
||||||
|
|
||||||
|
/* locate the function's extent from .pdata (count then gather). */
|
||||||
|
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||||
|
if (nfn < 0) { return 0; }
|
||||||
|
func_range stack_fr[256];
|
||||||
|
func_range* fr = stack_fr;
|
||||||
|
func_range* heap_fr = NULL;
|
||||||
|
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||||
|
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||||
|
if (!heap_fr) { return 0; }
|
||||||
|
fr = heap_fr;
|
||||||
|
}
|
||||||
|
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||||
|
if (got < 0) { free(heap_fr); return 0; }
|
||||||
|
uint32_t fn_size = 0;
|
||||||
|
for (int f = 0; f < got; f++) {
|
||||||
|
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
|
||||||
|
}
|
||||||
|
free(heap_fr);
|
||||||
|
if (fn_size == 0) { return 0; } /* not a known function start */
|
||||||
|
|
||||||
|
/* gather the body and view it SECTION_LOCAL (base_va = 0): semsig_hash is
|
||||||
|
* position-independent, so the local base is enough for a stable value. */
|
||||||
|
uint8_t* fb = malloc(fn_size);
|
||||||
|
if (!fb) { return 0; }
|
||||||
|
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
|
||||||
|
free(fb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
|
||||||
|
const uint64_t h = semsig_hash(view);
|
||||||
|
|
||||||
|
free(fb);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Register def-use of one function by func_rva: reuse the same .pdata extent
|
||||||
|
* gather as vmie_win32_func_semsig (locate the function, read the body), then
|
||||||
|
* delegate to the generic func_usedef over a SECTION_LOCAL view, PASSING
|
||||||
|
* TRACE_ABI_WIN64 - the OS-specific "choose the Win64 convention" decision lives
|
||||||
|
* here, in the layer that already owns .pdata / cr3 / module_base, exactly as the
|
||||||
|
* extent lookup does. The analysis (block walk, the volatile clobber merge) is
|
||||||
|
* NOT reimplemented or applied here; that lives once in func_usedef. No backend
|
||||||
|
* header is included - the feature is inherited via the generic (a stub returning
|
||||||
|
* -1 in an OFF build). Cold: one-shot per function. */
|
||||||
|
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||||
|
uint32_t func_rva, insn_usedef* out, int max)
|
||||||
|
__attribute__((cold));
|
||||||
|
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||||
|
uint32_t func_rva, insn_usedef* out, int max) {
|
||||||
|
vmie_mem* m = vmie_win32_mem(v);
|
||||||
|
if (!m) { return -1; }
|
||||||
|
|
||||||
|
/* locate the function's extent from .pdata (count then gather). */
|
||||||
|
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
|
||||||
|
if (nfn < 0) { return -1; }
|
||||||
|
func_range stack_fr[256];
|
||||||
|
func_range* fr = stack_fr;
|
||||||
|
func_range* heap_fr = NULL;
|
||||||
|
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
|
||||||
|
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
|
||||||
|
if (!heap_fr) { return -1; }
|
||||||
|
fr = heap_fr;
|
||||||
|
}
|
||||||
|
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
|
||||||
|
if (got < 0) { free(heap_fr); return -1; }
|
||||||
|
uint32_t fn_size = 0;
|
||||||
|
for (int f = 0; f < got; f++) {
|
||||||
|
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
|
||||||
|
}
|
||||||
|
free(heap_fr);
|
||||||
|
if (fn_size == 0) { return -1; } /* not a known function start */
|
||||||
|
|
||||||
|
/* gather the body and view it SECTION_LOCAL (base_va = 0): func_usedef reports
|
||||||
|
* in the view's own coordinate space, so off is a function-local byte offset. */
|
||||||
|
uint8_t* fb = malloc(fn_size);
|
||||||
|
if (!fb) { return -1; }
|
||||||
|
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
|
||||||
|
free(fb);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
|
||||||
|
const int n = func_usedef(view, TRACE_ABI_WIN64, out, max);
|
||||||
|
|
||||||
|
free(fb);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ static int xref_sweep_cb(void* u, const uint8_t* data, size_t len,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int gva_code_xref(vmie_mem* m, uintptr_t cr3, uint64_t lo, uint64_t hi,
|
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);
|
struct xref_cb c; memset(&c, 0, sizeof c);
|
||||||
c.target = target_va; c.out = out; c.max = max;
|
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 -1;
|
||||||
}
|
}
|
||||||
return c.n;
|
return c.n;
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/* codetrace.c - generic register def-use over one function (see codetrace.h).
|
||||||
|
*
|
||||||
|
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||||
|
* OFF build compiles insndec_stub.c (which also carries func_usedef's sentinel).
|
||||||
|
* It turns one function view into per-instruction register use/def:
|
||||||
|
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||||
|
* not a second splitter - to validate the partition and iterate in order;
|
||||||
|
* 2. step each block with insn_decode (insndec.h), and from each instruction's
|
||||||
|
* aggregate reads[]/writes[] build the use/def/def_partial/cond_def sets;
|
||||||
|
* 3. at a call site, under a non-NONE trace_abi, merge the static ABI volatile
|
||||||
|
* clobber into `def` as a FULL kill.
|
||||||
|
*
|
||||||
|
* Boundary: includes ONLY insndec.h, codeanalysis.h (cfg_blocks/code_block) and
|
||||||
|
* codetrace.h plus standard headers. It NEVER includes a backend's own headers -
|
||||||
|
* the decode rides insn_decode, which is itself behind the one backend seam.
|
||||||
|
*
|
||||||
|
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h> /* malloc/free (block-list overflow only) */
|
||||||
|
#include "insndec.h"
|
||||||
|
#include "codetrace.h"
|
||||||
|
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||||
|
|
||||||
|
/* Cap on the stack block array; denser functions spill the list to the heap
|
||||||
|
* (same scheme as semsig.c). */
|
||||||
|
#define CFG_MAX_BLOCKS 512
|
||||||
|
|
||||||
|
/* ---- regset: a bitset over canonical reg_id roots ------------------------ */
|
||||||
|
|
||||||
|
/* Set the bit for one reg_id root in a regset. The category fold mirrors the
|
||||||
|
* regset field layout documented in codetrace.h:
|
||||||
|
* GPR roots REGID_RAX..REGID_R15 -> gpr bit (root - REGID_RAX)
|
||||||
|
* vec roots REGID_VEC0..REGID_VEC31 -> vec_lo bit (root - REGID_VEC0)
|
||||||
|
* mask roots REGID_K0..REGID_K7 -> misc bit (root - REGID_K0)
|
||||||
|
* FLAGS -> misc bit 8
|
||||||
|
* OTHER -> misc bit 9
|
||||||
|
* REGID_NONE and out-of-range ids are ignored (no overlap to track). */
|
||||||
|
static void rs_set(regset* s, uint8_t root) {
|
||||||
|
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||||
|
s->gpr |= (uint16_t)(1u << (root - REGID_RAX));
|
||||||
|
} else if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||||
|
s->vec_lo |= (uint32_t)1u << (root - REGID_VEC0);
|
||||||
|
} else if (root >= REGID_K0 && root <= REGID_K7) {
|
||||||
|
s->misc |= (uint16_t)(1u << (root - REGID_K0));
|
||||||
|
} else if (root == REGID_FLAGS) {
|
||||||
|
s->misc |= (uint16_t)(1u << 8);
|
||||||
|
} else if (root == REGID_OTHER) {
|
||||||
|
s->misc |= (uint16_t)(1u << 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Union b into a (pure value op; no branching on a concrete register). */
|
||||||
|
static void rs_or(regset* a, const regset* b) {
|
||||||
|
a->gpr |= b->gpr;
|
||||||
|
a->vec_lo |= b->vec_lo;
|
||||||
|
a->misc |= b->misc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Win64 caller-saved (volatile) clobber table ------------------------- */
|
||||||
|
|
||||||
|
/* Caller-saved (volatile) roots clobbered across a Win64 call site. Applied as a
|
||||||
|
* FULL kill in `def` (NOT def_partial/cond_def): after a standard Win64 call
|
||||||
|
* these roots are dead from the caller's view. RAX additionally carries the
|
||||||
|
* return value, but RAX is already in this set, so a return value needs no
|
||||||
|
* separate def. Callee-saved roots are deliberately ABSENT (live across a call):
|
||||||
|
* GPR saved: RBX RBP RDI RSI RSP R12 R13 R14 R15
|
||||||
|
* vec saved: XMM6..XMM15 (VEC6..VEC15 roots)
|
||||||
|
* RSP changes architecturally via call/ret itself (already reported by
|
||||||
|
* insn_decode) and is callee-saved by VALUE, so it is NOT a clobber here.
|
||||||
|
*
|
||||||
|
* Selected by trace_abi: TRACE_ABI_WIN64 -> this set; TRACE_ABI_NONE -> the empty
|
||||||
|
* set, reproducing the prior conservative behavior (no volatile kill on a call).
|
||||||
|
*
|
||||||
|
* HONEST LIMITS (named, not hidden): this models the STANDARD Win64 convention
|
||||||
|
* ONLY - it does NOT catch __vectorcall / custom conventions; the return value is
|
||||||
|
* not a separate def (RAX already covers it); the indirect-call target register
|
||||||
|
* read is supplied by insn_decode, not by this table. */
|
||||||
|
static regset win64_clobber(void) {
|
||||||
|
regset s = (regset){ 0, 0, 0 };
|
||||||
|
/* volatile GPR: RAX RCX RDX R8 R9 R10 R11 */
|
||||||
|
rs_set(&s, REGID_RAX); rs_set(&s, REGID_RCX); rs_set(&s, REGID_RDX);
|
||||||
|
rs_set(&s, REGID_R8); rs_set(&s, REGID_R9);
|
||||||
|
rs_set(&s, REGID_R10); rs_set(&s, REGID_R11);
|
||||||
|
/* volatile vector: XMM0..XMM5 (VEC0..VEC5 and their YMM/ZMM upper) */
|
||||||
|
rs_set(&s, REGID_VEC0); rs_set(&s, REGID_VEC0 + 1); rs_set(&s, REGID_VEC0 + 2);
|
||||||
|
rs_set(&s, REGID_VEC0 + 3); rs_set(&s, REGID_VEC0 + 4); rs_set(&s, REGID_VEC0 + 5);
|
||||||
|
/* FLAGS */
|
||||||
|
rs_set(&s, REGID_FLAGS);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The clobber selected by trace_abi (table ABI -> regset, by value). An unknown
|
||||||
|
* abi falls to the conservative empty set. */
|
||||||
|
static regset abi_clobber(trace_abi abi) {
|
||||||
|
switch (abi) {
|
||||||
|
case TRACE_ABI_WIN64: return win64_clobber();
|
||||||
|
case TRACE_ABI_NONE:
|
||||||
|
default: return (regset){ 0, 0, 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- per-instruction use/def -------------------------------------------- */
|
||||||
|
|
||||||
|
/* Build one insn_usedef from a decoded instruction. A write that zero-extends
|
||||||
|
* (full/64-bit GPR or VEX/EVEX vector) goes to `def` (it KILLS the root); an
|
||||||
|
* 8/16-bit GPR or legacy-SSE write is PARTIAL (-> def_partial, no kill); a
|
||||||
|
* conditional write (cmovcc/setcc) goes to cond_def (no kill). Reads always go to
|
||||||
|
* `use`. The aggregate sets from insn_decode already include implicit operands
|
||||||
|
* (rsp/rdx:rax/FLAGS) and the address registers of memory operands. */
|
||||||
|
static void usedef_of(const insn_decoded* d, insn_usedef* ud) {
|
||||||
|
for (uint8_t i = 0; i < d->nreads && i < INSN_MAX_REGREFS; i++) {
|
||||||
|
rs_set(&ud->use, d->reads[i].root);
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < d->nwrites && i < INSN_MAX_REGREFS; i++) {
|
||||||
|
const reg_ref* w = &d->writes[i];
|
||||||
|
if (w->access == ACC_COND_WRITE) {
|
||||||
|
rs_set(&ud->cond_def, w->root);
|
||||||
|
} else if (w->writes_zero_extend ||
|
||||||
|
(w->root >= REGID_RAX && w->root <= REGID_R15 &&
|
||||||
|
w->width_log2 >= 3)) {
|
||||||
|
/* zero-extending (32-bit) OR a full 64-bit GPR write: a full kill. */
|
||||||
|
rs_set(&ud->def, w->root);
|
||||||
|
} else if (w->root >= REGID_VEC0 && w->root <= REGID_VEC31 &&
|
||||||
|
w->writes_zero_extend) {
|
||||||
|
rs_set(&ud->def, w->root);
|
||||||
|
} else if (w->root == REGID_FLAGS || w->root >= REGID_K0) {
|
||||||
|
/* FLAGS / mask / other roots: treat a write as a full def (no narrow
|
||||||
|
* sub-register concept to preserve). */
|
||||||
|
rs_set(&ud->def, w->root);
|
||||||
|
} else {
|
||||||
|
/* 8/16-bit GPR or legacy-SSE write: partial, does not kill the root. */
|
||||||
|
rs_set(&ud->def_partial, w->root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
|
||||||
|
if (!fn.data || fn.size == 0) { return 0; }
|
||||||
|
|
||||||
|
/* Validate the partition with cfg_blocks (count then gather); -1 is a desync. */
|
||||||
|
const int nb = cfg_blocks(fn, NULL, 0);
|
||||||
|
if (nb < 0) { return -1; }
|
||||||
|
if (nb == 0) { return 0; }
|
||||||
|
|
||||||
|
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||||
|
code_block* bb = stack_bb;
|
||||||
|
code_block* heap_bb = NULL;
|
||||||
|
if (nb > CFG_MAX_BLOCKS) {
|
||||||
|
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||||
|
if (!heap_bb) { return -1; }
|
||||||
|
bb = heap_bb;
|
||||||
|
}
|
||||||
|
const int got = cfg_blocks(fn, bb, nb);
|
||||||
|
if (got < 0) { free(heap_bb); return -1; }
|
||||||
|
const int use = got < nb ? got : nb;
|
||||||
|
|
||||||
|
const regset clob = abi_clobber(abi);
|
||||||
|
|
||||||
|
/* Step instructions in ascending block order; cfg_blocks already partitions
|
||||||
|
* [0, fn.size) with no gaps, so a linear walk per block covers the function. */
|
||||||
|
int total = 0;
|
||||||
|
for (int b = 0; b < use; b++) {
|
||||||
|
const size_t start = bb[b].start;
|
||||||
|
const size_t end = bb[b].end <= fn.size ? bb[b].end : fn.size;
|
||||||
|
for (size_t off = start; off < end; ) {
|
||||||
|
insn_decoded d;
|
||||||
|
if (!insn_decode(fn.data, end, off, &d) || d.length == 0) {
|
||||||
|
free(heap_bb);
|
||||||
|
return -1; /* backend desync */
|
||||||
|
}
|
||||||
|
insn_usedef ud = (insn_usedef){ 0 };
|
||||||
|
ud.off = (uint32_t)off;
|
||||||
|
ud.len = d.length;
|
||||||
|
usedef_of(&d, &ud);
|
||||||
|
/* Win64 (or any non-NONE abi) call site: merge the volatile clobber
|
||||||
|
* into `def` as a full kill. TRACE_ABI_NONE -> clob is empty (no-op). */
|
||||||
|
if (d.mnemonic_class == INSN_CALL) {
|
||||||
|
rs_or(&ud.def, &clob);
|
||||||
|
}
|
||||||
|
if (out && total < max) { out[total] = ud; }
|
||||||
|
total++;
|
||||||
|
off += d.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(heap_bb);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/* insndec.c - generic delegate for the readable per-instruction decode.
|
||||||
|
*
|
||||||
|
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||||
|
* OFF build compiles insndec_stub.c instead. It is a thin, ABI-agnostic wrapper
|
||||||
|
* over the ONE backend seam (semsig_backend.h: insn_decode_full): it validates
|
||||||
|
* the byte range and forwards to the backend, which fills the public insn_decoded.
|
||||||
|
*
|
||||||
|
* Boundary: includes ONLY insndec.h and semsig_backend.h (the seam) plus standard
|
||||||
|
* headers. It NEVER includes a backend's own headers (Zydis/Capstone) - the rich
|
||||||
|
* decode and the lossy semsig share that single seam. The backend name is the
|
||||||
|
* same backend as semsig, so insndec_backend_name delegates to it.
|
||||||
|
*
|
||||||
|
* ABI-agnostic: nothing here knows about any calling convention. The Win64
|
||||||
|
* volatile clobber lives in codetrace.c, never here.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "insndec.h"
|
||||||
|
#include "semsig_backend.h" /* insn_decode_full, semsig_backend_name (seam) */
|
||||||
|
|
||||||
|
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||||
|
insn_decoded* out) {
|
||||||
|
if (!view || !out || off >= view_size) { return 0; }
|
||||||
|
return insn_decode_full(view + off, view_size - off, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* insndec_backend_name(void) {
|
||||||
|
/* One backend, one identity: insndec rides the same decoder as semsig. */
|
||||||
|
return semsig_backend_name();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/* insndec_stub.c - the OFF (no-backend) path of the readable decode + def-use.
|
||||||
|
*
|
||||||
|
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides the
|
||||||
|
* full public generic surface of BOTH features so the ABI is stable - linking
|
||||||
|
* never fails for a missing symbol - while the features are not present:
|
||||||
|
* insn_decode -> 0 ("not decoded": same idea as the semsig sentinel)
|
||||||
|
* insndec_backend_name -> "none"
|
||||||
|
* func_usedef -> -1 (the cfg_blocks/desync sentinel; no backend)
|
||||||
|
* Callers tell "feature off" from a real empty/undecodable input via the compile-
|
||||||
|
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime sentinel.
|
||||||
|
*
|
||||||
|
* One stub carries both surfaces (fewer TUs). The pure types trace_abi / regset /
|
||||||
|
* insn_decoded are visible from the headers even in OFF. In the OFF build
|
||||||
|
* insndec.c / codetrace.c are NOT compiled (they call the backend, absent there);
|
||||||
|
* this stub is the whole generic symbol. See CMakeLists.txt for the selection.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "insndec.h"
|
||||||
|
#include "codetrace.h"
|
||||||
|
|
||||||
|
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
|
||||||
|
insn_decoded* out) {
|
||||||
|
(void)view; (void)view_size; (void)off; (void)out;
|
||||||
|
return 0; /* feature not built: nothing decoded */
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* insndec_backend_name(void) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
|
||||||
|
(void)fn; (void)abi; (void)out; (void)max;
|
||||||
|
return -1; /* feature not built: same desync/no-backend sentinel as cfg */
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
/* semsig.c - normalized (semantic) code-signature core (see semsig.h).
|
||||||
|
*
|
||||||
|
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||||
|
* OFF build compiles semsig_stub.c instead. It turns one function view into a
|
||||||
|
* register-/value-/reorder-canonical 64-bit hash:
|
||||||
|
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||||
|
* not a second splitter;
|
||||||
|
* 2. step each block with the BACKEND decoder (semsig_backend.h), normalize
|
||||||
|
* each instruction into a fixed token tuple (the layout documented in
|
||||||
|
* semsig.h), and FNV-1a-fold that tuple into a per-instruction hash;
|
||||||
|
* 3. fold a block's per-instruction hashes ORDER-INSENSITIVELY (sort, then
|
||||||
|
* fold) so intra-block instruction scheduling does not change the result;
|
||||||
|
* 4. fold the block hashes into the final hash IN cfg_blocks' order (block
|
||||||
|
* order is the CFG structure and stays significant).
|
||||||
|
*
|
||||||
|
* Boundary: includes ONLY semsig.h, semsig_backend.h, codeanalysis.h (for
|
||||||
|
* cfg_blocks / code_block) and the standard headers. It NEVER includes a
|
||||||
|
* backend's own headers (Zydis/Capstone) - the seam is semsig_backend.h. The
|
||||||
|
* pure core (x86dec, codeanalysis, ...) is reused, never modified.
|
||||||
|
*
|
||||||
|
* FNV-1a uses the SAME constants and scheme as codeanalysis.c so the hash world
|
||||||
|
* stays stylistically consistent. They are NOT factored into a shared header for
|
||||||
|
* a single second consumer (Rule of three: a tolerated 2nd appearance of two
|
||||||
|
* short #defines); a 3rd consumer would justify a private util.
|
||||||
|
*
|
||||||
|
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h> /* malloc/free (per-block hash buffer overflow only) */
|
||||||
|
#include "semsig.h"
|
||||||
|
#include "semsig_backend.h"
|
||||||
|
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||||
|
|
||||||
|
#define FNV64_OFFSET 0xcbf29ce484222325ull
|
||||||
|
#define FNV64_PRIME 0x00000100000001b3ull
|
||||||
|
|
||||||
|
/* Caps. CFG_MAX_BLOCKS bounds the stack block array; functions with more blocks
|
||||||
|
* spill the block list to the heap. INSN_STACK bounds the per-block per-insn
|
||||||
|
* hash array on the stack; a denser block spills that one buffer to the heap. */
|
||||||
|
#define CFG_MAX_BLOCKS 512
|
||||||
|
#define INSN_STACK 256
|
||||||
|
|
||||||
|
/* Fold one byte into an FNV-1a accumulator. */
|
||||||
|
static inline uint64_t fnv_byte(uint64_t h, uint8_t b) {
|
||||||
|
h ^= b;
|
||||||
|
h *= FNV64_PRIME;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normalize ONE decoded instruction into its token tuple and return the
|
||||||
|
* per-instruction FNV-1a hash of that tuple. The tuple layout is the stability
|
||||||
|
* contract documented in semsig.h: mnemonic class, noperands, a flags byte, then
|
||||||
|
* 2 bytes per operand (kind|reg_class, then width_log2|mem-shape flags), plus
|
||||||
|
* the raw opcode for the OTHER class so unclassified instructions still differ.
|
||||||
|
* Pure data->data: no branching on a concrete register, only on its class. */
|
||||||
|
static uint64_t token_hash(const sem_insn* in) {
|
||||||
|
uint64_t h = FNV64_OFFSET;
|
||||||
|
h = fnv_byte(h, (uint8_t)(in->mnemonic_class & 0xff));
|
||||||
|
h = fnv_byte(h, (uint8_t)((in->mnemonic_class >> 8) & 0xff));
|
||||||
|
const uint8_t nop = in->noperands <= 4 ? in->noperands : 4;
|
||||||
|
h = fnv_byte(h, nop);
|
||||||
|
h = fnv_byte(h, (uint8_t)(in->is_control_flow ? 0x01u : 0x00u));
|
||||||
|
for (uint8_t i = 0; i < nop; i++) {
|
||||||
|
const uint8_t b0 = (uint8_t)((in->op[i].kind & 0x0f) |
|
||||||
|
((in->op[i].reg_class & 0x0f) << 4));
|
||||||
|
const uint8_t b1 = (uint8_t)((in->op[i].width_log2 & 0x07) |
|
||||||
|
(in->op[i].mem_has_base ? 0x08u : 0u) |
|
||||||
|
(in->op[i].mem_has_index ? 0x10u : 0u) |
|
||||||
|
(in->op[i].mem_is_riprel ? 0x20u : 0u));
|
||||||
|
h = fnv_byte(h, b0);
|
||||||
|
h = fnv_byte(h, b1);
|
||||||
|
}
|
||||||
|
if (in->mnemonic_class == SEM_MN_OTHER) {
|
||||||
|
h = fnv_byte(h, in->raw_opcode); /* keep unclassified ops distinct */
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ascending uint64_t comparator for the deterministic per-block sort. The sort
|
||||||
|
* is what makes the within-block fold order-INSENSITIVE while keeping the
|
||||||
|
* MULTISET (duplicates survive, their count is significant). A plain xor/sum was
|
||||||
|
* REJECTED: xor cancels identical-instruction pairs (a block "loses" two equal
|
||||||
|
* moves) and sum collides easily on multiset permutations; sort-then-fold avoids
|
||||||
|
* both. */
|
||||||
|
static int cmp_u64(const void* a, const void* b) {
|
||||||
|
const uint64_t x = *(const uint64_t*)a;
|
||||||
|
const uint64_t y = *(const uint64_t*)b;
|
||||||
|
return (x > y) - (x < y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fold one basic block [start, end) of `fn` into a block hash, stepping the
|
||||||
|
* BACKEND decoder from start to end and order-insensitively folding the per-insn
|
||||||
|
* hashes. Returns 1 on success and writes *out; returns 0 on backend desync
|
||||||
|
* (an undecodable instruction inside the block) - the caller turns that into the
|
||||||
|
* 0 "no hash" sentinel. */
|
||||||
|
static int fold_block(mem_view_t fn, const code_block* blk, uint64_t* out) {
|
||||||
|
const size_t start = blk->start;
|
||||||
|
const size_t end = blk->end <= fn.size ? blk->end : fn.size;
|
||||||
|
if (end <= start) { *out = FNV64_OFFSET; return 1; } /* empty block */
|
||||||
|
|
||||||
|
uint64_t stack_h[INSN_STACK];
|
||||||
|
uint64_t* hh = stack_h;
|
||||||
|
uint64_t* heap_h = NULL;
|
||||||
|
const size_t span = end - start; /* >= 1 insn per byte */
|
||||||
|
if (span > INSN_STACK) {
|
||||||
|
heap_h = malloc(span * sizeof *heap_h);
|
||||||
|
if (!heap_h) { return 0; }
|
||||||
|
hh = heap_h;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t n = 0;
|
||||||
|
int ok = 1;
|
||||||
|
for (size_t off = start; off < end; ) {
|
||||||
|
sem_insn in;
|
||||||
|
if (!semsig_backend_decode(fn.data + off, end - off, &in) ||
|
||||||
|
in.length == 0) {
|
||||||
|
ok = 0;
|
||||||
|
break; /* backend desync */
|
||||||
|
}
|
||||||
|
hh[n++] = token_hash(&in);
|
||||||
|
off += in.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
qsort(hh, n, sizeof *hh, cmp_u64); /* order-insensitive */
|
||||||
|
uint64_t h = FNV64_OFFSET;
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
const uint64_t v = hh[i];
|
||||||
|
for (int b = 0; b < 8; b++) {
|
||||||
|
h = fnv_byte(h, (uint8_t)((v >> (b * 8)) & 0xff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(heap_h);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||||
|
uint64_t semsig_hash(mem_view_t fn) {
|
||||||
|
if (!fn.data || fn.size == 0) { return 0; }
|
||||||
|
|
||||||
|
/* Block partition (count then gather), REUSING cfg_blocks - -1 is a desync
|
||||||
|
* and maps to the 0 sentinel. cfg_blocks gives ORDER-of-blocks boundaries;
|
||||||
|
* the within-block step uses the backend, so a backend/x86_decode length
|
||||||
|
* disagreement only ever turns into the 0 sentinel, never a wrong hash. */
|
||||||
|
const int nb = cfg_blocks(fn, NULL, 0);
|
||||||
|
if (nb <= 0) { return 0; }
|
||||||
|
|
||||||
|
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||||
|
code_block* bb = stack_bb;
|
||||||
|
code_block* heap_bb = NULL;
|
||||||
|
if (nb > CFG_MAX_BLOCKS) {
|
||||||
|
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||||
|
if (!heap_bb) { return 0; }
|
||||||
|
bb = heap_bb;
|
||||||
|
}
|
||||||
|
const int got = cfg_blocks(fn, bb, nb);
|
||||||
|
if (got <= 0) { free(heap_bb); return 0; }
|
||||||
|
const int use = got < nb ? got : nb;
|
||||||
|
|
||||||
|
/* Fold block hashes into the final hash IN cfg_blocks' ascending-start order:
|
||||||
|
* block order is the CFG structure and stays significant (only WITHIN a
|
||||||
|
* block is the fold order-insensitive). */
|
||||||
|
uint64_t h = FNV64_OFFSET;
|
||||||
|
for (int i = 0; i < use; i++) {
|
||||||
|
uint64_t bh;
|
||||||
|
if (!fold_block(fn, &bb[i], &bh)) { /* backend desync */
|
||||||
|
free(heap_bb);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (int b = 0; b < 8; b++) {
|
||||||
|
h = fnv_byte(h, (uint8_t)((bh >> (b * 8)) & 0xff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(heap_bb);
|
||||||
|
return h;
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/* semsig_backend.h - backend-agnostic operand-decode boundary (PRIVATE).
|
||||||
|
*
|
||||||
|
* The ONE place semsig.c talks to an external disassembler. Each concrete
|
||||||
|
* backend (Zydis, Capstone, ...) provides exactly the entities below; semsig.c
|
||||||
|
* includes THIS header and NEVER a backend's own headers. That keeps the
|
||||||
|
* operand-decode dependency behind a single seam so the feature core stays
|
||||||
|
* backend-neutral and the resulting hash is domain-stable across backends.
|
||||||
|
*
|
||||||
|
* This header is intentionally NOT in include/ - it is an implementation detail
|
||||||
|
* of the feature, not public API. The public surface is semsig.h.
|
||||||
|
*/
|
||||||
|
#ifndef VMIE_SEMSIG_BACKEND_H
|
||||||
|
#define VMIE_SEMSIG_BACKEND_H
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "insndec.h" /* insn_decoded - the rich decode type shared with the
|
||||||
|
* public insndec.h surface; backend TUs fill it here so
|
||||||
|
* the type is defined ONCE, not duplicated at the seam.
|
||||||
|
* Raw backend enums still never leak (normalized in TU). */
|
||||||
|
|
||||||
|
/* Operand kind, normalized. Carried in sem_insn.op[].kind. */
|
||||||
|
enum {
|
||||||
|
SEMOP_NONE = 0, /* no operand in this slot */
|
||||||
|
SEMOP_REG, /* register operand (identity erased, class kept) */
|
||||||
|
SEMOP_MEM, /* memory operand (shape kept: base/index/riprel flags) */
|
||||||
|
SEMOP_IMM /* immediate operand (value erased, width kept) */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Canonical register class. Carried in sem_insn.op[].reg_class. The concrete
|
||||||
|
* register id is NEVER carried across the seam - only its class - so a compiler
|
||||||
|
* register reshuffle (rax<->rcx) does not change the normalized token. */
|
||||||
|
enum {
|
||||||
|
SEM_RC_NONE = 0, /* not a register / unknown */
|
||||||
|
SEM_RC_GPR, /* general-purpose integer register (al..r15) */
|
||||||
|
SEM_RC_XMM, /* vector register (xmm/ymm/zmm) */
|
||||||
|
SEM_RC_MASK, /* AVX-512 mask register (k0..k7) */
|
||||||
|
SEM_RC_SEG, /* segment register */
|
||||||
|
SEM_RC_FLAGS, /* flags / control / other special register */
|
||||||
|
SEM_RC_OTHER /* any register class not distinguished above */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Backend-neutral mnemonic CLASS ids. NOT a 1:1 map of every mnemonic: only the
|
||||||
|
* groups that matter for normalization. Each backend TU maps its own mnemonic
|
||||||
|
* enum into these with a static table; unclassified mnemonics fall to
|
||||||
|
* SEM_MN_OTHER, for which semsig.c additionally folds the raw opcode byte(s) so
|
||||||
|
* they still differ from one another. These ids are part of the hash's
|
||||||
|
* stability contract (see the token layout in semsig.h). */
|
||||||
|
enum {
|
||||||
|
SEM_MN_OTHER = 0, /* unclassified - raw opcode folded in (see semsig.h) */
|
||||||
|
SEM_MN_MOV, /* register/memory moves (mov, movzx, movsx, lea, ...) */
|
||||||
|
SEM_MN_ARITH, /* add/sub/inc/dec/neg/adc/sbb/mul/imul/div/idiv */
|
||||||
|
SEM_MN_LOGIC, /* and/or/xor/not/shl/shr/sar/rol/ror */
|
||||||
|
SEM_MN_CMP, /* cmp */
|
||||||
|
SEM_MN_TEST, /* test */
|
||||||
|
SEM_MN_BRANCH, /* conditional jump (jcc) / setcc / cmovcc */
|
||||||
|
SEM_MN_JMP, /* unconditional jmp */
|
||||||
|
SEM_MN_CALL, /* call */
|
||||||
|
SEM_MN_RET, /* ret / retf / leave epilogue return */
|
||||||
|
SEM_MN_PUSH, /* push */
|
||||||
|
SEM_MN_POP, /* pop */
|
||||||
|
SEM_MN_LEA, /* lea (address computation; kept distinct from mov) */
|
||||||
|
SEM_MN_NOP, /* nop / padding */
|
||||||
|
SEM_MN_FLOAT, /* x87/SSE/AVX float arithmetic */
|
||||||
|
SEM_MN_VEC /* integer/other vector ops */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* A decoded instruction reduced to ONLY what normalization needs - no full
|
||||||
|
* operand model leaks across the seam. */
|
||||||
|
typedef struct {
|
||||||
|
uint8_t length; /* full instruction length in bytes (>= 1) */
|
||||||
|
uint16_t mnemonic_class; /* one of SEM_MN_* (backend-neutral group id) */
|
||||||
|
uint8_t is_control_flow; /* 1 if branch/call/ret (affects normalization) */
|
||||||
|
uint8_t noperands; /* number of meaningful operands (0..4) */
|
||||||
|
uint8_t raw_opcode; /* for SEM_MN_OTHER: a raw opcode byte to fold,
|
||||||
|
* so unclassified instructions still differ; 0
|
||||||
|
* (and unused) for classified instructions */
|
||||||
|
struct {
|
||||||
|
uint8_t kind; /* SEMOP_NONE / SEMOP_REG / SEMOP_MEM / SEMOP_IMM */
|
||||||
|
uint8_t reg_class; /* SEM_RC_* canonical register class (not an id) */
|
||||||
|
uint8_t width_log2; /* operand size as log2 bytes (0=1B..4=16B), or 0 */
|
||||||
|
uint8_t mem_has_base; /* memory operand has a base register */
|
||||||
|
uint8_t mem_has_index;/* memory operand has an index register */
|
||||||
|
uint8_t mem_is_riprel;/* memory operand is RIP-relative */
|
||||||
|
} op[4];
|
||||||
|
} sem_insn;
|
||||||
|
|
||||||
|
/* Decode ONE instruction at code[0..avail). Returns 1 on success (fills *out),
|
||||||
|
* 0 on undecodable / not enough bytes. Stateless: no allocation, no I/O, no
|
||||||
|
* shared mutable state - reentrant. */
|
||||||
|
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out);
|
||||||
|
|
||||||
|
/* The SECOND projection of the same backend parse: a full, lossless decode into
|
||||||
|
* the public insn_decoded (insndec.h) - concrete register roots, values, sizes,
|
||||||
|
* per-operand access, and the aggregate implicit-or-explicit read/write sets
|
||||||
|
* (incl. rsp/rdx:rax/FLAGS). ABI-AGNOSTIC: `call` reports only architectural
|
||||||
|
* effects (rsp/rip write, indirect target read), NEVER a volatile clobber. Each
|
||||||
|
* backend TU implements BOTH this and semsig_backend_decode behind the ONE seam.
|
||||||
|
* Returns 1 on success (fills *out), 0 on undecodable / not enough bytes.
|
||||||
|
* Stateless / reentrant - no allocation, no I/O, no shared mutable state. */
|
||||||
|
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out);
|
||||||
|
|
||||||
|
/* Stable backend identity for diagnostics / hash-domain tagging. Static,
|
||||||
|
* never-NULL. This declaration is shared with semsig.h's public symbol of the
|
||||||
|
* same name; the concrete backend TU (or the stub) defines it once. */
|
||||||
|
const char* semsig_backend_name(void);
|
||||||
|
|
||||||
|
#endif /* VMIE_SEMSIG_BACKEND_H */
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/* semsig_backend_capstone.c - Capstone implementation of the operand-decode seam.
|
||||||
|
*
|
||||||
|
* The ONE TU that includes Capstone headers and links its library. It implements
|
||||||
|
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||||
|
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||||
|
* backend name. Built ONLY when VMIE_DISASM=capstone (see CMakeLists.txt).
|
||||||
|
*
|
||||||
|
* Stateless: a Capstone handle is opened/closed on the call stack per decode, no
|
||||||
|
* global mutable state - reentrant. Capstone allocates internally for cs_disasm;
|
||||||
|
* the buffer is freed before return so there is no leak across the seam.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <capstone/capstone.h>
|
||||||
|
#include "semsig_backend.h"
|
||||||
|
|
||||||
|
/* Map a Capstone x86 instruction id to a backend-neutral SEM_MN_* class. STATIC
|
||||||
|
* table, not a switch forest; unlisted ids fall through to SEM_MN_OTHER (where
|
||||||
|
* the core folds the raw opcode). Conditional branches (Jcc) are a family folded
|
||||||
|
* by Capstone group, handled before the table lookup. */
|
||||||
|
typedef struct { unsigned int id; uint16_t cls; } mn_map;
|
||||||
|
|
||||||
|
static const mn_map MN_TABLE[] = {
|
||||||
|
/* moves */
|
||||||
|
{ X86_INS_MOV, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVZX, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVSX, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVSXD, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVAPS, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVAPD, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVDQA, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVDQU, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVD, SEM_MN_MOV },
|
||||||
|
{ X86_INS_MOVQ, SEM_MN_MOV },
|
||||||
|
{ X86_INS_XCHG, SEM_MN_MOV },
|
||||||
|
{ X86_INS_LEA, SEM_MN_LEA },
|
||||||
|
/* arithmetic */
|
||||||
|
{ X86_INS_ADD, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_ADC, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_SUB, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_SBB, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_INC, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_DEC, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_NEG, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_MUL, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_IMUL, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_DIV, SEM_MN_ARITH },
|
||||||
|
{ X86_INS_IDIV, SEM_MN_ARITH },
|
||||||
|
/* logic / shifts */
|
||||||
|
{ X86_INS_AND, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_OR, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_XOR, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_NOT, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_SHL, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_SHR, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_SAR, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_ROL, SEM_MN_LOGIC },
|
||||||
|
{ X86_INS_ROR, SEM_MN_LOGIC },
|
||||||
|
/* compare / test */
|
||||||
|
{ X86_INS_CMP, SEM_MN_CMP },
|
||||||
|
{ X86_INS_TEST, SEM_MN_TEST },
|
||||||
|
/* control flow */
|
||||||
|
{ X86_INS_JMP, SEM_MN_JMP },
|
||||||
|
{ X86_INS_CALL, SEM_MN_CALL },
|
||||||
|
{ X86_INS_RET, SEM_MN_RET },
|
||||||
|
{ X86_INS_RETF, SEM_MN_RET },
|
||||||
|
{ X86_INS_RETFQ, SEM_MN_RET },
|
||||||
|
{ X86_INS_PUSH, SEM_MN_PUSH },
|
||||||
|
{ X86_INS_POP, SEM_MN_POP },
|
||||||
|
{ X86_INS_NOP, SEM_MN_NOP },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* True if `g` is among the instruction's Capstone groups. */
|
||||||
|
static int has_group(const cs_insn* in, uint8_t g) {
|
||||||
|
const cs_detail* d = in->detail;
|
||||||
|
if (!d) { return 0; }
|
||||||
|
for (uint8_t i = 0; i < d->groups_count; i++) {
|
||||||
|
if (d->groups[i] == g) { return 1; }
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t classify_mnemonic(const cs_insn* in) {
|
||||||
|
if (has_group(in, X86_GRP_CALL)) { return SEM_MN_CALL; }
|
||||||
|
if (has_group(in, X86_GRP_RET)) { return SEM_MN_RET; }
|
||||||
|
if (has_group(in, X86_GRP_JUMP)) {
|
||||||
|
/* unconditional jmp vs conditional jcc: JMP id is unconditional. */
|
||||||
|
return (in->id == X86_INS_JMP) ? SEM_MN_JMP : SEM_MN_BRANCH;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||||
|
if (MN_TABLE[i].id == in->id) { return MN_TABLE[i].cls; }
|
||||||
|
}
|
||||||
|
if (has_group(in, X86_GRP_FPU)) { return SEM_MN_FLOAT; }
|
||||||
|
if (has_group(in, X86_GRP_SSE) || has_group(in, X86_GRP_SSE1) ||
|
||||||
|
has_group(in, X86_GRP_SSE2)) { return SEM_MN_VEC; }
|
||||||
|
return SEM_MN_OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map a Capstone x86 register id to a canonical SEM_RC_* class. Capstone has no
|
||||||
|
* single "register class" accessor; classify by the documented id ranges. */
|
||||||
|
static uint8_t reg_class_of(unsigned int reg) {
|
||||||
|
if (reg == X86_REG_INVALID) { return SEM_RC_NONE; }
|
||||||
|
if ((reg >= X86_REG_AH && reg <= X86_REG_BH) ||
|
||||||
|
(reg >= X86_REG_AL && reg <= X86_REG_R15D) ||
|
||||||
|
(reg >= X86_REG_RAX && reg <= X86_REG_RBP) ||
|
||||||
|
(reg >= X86_REG_RSP && reg <= X86_REG_RDI) ||
|
||||||
|
(reg >= X86_REG_R8 && reg <= X86_REG_R15)) { return SEM_RC_GPR; }
|
||||||
|
if (reg >= X86_REG_XMM0 && reg <= X86_REG_ZMM31) { return SEM_RC_XMM; }
|
||||||
|
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) { return SEM_RC_MASK; }
|
||||||
|
if (reg >= X86_REG_CS && reg <= X86_REG_SS) { return SEM_RC_SEG; }
|
||||||
|
if (reg >= X86_REG_EFLAGS && reg <= X86_REG_RIP) { return SEM_RC_FLAGS; }
|
||||||
|
return SEM_RC_OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t width_log2_of(uint8_t size_bytes) {
|
||||||
|
uint8_t l = 0;
|
||||||
|
uint8_t v = size_bytes;
|
||||||
|
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||||
|
if (!code || !out || avail == 0) { return 0; }
|
||||||
|
|
||||||
|
csh h;
|
||||||
|
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
|
||||||
|
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
|
||||||
|
|
||||||
|
cs_insn* insn = NULL;
|
||||||
|
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
|
||||||
|
if (n < 1 || !insn) {
|
||||||
|
if (insn) { cs_free(insn, n); }
|
||||||
|
cs_close(&h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||||
|
out->length = (uint8_t)insn->size;
|
||||||
|
out->mnemonic_class = classify_mnemonic(insn);
|
||||||
|
out->is_control_flow =
|
||||||
|
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
|
||||||
|
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
|
||||||
|
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER && insn->detail)
|
||||||
|
? insn->detail->x86.opcode[0] : 0u;
|
||||||
|
|
||||||
|
uint8_t no = 0;
|
||||||
|
if (insn->detail) {
|
||||||
|
const cs_x86* x = &insn->detail->x86;
|
||||||
|
for (uint8_t i = 0; i < x->op_count && no < 4; i++) {
|
||||||
|
const cs_x86_op* o = &x->operands[i];
|
||||||
|
switch (o->type) {
|
||||||
|
case X86_OP_REG:
|
||||||
|
out->op[no].kind = SEMOP_REG;
|
||||||
|
out->op[no].reg_class = reg_class_of(o->reg);
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
case X86_OP_MEM:
|
||||||
|
out->op[no].kind = SEMOP_MEM;
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
out->op[no].mem_has_base =
|
||||||
|
(o->mem.base != X86_REG_INVALID &&
|
||||||
|
o->mem.base != X86_REG_RIP) ? 1u : 0u;
|
||||||
|
out->op[no].mem_has_index =
|
||||||
|
(o->mem.index != X86_REG_INVALID) ? 1u : 0u;
|
||||||
|
out->op[no].mem_is_riprel =
|
||||||
|
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
case X86_OP_IMM:
|
||||||
|
out->op[no].kind = SEMOP_IMM;
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out->noperands = no;
|
||||||
|
|
||||||
|
cs_free(insn, n);
|
||||||
|
cs_close(&h);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
|
||||||
|
|
||||||
|
/* Map a Capstone x86 register id to a canonical reg_id ROOT (al/ax/eax/rax ->
|
||||||
|
* REGID_RAX, xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). Capstone has
|
||||||
|
* no "largest enclosing" helper, so fold by the documented id ranges (the same
|
||||||
|
* approach reg_class_of already uses). The raw x86_reg never leaves this TU. */
|
||||||
|
static uint8_t reg_root_of(unsigned int reg) {
|
||||||
|
switch (reg) {
|
||||||
|
case X86_REG_AL: case X86_REG_AH: case X86_REG_AX:
|
||||||
|
case X86_REG_EAX: case X86_REG_RAX: return REGID_RAX;
|
||||||
|
case X86_REG_CL: case X86_REG_CH: case X86_REG_CX:
|
||||||
|
case X86_REG_ECX: case X86_REG_RCX: return REGID_RCX;
|
||||||
|
case X86_REG_DL: case X86_REG_DH: case X86_REG_DX:
|
||||||
|
case X86_REG_EDX: case X86_REG_RDX: return REGID_RDX;
|
||||||
|
case X86_REG_BL: case X86_REG_BH: case X86_REG_BX:
|
||||||
|
case X86_REG_EBX: case X86_REG_RBX: return REGID_RBX;
|
||||||
|
case X86_REG_SPL: case X86_REG_SP: case X86_REG_ESP:
|
||||||
|
case X86_REG_RSP: return REGID_RSP;
|
||||||
|
case X86_REG_BPL: case X86_REG_BP: case X86_REG_EBP:
|
||||||
|
case X86_REG_RBP: return REGID_RBP;
|
||||||
|
case X86_REG_SIL: case X86_REG_SI: case X86_REG_ESI:
|
||||||
|
case X86_REG_RSI: return REGID_RSI;
|
||||||
|
case X86_REG_DIL: case X86_REG_DI: case X86_REG_EDI:
|
||||||
|
case X86_REG_RDI: return REGID_RDI;
|
||||||
|
case X86_REG_R8B: case X86_REG_R8W: case X86_REG_R8D:
|
||||||
|
case X86_REG_R8: return REGID_R8;
|
||||||
|
case X86_REG_R9B: case X86_REG_R9W: case X86_REG_R9D:
|
||||||
|
case X86_REG_R9: return REGID_R9;
|
||||||
|
case X86_REG_R10B: case X86_REG_R10W: case X86_REG_R10D:
|
||||||
|
case X86_REG_R10: return REGID_R10;
|
||||||
|
case X86_REG_R11B: case X86_REG_R11W: case X86_REG_R11D:
|
||||||
|
case X86_REG_R11: return REGID_R11;
|
||||||
|
case X86_REG_R12B: case X86_REG_R12W: case X86_REG_R12D:
|
||||||
|
case X86_REG_R12: return REGID_R12;
|
||||||
|
case X86_REG_R13B: case X86_REG_R13W: case X86_REG_R13D:
|
||||||
|
case X86_REG_R13: return REGID_R13;
|
||||||
|
case X86_REG_R14B: case X86_REG_R14W: case X86_REG_R14D:
|
||||||
|
case X86_REG_R14: return REGID_R14;
|
||||||
|
case X86_REG_R15B: case X86_REG_R15W: case X86_REG_R15D:
|
||||||
|
case X86_REG_R15: return REGID_R15;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) {
|
||||||
|
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_XMM0));
|
||||||
|
}
|
||||||
|
if (reg >= X86_REG_YMM0 && reg <= X86_REG_YMM31) {
|
||||||
|
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_YMM0));
|
||||||
|
}
|
||||||
|
if (reg >= X86_REG_ZMM0 && reg <= X86_REG_ZMM31) {
|
||||||
|
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_ZMM0));
|
||||||
|
}
|
||||||
|
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) {
|
||||||
|
return (uint8_t)(REGID_K0 + (reg - X86_REG_K0));
|
||||||
|
}
|
||||||
|
if (reg == X86_REG_EFLAGS) { return REGID_FLAGS; }
|
||||||
|
if (reg == X86_REG_INVALID) { return REGID_NONE; }
|
||||||
|
return REGID_OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Approximate the ACCESS width of a register in log2 bytes from its sub-register
|
||||||
|
* id (Capstone register ids encode the width); used when only an id is known
|
||||||
|
* (cs_regs_access returns ids without sizes). Coarse but enough for overlap. */
|
||||||
|
static uint8_t reg_width_log2(unsigned int reg) {
|
||||||
|
switch (reg) {
|
||||||
|
case X86_REG_AL: case X86_REG_AH: case X86_REG_CL: case X86_REG_CH:
|
||||||
|
case X86_REG_DL: case X86_REG_DH: case X86_REG_BL: case X86_REG_BH:
|
||||||
|
case X86_REG_SPL: case X86_REG_BPL: case X86_REG_SIL: case X86_REG_DIL:
|
||||||
|
case X86_REG_R8B: case X86_REG_R9B: case X86_REG_R10B:
|
||||||
|
case X86_REG_R11B: case X86_REG_R12B: case X86_REG_R13B:
|
||||||
|
case X86_REG_R14B: case X86_REG_R15B: return 0;
|
||||||
|
case X86_REG_AX: case X86_REG_CX: case X86_REG_DX: case X86_REG_BX:
|
||||||
|
case X86_REG_SP: case X86_REG_BP: case X86_REG_SI: case X86_REG_DI:
|
||||||
|
case X86_REG_R8W: case X86_REG_R9W: case X86_REG_R10W:
|
||||||
|
case X86_REG_R11W: case X86_REG_R12W: case X86_REG_R13W:
|
||||||
|
case X86_REG_R14W: case X86_REG_R15W: return 1;
|
||||||
|
case X86_REG_EAX: case X86_REG_ECX: case X86_REG_EDX: case X86_REG_EBX:
|
||||||
|
case X86_REG_ESP: case X86_REG_EBP: case X86_REG_ESI: case X86_REG_EDI:
|
||||||
|
case X86_REG_R8D: case X86_REG_R9D: case X86_REG_R10D:
|
||||||
|
case X86_REG_R11D: case X86_REG_R12D: case X86_REG_R13D:
|
||||||
|
case X86_REG_R14D: case X86_REG_R15D: return 2;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) { return 4; }
|
||||||
|
return 3; /* 64-bit GPR / vector-as-root / other: default qword */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backend-neutral mnemonic CLASS for the rich decode (insn_class enum), by
|
||||||
|
* translating the existing classify_mnemonic SEM_MN_* result. */
|
||||||
|
static uint8_t insn_class_of(const cs_insn* in) {
|
||||||
|
switch (classify_mnemonic(in)) {
|
||||||
|
case SEM_MN_MOV: return INSN_MOV;
|
||||||
|
case SEM_MN_ARITH: return INSN_ARITH;
|
||||||
|
case SEM_MN_LOGIC: return INSN_LOGIC;
|
||||||
|
case SEM_MN_CMP: return INSN_CMP;
|
||||||
|
case SEM_MN_TEST: return INSN_TEST;
|
||||||
|
case SEM_MN_BRANCH: return INSN_BRANCH;
|
||||||
|
case SEM_MN_JMP: return INSN_JMP;
|
||||||
|
case SEM_MN_CALL: return INSN_CALL;
|
||||||
|
case SEM_MN_RET: return INSN_RET;
|
||||||
|
case SEM_MN_PUSH: return INSN_PUSH;
|
||||||
|
case SEM_MN_POP: return INSN_POP;
|
||||||
|
case SEM_MN_LEA: return INSN_LEA;
|
||||||
|
case SEM_MN_NOP: return INSN_NOP;
|
||||||
|
case SEM_MN_FLOAT: return INSN_FLOAT;
|
||||||
|
case SEM_MN_VEC: return INSN_VEC;
|
||||||
|
default: return INSN_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normalize a Capstone cs_ac_type access mask to a reg_access. */
|
||||||
|
static uint8_t access_of(uint8_t a) {
|
||||||
|
const int r = (a & CS_AC_READ) != 0;
|
||||||
|
const int w = (a & CS_AC_WRITE) != 0;
|
||||||
|
if (r && w) { return ACC_READWRITE; }
|
||||||
|
if (r) { return ACC_READ; }
|
||||||
|
if (w) { return ACC_WRITE; }
|
||||||
|
return ACC_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* writes_zero_extend rule (see insndec.h): 32-bit GPR write zero-extends; EVEX/
|
||||||
|
* VEX vector write zero-extends the upper part; 8/16-bit and legacy-SSE are
|
||||||
|
* partial. */
|
||||||
|
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
|
||||||
|
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||||
|
return (width_log2 == 2) ? 1u : 0u;
|
||||||
|
}
|
||||||
|
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||||
|
return vector_evex_vex ? 1u : 0u;
|
||||||
|
}
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
|
||||||
|
uint8_t width_log2, uint8_t zx, uint8_t access) {
|
||||||
|
if (root == REGID_NONE) { return; }
|
||||||
|
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
|
||||||
|
if (set[i].root == root) {
|
||||||
|
if (width_log2 > set[i].width_log2) {
|
||||||
|
set[i].width_log2 = width_log2;
|
||||||
|
set[i].writes_zero_extend = zx;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (*n < INSN_MAX_REGREFS) {
|
||||||
|
set[*n].root = root;
|
||||||
|
set[*n].width_log2 = width_log2;
|
||||||
|
set[*n].writes_zero_extend = zx;
|
||||||
|
set[*n].access = access;
|
||||||
|
}
|
||||||
|
(*n)++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
|
||||||
|
if (!code || !out || avail == 0) { return 0; }
|
||||||
|
|
||||||
|
csh h;
|
||||||
|
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
|
||||||
|
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
|
||||||
|
|
||||||
|
cs_insn* insn = NULL;
|
||||||
|
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
|
||||||
|
if (n < 1 || !insn) {
|
||||||
|
if (insn) { cs_free(insn, n); }
|
||||||
|
cs_close(&h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||||
|
out->length = (uint8_t)insn->size;
|
||||||
|
out->mnemonic_class = insn_class_of(insn);
|
||||||
|
out->is_control_flow =
|
||||||
|
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
|
||||||
|
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
|
||||||
|
|
||||||
|
if (insn->detail) {
|
||||||
|
const cs_x86* x = &insn->detail->x86;
|
||||||
|
/* A VEX/EVEX form zero-extends the upper part of a written vector root;
|
||||||
|
* legacy-SSE preserves it. Detect by the VEX/EVEX leading opcode byte. */
|
||||||
|
const int is_vex_evex =
|
||||||
|
(x->opcode[0] == 0xC4 || x->opcode[0] == 0xC5 || x->opcode[0] == 0x62);
|
||||||
|
|
||||||
|
/* Explicit operands (readable view): op[]. */
|
||||||
|
uint8_t no = 0;
|
||||||
|
for (uint8_t i = 0; i < x->op_count && no < INSN_MAX_OPERANDS; i++) {
|
||||||
|
const cs_x86_op* o = &x->operands[i];
|
||||||
|
const uint8_t acc = access_of(o->access);
|
||||||
|
const uint8_t wl = width_log2_of(o->size);
|
||||||
|
switch (o->type) {
|
||||||
|
case X86_OP_REG: {
|
||||||
|
const uint8_t root = reg_root_of(o->reg);
|
||||||
|
out->op[no].kind = OPND_REG;
|
||||||
|
out->op[no].access = acc;
|
||||||
|
out->op[no].width_log2 = wl;
|
||||||
|
out->op[no].reg_root = root;
|
||||||
|
out->op[no].reg_zero_extend = zx_of(root, wl, is_vex_evex);
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case X86_OP_MEM:
|
||||||
|
out->op[no].kind = OPND_MEM;
|
||||||
|
out->op[no].access = acc;
|
||||||
|
out->op[no].width_log2 = wl;
|
||||||
|
out->op[no].mem_base =
|
||||||
|
(o->mem.base == X86_REG_RIP) ? REGID_NONE
|
||||||
|
: reg_root_of(o->mem.base);
|
||||||
|
out->op[no].mem_index = reg_root_of(o->mem.index);
|
||||||
|
out->op[no].mem_scale = (uint8_t)o->mem.scale;
|
||||||
|
out->op[no].mem_is_riprel =
|
||||||
|
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
|
||||||
|
out->op[no].mem_disp = o->mem.disp;
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
case X86_OP_IMM:
|
||||||
|
out->op[no].kind = OPND_IMM;
|
||||||
|
out->op[no].access = ACC_READ;
|
||||||
|
out->op[no].imm_width_log2 = wl;
|
||||||
|
out->op[no].imm_value = o->imm;
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out->noperands = no;
|
||||||
|
|
||||||
|
/* Aggregate implicit-or-explicit register sets via cs_regs_access: the
|
||||||
|
* FULL read/write sets including rsp/rdx:rax/EFLAGS. EFLAGS -> REGID_FLAGS;
|
||||||
|
* vector writes carry zero-extend by form. */
|
||||||
|
cs_regs rd, wr;
|
||||||
|
uint8_t nrd = 0, nwr = 0;
|
||||||
|
if (cs_regs_access(h, insn, rd, &nrd, wr, &nwr) == CS_ERR_OK) {
|
||||||
|
for (uint8_t i = 0; i < nrd; i++) {
|
||||||
|
const uint8_t root = reg_root_of(rd[i]);
|
||||||
|
regset_add(out->reads, &out->nreads, root,
|
||||||
|
reg_width_log2(rd[i]), 0, ACC_READ);
|
||||||
|
}
|
||||||
|
for (uint8_t i = 0; i < nwr; i++) {
|
||||||
|
const uint8_t root = reg_root_of(wr[i]);
|
||||||
|
const uint8_t wl = reg_width_log2(wr[i]);
|
||||||
|
regset_add(out->writes, &out->nwrites, root, wl,
|
||||||
|
zx_of(root, wl, is_vex_evex), ACC_WRITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cs_free(insn, n);
|
||||||
|
cs_close(&h);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* semsig_backend_name(void) {
|
||||||
|
return "capstone";
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
/* semsig_backend_zydis.c - Zydis implementation of the operand-decode seam.
|
||||||
|
*
|
||||||
|
* The ONE TU that includes Zydis headers and links its library. It implements
|
||||||
|
* the semsig_backend.h contract: decode one x86-64 instruction into a sem_insn
|
||||||
|
* (length, backend-neutral mnemonic class, normalized operands), and report the
|
||||||
|
* backend name. Built ONLY when VMIE_DISASM=zydis (see CMakeLists.txt).
|
||||||
|
*
|
||||||
|
* Stateless: a ZydisDecoder is initialized on the call stack per decode, no
|
||||||
|
* global mutable state - reentrant, no allocation, no I/O.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <Zydis/Zydis.h>
|
||||||
|
#include "semsig_backend.h"
|
||||||
|
|
||||||
|
/* Map a Zydis mnemonic to a backend-neutral SEM_MN_* class. A STATIC table, not
|
||||||
|
* a switch forest: only the groups that matter for normalization are listed;
|
||||||
|
* everything else falls through to SEM_MN_OTHER (where the core folds the raw
|
||||||
|
* opcode so unclassified instructions still differ). Keep entries sorted by
|
||||||
|
* mnemonic for readability; lookup is a linear scan (cold path, one per insn). */
|
||||||
|
typedef struct { ZydisMnemonic mn; uint16_t cls; } mn_map;
|
||||||
|
|
||||||
|
static const mn_map MN_TABLE[] = {
|
||||||
|
/* moves */
|
||||||
|
{ ZYDIS_MNEMONIC_MOV, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVZX, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVSX, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVSXD, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVAPS, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVAPD, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVDQA, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVDQU, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVD, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_MOVQ, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_XCHG, SEM_MN_MOV },
|
||||||
|
{ ZYDIS_MNEMONIC_LEA, SEM_MN_LEA },
|
||||||
|
/* arithmetic */
|
||||||
|
{ ZYDIS_MNEMONIC_ADD, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_ADC, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_SUB, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_SBB, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_INC, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_DEC, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_NEG, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_MUL, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_IMUL, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_DIV, SEM_MN_ARITH },
|
||||||
|
{ ZYDIS_MNEMONIC_IDIV, SEM_MN_ARITH },
|
||||||
|
/* logic / shifts */
|
||||||
|
{ ZYDIS_MNEMONIC_AND, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_OR, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_XOR, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_NOT, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_SHL, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_SHR, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_SAR, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_ROL, SEM_MN_LOGIC },
|
||||||
|
{ ZYDIS_MNEMONIC_ROR, SEM_MN_LOGIC },
|
||||||
|
/* compare / test */
|
||||||
|
{ ZYDIS_MNEMONIC_CMP, SEM_MN_CMP },
|
||||||
|
{ ZYDIS_MNEMONIC_TEST, SEM_MN_TEST },
|
||||||
|
/* control flow */
|
||||||
|
{ ZYDIS_MNEMONIC_JMP, SEM_MN_JMP },
|
||||||
|
{ ZYDIS_MNEMONIC_CALL, SEM_MN_CALL },
|
||||||
|
{ ZYDIS_MNEMONIC_RET, SEM_MN_RET },
|
||||||
|
{ ZYDIS_MNEMONIC_PUSH, SEM_MN_PUSH },
|
||||||
|
{ ZYDIS_MNEMONIC_POP, SEM_MN_POP },
|
||||||
|
{ ZYDIS_MNEMONIC_NOP, SEM_MN_NOP },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* jcc / setcc / cmovcc are a wide family; classify them by Zydis meta category
|
||||||
|
* rather than enumerating every conditional mnemonic. */
|
||||||
|
static uint16_t classify_mnemonic(const ZydisDecodedInstruction* insn) {
|
||||||
|
/* conditional branch family -> BRANCH */
|
||||||
|
if (insn->meta.category == ZYDIS_CATEGORY_COND_BR) { return SEM_MN_BRANCH; }
|
||||||
|
if (insn->meta.category == ZYDIS_CATEGORY_UNCOND_BR) { return SEM_MN_JMP; }
|
||||||
|
if (insn->meta.category == ZYDIS_CATEGORY_CALL) { return SEM_MN_CALL; }
|
||||||
|
if (insn->meta.category == ZYDIS_CATEGORY_RET) { return SEM_MN_RET; }
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof MN_TABLE / sizeof MN_TABLE[0]; i++) {
|
||||||
|
if (MN_TABLE[i].mn == insn->mnemonic) { return MN_TABLE[i].cls; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* float / vector fall-backs by meta category, else OTHER. */
|
||||||
|
if (insn->meta.category == ZYDIS_CATEGORY_X87_ALU) { return SEM_MN_FLOAT; }
|
||||||
|
if (insn->meta.isa_set >= ZYDIS_ISA_SET_SSE &&
|
||||||
|
insn->meta.isa_set <= ZYDIS_ISA_SET_SSE4A) { return SEM_MN_VEC; }
|
||||||
|
return SEM_MN_OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map a Zydis register to a canonical SEM_RC_* class (identity erased). */
|
||||||
|
static uint8_t reg_class_of(ZydisRegister reg) {
|
||||||
|
const ZydisRegisterClass rc = ZydisRegisterGetClass(reg);
|
||||||
|
switch (rc) {
|
||||||
|
case ZYDIS_REGCLASS_GPR8:
|
||||||
|
case ZYDIS_REGCLASS_GPR16:
|
||||||
|
case ZYDIS_REGCLASS_GPR32:
|
||||||
|
case ZYDIS_REGCLASS_GPR64: return SEM_RC_GPR;
|
||||||
|
case ZYDIS_REGCLASS_XMM:
|
||||||
|
case ZYDIS_REGCLASS_YMM:
|
||||||
|
case ZYDIS_REGCLASS_ZMM: return SEM_RC_XMM;
|
||||||
|
case ZYDIS_REGCLASS_MASK: return SEM_RC_MASK;
|
||||||
|
case ZYDIS_REGCLASS_SEGMENT: return SEM_RC_SEG;
|
||||||
|
case ZYDIS_REGCLASS_FLAGS:
|
||||||
|
case ZYDIS_REGCLASS_IP: return SEM_RC_FLAGS;
|
||||||
|
case ZYDIS_REGCLASS_INVALID: return SEM_RC_NONE;
|
||||||
|
default: return SEM_RC_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* width in bytes -> log2 (0..4), saturating. */
|
||||||
|
static uint8_t width_log2_of(uint16_t size_bits) {
|
||||||
|
const uint32_t bytes = (uint32_t)size_bits / 8u;
|
||||||
|
uint8_t l = 0;
|
||||||
|
uint32_t v = bytes;
|
||||||
|
while (v > 1u && l < 4u) { v >>= 1; l++; }
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
||||||
|
if (!code || !out || avail == 0) { return 0; }
|
||||||
|
|
||||||
|
ZydisDecoder dec;
|
||||||
|
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
|
||||||
|
ZYDIS_STACK_WIDTH_64))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZydisDecodedInstruction insn;
|
||||||
|
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
|
||||||
|
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||||
|
out->length = (uint8_t)insn.length;
|
||||||
|
out->mnemonic_class = classify_mnemonic(&insn);
|
||||||
|
out->is_control_flow =
|
||||||
|
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_CALL ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
|
||||||
|
out->raw_opcode = (out->mnemonic_class == SEM_MN_OTHER) ? insn.opcode : 0u;
|
||||||
|
|
||||||
|
/* Reduce only EXPLICIT operands (implicit ones - e.g. flags, rsp on push -
|
||||||
|
* are decoder noise that would differ pointlessly across forms). */
|
||||||
|
uint8_t no = 0;
|
||||||
|
for (uint8_t i = 0; i < insn.operand_count && no < 4; i++) {
|
||||||
|
const ZydisDecodedOperand* o = &ops[i];
|
||||||
|
if (o->visibility != ZYDIS_OPERAND_VISIBILITY_EXPLICIT) { continue; }
|
||||||
|
switch (o->type) {
|
||||||
|
case ZYDIS_OPERAND_TYPE_REGISTER:
|
||||||
|
out->op[no].kind = SEMOP_REG;
|
||||||
|
out->op[no].reg_class = reg_class_of(o->reg.value);
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
case ZYDIS_OPERAND_TYPE_MEMORY:
|
||||||
|
out->op[no].kind = SEMOP_MEM;
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
out->op[no].mem_has_base =
|
||||||
|
(o->mem.base != ZYDIS_REGISTER_NONE &&
|
||||||
|
o->mem.base != ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||||
|
out->op[no].mem_has_index =
|
||||||
|
(o->mem.index != ZYDIS_REGISTER_NONE) ? 1u : 0u;
|
||||||
|
out->op[no].mem_is_riprel =
|
||||||
|
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
case ZYDIS_OPERAND_TYPE_IMMEDIATE:
|
||||||
|
out->op[no].kind = SEMOP_IMM;
|
||||||
|
out->op[no].width_log2 = width_log2_of(o->size);
|
||||||
|
no++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; /* pointer/unused: ignored */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out->noperands = no;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
|
||||||
|
|
||||||
|
/* Map a Zydis register to a canonical reg_id ROOT (al/ax/eax/rax -> REGID_RAX,
|
||||||
|
* xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). The enclosing fold is
|
||||||
|
* done with ZydisRegisterGetLargestEnclosing first (GPR -> RAX..R15, vector ->
|
||||||
|
* ZMM0..ZMM31); then a class-keyed offset turns the backend enum into the stable
|
||||||
|
* reg_id. The raw ZydisRegister never leaves this TU. */
|
||||||
|
static uint8_t reg_root_of(ZydisRegister reg) {
|
||||||
|
if (reg == ZYDIS_REGISTER_NONE) { return REGID_NONE; }
|
||||||
|
|
||||||
|
const ZydisRegister enc =
|
||||||
|
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg);
|
||||||
|
const ZydisRegister r = (enc == ZYDIS_REGISTER_NONE) ? reg : enc;
|
||||||
|
|
||||||
|
if (r >= ZYDIS_REGISTER_RAX && r <= ZYDIS_REGISTER_R15) {
|
||||||
|
return (uint8_t)(REGID_RAX + (r - ZYDIS_REGISTER_RAX));
|
||||||
|
}
|
||||||
|
if (r >= ZYDIS_REGISTER_ZMM0 && r <= ZYDIS_REGISTER_ZMM31) {
|
||||||
|
return (uint8_t)(REGID_VEC0 + (r - ZYDIS_REGISTER_ZMM0));
|
||||||
|
}
|
||||||
|
if (r >= ZYDIS_REGISTER_K0 && r <= ZYDIS_REGISTER_K7) {
|
||||||
|
return (uint8_t)(REGID_K0 + (r - ZYDIS_REGISTER_K0));
|
||||||
|
}
|
||||||
|
if (r == ZYDIS_REGISTER_FLAGS || r == ZYDIS_REGISTER_EFLAGS ||
|
||||||
|
r == ZYDIS_REGISTER_RFLAGS) {
|
||||||
|
return REGID_FLAGS;
|
||||||
|
}
|
||||||
|
return REGID_OTHER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backend-neutral mnemonic CLASS for the rich decode (the insn_class enum). The
|
||||||
|
* groups mirror SEM_MN_* but the values are the PUBLIC insn_class ids; reuse the
|
||||||
|
* existing classify_mnemonic by translating its SEM_MN_* result. */
|
||||||
|
static uint8_t insn_class_of(const ZydisDecodedInstruction* insn) {
|
||||||
|
switch (classify_mnemonic(insn)) {
|
||||||
|
case SEM_MN_MOV: return INSN_MOV;
|
||||||
|
case SEM_MN_ARITH: return INSN_ARITH;
|
||||||
|
case SEM_MN_LOGIC: return INSN_LOGIC;
|
||||||
|
case SEM_MN_CMP: return INSN_CMP;
|
||||||
|
case SEM_MN_TEST: return INSN_TEST;
|
||||||
|
case SEM_MN_BRANCH: return INSN_BRANCH;
|
||||||
|
case SEM_MN_JMP: return INSN_JMP;
|
||||||
|
case SEM_MN_CALL: return INSN_CALL;
|
||||||
|
case SEM_MN_RET: return INSN_RET;
|
||||||
|
case SEM_MN_PUSH: return INSN_PUSH;
|
||||||
|
case SEM_MN_POP: return INSN_POP;
|
||||||
|
case SEM_MN_LEA: return INSN_LEA;
|
||||||
|
case SEM_MN_NOP: return INSN_NOP;
|
||||||
|
case SEM_MN_FLOAT: return INSN_FLOAT;
|
||||||
|
case SEM_MN_VEC: return INSN_VEC;
|
||||||
|
default: return INSN_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Normalize ZydisOperandActions to a reg_access. */
|
||||||
|
static uint8_t access_of(ZydisOperandActions a) {
|
||||||
|
const int r = (a & ZYDIS_OPERAND_ACTION_READ) != 0;
|
||||||
|
const int w = (a & ZYDIS_OPERAND_ACTION_WRITE) != 0;
|
||||||
|
const int cr = (a & ZYDIS_OPERAND_ACTION_CONDREAD) != 0;
|
||||||
|
const int cw = (a & ZYDIS_OPERAND_ACTION_CONDWRITE) != 0;
|
||||||
|
if (r && w) { return ACC_READWRITE; }
|
||||||
|
if (r) { return ACC_READ; }
|
||||||
|
if (w) { return ACC_WRITE; }
|
||||||
|
if (cw) { return ACC_COND_WRITE; }
|
||||||
|
if (cr) { return ACC_COND_READ; }
|
||||||
|
return ACC_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* writes_zero_extend rule (see insndec.h): a 32-bit GPR write zero-extends into
|
||||||
|
* its 64-bit root; an EVEX/VEX vector write zero-extends the upper YMM/ZMM. 8/16-
|
||||||
|
* bit GPR and legacy-SSE writes are PARTIAL. Computed from width + form, never by
|
||||||
|
* branching on a concrete register. */
|
||||||
|
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
|
||||||
|
if (root >= REGID_RAX && root <= REGID_R15) {
|
||||||
|
return (width_log2 == 2) ? 1u : 0u; /* 32-bit GPR write zero-extends */
|
||||||
|
}
|
||||||
|
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
|
||||||
|
return vector_evex_vex ? 1u : 0u; /* VEX/EVEX zeroes the upper part */
|
||||||
|
}
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Append a reg_ref to a set (dedup by root: keep the WIDEST width / strongest
|
||||||
|
* access so a root appears once). Count grows past the cap so truncation is
|
||||||
|
* detectable; writes stop at INSN_MAX_REGREFS. */
|
||||||
|
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
|
||||||
|
uint8_t width_log2, uint8_t zx, uint8_t access) {
|
||||||
|
if (root == REGID_NONE) { return; }
|
||||||
|
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
|
||||||
|
if (set[i].root == root) {
|
||||||
|
if (width_log2 > set[i].width_log2) {
|
||||||
|
set[i].width_log2 = width_log2;
|
||||||
|
set[i].writes_zero_extend = zx;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (*n < INSN_MAX_REGREFS) {
|
||||||
|
set[*n].root = root;
|
||||||
|
set[*n].width_log2 = width_log2;
|
||||||
|
set[*n].writes_zero_extend = zx;
|
||||||
|
set[*n].access = access;
|
||||||
|
}
|
||||||
|
(*n)++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
|
||||||
|
if (!code || !out || avail == 0) { return 0; }
|
||||||
|
|
||||||
|
ZydisDecoder dec;
|
||||||
|
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
|
||||||
|
ZYDIS_STACK_WIDTH_64))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ZydisDecodedInstruction insn;
|
||||||
|
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
|
||||||
|
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
|
||||||
|
out->length = (uint8_t)insn.length;
|
||||||
|
out->mnemonic_class = insn_class_of(&insn);
|
||||||
|
out->is_control_flow =
|
||||||
|
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_CALL ||
|
||||||
|
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
|
||||||
|
|
||||||
|
/* A VEX/EVEX-encoded vector form zero-extends the upper part of a written
|
||||||
|
* vector root; legacy-SSE preserves it. Approximate from the encoding. */
|
||||||
|
const int vec_zx = (insn.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX ||
|
||||||
|
insn.encoding == ZYDIS_INSTRUCTION_ENCODING_EVEX);
|
||||||
|
|
||||||
|
/* Pass over ALL operands. EXPLICIT ones populate op[] (readable view); EVERY
|
||||||
|
* operand (explicit AND implicit/hidden: rsp on push/pop, rdx:rax on mul, the
|
||||||
|
* memory address registers) feeds the aggregate reads[]/writes[] sets. */
|
||||||
|
uint8_t no = 0;
|
||||||
|
for (uint8_t i = 0; i < insn.operand_count && i < ZYDIS_MAX_OPERAND_COUNT;
|
||||||
|
i++) {
|
||||||
|
const ZydisDecodedOperand* o = &ops[i];
|
||||||
|
const uint8_t acc = access_of(o->actions);
|
||||||
|
const uint8_t wl = width_log2_of(o->size);
|
||||||
|
|
||||||
|
if (o->type == ZYDIS_OPERAND_TYPE_REGISTER) {
|
||||||
|
const uint8_t root = reg_root_of(o->reg.value);
|
||||||
|
const uint8_t zx = zx_of(root, wl, vec_zx);
|
||||||
|
if (acc == ACC_READ || acc == ACC_READWRITE || acc == ACC_COND_READ) {
|
||||||
|
regset_add(out->reads, &out->nreads, root, wl, 0, acc);
|
||||||
|
}
|
||||||
|
if (acc == ACC_WRITE || acc == ACC_READWRITE ||
|
||||||
|
acc == ACC_COND_WRITE) {
|
||||||
|
regset_add(out->writes, &out->nwrites, root, wl, zx, acc);
|
||||||
|
}
|
||||||
|
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||||
|
no < INSN_MAX_OPERANDS) {
|
||||||
|
out->op[no].kind = OPND_REG;
|
||||||
|
out->op[no].access = acc;
|
||||||
|
out->op[no].width_log2 = wl;
|
||||||
|
out->op[no].reg_root = root;
|
||||||
|
out->op[no].reg_zero_extend = zx;
|
||||||
|
no++;
|
||||||
|
}
|
||||||
|
} else if (o->type == ZYDIS_OPERAND_TYPE_MEMORY) {
|
||||||
|
/* Address registers are READ regardless of the operand's own access. */
|
||||||
|
const uint8_t base = (o->mem.base == ZYDIS_REGISTER_RIP)
|
||||||
|
? REGID_NONE : reg_root_of(o->mem.base);
|
||||||
|
const uint8_t index = reg_root_of(o->mem.index);
|
||||||
|
if (base != REGID_NONE) {
|
||||||
|
regset_add(out->reads, &out->nreads, base, 3, 0, ACC_READ);
|
||||||
|
}
|
||||||
|
if (index != REGID_NONE) {
|
||||||
|
regset_add(out->reads, &out->nreads, index, 3, 0, ACC_READ);
|
||||||
|
}
|
||||||
|
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||||
|
no < INSN_MAX_OPERANDS) {
|
||||||
|
out->op[no].kind = OPND_MEM;
|
||||||
|
out->op[no].access = acc;
|
||||||
|
out->op[no].width_log2 = wl;
|
||||||
|
out->op[no].mem_base = base;
|
||||||
|
out->op[no].mem_index = index;
|
||||||
|
out->op[no].mem_scale = o->mem.scale;
|
||||||
|
out->op[no].mem_is_riprel =
|
||||||
|
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
|
||||||
|
out->op[no].mem_disp =
|
||||||
|
o->mem.disp.has_displacement ? o->mem.disp.value : 0;
|
||||||
|
no++;
|
||||||
|
}
|
||||||
|
} else if (o->type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
|
||||||
|
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
|
||||||
|
no < INSN_MAX_OPERANDS) {
|
||||||
|
out->op[no].kind = OPND_IMM;
|
||||||
|
out->op[no].access = ACC_READ;
|
||||||
|
out->op[no].imm_width_log2 = wl;
|
||||||
|
out->op[no].imm_value = o->imm.is_signed
|
||||||
|
? o->imm.value.s
|
||||||
|
: (int64_t)o->imm.value.u;
|
||||||
|
no++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out->noperands = no;
|
||||||
|
|
||||||
|
/* FLAGS as ONE pseudo-register: read = TESTED, write = anything the
|
||||||
|
* instruction MODIFIES / SET_0 / SET_1 / leaves UNDEFINED. */
|
||||||
|
if (insn.cpu_flags) {
|
||||||
|
if (insn.cpu_flags->tested) {
|
||||||
|
regset_add(out->reads, &out->nreads, REGID_FLAGS, 2, 0, ACC_READ);
|
||||||
|
}
|
||||||
|
if (insn.cpu_flags->modified || insn.cpu_flags->set_0 ||
|
||||||
|
insn.cpu_flags->set_1 || insn.cpu_flags->undefined) {
|
||||||
|
regset_add(out->writes, &out->nwrites, REGID_FLAGS, 2, 0, ACC_WRITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* semsig_backend_name(void) {
|
||||||
|
return "zydis";
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* semsig_stub.c - the OFF (no-backend) path of the semantic-signature feature.
|
||||||
|
*
|
||||||
|
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides
|
||||||
|
* the full public generic surface so the ABI is stable - linking never fails
|
||||||
|
* for a missing symbol - while the feature is not present:
|
||||||
|
* semsig_hash -> 0 (the same "no hash" sentinel as func_hash)
|
||||||
|
* semsig_backend_name -> "none"
|
||||||
|
* Callers tell "feature off" from "empty/undecodable function" via the compile-
|
||||||
|
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime 0.
|
||||||
|
*
|
||||||
|
* In the OFF build semsig.c is NOT compiled (it calls the backend decode, which
|
||||||
|
* does not exist without a backend); this stub is the whole generic symbol. See
|
||||||
|
* CMakeLists.txt for the source-selection logic.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include "semsig.h"
|
||||||
|
|
||||||
|
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||||
|
uint64_t semsig_hash(mem_view_t fn) {
|
||||||
|
(void)fn;
|
||||||
|
return 0; /* feature not built: same sentinel as func_hash */
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* semsig_backend_name(void) {
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user