Add readable operand decode and register def-use analysis

insndec.h: insn_decode() decodes one instruction into a readable form -
concrete registers as canonical roots (al/ah/ax/eax/rax -> one root), memory
base/index/scale/disp, immediate values, operand sizes, per-operand read/write,
and implicit operands/effects (rsp on push/pop, rdx:rax on mul/div, FLAGS). It
is ABI-agnostic: a call reports only its architectural effects.

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

Both ride the existing optional disassembler seam (a second insn_decode_full
is added to semsig_backend.h; sem_insn and semsig_hash are untouched), gated
behind VMIE_HAVE_DISASM; OFF builds a stub. Adds the vmie_win32_func_usedef
wrapper. No new build option - the feature shares VMIE_DISASM with semsig.
This commit is contained in:
2026-06-28 23:02:40 +03:00
parent 51feb8d39c
commit 244511b66e
11 changed files with 1133 additions and 1 deletions
+6 -1
View File
@@ -49,12 +49,17 @@ add_library(vmie_obj OBJECT
# 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)
target_sources(vmie_obj PRIVATE src/handlers/semsig_stub.c
src/handlers/insndec_stub.c)
elseif(VMIE_DISASM STREQUAL "zydis")
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
src/handlers/insndec.c
src/handlers/codetrace.c
src/handlers/semsig_backend_zydis.c)
elseif(VMIE_DISASM STREQUAL "capstone")
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
src/handlers/insndec.c
src/handlers/codetrace.c
src/handlers/semsig_backend_capstone.c)
else()
message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')")
+106
View File
@@ -0,0 +1,106 @@
/* codetrace.h - generic (OS-agnostic) register def-use over one function.
*
* Handler layer, gated like semsig.h: a process-scoped, STATIC register def-use
* analysis built on insn_decode (insndec.h) and the basic-block partition of
* cfg_blocks (codeanalysis.h, REUSED - this header writes no second splitter).
* For one function view it computes per-instruction register read (use) / write
* (def) sets - the building material a caller needs for reaching-definitions.
*
* It returns DATA. It does NOT execute or model values, does NOT unroll loops,
* does NOT build use-def CHAINS, and carries no dataflow solver: those are the
* caller's analysis. v1 deliberately stops at per-instruction use/def so the
* surface does not predetermine the caller's algorithm.
*
* Family placement. codeanalysis.h is PURE / always available; semsig.h is gated
* on the disassembler backend. codetrace.h sits in the second row with semsig: it
* needs the backend decode (via insndec.h) plus cfg_blocks. With no backend the
* symbol still exists (ABI stable) but returns the sentinel, and VMIE_HAVE_DISASM
* is 0. The chosen split keeps codeanalysis.h's "zero-dep, works always" contract
* literally true.
*
* GENERIC / OS-agnostic. The analysis names no OS. A calling convention enters
* ONLY through the explicit trace_abi argument, which selects a static caller-
* saved (volatile) clobber table applied at call sites. Win64 is just one enum
* value, not a hardcoded dependency; TRACE_ABI_NONE reproduces conservative
* behavior for any non-Windows consumer. The OS-specific CHOICE of Win64 is made
* by the win32 wrapper (vmie_win32_func_usedef, win32.h), not by this header.
*/
#ifndef VMIE_CODETRACE_H
#define VMIE_CODETRACE_H
#include <stdint.h>
#include <stddef.h>
#include "memmodel.h" /* mem_view_t (the single owner of the view type) */
/* Shared feature gate (see semsig.h / insndec.h). Default keeps the header valid
* when compiled outside the project build. */
#ifndef VMIE_HAVE_DISASM
#define VMIE_HAVE_DISASM 0
#endif
/* Calling convention applied to call sites when computing def-use. The analysis
* itself stays OS-agnostic: an ABI is just a parameter selecting a static
* caller-saved (volatile) clobber table. An unknown value is treated as
* TRACE_ABI_NONE (conservative), not an error. */
typedef enum {
TRACE_ABI_NONE = 0, /* conservative: a call clobbers only what insn_decode
* reports architecturally (rsp/rip write, indirect
* target read); no volatile registers are killed. */
TRACE_ABI_WIN64 /* standard Win64: volatile GPR/vec/FLAGS are killed
* (full-kill) across every call site (see codetrace.c
* for the exact set and its honest limits). */
} trace_abi;
/* A compact register set over canonical reg_id roots (a bitset). The field split
* is an implementation detail of the struct; consumers should treat it opaquely
* and compare/inspect via the documented def/use semantics, not the raw words.
* gpr - the 16 GPR roots (REGID_RAX..REGID_R15)
* vec_lo - vector roots REGID_VEC0..REGID_VEC31
* misc - mask roots (k0..k7), REGID_FLAGS, REGID_OTHER */
typedef struct {
uint16_t gpr; /* bit i set => REGID_RAX + i present (16 GPR roots) */
uint32_t vec_lo; /* bit i set => REGID_VEC0 + i present (32 vector roots) */
uint16_t misc; /* bit i set => (REGID_K0 + i) for i<8, then FLAGS, OTHER */
} regset;
/* Per-instruction use/def in the function view's coordinate space.
* off - byte offset of the instruction within fn
* len - instruction length
* use - roots READ (incl. implicit, incl. address regs of mem operands)
* def - roots WRITTEN as a FULL/zero-extending write (these KILL the
* enclosing root); for a call site under a non-NONE trace_abi the
* ABI volatile clobber is merged here as a full kill too
* def_partial - roots written PARTIALLY (8/16-bit GPR, legacy-SSE): they do NOT
* kill the enclosing root, so a wider live root survives
* cond_def - roots written CONDITIONALLY (cmovcc/setcc): do not kill either */
typedef struct {
uint32_t off;
uint8_t len;
regset use, def, def_partial, cond_def;
} insn_usedef;
/* Compute per-instruction use/def for one function view under calling convention
* `abi`. `fn` is a view spanning EXACTLY one function (as for cfg_blocks /
* semsig_hash). REUSES cfg_blocks only to validate the partition and iterate in
* block order; the use/def itself comes from insn_decode per instruction.
*
* `abi` selects the call-site volatile clobber: TRACE_ABI_NONE = conservative (no
* volatile kill, only the architectural rsp/rip write insn_decode reports);
* TRACE_ABI_WIN64 = full-kill of the Win64 volatile roots at every call site.
* The analysis stays OS-agnostic: `abi` is just a parameter, not a hardcoded OS.
*
* Writes up to `max` records in ascending off order (out=NULL to count). Returns
* the TOTAL instruction count, 0 if `fn` is empty, or -1 on a decode desync
* (cfg_blocks could not split, or the backend could not decode an instruction) /
* a build with no backend (VMIE_HAVE_DISASM==0). The sentinel convention mirrors
* cfg_blocks: -1 = desync/no-backend, 0 = empty.
*
* PURE: only the view and the decoder, no vmie_mem / no I/O (a stack buffer with
* a heap spill only when the block count overflows, like semsig_hash).
*
* Example - registers killed by each instruction of a function:
* insn_usedef ud[512];
* int n = func_usedef(fn, TRACE_ABI_WIN64, ud, 512);
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max);
#endif /* VMIE_CODETRACE_H */
+194
View File
@@ -0,0 +1,194 @@
/* insndec.h - readable per-instruction operand decode (generic, ABI-agnostic).
*
* Handler layer: a RICH decode of ONE x86-64 instruction into concrete operands
* (concrete registers as a backend-neutral stable id, memory base/index/scale/
* disp, immediate VALUES, sizes), per-operand read/write, AND the full aggregate
* register read/write sets INCLUDING IMPLICIT operands/effects (rsp on push/pop,
* rdx:rax on mul/div, FLAGS on cmp/jcc, ...). This is the "perception" primitive
* the register def-use analysis (codetrace.h) is built on.
*
* Relationship to semsig.h (shared backend, DIFFERENT purpose). semsig_hash
* folds a NORMALIZED, LOSSY token stream: it erases register identity (keeps only
* a class), erases immediate/displacement values, and carries no read/write - by
* design, so the hash is register-/value-/reorder-stable. That makes semsig
* irreversible: you cannot trace registers through it. insn_decode is the
* opposite: it KEEPS register identity, values, and read/write, so an analysis
* can reason about WHICH physical register an instruction touches. Both ride the
* SAME optional external disassembler (Zydis/Capstone) behind the same private
* seam, but insn_decode is NOT a hash and NOT lossy.
*
* Operand decode is NOT in the light decoder (x86dec.h is deliberately pure and
* length-only) and NOT a second hand-rolled operand decoder: it is supplied by
* the optional backend, so this primitive is FEATURE-GATED exactly like semsig.
* With no backend the symbol still exists (ABI stable) but returns 0, and
* VMIE_HAVE_DISASM is 0.
*
* ABI-AGNOSTIC. insn_decode reports ONLY what an instruction does
* ARCHITECTURALLY. In particular a `call` writes rsp and rip and (for an
* indirect call) reads its target register/memory - that is ALL. It does NOT
* mark caller-saved (volatile) registers as written: that is a property of the
* CALLED function / the calling convention, not of the instruction. The Win64
* volatile clobber lives in the def-use layer (codetrace.h/.c) behind an explicit
* trace_abi, never here.
*/
#ifndef VMIE_INSNDEC_H
#define VMIE_INSNDEC_H
#include <stdint.h>
#include <stddef.h>
/* Compile-time feature availability, shared with semsig (one backend, one gate).
* Pushed from CMake as a PUBLIC compile definition so consumers see the SAME
* value the library was built with:
* VMIE_HAVE_DISASM == 1 built with a disassembler backend; insn_decode is
* live, insndec_backend_name() names the backend.
* VMIE_HAVE_DISASM == 0 built without a backend (the default); insn_decode
* always returns 0 and insndec_backend_name() == "none".
* The default here keeps the header valid when compiled outside the project. */
#ifndef VMIE_HAVE_DISASM
#define VMIE_HAVE_DISASM 0
#endif
/* Canonical full-register root id. al/ah/ax/eax/rax all map to REGID_RAX;
* xmm0/ymm0/zmm0 -> REGID_VEC0; etc. Identity is KEPT here (unlike sem_insn,
* which erases it) so def-use can reason about WHICH physical register. The
* concrete backend register enum is NEVER carried across the seam - the backend
* TU normalizes into THIS stable id, exactly as it does for mnemonic classes. */
typedef enum {
REGID_NONE = 0,
/* 16 GPR roots; al/ax/eax/rax -> REGID_RAX, etc. */
REGID_RAX, REGID_RCX, REGID_RDX, REGID_RBX,
REGID_RSP, REGID_RBP, REGID_RSI, REGID_RDI,
REGID_R8, REGID_R9, REGID_R10, REGID_R11,
REGID_R12, REGID_R13, REGID_R14, REGID_R15,
/* 32 vector roots (zmm0..zmm31; xmm/ymm fold into the zmm root). */
REGID_VEC0,
REGID_VEC31 = REGID_VEC0 + 31,
/* AVX-512 mask k0..k7. */
REGID_K0,
REGID_K7 = REGID_K0 + 7,
/* FLAGS as ONE pseudo-register root (NOT per-bit CF/ZF/...): enough for
* def-use "cmp writes flags, jcc/cmov/setcc read flags"; per-bit is not v1. */
REGID_FLAGS,
/* rip / segment / other roots kept coarse (def-use rarely tracks them). */
REGID_OTHER,
REGID_COUNT
} reg_id;
/* Per-operand / per-register access, normalized (raw backend enums never leak). */
typedef enum {
ACC_NONE = 0,
ACC_READ, /* read only */
ACC_WRITE, /* write only */
ACC_READWRITE, /* read-modify-write (add, sub, ...) */
ACC_COND_READ, /* conditionally read (cmovcc src, ...) */
ACC_COND_WRITE /* conditionally written (cmovcc dst, setcc) */
} reg_access;
/* Explicit-operand kind (the readable, perception side of the decode). */
typedef enum {
OPND_NONE = 0,
OPND_REG, /* register operand */
OPND_MEM, /* memory operand (base/index/scale/disp) */
OPND_IMM /* immediate operand (value kept) */
} operand_kind;
/* One register reference, used both for explicit register operands and for the
* aggregate implicit-or-explicit read/write sets. Sub-register overlap is modeled
* EXPLICITLY and backend-neutrally:
* root - canonical full-register root (al/ax/eax/rax -> REGID_RAX)
* width_log2 - ACCESS width in log2 bytes (0=1B .. 4=16B), as
* width_log2_of already computes for semsig
* writes_zero_extend- on a WRITE, 1 if the upper part of the root is zeroed
* (a 32-bit GPR write, e.g. `eax`, zeroes the upper 32 bits
* of `rax`; a VEX/EVEX vector write zeroes the upper YMM/ZMM)
* and 0 if it is a PARTIAL write that preserves the upper
* part (8/16-bit GPR write, legacy-SSE vector write). This
* ONE flag is what def-use needs to decide overlap without
* carrying a raw bit mask.
* access - ACC_* for this reference */
typedef struct {
uint8_t root; /* reg_id */
uint8_t width_log2; /* access width, log2 bytes (0..4+) */
uint8_t writes_zero_extend;
uint8_t access; /* reg_access */
} reg_ref;
/* One explicit operand (readable / perception view). */
typedef struct {
uint8_t kind; /* operand_kind */
uint8_t access; /* reg_access for the operand as a whole */
uint8_t width_log2; /* access width, log2 bytes */
uint8_t mem_scale; /* MEM: index scale (1/2/4/8), 0 if no index */
uint8_t mem_is_riprel; /* MEM: 1 if RIP-relative */
uint8_t reg_root; /* REG: reg_id of the operand register */
uint8_t reg_zero_extend; /* REG: writes_zero_extend for a write operand */
uint8_t imm_width_log2; /* IMM: value width, log2 bytes */
uint8_t mem_base; /* MEM: reg_id of the base register (read) */
uint8_t mem_index; /* MEM: reg_id of the index register (read) */
int64_t mem_disp; /* MEM: displacement value (signed) */
int64_t imm_value; /* IMM: immediate value (sign-extended) */
} operand;
/* Mnemonic-class group, backend-neutral. Mirrors the semsig SEM_MN_* groups so a
* consumer can switch on the instruction kind without backend enums; def-use uses
* only INSN_CALL (call sites) but the rest aids readability of the decode. */
typedef enum {
INSN_OTHER = 0,
INSN_MOV, INSN_ARITH, INSN_LOGIC, INSN_CMP, INSN_TEST,
INSN_BRANCH, /* conditional jcc / setcc / cmovcc */
INSN_JMP, /* unconditional jmp */
INSN_CALL, INSN_RET, INSN_PUSH, INSN_POP, INSN_LEA, INSN_NOP,
INSN_FLOAT, INSN_VEC
} insn_class;
/* Caps on the rich decode. Four explicit operands cover x86-64; eight register
* refs per side cover the widest implicit sets (e.g. mul touching rax/rdx plus an
* explicit source) with margin. Anything beyond is truncated (count still grows
* past the cap so a caller can detect it), which never happens for real code. */
#define INSN_MAX_OPERANDS 4
#define INSN_MAX_REGREFS 8
/* A fully decoded instruction. Two complementary representations:
* op[] - explicit operands, in instruction operand order (readability);
* reads[] / writes[] - aggregate register sets over canonical roots, UNIONING
* explicit AND implicit effects (this is what def-use consumes). A
* memory operand's base/index registers appear in reads[] as address
* reads; FLAGS appears as REGID_FLAGS; rsp/rdx:rax appear via the
* backend's implicit operands. */
typedef struct {
uint8_t length; /* full instruction length in bytes (>= 1) */
uint8_t mnemonic_class; /* insn_class */
uint8_t is_control_flow; /* 1 if branch/call/ret */
uint8_t noperands; /* explicit operands in op[] (<= INSN_MAX_OPERANDS)*/
uint8_t nreads; /* register refs in reads[] */
uint8_t nwrites; /* register refs in writes[] */
operand op[INSN_MAX_OPERANDS];
reg_ref reads[INSN_MAX_REGREFS];
reg_ref writes[INSN_MAX_REGREFS];
} insn_decoded;
/* Decode ONE instruction at view[off .. ]. `view` is a flat byte span and `off`
* is the byte offset of the instruction within it; at most (view_size - off)
* bytes are available to the backend. Returns 1 on success (fills *out), 0 on
* undecodable / not enough bytes / a build with no backend (VMIE_HAVE_DISASM==0).
* out->length == 0 is also treated as a desync by callers. PURE: only the byte
* span and the decoder, no I/O, no allocation, reentrant.
*
* `view` / `view_size` are a flat buffer (not mem_view_t) so this primitive
* stays dependency-light; the caller owns the buffer.
*
* Example - read the registers an instruction writes:
* insn_decoded d;
* if (insn_decode(code, n, 0, &d))
* for (int i = 0; i < d.nwrites; i++)
* printf("writes root %u (zx=%u)\n",
* d.writes[i].root, d.writes[i].writes_zero_extend); */
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
insn_decoded* out);
/* Stable identity of the compiled disassembler backend ("zydis"/"capstone", or
* "none" without a backend). Shares the backend with semsig; this name is the
* same backend. Static, never-NULL. */
const char* insndec_backend_name(void);
#endif /* VMIE_INSNDEC_H */
+35
View File
@@ -23,6 +23,7 @@
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
#include "codetrace.h" /* insn_usedef, trace_abi (register def-use surface) */
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
@@ -492,6 +493,40 @@ int vmie_win32_func_imports(vmie_win32* v, uint64_t cr3, uint64_t module_base,
uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
uint64_t module_base, uint32_t func_rva);
/* Register def-use of ONE function, addressed by its func_rva (a .pdata function
* start, as from vmie_win32_functions or an export). The win32 convenience entry
* point over the generic func_usedef (codetrace.h): it locates the function
* extent from .pdata (like vmie_win32_func_semsig), gathers its body with
* gva_read, builds a SECTION_LOCAL mem_view_t, and DELEGATES to func_usedef -
* PASSING TRACE_ABI_WIN64, because choosing the Win64 calling convention is the
* OS-specific decision that belongs in this layer (which already owns .pdata /
* cr3 / module_base). It does NOT reimplement the analysis or apply the volatile
* clobber itself (func_usedef does), and pulls in no disassembler backend (the
* feature is inherited transitively via the same VMIE_HAVE_DISASM as the generic).
*
* v - engine handle.
* cr3 - the process address space the module is mapped in.
* module_base - image base VA (its PE headers must be resident).
* func_rva - function start RVA (must match a .pdata function start).
* out, max - caller array receiving up to `max` per-instruction insn_usedef
* records in ascending function-local offset order (NULL to count).
*
* Returns the TOTAL instruction count, 0 if the function is empty, or -1 on any
* of: func_rva is not a known function start, the body is unreadable (paged out),
* a decode desync, or a build with no disassembler backend (VMIE_HAVE_DISASM==0).
* The off field of each record is a function-local byte offset (SECTION_LOCAL).
*
* The Win64 volatile clobber applied at call sites - and its honest limits (only
* the standard Win64 convention; not __vectorcall/custom; the return value is not
* a separate def) - are documented at the clobber table in codetrace.c.
*
* Example - registers each instruction of a function kills:
* insn_usedef ud[512];
* int n = vmie_win32_func_usedef(v, pr->cr3, m.base, fn_rva, ud, 512);
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
uint32_t func_rva, insn_usedef* out, int max);
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
* COMPOSITION of primitives the engine already exposes:
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
+53
View File
@@ -7,6 +7,7 @@
#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) */
#include "codetrace.h" /* func_usedef (generic def-use; no backend header here) */
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
@@ -824,3 +825,55 @@ uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
free(fb);
return h;
}
/* Register def-use of one function by func_rva: reuse the same .pdata extent
* gather as vmie_win32_func_semsig (locate the function, read the body), then
* delegate to the generic func_usedef over a SECTION_LOCAL view, PASSING
* TRACE_ABI_WIN64 - the OS-specific "choose the Win64 convention" decision lives
* here, in the layer that already owns .pdata / cr3 / module_base, exactly as the
* extent lookup does. The analysis (block walk, the volatile clobber merge) is
* NOT reimplemented or applied here; that lives once in func_usedef. No backend
* header is included - the feature is inherited via the generic (a stub returning
* -1 in an OFF build). Cold: one-shot per function. */
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
uint32_t func_rva, insn_usedef* out, int max)
__attribute__((cold));
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
uint32_t func_rva, insn_usedef* out, int max) {
vmie_mem* m = vmie_win32_mem(v);
if (!m) { return -1; }
/* locate the function's extent from .pdata (count then gather). */
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
if (nfn < 0) { return -1; }
func_range stack_fr[256];
func_range* fr = stack_fr;
func_range* heap_fr = NULL;
if (nfn > (int)(sizeof stack_fr / sizeof stack_fr[0])) {
heap_fr = malloc((size_t)nfn * sizeof *heap_fr);
if (!heap_fr) { return -1; }
fr = heap_fr;
}
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
if (got < 0) { free(heap_fr); return -1; }
uint32_t fn_size = 0;
for (int f = 0; f < got; f++) {
if (fr[f].rva == func_rva) { fn_size = fr[f].size; break; }
}
free(heap_fr);
if (fn_size == 0) { return -1; } /* not a known function start */
/* gather the body and view it SECTION_LOCAL (base_va = 0): func_usedef reports
* in the view's own coordinate space, so off is a function-local byte offset. */
uint8_t* fb = malloc(fn_size);
if (!fb) { return -1; }
if (gva_read(m, cr3, module_base + func_rva, fb, fn_size)) {
free(fb);
return -1;
}
const mem_view_t view = { .data = fb, .size = fn_size, .base_va = 0 };
const int n = func_usedef(view, TRACE_ABI_WIN64, out, max);
free(fb);
return n;
}
+190
View File
@@ -0,0 +1,190 @@
/* codetrace.c - generic register def-use over one function (see codetrace.h).
*
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
* OFF build compiles insndec_stub.c (which also carries func_usedef's sentinel).
* It turns one function view into per-instruction register use/def:
* 1. split the function into basic blocks - REUSING cfg_blocks (codeanalysis.h),
* not a second splitter - to validate the partition and iterate in order;
* 2. step each block with insn_decode (insndec.h), and from each instruction's
* aggregate reads[]/writes[] build the use/def/def_partial/cond_def sets;
* 3. at a call site, under a non-NONE trace_abi, merge the static ABI volatile
* clobber into `def` as a FULL kill.
*
* Boundary: includes ONLY insndec.h, codeanalysis.h (cfg_blocks/code_block) and
* codetrace.h plus standard headers. It NEVER includes a backend's own headers -
* the decode rides insn_decode, which is itself behind the one backend seam.
*
* Cold path: a one-shot pass over one function body, not a hot loop.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h> /* malloc/free (block-list overflow only) */
#include "insndec.h"
#include "codetrace.h"
#include "codeanalysis.h" /* cfg_blocks, code_block (REUSED splitter) */
/* Cap on the stack block array; denser functions spill the list to the heap
* (same scheme as semsig.c). */
#define CFG_MAX_BLOCKS 512
/* ---- regset: a bitset over canonical reg_id roots ------------------------ */
/* Set the bit for one reg_id root in a regset. The category fold mirrors the
* regset field layout documented in codetrace.h:
* GPR roots REGID_RAX..REGID_R15 -> gpr bit (root - REGID_RAX)
* vec roots REGID_VEC0..REGID_VEC31 -> vec_lo bit (root - REGID_VEC0)
* mask roots REGID_K0..REGID_K7 -> misc bit (root - REGID_K0)
* FLAGS -> misc bit 8
* OTHER -> misc bit 9
* REGID_NONE and out-of-range ids are ignored (no overlap to track). */
static void rs_set(regset* s, uint8_t root) {
if (root >= REGID_RAX && root <= REGID_R15) {
s->gpr |= (uint16_t)(1u << (root - REGID_RAX));
} else if (root >= REGID_VEC0 && root <= REGID_VEC31) {
s->vec_lo |= (uint32_t)1u << (root - REGID_VEC0);
} else if (root >= REGID_K0 && root <= REGID_K7) {
s->misc |= (uint16_t)(1u << (root - REGID_K0));
} else if (root == REGID_FLAGS) {
s->misc |= (uint16_t)(1u << 8);
} else if (root == REGID_OTHER) {
s->misc |= (uint16_t)(1u << 9);
}
}
/* Union b into a (pure value op; no branching on a concrete register). */
static void rs_or(regset* a, const regset* b) {
a->gpr |= b->gpr;
a->vec_lo |= b->vec_lo;
a->misc |= b->misc;
}
/* ---- Win64 caller-saved (volatile) clobber table ------------------------- */
/* Caller-saved (volatile) roots clobbered across a Win64 call site. Applied as a
* FULL kill in `def` (NOT def_partial/cond_def): after a standard Win64 call
* these roots are dead from the caller's view. RAX additionally carries the
* return value, but RAX is already in this set, so a return value needs no
* separate def. Callee-saved roots are deliberately ABSENT (live across a call):
* GPR saved: RBX RBP RDI RSI RSP R12 R13 R14 R15
* vec saved: XMM6..XMM15 (VEC6..VEC15 roots)
* RSP changes architecturally via call/ret itself (already reported by
* insn_decode) and is callee-saved by VALUE, so it is NOT a clobber here.
*
* Selected by trace_abi: TRACE_ABI_WIN64 -> this set; TRACE_ABI_NONE -> the empty
* set, reproducing the prior conservative behavior (no volatile kill on a call).
*
* HONEST LIMITS (named, not hidden): this models the STANDARD Win64 convention
* ONLY - it does NOT catch __vectorcall / custom conventions; the return value is
* not a separate def (RAX already covers it); the indirect-call target register
* read is supplied by insn_decode, not by this table. */
static regset win64_clobber(void) {
regset s = (regset){ 0, 0, 0 };
/* volatile GPR: RAX RCX RDX R8 R9 R10 R11 */
rs_set(&s, REGID_RAX); rs_set(&s, REGID_RCX); rs_set(&s, REGID_RDX);
rs_set(&s, REGID_R8); rs_set(&s, REGID_R9);
rs_set(&s, REGID_R10); rs_set(&s, REGID_R11);
/* volatile vector: XMM0..XMM5 (VEC0..VEC5 and their YMM/ZMM upper) */
rs_set(&s, REGID_VEC0); rs_set(&s, REGID_VEC0 + 1); rs_set(&s, REGID_VEC0 + 2);
rs_set(&s, REGID_VEC0 + 3); rs_set(&s, REGID_VEC0 + 4); rs_set(&s, REGID_VEC0 + 5);
/* FLAGS */
rs_set(&s, REGID_FLAGS);
return s;
}
/* The clobber selected by trace_abi (table ABI -> regset, by value). An unknown
* abi falls to the conservative empty set. */
static regset abi_clobber(trace_abi abi) {
switch (abi) {
case TRACE_ABI_WIN64: return win64_clobber();
case TRACE_ABI_NONE:
default: return (regset){ 0, 0, 0 };
}
}
/* ---- per-instruction use/def -------------------------------------------- */
/* Build one insn_usedef from a decoded instruction. A write that zero-extends
* (full/64-bit GPR or VEX/EVEX vector) goes to `def` (it KILLS the root); an
* 8/16-bit GPR or legacy-SSE write is PARTIAL (-> def_partial, no kill); a
* conditional write (cmovcc/setcc) goes to cond_def (no kill). Reads always go to
* `use`. The aggregate sets from insn_decode already include implicit operands
* (rsp/rdx:rax/FLAGS) and the address registers of memory operands. */
static void usedef_of(const insn_decoded* d, insn_usedef* ud) {
for (uint8_t i = 0; i < d->nreads && i < INSN_MAX_REGREFS; i++) {
rs_set(&ud->use, d->reads[i].root);
}
for (uint8_t i = 0; i < d->nwrites && i < INSN_MAX_REGREFS; i++) {
const reg_ref* w = &d->writes[i];
if (w->access == ACC_COND_WRITE) {
rs_set(&ud->cond_def, w->root);
} else if (w->writes_zero_extend ||
(w->root >= REGID_RAX && w->root <= REGID_R15 &&
w->width_log2 >= 3)) {
/* zero-extending (32-bit) OR a full 64-bit GPR write: a full kill. */
rs_set(&ud->def, w->root);
} else if (w->root >= REGID_VEC0 && w->root <= REGID_VEC31 &&
w->writes_zero_extend) {
rs_set(&ud->def, w->root);
} else if (w->root == REGID_FLAGS || w->root >= REGID_K0) {
/* FLAGS / mask / other roots: treat a write as a full def (no narrow
* sub-register concept to preserve). */
rs_set(&ud->def, w->root);
} else {
/* 8/16-bit GPR or legacy-SSE write: partial, does not kill the root. */
rs_set(&ud->def_partial, w->root);
}
}
}
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
if (!fn.data || fn.size == 0) { return 0; }
/* Validate the partition with cfg_blocks (count then gather); -1 is a desync. */
const int nb = cfg_blocks(fn, NULL, 0);
if (nb < 0) { return -1; }
if (nb == 0) { return 0; }
code_block stack_bb[CFG_MAX_BLOCKS];
code_block* bb = stack_bb;
code_block* heap_bb = NULL;
if (nb > CFG_MAX_BLOCKS) {
heap_bb = malloc((size_t)nb * sizeof *heap_bb);
if (!heap_bb) { return -1; }
bb = heap_bb;
}
const int got = cfg_blocks(fn, bb, nb);
if (got < 0) { free(heap_bb); return -1; }
const int use = got < nb ? got : nb;
const regset clob = abi_clobber(abi);
/* Step instructions in ascending block order; cfg_blocks already partitions
* [0, fn.size) with no gaps, so a linear walk per block covers the function. */
int total = 0;
for (int b = 0; b < use; b++) {
const size_t start = bb[b].start;
const size_t end = bb[b].end <= fn.size ? bb[b].end : fn.size;
for (size_t off = start; off < end; ) {
insn_decoded d;
if (!insn_decode(fn.data, end, off, &d) || d.length == 0) {
free(heap_bb);
return -1; /* backend desync */
}
insn_usedef ud = (insn_usedef){ 0 };
ud.off = (uint32_t)off;
ud.len = d.length;
usedef_of(&d, &ud);
/* Win64 (or any non-NONE abi) call site: merge the volatile clobber
* into `def` as a full kill. TRACE_ABI_NONE -> clob is empty (no-op). */
if (d.mnemonic_class == INSN_CALL) {
rs_or(&ud.def, &clob);
}
if (out && total < max) { out[total] = ud; }
total++;
off += d.length;
}
}
free(heap_bb);
return total;
}
+30
View File
@@ -0,0 +1,30 @@
/* insndec.c - generic delegate for the readable per-instruction decode.
*
* Built ONLY when a disassembler backend is selected (VMIE_DISASM != OFF); the
* OFF build compiles insndec_stub.c instead. It is a thin, ABI-agnostic wrapper
* over the ONE backend seam (semsig_backend.h: insn_decode_full): it validates
* the byte range and forwards to the backend, which fills the public insn_decoded.
*
* Boundary: includes ONLY insndec.h and semsig_backend.h (the seam) plus standard
* headers. It NEVER includes a backend's own headers (Zydis/Capstone) - the rich
* decode and the lossy semsig share that single seam. The backend name is the
* same backend as semsig, so insndec_backend_name delegates to it.
*
* ABI-agnostic: nothing here knows about any calling convention. The Win64
* volatile clobber lives in codetrace.c, never here.
*/
#include <stdint.h>
#include <stddef.h>
#include "insndec.h"
#include "semsig_backend.h" /* insn_decode_full, semsig_backend_name (seam) */
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
insn_decoded* out) {
if (!view || !out || off >= view_size) { return 0; }
return insn_decode_full(view + off, view_size - off, out);
}
const char* insndec_backend_name(void) {
/* One backend, one identity: insndec rides the same decoder as semsig. */
return semsig_backend_name();
}
+35
View File
@@ -0,0 +1,35 @@
/* insndec_stub.c - the OFF (no-backend) path of the readable decode + def-use.
*
* Built into the library ONLY when VMIE_DISASM=OFF (the default). It provides the
* full public generic surface of BOTH features so the ABI is stable - linking
* never fails for a missing symbol - while the features are not present:
* insn_decode -> 0 ("not decoded": same idea as the semsig sentinel)
* insndec_backend_name -> "none"
* func_usedef -> -1 (the cfg_blocks/desync sentinel; no backend)
* Callers tell "feature off" from a real empty/undecodable input via the compile-
* time macro VMIE_HAVE_DISASM (0 here), NOT via the runtime sentinel.
*
* One stub carries both surfaces (fewer TUs). The pure types trace_abi / regset /
* insn_decoded are visible from the headers even in OFF. In the OFF build
* insndec.c / codetrace.c are NOT compiled (they call the backend, absent there);
* this stub is the whole generic symbol. See CMakeLists.txt for the selection.
*/
#include <stdint.h>
#include <stddef.h>
#include "insndec.h"
#include "codetrace.h"
int insn_decode(const uint8_t* view, size_t view_size, size_t off,
insn_decoded* out) {
(void)view; (void)view_size; (void)off; (void)out;
return 0; /* feature not built: nothing decoded */
}
const char* insndec_backend_name(void) {
return "none";
}
int func_usedef(mem_view_t fn, trace_abi abi, insn_usedef* out, int max) {
(void)fn; (void)abi; (void)out; (void)max;
return -1; /* feature not built: same desync/no-backend sentinel as cfg */
}
+14
View File
@@ -13,6 +13,10 @@
#define VMIE_SEMSIG_BACKEND_H
#include <stdint.h>
#include <stddef.h>
#include "insndec.h" /* insn_decoded - the rich decode type shared with the
* public insndec.h surface; backend TUs fill it here so
* the type is defined ONCE, not duplicated at the seam.
* Raw backend enums still never leak (normalized in TU). */
/* Operand kind, normalized. Carried in sem_insn.op[].kind. */
enum {
@@ -85,6 +89,16 @@ typedef struct {
* shared mutable state - reentrant. */
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out);
/* The SECOND projection of the same backend parse: a full, lossless decode into
* the public insn_decoded (insndec.h) - concrete register roots, values, sizes,
* per-operand access, and the aggregate implicit-or-explicit read/write sets
* (incl. rsp/rdx:rax/FLAGS). ABI-AGNOSTIC: `call` reports only architectural
* effects (rsp/rip write, indirect target read), NEVER a volatile clobber. Each
* backend TU implements BOTH this and semsig_backend_decode behind the ONE seam.
* Returns 1 on success (fills *out), 0 on undecodable / not enough bytes.
* Stateless / reentrant - no allocation, no I/O, no shared mutable state. */
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out);
/* Stable backend identity for diagnostics / hash-domain tagging. Static,
* never-NULL. This declaration is shared with semsig.h's public symbol of the
* same name; the concrete backend TU (or the stub) defines it once. */
+251
View File
@@ -184,6 +184,257 @@ int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
return 1;
}
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
/* Map a Capstone x86 register id to a canonical reg_id ROOT (al/ax/eax/rax ->
* REGID_RAX, xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). Capstone has
* no "largest enclosing" helper, so fold by the documented id ranges (the same
* approach reg_class_of already uses). The raw x86_reg never leaves this TU. */
static uint8_t reg_root_of(unsigned int reg) {
switch (reg) {
case X86_REG_AL: case X86_REG_AH: case X86_REG_AX:
case X86_REG_EAX: case X86_REG_RAX: return REGID_RAX;
case X86_REG_CL: case X86_REG_CH: case X86_REG_CX:
case X86_REG_ECX: case X86_REG_RCX: return REGID_RCX;
case X86_REG_DL: case X86_REG_DH: case X86_REG_DX:
case X86_REG_EDX: case X86_REG_RDX: return REGID_RDX;
case X86_REG_BL: case X86_REG_BH: case X86_REG_BX:
case X86_REG_EBX: case X86_REG_RBX: return REGID_RBX;
case X86_REG_SPL: case X86_REG_SP: case X86_REG_ESP:
case X86_REG_RSP: return REGID_RSP;
case X86_REG_BPL: case X86_REG_BP: case X86_REG_EBP:
case X86_REG_RBP: return REGID_RBP;
case X86_REG_SIL: case X86_REG_SI: case X86_REG_ESI:
case X86_REG_RSI: return REGID_RSI;
case X86_REG_DIL: case X86_REG_DI: case X86_REG_EDI:
case X86_REG_RDI: return REGID_RDI;
case X86_REG_R8B: case X86_REG_R8W: case X86_REG_R8D:
case X86_REG_R8: return REGID_R8;
case X86_REG_R9B: case X86_REG_R9W: case X86_REG_R9D:
case X86_REG_R9: return REGID_R9;
case X86_REG_R10B: case X86_REG_R10W: case X86_REG_R10D:
case X86_REG_R10: return REGID_R10;
case X86_REG_R11B: case X86_REG_R11W: case X86_REG_R11D:
case X86_REG_R11: return REGID_R11;
case X86_REG_R12B: case X86_REG_R12W: case X86_REG_R12D:
case X86_REG_R12: return REGID_R12;
case X86_REG_R13B: case X86_REG_R13W: case X86_REG_R13D:
case X86_REG_R13: return REGID_R13;
case X86_REG_R14B: case X86_REG_R14W: case X86_REG_R14D:
case X86_REG_R14: return REGID_R14;
case X86_REG_R15B: case X86_REG_R15W: case X86_REG_R15D:
case X86_REG_R15: return REGID_R15;
default: break;
}
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) {
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_XMM0));
}
if (reg >= X86_REG_YMM0 && reg <= X86_REG_YMM31) {
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_YMM0));
}
if (reg >= X86_REG_ZMM0 && reg <= X86_REG_ZMM31) {
return (uint8_t)(REGID_VEC0 + (reg - X86_REG_ZMM0));
}
if (reg >= X86_REG_K0 && reg <= X86_REG_K7) {
return (uint8_t)(REGID_K0 + (reg - X86_REG_K0));
}
if (reg == X86_REG_EFLAGS) { return REGID_FLAGS; }
if (reg == X86_REG_INVALID) { return REGID_NONE; }
return REGID_OTHER;
}
/* Approximate the ACCESS width of a register in log2 bytes from its sub-register
* id (Capstone register ids encode the width); used when only an id is known
* (cs_regs_access returns ids without sizes). Coarse but enough for overlap. */
static uint8_t reg_width_log2(unsigned int reg) {
switch (reg) {
case X86_REG_AL: case X86_REG_AH: case X86_REG_CL: case X86_REG_CH:
case X86_REG_DL: case X86_REG_DH: case X86_REG_BL: case X86_REG_BH:
case X86_REG_SPL: case X86_REG_BPL: case X86_REG_SIL: case X86_REG_DIL:
case X86_REG_R8B: case X86_REG_R9B: case X86_REG_R10B:
case X86_REG_R11B: case X86_REG_R12B: case X86_REG_R13B:
case X86_REG_R14B: case X86_REG_R15B: return 0;
case X86_REG_AX: case X86_REG_CX: case X86_REG_DX: case X86_REG_BX:
case X86_REG_SP: case X86_REG_BP: case X86_REG_SI: case X86_REG_DI:
case X86_REG_R8W: case X86_REG_R9W: case X86_REG_R10W:
case X86_REG_R11W: case X86_REG_R12W: case X86_REG_R13W:
case X86_REG_R14W: case X86_REG_R15W: return 1;
case X86_REG_EAX: case X86_REG_ECX: case X86_REG_EDX: case X86_REG_EBX:
case X86_REG_ESP: case X86_REG_EBP: case X86_REG_ESI: case X86_REG_EDI:
case X86_REG_R8D: case X86_REG_R9D: case X86_REG_R10D:
case X86_REG_R11D: case X86_REG_R12D: case X86_REG_R13D:
case X86_REG_R14D: case X86_REG_R15D: return 2;
default: break;
}
if (reg >= X86_REG_XMM0 && reg <= X86_REG_XMM31) { return 4; }
return 3; /* 64-bit GPR / vector-as-root / other: default qword */
}
/* Backend-neutral mnemonic CLASS for the rich decode (insn_class enum), by
* translating the existing classify_mnemonic SEM_MN_* result. */
static uint8_t insn_class_of(const cs_insn* in) {
switch (classify_mnemonic(in)) {
case SEM_MN_MOV: return INSN_MOV;
case SEM_MN_ARITH: return INSN_ARITH;
case SEM_MN_LOGIC: return INSN_LOGIC;
case SEM_MN_CMP: return INSN_CMP;
case SEM_MN_TEST: return INSN_TEST;
case SEM_MN_BRANCH: return INSN_BRANCH;
case SEM_MN_JMP: return INSN_JMP;
case SEM_MN_CALL: return INSN_CALL;
case SEM_MN_RET: return INSN_RET;
case SEM_MN_PUSH: return INSN_PUSH;
case SEM_MN_POP: return INSN_POP;
case SEM_MN_LEA: return INSN_LEA;
case SEM_MN_NOP: return INSN_NOP;
case SEM_MN_FLOAT: return INSN_FLOAT;
case SEM_MN_VEC: return INSN_VEC;
default: return INSN_OTHER;
}
}
/* Normalize a Capstone cs_ac_type access mask to a reg_access. */
static uint8_t access_of(uint8_t a) {
const int r = (a & CS_AC_READ) != 0;
const int w = (a & CS_AC_WRITE) != 0;
if (r && w) { return ACC_READWRITE; }
if (r) { return ACC_READ; }
if (w) { return ACC_WRITE; }
return ACC_NONE;
}
/* writes_zero_extend rule (see insndec.h): 32-bit GPR write zero-extends; EVEX/
* VEX vector write zero-extends the upper part; 8/16-bit and legacy-SSE are
* partial. */
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
if (root >= REGID_RAX && root <= REGID_R15) {
return (width_log2 == 2) ? 1u : 0u;
}
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
return vector_evex_vex ? 1u : 0u;
}
return 0u;
}
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
uint8_t width_log2, uint8_t zx, uint8_t access) {
if (root == REGID_NONE) { return; }
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
if (set[i].root == root) {
if (width_log2 > set[i].width_log2) {
set[i].width_log2 = width_log2;
set[i].writes_zero_extend = zx;
}
return;
}
}
if (*n < INSN_MAX_REGREFS) {
set[*n].root = root;
set[*n].width_log2 = width_log2;
set[*n].writes_zero_extend = zx;
set[*n].access = access;
}
(*n)++;
}
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
if (!code || !out || avail == 0) { return 0; }
csh h;
if (cs_open(CS_ARCH_X86, CS_MODE_64, &h) != CS_ERR_OK) { return 0; }
cs_option(h, CS_OPT_DETAIL, CS_OPT_ON);
cs_insn* insn = NULL;
const size_t n = cs_disasm(h, code, avail, 0, 1, &insn);
if (n < 1 || !insn) {
if (insn) { cs_free(insn, n); }
cs_close(&h);
return 0;
}
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
out->length = (uint8_t)insn->size;
out->mnemonic_class = insn_class_of(insn);
out->is_control_flow =
(has_group(insn, X86_GRP_JUMP) || has_group(insn, X86_GRP_CALL) ||
has_group(insn, X86_GRP_RET)) ? 1u : 0u;
if (insn->detail) {
const cs_x86* x = &insn->detail->x86;
/* A VEX/EVEX form zero-extends the upper part of a written vector root;
* legacy-SSE preserves it. Detect by the VEX/EVEX leading opcode byte. */
const int is_vex_evex =
(x->opcode[0] == 0xC4 || x->opcode[0] == 0xC5 || x->opcode[0] == 0x62);
/* Explicit operands (readable view): op[]. */
uint8_t no = 0;
for (uint8_t i = 0; i < x->op_count && no < INSN_MAX_OPERANDS; i++) {
const cs_x86_op* o = &x->operands[i];
const uint8_t acc = access_of(o->access);
const uint8_t wl = width_log2_of(o->size);
switch (o->type) {
case X86_OP_REG: {
const uint8_t root = reg_root_of(o->reg);
out->op[no].kind = OPND_REG;
out->op[no].access = acc;
out->op[no].width_log2 = wl;
out->op[no].reg_root = root;
out->op[no].reg_zero_extend = zx_of(root, wl, is_vex_evex);
no++;
break;
}
case X86_OP_MEM:
out->op[no].kind = OPND_MEM;
out->op[no].access = acc;
out->op[no].width_log2 = wl;
out->op[no].mem_base =
(o->mem.base == X86_REG_RIP) ? REGID_NONE
: reg_root_of(o->mem.base);
out->op[no].mem_index = reg_root_of(o->mem.index);
out->op[no].mem_scale = (uint8_t)o->mem.scale;
out->op[no].mem_is_riprel =
(o->mem.base == X86_REG_RIP) ? 1u : 0u;
out->op[no].mem_disp = o->mem.disp;
no++;
break;
case X86_OP_IMM:
out->op[no].kind = OPND_IMM;
out->op[no].access = ACC_READ;
out->op[no].imm_width_log2 = wl;
out->op[no].imm_value = o->imm;
no++;
break;
default:
break;
}
}
out->noperands = no;
/* Aggregate implicit-or-explicit register sets via cs_regs_access: the
* FULL read/write sets including rsp/rdx:rax/EFLAGS. EFLAGS -> REGID_FLAGS;
* vector writes carry zero-extend by form. */
cs_regs rd, wr;
uint8_t nrd = 0, nwr = 0;
if (cs_regs_access(h, insn, rd, &nrd, wr, &nwr) == CS_ERR_OK) {
for (uint8_t i = 0; i < nrd; i++) {
const uint8_t root = reg_root_of(rd[i]);
regset_add(out->reads, &out->nreads, root,
reg_width_log2(rd[i]), 0, ACC_READ);
}
for (uint8_t i = 0; i < nwr; i++) {
const uint8_t root = reg_root_of(wr[i]);
const uint8_t wl = reg_width_log2(wr[i]);
regset_add(out->writes, &out->nwrites, root, wl,
zx_of(root, wl, is_vex_evex), ACC_WRITE);
}
}
}
cs_free(insn, n);
cs_close(&h);
return 1;
}
const char* semsig_backend_name(void) {
return "capstone";
}
+219
View File
@@ -180,6 +180,225 @@ int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
return 1;
}
/* ---- rich decode (insn_decode_full): the lossless projection ------------- */
/* Map a Zydis register to a canonical reg_id ROOT (al/ax/eax/rax -> REGID_RAX,
* xmm/ymm/zmm -> a REGID_VEC* root, k0..k7 -> REGID_K*). The enclosing fold is
* done with ZydisRegisterGetLargestEnclosing first (GPR -> RAX..R15, vector ->
* ZMM0..ZMM31); then a class-keyed offset turns the backend enum into the stable
* reg_id. The raw ZydisRegister never leaves this TU. */
static uint8_t reg_root_of(ZydisRegister reg) {
if (reg == ZYDIS_REGISTER_NONE) { return REGID_NONE; }
const ZydisRegister enc =
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg);
const ZydisRegister r = (enc == ZYDIS_REGISTER_NONE) ? reg : enc;
if (r >= ZYDIS_REGISTER_RAX && r <= ZYDIS_REGISTER_R15) {
return (uint8_t)(REGID_RAX + (r - ZYDIS_REGISTER_RAX));
}
if (r >= ZYDIS_REGISTER_ZMM0 && r <= ZYDIS_REGISTER_ZMM31) {
return (uint8_t)(REGID_VEC0 + (r - ZYDIS_REGISTER_ZMM0));
}
if (r >= ZYDIS_REGISTER_K0 && r <= ZYDIS_REGISTER_K7) {
return (uint8_t)(REGID_K0 + (r - ZYDIS_REGISTER_K0));
}
if (r == ZYDIS_REGISTER_FLAGS || r == ZYDIS_REGISTER_EFLAGS ||
r == ZYDIS_REGISTER_RFLAGS) {
return REGID_FLAGS;
}
return REGID_OTHER;
}
/* Backend-neutral mnemonic CLASS for the rich decode (the insn_class enum). The
* groups mirror SEM_MN_* but the values are the PUBLIC insn_class ids; reuse the
* existing classify_mnemonic by translating its SEM_MN_* result. */
static uint8_t insn_class_of(const ZydisDecodedInstruction* insn) {
switch (classify_mnemonic(insn)) {
case SEM_MN_MOV: return INSN_MOV;
case SEM_MN_ARITH: return INSN_ARITH;
case SEM_MN_LOGIC: return INSN_LOGIC;
case SEM_MN_CMP: return INSN_CMP;
case SEM_MN_TEST: return INSN_TEST;
case SEM_MN_BRANCH: return INSN_BRANCH;
case SEM_MN_JMP: return INSN_JMP;
case SEM_MN_CALL: return INSN_CALL;
case SEM_MN_RET: return INSN_RET;
case SEM_MN_PUSH: return INSN_PUSH;
case SEM_MN_POP: return INSN_POP;
case SEM_MN_LEA: return INSN_LEA;
case SEM_MN_NOP: return INSN_NOP;
case SEM_MN_FLOAT: return INSN_FLOAT;
case SEM_MN_VEC: return INSN_VEC;
default: return INSN_OTHER;
}
}
/* Normalize ZydisOperandActions to a reg_access. */
static uint8_t access_of(ZydisOperandActions a) {
const int r = (a & ZYDIS_OPERAND_ACTION_READ) != 0;
const int w = (a & ZYDIS_OPERAND_ACTION_WRITE) != 0;
const int cr = (a & ZYDIS_OPERAND_ACTION_CONDREAD) != 0;
const int cw = (a & ZYDIS_OPERAND_ACTION_CONDWRITE) != 0;
if (r && w) { return ACC_READWRITE; }
if (r) { return ACC_READ; }
if (w) { return ACC_WRITE; }
if (cw) { return ACC_COND_WRITE; }
if (cr) { return ACC_COND_READ; }
return ACC_NONE;
}
/* writes_zero_extend rule (see insndec.h): a 32-bit GPR write zero-extends into
* its 64-bit root; an EVEX/VEX vector write zero-extends the upper YMM/ZMM. 8/16-
* bit GPR and legacy-SSE writes are PARTIAL. Computed from width + form, never by
* branching on a concrete register. */
static uint8_t zx_of(uint8_t root, uint8_t width_log2, int vector_evex_vex) {
if (root >= REGID_RAX && root <= REGID_R15) {
return (width_log2 == 2) ? 1u : 0u; /* 32-bit GPR write zero-extends */
}
if (root >= REGID_VEC0 && root <= REGID_VEC31) {
return vector_evex_vex ? 1u : 0u; /* VEX/EVEX zeroes the upper part */
}
return 0u;
}
/* Append a reg_ref to a set (dedup by root: keep the WIDEST width / strongest
* access so a root appears once). Count grows past the cap so truncation is
* detectable; writes stop at INSN_MAX_REGREFS. */
static void regset_add(reg_ref* set, uint8_t* n, uint8_t root,
uint8_t width_log2, uint8_t zx, uint8_t access) {
if (root == REGID_NONE) { return; }
for (uint8_t i = 0; i < *n && i < INSN_MAX_REGREFS; i++) {
if (set[i].root == root) {
if (width_log2 > set[i].width_log2) {
set[i].width_log2 = width_log2;
set[i].writes_zero_extend = zx;
}
return;
}
}
if (*n < INSN_MAX_REGREFS) {
set[*n].root = root;
set[*n].width_log2 = width_log2;
set[*n].writes_zero_extend = zx;
set[*n].access = access;
}
(*n)++;
}
int insn_decode_full(const uint8_t* code, size_t avail, insn_decoded* out) {
if (!code || !out || avail == 0) { return 0; }
ZydisDecoder dec;
if (ZYAN_FAILED(ZydisDecoderInit(&dec, ZYDIS_MACHINE_MODE_LONG_64,
ZYDIS_STACK_WIDTH_64))) {
return 0;
}
ZydisDecodedInstruction insn;
ZydisDecodedOperand ops[ZYDIS_MAX_OPERAND_COUNT];
if (ZYAN_FAILED(ZydisDecoderDecodeFull(&dec, code, avail, &insn, ops))) {
return 0;
}
for (size_t i = 0; i < sizeof *out; i++) { ((uint8_t*)out)[i] = 0; }
out->length = (uint8_t)insn.length;
out->mnemonic_class = insn_class_of(&insn);
out->is_control_flow =
(insn.meta.category == ZYDIS_CATEGORY_COND_BR ||
insn.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
insn.meta.category == ZYDIS_CATEGORY_CALL ||
insn.meta.category == ZYDIS_CATEGORY_RET) ? 1u : 0u;
/* A VEX/EVEX-encoded vector form zero-extends the upper part of a written
* vector root; legacy-SSE preserves it. Approximate from the encoding. */
const int vec_zx = (insn.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX ||
insn.encoding == ZYDIS_INSTRUCTION_ENCODING_EVEX);
/* Pass over ALL operands. EXPLICIT ones populate op[] (readable view); EVERY
* operand (explicit AND implicit/hidden: rsp on push/pop, rdx:rax on mul, the
* memory address registers) feeds the aggregate reads[]/writes[] sets. */
uint8_t no = 0;
for (uint8_t i = 0; i < insn.operand_count && i < ZYDIS_MAX_OPERAND_COUNT;
i++) {
const ZydisDecodedOperand* o = &ops[i];
const uint8_t acc = access_of(o->actions);
const uint8_t wl = width_log2_of(o->size);
if (o->type == ZYDIS_OPERAND_TYPE_REGISTER) {
const uint8_t root = reg_root_of(o->reg.value);
const uint8_t zx = zx_of(root, wl, vec_zx);
if (acc == ACC_READ || acc == ACC_READWRITE || acc == ACC_COND_READ) {
regset_add(out->reads, &out->nreads, root, wl, 0, acc);
}
if (acc == ACC_WRITE || acc == ACC_READWRITE ||
acc == ACC_COND_WRITE) {
regset_add(out->writes, &out->nwrites, root, wl, zx, acc);
}
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
no < INSN_MAX_OPERANDS) {
out->op[no].kind = OPND_REG;
out->op[no].access = acc;
out->op[no].width_log2 = wl;
out->op[no].reg_root = root;
out->op[no].reg_zero_extend = zx;
no++;
}
} else if (o->type == ZYDIS_OPERAND_TYPE_MEMORY) {
/* Address registers are READ regardless of the operand's own access. */
const uint8_t base = (o->mem.base == ZYDIS_REGISTER_RIP)
? REGID_NONE : reg_root_of(o->mem.base);
const uint8_t index = reg_root_of(o->mem.index);
if (base != REGID_NONE) {
regset_add(out->reads, &out->nreads, base, 3, 0, ACC_READ);
}
if (index != REGID_NONE) {
regset_add(out->reads, &out->nreads, index, 3, 0, ACC_READ);
}
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
no < INSN_MAX_OPERANDS) {
out->op[no].kind = OPND_MEM;
out->op[no].access = acc;
out->op[no].width_log2 = wl;
out->op[no].mem_base = base;
out->op[no].mem_index = index;
out->op[no].mem_scale = o->mem.scale;
out->op[no].mem_is_riprel =
(o->mem.base == ZYDIS_REGISTER_RIP) ? 1u : 0u;
out->op[no].mem_disp =
o->mem.disp.has_displacement ? o->mem.disp.value : 0;
no++;
}
} else if (o->type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
if (o->visibility == ZYDIS_OPERAND_VISIBILITY_EXPLICIT &&
no < INSN_MAX_OPERANDS) {
out->op[no].kind = OPND_IMM;
out->op[no].access = ACC_READ;
out->op[no].imm_width_log2 = wl;
out->op[no].imm_value = o->imm.is_signed
? o->imm.value.s
: (int64_t)o->imm.value.u;
no++;
}
}
}
out->noperands = no;
/* FLAGS as ONE pseudo-register: read = TESTED, write = anything the
* instruction MODIFIES / SET_0 / SET_1 / leaves UNDEFINED. */
if (insn.cpu_flags) {
if (insn.cpu_flags->tested) {
regset_add(out->reads, &out->nreads, REGID_FLAGS, 2, 0, ACC_READ);
}
if (insn.cpu_flags->modified || insn.cpu_flags->set_0 ||
insn.cpu_flags->set_1 || insn.cpu_flags->undefined) {
regset_add(out->writes, &out->nwrites, REGID_FLAGS, 2, 0, ACC_WRITE);
}
}
return 1;
}
const char* semsig_backend_name(void) {
return "zydis";
}