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.
This commit is contained in:
2026-06-28 21:51:47 +03:00
parent 8ca84a5861
commit 1b06206043
10 changed files with 955 additions and 1 deletions
+49
View File
@@ -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;
}
+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;
}
+93
View File
@@ -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 <stdint.h>
#include <stddef.h>
/* 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 */
+189
View File
@@ -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 <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;
}
const char* semsig_backend_name(void) {
return "capstone";
}
+185
View File
@@ -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 <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;
}
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";
}