mirror of
https://dev.lirent.ru/Vatrog/vm-introspection-engine.git
synced 2026-07-08 23:46:36 +03:00
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:
+6
-1
@@ -49,12 +49,17 @@ add_library(vmie_obj OBJECT
|
|||||||
# backend TU that talks to the disassembler. semsig.c is never built in OFF (it
|
# 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.
|
# calls the backend decode, absent there) - so there is no dead/unresolved call.
|
||||||
if(VMIE_DISASM STREQUAL "OFF")
|
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")
|
elseif(VMIE_DISASM STREQUAL "zydis")
|
||||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||||
|
src/handlers/insndec.c
|
||||||
|
src/handlers/codetrace.c
|
||||||
src/handlers/semsig_backend_zydis.c)
|
src/handlers/semsig_backend_zydis.c)
|
||||||
elseif(VMIE_DISASM STREQUAL "capstone")
|
elseif(VMIE_DISASM STREQUAL "capstone")
|
||||||
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
target_sources(vmie_obj PRIVATE src/handlers/semsig.c
|
||||||
|
src/handlers/insndec.c
|
||||||
|
src/handlers/codetrace.c
|
||||||
src/handlers/semsig_backend_capstone.c)
|
src/handlers/semsig_backend_capstone.c)
|
||||||
else()
|
else()
|
||||||
message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')")
|
message(FATAL_ERROR "VMIE_DISASM must be OFF, zydis, or capstone (got '${VMIE_DISASM}')")
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -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 */
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
#include "memmodel.h" /* vmie_mem, vregion/VR_*, task/range, gva_read/write/ptr/regions/sweep */
|
||||||
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
#include "sigscan.h" /* mem_view_t, sig_pattern_t */
|
||||||
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
|
#include "scan.h" /* scan_type, scan_ptr_path, generic scan surface */
|
||||||
|
#include "codetrace.h" /* insn_usedef, trace_abi (register def-use surface) */
|
||||||
|
|
||||||
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
|
/* Opaque introspection context. Completed in src/engine/win32/engine-win32.h;
|
||||||
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
|
* callers only ever hold a pointer. Created by vmie_win32_open(), populated by
|
||||||
@@ -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 vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
||||||
uint64_t module_base, uint32_t func_rva);
|
uint64_t module_base, uint32_t func_rva);
|
||||||
|
|
||||||
|
/* Register def-use of ONE function, addressed by its func_rva (a .pdata function
|
||||||
|
* start, as from vmie_win32_functions or an export). The win32 convenience entry
|
||||||
|
* point over the generic func_usedef (codetrace.h): it locates the function
|
||||||
|
* extent from .pdata (like vmie_win32_func_semsig), gathers its body with
|
||||||
|
* gva_read, builds a SECTION_LOCAL mem_view_t, and DELEGATES to func_usedef -
|
||||||
|
* PASSING TRACE_ABI_WIN64, because choosing the Win64 calling convention is the
|
||||||
|
* OS-specific decision that belongs in this layer (which already owns .pdata /
|
||||||
|
* cr3 / module_base). It does NOT reimplement the analysis or apply the volatile
|
||||||
|
* clobber itself (func_usedef does), and pulls in no disassembler backend (the
|
||||||
|
* feature is inherited transitively via the same VMIE_HAVE_DISASM as the generic).
|
||||||
|
*
|
||||||
|
* v - engine handle.
|
||||||
|
* cr3 - the process address space the module is mapped in.
|
||||||
|
* module_base - image base VA (its PE headers must be resident).
|
||||||
|
* func_rva - function start RVA (must match a .pdata function start).
|
||||||
|
* out, max - caller array receiving up to `max` per-instruction insn_usedef
|
||||||
|
* records in ascending function-local offset order (NULL to count).
|
||||||
|
*
|
||||||
|
* Returns the TOTAL instruction count, 0 if the function is empty, or -1 on any
|
||||||
|
* of: func_rva is not a known function start, the body is unreadable (paged out),
|
||||||
|
* a decode desync, or a build with no disassembler backend (VMIE_HAVE_DISASM==0).
|
||||||
|
* The off field of each record is a function-local byte offset (SECTION_LOCAL).
|
||||||
|
*
|
||||||
|
* The Win64 volatile clobber applied at call sites - and its honest limits (only
|
||||||
|
* the standard Win64 convention; not __vectorcall/custom; the return value is not
|
||||||
|
* a separate def) - are documented at the clobber table in codetrace.c.
|
||||||
|
*
|
||||||
|
* Example - registers each instruction of a function kills:
|
||||||
|
* insn_usedef ud[512];
|
||||||
|
* int n = vmie_win32_func_usedef(v, pr->cr3, m.base, fn_rva, ud, 512);
|
||||||
|
* for (int i = 0; i < n && i < 512; i++) ... inspect ud[i].def ... */
|
||||||
|
int vmie_win32_func_usedef(vmie_win32* v, uint64_t cr3, uint64_t module_base,
|
||||||
|
uint32_t func_rva, insn_usedef* out, int max);
|
||||||
|
|
||||||
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
/* Devirtualization (C++ vtables) needs NO dedicated symbol - it is a
|
||||||
* COMPOSITION of primitives the engine already exposes:
|
* COMPOSITION of primitives the engine already exposes:
|
||||||
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
* - a vtable at `vtable_va` is an array of code pointers, so its METHODS are
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
|
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
|
||||||
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
|
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
|
||||||
#include "semsig.h" /* semsig_hash (generic; no backend header pulled here) */
|
#include "semsig.h" /* semsig_hash (generic; no backend header pulled here) */
|
||||||
|
#include "codetrace.h" /* func_usedef (generic def-use; no backend header here) */
|
||||||
|
|
||||||
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
|
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
|
||||||
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
|
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
|
||||||
@@ -824,3 +825,55 @@ uint64_t vmie_win32_func_semsig(vmie_win32* v, uint64_t cr3,
|
|||||||
free(fb);
|
free(fb);
|
||||||
return h;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 */
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
#define VMIE_SEMSIG_BACKEND_H
|
#define VMIE_SEMSIG_BACKEND_H
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stddef.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. */
|
/* Operand kind, normalized. Carried in sem_insn.op[].kind. */
|
||||||
enum {
|
enum {
|
||||||
@@ -85,6 +89,16 @@ typedef struct {
|
|||||||
* shared mutable state - reentrant. */
|
* shared mutable state - reentrant. */
|
||||||
int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out);
|
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,
|
/* Stable backend identity for diagnostics / hash-domain tagging. Static,
|
||||||
* never-NULL. This declaration is shared with semsig.h's public symbol of the
|
* 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. */
|
* same name; the concrete backend TU (or the stub) defines it once. */
|
||||||
|
|||||||
@@ -184,6 +184,257 @@ int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
|||||||
return 1;
|
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) {
|
const char* semsig_backend_name(void) {
|
||||||
return "capstone";
|
return "capstone";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,225 @@ int semsig_backend_decode(const uint8_t* code, size_t avail, sem_insn* out) {
|
|||||||
return 1;
|
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) {
|
const char* semsig_backend_name(void) {
|
||||||
return "zydis";
|
return "zydis";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user