9 Commits

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

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

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

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

The CI deb job builds with the Zydis backend (libzydis-dev); the runtime
package dependency on libzydis is derived automatically by dpkg-shlibdeps.
2026-06-28 21:51:47 +03:00
lirent 8ca84a5861 Bump version to 0.1.6 2026-06-26 07:57:36 +03:00
lirent 91e5e05de4 Scope gva_code_xref by an explicit prot_any argument
It hard-coded VR_X, so a target whose page tables expose no executable
region silently yielded zero xrefs. Callers now pass the scope (VR_X to
restrict to code, VR_R when the X bit is unavailable); the decoder and
exact target match remain the correctness gate. Mirrors the prot_any
parameter gva_imm_xref already carries.
2026-06-26 07:57:35 +03:00
lirent 32440c7104 Drop the user-half top-level NX bit when deriving VR_X
Under KVA shadow (KPTI) the kernel CR3 marks user-half PML4Es NX as
hardening, so OR-ing that top-level NX down the walk made every user
region non-executable - even live code (ntdll .text read back r--u).
Ring 3 executes those pages via the user/shadow CR3, which reaches the
same shared lower tables through an NX-clear PML4E, so the kernel
table's user-half PML4E NX is not what enforces user execution. Ignore
it on the user half; sub-PML4E NX (shared between both CR3s) stays
authoritative.
2026-06-26 07:57:26 +03:00
lirent d26f6c0bf0 Install file(1) in the Debian CI job so shlibdeps can run
cpack's CPACK_DEBIAN_PACKAGE_SHLIBDEPS runs dpkg-shlibdeps, which needs
the file utility to detect ELF binaries and derive the runtime libc
dependency. The slim CI image does not ship it, so packaging failed at
the cpack step. Add file to the job toolchain.
2026-06-22 16:32:47 +03:00
lirent 0c3aa5ef25 Build a shared library and ship it as Debian packages
Factor the source list into an OBJECT library so the same objects back
both the existing static lib and a new shared libvmie.so (SONAME from the
project version; the version comes from the tag, falling back to a cached
default). Add install rules and a CPack DEB config that produce two
packages: libvmie0 (the runtime .so + SONAME symlink) and libvmie-dev
(public headers under include/vmie + the linker symlink), with the dev
package depending on the exact runtime version.

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