diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c5e6e9c..7d571a5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -87,12 +87,13 @@ jobs: apt-get update apt-get install -y --no-install-recommends \ cmake make gcc libc6-dev dpkg-dev file \ + libzydis-dev \ gcc-mingw-w64-x86-64 ca-certificates curl - name: Configure env: TAG: ${{ github.ref_name }} - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v} + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVMIE_VERSION=${TAG#v} -DVMIE_DISASM=zydis - name: Build shared library run: cmake --build build -j --target vmie_shared diff --git a/CMakeLists.txt b/CMakeLists.txt index baa89a5..cc507db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,18 @@ include(GNUInstallDirs) option(VMIE_LTO "Enable LTO" OFF) # build-only; shipped default is -O2, no LTO +# ---- optional semantic-signature backend (operand disassembler) ---------- +# VMIE_DISASM selects the OPTIONAL operand-decode backend for the semantic +# signature feature (semsig.h). OFF (default) ships the 0.1.6 behaviour: the +# generic symbol exists via semsig_stub.c, returns 0, VMIE_HAVE_DISASM=0, and no +# external library is pulled - CI and the .deb packages are unchanged. A backend +# (zydis|capstone) is NEVER vendored: it is brought from OUTSIDE the tree, either +# by VMIE_DISASM_SRC (a path to the backend's own source tree, add_subdirectory'd) +# or, failing that, by find_package of an already-installed library. +set(VMIE_DISASM "OFF" CACHE STRING "Semantic-signature backend: OFF | zydis | capstone") +set_property(CACHE VMIE_DISASM PROPERTY STRINGS OFF zydis capstone) +set(VMIE_DISASM_SRC "" CACHE PATH "Path to the disassembler backend source tree (external; add_subdirectory'd)") + # ---- host: VMI core objects (single owner of the source list) ----------- add_library(vmie_obj OBJECT src/core/gpa.c @@ -31,6 +43,23 @@ add_library(vmie_obj OBJECT src/handlers/x86dec.c src/handlers/pmap.c src/handlers/snapdiff.c) + +# semantic-signature TUs: OFF compiles ONLY the stub (which carries the whole +# generic symbol and returns 0); a backend compiles the core (semsig.c) + the one +# backend TU that talks to the disassembler. semsig.c is never built in OFF (it +# calls the backend decode, absent there) - so there is no dead/unresolved call. +if(VMIE_DISASM STREQUAL "OFF") + target_sources(vmie_obj PRIVATE src/handlers/semsig_stub.c) +elseif(VMIE_DISASM STREQUAL "zydis") + target_sources(vmie_obj PRIVATE src/handlers/semsig.c + src/handlers/semsig_backend_zydis.c) +elseif(VMIE_DISASM STREQUAL "capstone") + target_sources(vmie_obj PRIVATE src/handlers/semsig.c + src/handlers/semsig_backend_capstone.c) +else() + message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')") +endif() + target_include_directories(vmie_obj PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include # public API: include/*.h PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/core/include # private: core.h @@ -42,11 +71,49 @@ if(VMIE_LTO) target_link_options(vmie_obj PRIVATE -flto) endif() +# Resolve the backend target/package name (the actual link/define is applied to +# vmie_obj here AND re-applied to the static/shared libs below, because +# $ 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) -------------- # $ 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_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 $ 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 $) @@ -58,6 +125,12 @@ set_target_properties(vmie_shared PROPERTIES if(VMIE_LTO) target_link_options(vmie_shared PRIVATE -flto) endif() +if(VMIE_DISASM STREQUAL "OFF") + target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=0) +else() + target_compile_definitions(vmie_shared PUBLIC VMIE_HAVE_DISASM=1) + target_link_libraries(vmie_shared PRIVATE ${VMIE_DISASM_LINK}) +endif() # ---- host: CLI demonstrator over the library ---------------------------- add_executable(vmie_cli src/cli.c) diff --git a/include/semsig.h b/include/semsig.h new file mode 100644 index 0000000..cad4136 --- /dev/null +++ b/include/semsig.h @@ -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 +#include +#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 */ diff --git a/include/win32.h b/include/win32.h index e7926c7..04b5f71 100644 --- a/include/win32.h +++ b/include/win32.h @@ -461,6 +461,37 @@ int vmie_win32_inline_hooks(vmie_win32* v, uint64_t cr3, uint64_t module_base, int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base, uint32_t func_rva, uint32_t* iat_rvas, int max); +/* Reorder-/register-canonical SEMANTIC hash of ONE function, addressed by its + * func_rva (a .pdata function start, as from vmie_win32_functions or an export). + * The win32 convenience entry point over the generic semsig_hash (semsig.h): it + * locates the function extent from .pdata (like vmie_win32_func_imports), + * gathers its body with gva_read, builds a SECTION_LOCAL mem_view_t over those + * bytes, and DELEGATES to semsig_hash - it does NOT reimplement normalization or + * folding, and it pulls in no disassembler backend itself (the feature is + * inherited transitively via the same VMIE_HAVE_DISASM as the generic). + * + * v - engine handle. + * cr3 - the process address space the module is mapped in. + * module_base - image base VA (its PE headers must be resident). + * func_rva - function start RVA (must match a .pdata function start). + * + * Returns the 64-bit semantic hash, or 0 on any of: func_rva is not a known + * function start, the body is unreadable (paged out), a decode desync, or the + * build has no disassembler backend (VMIE_HAVE_DISASM == 0) - the SAME "no hash" + * sentinel as semsig_hash / func_hash. NOT COMPARABLE to func_hash: it is a + * different (normalized) domain; compare it only to other vmie_win32_func_semsig + * / semsig_hash values. semsig_hash is position-independent, so the SECTION_LOCAL + * gather gives a stable value regardless of where the module is based. + * + * Reorder invariance and its cost (lowered intra-block discrimination => higher + * collision risk) are properties of the underlying semsig_hash - see semsig.h. + * + * Example - name a function semantically against a known-hash table: + * uint64_t h = vmie_win32_func_semsig(v, pr->cr3, m.base, fn_rva); + * if (h && h == known_crt_strlen_semsig) puts("strlen"); */ +uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3, + uint64_t module_base, uint32_t func_rva); + /* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a * COMPOSITION of primitives the engine already exposes: * - a vtable at `vtable_va` is an array of code pointers, so its METHODS are diff --git a/src/engine/win32/pe.c b/src/engine/win32/pe.c index 1e7abff..765e482 100644 --- a/src/engine/win32/pe.c +++ b/src/engine/win32/pe.c @@ -6,6 +6,7 @@ #include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */ #include "win32.h" /* public surface: vmie_win32, section_desc, view_base */ #include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */ +#include "semsig.h" /* semsig_hash (generic; no backend header pulled here) */ /* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress * (+12), and Characteristics(+36); the header is 40 bytes wide. */ @@ -775,3 +776,51 @@ int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base, free(fb); return total; } + +/* Semantic hash of one function by func_rva: reuse the func_imports gather (find + * the extent from .pdata, read the body), then delegate to the generic + * semsig_hash over a SECTION_LOCAL view. No normalization/fold is duplicated + * here, and no backend header is included - the feature is inherited via the + * generic (a stub returning 0 in an OFF build). Cold: one-shot per function. */ +uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3, + uint64_t module_base, uint32_t func_rva) + __attribute__((cold)); +uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3, + uint64_t module_base, uint32_t func_rva) { + vmie_mem* m = vmie_win32_mem(v); + if (!m) { return 0; } + + /* locate the function's extent from .pdata (count then gather). */ + const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0); + if (nfn < 0) { return 0; } + func_range stack_fr[256]; + func_range* fr = stack_fr; + func_range* heap_fr = NULL; + if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) { + heap_fr = malloc((size_t)nfn * sizeof *heap_fr); + if (!heap_fr) { return 0; } + fr = heap_fr; + } + const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn); + if (got < 0) { free(heap_fr); return 0; } + uint32_t fn_size = 0; + for (int f = 0; f < got; f++) { + if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; } + } + free(heap_fr); + if (fn_size == 0) { return 0; } /* not a known function start */ + + /* gather the body and view it SECTION_LOCAL (base_va = 0): semsig_hash is + * position-independent, so the local base is enough for a stable value. */ + uint8_t* fb = malloc(fn_size); + if (!fb) { return 0; } + if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) { + free(fb); + return 0; + } + const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 }; + const uint64_t h = semsig_hash(view); + + free(fb); + return h; +} diff --git a/src/handlers/semsig.c b/src/handlers/semsig.c new file mode 100644 index 0000000..0a0a7a6 --- /dev/null +++ b/src/handlers/semsig.c @@ -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 +#include +#include /* 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; +} diff --git a/src/handlers/semsig_backend.h b/src/handlers/semsig_backend.h new file mode 100644 index 0000000..03a0a7c --- /dev/null +++ b/src/handlers/semsig_backend.h @@ -0,0 +1,93 @@ +/* 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 +#include + +/* 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); + +/* 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 */ diff --git a/src/handlers/semsig_backend_capstone.c b/src/handlers/semsig_backend_capstone.c new file mode 100644 index 0000000..1bdca74 --- /dev/null +++ b/src/handlers/semsig_backend_capstone.c @@ -0,0 +1,189 @@ +/* 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 +#include +#include +#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; +} + +const char* semsig_backend_name(void) { + return "capstone"; +} diff --git a/src/handlers/semsig_backend_zydis.c b/src/handlers/semsig_backend_zydis.c new file mode 100644 index 0000000..5c63016 --- /dev/null +++ b/src/handlers/semsig_backend_zydis.c @@ -0,0 +1,185 @@ +/* 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 +#include +#include +#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; +} + +const char* semsig_backend_name(void) { + return "zydis"; +} diff --git a/src/handlers/semsig_stub.c b/src/handlers/semsig_stub.c new file mode 100644 index 0000000..195bd3f --- /dev/null +++ b/src/handlers/semsig_stub.c @@ -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 +#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"; +}