diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e7e367..07505ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}')") diff --git a/include/codetrace.h b/include/codetrace.h new file mode 100644 index 0000000..7d9610b --- /dev/null +++ b/include/codetrace.h @@ -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 +#include +#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 */ diff --git a/include/insndec.h b/include/insndec.h new file mode 100644 index 0000000..91c0545 --- /dev/null +++ b/include/insndec.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 +#include + +/* 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 */ diff --git a/include/win32.h b/include/win32.h index 04b5f71..3c3aa93 100644 --- a/include/win32.h +++ b/include/win32.h @@ -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 diff --git a/src/engine/win32/pe.c b/src/engine/win32/pe.c index 765e482..1eb7ecf 100644 --- a/src/engine/win32/pe.c +++ b/src/engine/win32/pe.c @@ -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; +} diff --git a/src/handlers/codetrace.c b/src/handlers/codetrace.c new file mode 100644 index 0000000..dd0be03 --- /dev/null +++ b/src/handlers/codetrace.c @@ -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 +#include +#include /* 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; +} diff --git a/src/handlers/insndec.c b/src/handlers/insndec.c new file mode 100644 index 0000000..6de59f0 --- /dev/null +++ b/src/handlers/insndec.c @@ -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 +#include +#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(); +} diff --git a/src/handlers/insndec_stub.c b/src/handlers/insndec_stub.c new file mode 100644 index 0000000..bee0b7b --- /dev/null +++ b/src/handlers/insndec_stub.c @@ -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 +#include +#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 */ +} diff --git a/src/handlers/semsig_backend.h b/src/handlers/semsig_backend.h index 03a0a7c..1fb9f79 100644 --- a/src/handlers/semsig_backend.h +++ b/src/handlers/semsig_backend.h @@ -13,6 +13,10 @@ #define VMIE_SEMSIG_BACKEND_H #include #include +#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. */ diff --git a/src/handlers/semsig_backend_capstone.c b/src/handlers/semsig_backend_capstone.c index 1bdca74..ce2d4c7 100644 --- a/src/handlers/semsig_backend_capstone.c +++ b/src/handlers/semsig_backend_capstone.c @@ -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"; } diff --git a/src/handlers/semsig_backend_zydis.c b/src/handlers/semsig_backend_zydis.c index 5c63016..a0cd45b 100644 --- a/src/handlers/semsig_backend_zydis.c +++ b/src/handlers/semsig_backend_zydis.c @@ -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"; }