mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-09 00:46:36 +03:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* semsig.c - normalized (semantic) code-signature core (see semsig.h).
|
||||
*
|
||||
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
|
||||
* OFF build compiles semsig_stub.c instead. It turns one function view into a
|
||||
* register-/value-/reorder-canonical 64-bit hash:
|
||||
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
|
||||
* not a second splitter;
|
||||
* 2. step each block with the BACKEND decoder (semsig_backend.h), normalize
|
||||
* each instruction into a fixed token tuple (the layout documented in
|
||||
* semsig.h), and FNV-1a-fold that tuple into a per-instruction hash;
|
||||
* 3. fold a block's per-instruction hashes ORDER-INSENSITIVELY (sort, then
|
||||
* fold) so intra-block instruction scheduling does not change the result;
|
||||
* 4. fold the block hashes into the final hash IN cfg_blocks' order (block
|
||||
* order is the CFG structure and stays significant).
|
||||
*
|
||||
* Boundary: includes ONLY semsig.h, semsig_backend.h, codeanalysis.h (for
|
||||
* cfg_blocks / code_block) and the standard headers. It NEVER includes a
|
||||
* backend's own headers (Zydis/Capstone) - the seam is semsig_backend.h. The
|
||||
* pure core (x86dec, codeanalysis, ...) is reused, never modified.
|
||||
*
|
||||
* FNV-1a uses the SAME constants and scheme as codeanalysis.c so the hash world
|
||||
* stays stylistically consistent. They are NOT factored into a shared header for
|
||||
* a single second consumer (Rule of three: a tolerated 2nd appearance of two
|
||||
* short #defines); a 3rd consumer would justify a private util.
|
||||
*
|
||||
* Cold path: a one-shot pass over one function body, not a hot loop.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h> /* malloc/free (per-block hash buffer overflow only) */
|
||||
#include "semsig.h"
|
||||
#include "semsig_backend.h"
|
||||
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
|
||||
|
||||
#define FNV64_OFFSET 0xcbf29ce484222325ull
|
||||
#define FNV64_PRIME 0x00000100000001b3ull
|
||||
|
||||
/* Caps. CFG_MAX_BLOCKS bounds the stack block array; functions with more blocks
|
||||
* spill the block list to the heap. INSN_STACK bounds the per-block per-insn
|
||||
* hash array on the stack; a denser block spills that one buffer to the heap. */
|
||||
#define CFG_MAX_BLOCKS 512
|
||||
#define INSN_STACK 256
|
||||
|
||||
/* Fold one byte into an FNV-1a accumulator. */
|
||||
static inline uint64_t fnv_byte(uint64_t h, uint8_t b) {
|
||||
h ^= b;
|
||||
h *= FNV64_PRIME;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Normalize ONE decoded instruction into its token tuple and return the
|
||||
* per-instruction FNV-1a hash of that tuple. The tuple layout is the stability
|
||||
* contract documented in semsig.h: mnemonic class, noperands, a flags byte, then
|
||||
* 2 bytes per operand (kind|reg_class, then width_log2|mem-shape flags), plus
|
||||
* the raw opcode for the OTHER class so unclassified instructions still differ.
|
||||
* Pure data->data: no branching on a concrete register, only on its class. */
|
||||
static uint64_t token_hash(const sem_insn* in) {
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
h = fnv_byte(h, (uint8_t)(in->mnemonic_class & 0xff));
|
||||
h = fnv_byte(h, (uint8_t)((in->mnemonic_class >> 8) & 0xff));
|
||||
const uint8_t nop = in->noperands <= 4 ? in->noperands : 4;
|
||||
h = fnv_byte(h, nop);
|
||||
h = fnv_byte(h, (uint8_t)(in->is_control_flow ? 0x01u : 0x00u));
|
||||
for (uint8_t i = 0; i < nop; i++) {
|
||||
const uint8_t b0 = (uint8_t)((in->op[i].kind & 0x0f) |
|
||||
((in->op[i].reg_class & 0x0f) << 4));
|
||||
const uint8_t b1 = (uint8_t)((in->op[i].width_log2 & 0x07) |
|
||||
(in->op[i].mem_has_base ? 0x08u : 0u) |
|
||||
(in->op[i].mem_has_index ? 0x10u : 0u) |
|
||||
(in->op[i].mem_is_riprel ? 0x20u : 0u));
|
||||
h = fnv_byte(h, b0);
|
||||
h = fnv_byte(h, b1);
|
||||
}
|
||||
if (in->mnemonic_class == SEM_MN_OTHER) {
|
||||
h = fnv_byte(h, in->raw_opcode); /* keep unclassified ops distinct */
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Ascending uint64_t comparator for the deterministic per-block sort. The sort
|
||||
* is what makes the within-block fold order-INSENSITIVE while keeping the
|
||||
* MULTISET (duplicates survive, their count is significant). A plain xor/sum was
|
||||
* REJECTED: xor cancels identical-instruction pairs (a block "loses" two equal
|
||||
* moves) and sum collides easily on multiset permutations; sort-then-fold avoids
|
||||
* both. */
|
||||
static int cmp_u64(const void* a, const void* b) {
|
||||
const uint64_t x = *(const uint64_t*)a;
|
||||
const uint64_t y = *(const uint64_t*)b;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
/* Fold one basic block [start, end) of `fn` into a block hash, stepping the
|
||||
* BACKEND decoder from start to end and order-insensitively folding the per-insn
|
||||
* hashes. Returns 1 on success and writes *out; returns 0 on backend desync
|
||||
* (an undecodable instruction inside the block) - the caller turns that into the
|
||||
* 0 "no hash" sentinel. */
|
||||
static int fold_block(mem_view_t fn, const code_block* blk, uint64_t* out) {
|
||||
const size_t start = blk->start;
|
||||
const size_t end = blk->end <= fn.size ? blk->end : fn.size;
|
||||
if (end <= start) { *out = FNV64_OFFSET; return 1; } /* empty block */
|
||||
|
||||
uint64_t stack_h[INSN_STACK];
|
||||
uint64_t* hh = stack_h;
|
||||
uint64_t* heap_h = NULL;
|
||||
const size_t span = end - start; /* >= 1 insn per byte */
|
||||
if (span > INSN_STACK) {
|
||||
heap_h = malloc(span * sizeof *heap_h);
|
||||
if (!heap_h) { return 0; }
|
||||
hh = heap_h;
|
||||
}
|
||||
|
||||
size_t n = 0;
|
||||
int ok = 1;
|
||||
for (size_t off = start; off < end; ) {
|
||||
sem_insn in;
|
||||
if (!semsig_backend_decode(fn.data + off, end - off, &in) ||
|
||||
in.length == 0) {
|
||||
ok = 0;
|
||||
break; /* backend desync */
|
||||
}
|
||||
hh[n++] = token_hash(&in);
|
||||
off += in.length;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
qsort(hh, n, sizeof *hh, cmp_u64); /* order-insensitive */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const uint64_t v = hh[i];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((v >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
*out = h;
|
||||
}
|
||||
|
||||
free(heap_h);
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
if (!fn.data || fn.size == 0) { return 0; }
|
||||
|
||||
/* Block partition (count then gather), REUSING cfg_blocks - -1 is a desync
|
||||
* and maps to the 0 sentinel. cfg_blocks gives ORDER-of-blocks boundaries;
|
||||
* the within-block step uses the backend, so a backend/x86_decode length
|
||||
* disagreement only ever turns into the 0 sentinel, never a wrong hash. */
|
||||
const int nb = cfg_blocks(fn, NULL, 0);
|
||||
if (nb <= 0) { return 0; }
|
||||
|
||||
code_block stack_bb[CFG_MAX_BLOCKS];
|
||||
code_block* bb = stack_bb;
|
||||
code_block* heap_bb = NULL;
|
||||
if (nb > CFG_MAX_BLOCKS) {
|
||||
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
|
||||
if (!heap_bb) { return 0; }
|
||||
bb = heap_bb;
|
||||
}
|
||||
const int got = cfg_blocks(fn, bb, nb);
|
||||
if (got <= 0) { free(heap_bb); return 0; }
|
||||
const int use = got < nb ? got : nb;
|
||||
|
||||
/* Fold block hashes into the final hash IN cfg_blocks' ascending-start order:
|
||||
* block order is the CFG structure and stays significant (only WITHIN a
|
||||
* block is the fold order-insensitive). */
|
||||
uint64_t h = FNV64_OFFSET;
|
||||
for (int i = 0; i < use; i++) {
|
||||
uint64_t bh;
|
||||
if (!fold_block(fn, &bb[i], &bh)) { /* backend desync */
|
||||
free(heap_bb);
|
||||
return 0;
|
||||
}
|
||||
for (int b = 0; b < 8; b++) {
|
||||
h = fnv_byte(h, (uint8_t)((bh >> (b * 8)) & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
free(heap_bb);
|
||||
return h;
|
||||
}
|
||||
@@ -0,0 +1,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 */
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* semsig_stub.c - the OFF (no-backend) path of the semantic-signature feature.
|
||||
*
|
||||
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides
|
||||
* the full public generic surface so the ABI is stable - linking never fails
|
||||
* for a missing symbol - while the feature is not present:
|
||||
* semsig_hash -> 0 (the same "no hash" sentinel as func_hash)
|
||||
* semsig_backend_name -> "none"
|
||||
* Callers tell "feature off" from "empty/undecodable function" via the compile-
|
||||
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime 0.
|
||||
*
|
||||
* In the OFF build semsig.c is NOT compiled (it calls the backend decode, which
|
||||
* does not exist without a backend); this stub is the whole generic symbol. See
|
||||
* CMakeLists.txt for the source-selection logic.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include "semsig.h"
|
||||
|
||||
uint64_t semsig_hash(mem_view_t fn) __attribute__((cold));
|
||||
uint64_t semsig_hash(mem_view_t fn) {
|
||||
(void)fn;
|
||||
return 0; /* feature not built: same sentinel as func_hash */
|
||||
}
|
||||
|
||||
const char* semsig_backend_name(void) {
|
||||
return "none";
|
||||
}
|
||||
Reference in New Issue
Block a user