Add code-structure analysis: call graph, jump tables, basic blocks, constant xref

Wave 1 of the code-analysis layer, built on the x86-64 decoder:

- vmie_win32_callgraph walks each .pdata function with the decoder and emits an
  edge for every direct call/jmp whose target lands in the module - the
  intra-module call graph. Indirect edges are left to the IAT and jump tables.
- gva_jumptable recovers a switch's case targets from an indirect jump's table:
  consecutive pointer entries that land in an executable region.
- cfg_blocks splits one function view into basic blocks (a generic handler:
  leaders from intra-function branch targets, cut after jmp/jcc/ret).
- gva_imm_xref finds the instructions whose immediate operand equals a constant
  - the dual of code-xref for magic values, error codes, syscall numbers.

The decoder now also reports imm_off/imm_len so a caller can read or match the
immediate operand. The generic primitives live in the new codeanalysis.h
(jump tables, basic blocks) and scan.h (constant xref); the .pdata-bound call
graph stays on the win32 surface and reuses the existing function/section/decode
primitives - no second PE or instruction parser.
This commit is contained in:
2026-06-16 19:52:25 +03:00
parent c4419964aa
commit 79e82ffc6a
9 changed files with 505 additions and 1 deletions
+110
View File
@@ -1,9 +1,11 @@
#include "pe.h"
#include <string.h>
#include <stdlib.h> /* malloc/free (cold call-graph gather only) */
#include "memmodel.h" /* gva_read, VR_* */
#include "sigscan.h" /* mem_sub (pure matcher; engine may use it) */
#include "win32.h" /* public surface: vmie_win32, section_desc, view_base */
#include "x86dec.h" /* x86_decode / x86_branch_target (call-graph step) */
/* IMAGE_SECTION_HEADER: 8-byte Name, then Misc.VirtualSize(+8), VirtualAddress
* (+12), and Characteristics(+36); the header is 40 bytes wide. */
@@ -406,3 +408,111 @@ int vmie_win32_section_view(vmie_win32* v, uint64_t cr3, uint64_t module_base,
out->data = buf; out->size = n; out->base_va = base_va;
return 0;
}
/* ---- public win32 surface: intra-module call graph ----------------------- *
* Reuses the existing primitives only: vmie_win32_functions (.pdata starts),
* vmie_win32_section_view (.text bytes), and x86_decode (the light decoder) -
* there is no second PE parser and no second decoder here. For each function it
* steps the bytes linearly and, on a DIRECT call/jmp (has_rel), resolves the
* target and, if it lands inside the image, emits one {from, to, kind} edge.
* Cold: one-shot directory + section gather, not a hot loop. */
/* SizeOfImage lives in the PE32+ OptionalHeader at +0x38; the OptionalHeader
* begins at NT(base+lfanew)+0x18 (Signature(4)+FileHeader(20)). */
#define OPT_SIZEOFIMAGE_OFF 0x38u
int vmie_win32_callgraph(vmie_win32* v, uint64_t cr3, uint64_t module_base,
call_edge* out, int max) __attribute__((cold));
int vmie_win32_callgraph(vmie_win32* v, uint64_t cr3, uint64_t module_base,
call_edge* out, int max) {
vmie_mem* m = vmie_win32_mem(v);
if (!m) { return -1; }
/* image bounds: [module_base, module_base + SizeOfImage). */
uint32_t lfanew;
if (gva_read(m, cr3, module_base + 0x3C, &lfanew, 4)) { return -1; }
uint32_t size_of_image;
if (gva_read(m, cr3, module_base + lfanew + 0x18 + OPT_SIZEOFIMAGE_OFF,
&size_of_image, 4)) {
return -1;
}
/* locate .text (the executable section the .pdata functions live in). */
section_desc sd[96];
const int ns = vmie_win32_sections(v, cr3, module_base, sd, 96);
if (ns < 0) { return -1; }
const int nsuse = ns < 96 ? ns : 96;
const section_desc* text = NULL;
for (int i = 0; i < nsuse; i++) {
if (strcmp(sd[i].name, ".text") == 0) { text = &sd[i]; break; }
}
if (!text) {
/* fall back to the first executable section */
for (int i = 0; i < nsuse; i++) {
if (sd[i].prot & VR_X) { text = &sd[i]; break; }
}
}
if (!text) { return -1; }
/* gather the executable section once, addressed at its absolute VA so a
* decoded branch target is directly an absolute VA. */
uint8_t* tbuf = malloc(text->vsize);
if (!tbuf) { return -1; }
mem_view_t tv;
if (vmie_win32_section_view(v, cr3, module_base, text, ABSOLUTE_VA,
tbuf, text->vsize, &tv) != 0) {
free(tbuf);
return -1;
}
const uint64_t text_lo = module_base + text->rva; /* tv.base_va */
const uint64_t text_hi = text_lo + tv.size; /* exclusive */
/* function inventory: count, then gather (stack for the common case, heap on
* overflow) so every function is stepped, none silently dropped. */
const int nfn = vmie_win32_functions(v, cr3, module_base, NULL, 0);
if (nfn < 0) { free(tbuf); 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) { free(tbuf); return -1; }
fr = heap_fr;
}
const int got = vmie_win32_functions(v, cr3, module_base, fr, nfn);
if (got < 0) { free(heap_fr); free(tbuf); return -1; }
int total = 0;
for (int f = 0; f < got; f++) {
const uint64_t fn_lo = module_base + fr[f].rva;
const uint64_t fn_hi = fn_lo + fr[f].size;
/* the function must lie inside the gathered section. */
if (fn_lo < text_lo || fn_hi > text_hi) { continue; }
size_t off = (size_t)(fn_lo - text_lo);
const size_t end = (size_t)(fn_hi - text_lo);
while (off < end) {
x86_insn in;
const int ilen = x86_decode(tv.data + off, end - off, &in);
if (ilen <= 0) { break; } /* desync: stop this fn */
const uint64_t ip = text_lo + off;
if (in.has_rel && (in.flow == X86_CALL || in.flow == X86_JMP)) {
const uint64_t tgt = x86_branch_target(ip, &in);
if (tgt >= module_base &&
tgt < module_base + (uint64_t)size_of_image) {
if (out && total < max) {
out[total].from = fr[f].rva;
out[total].to = (uint32_t)(tgt - module_base);
out[total].kind = (in.flow == X86_CALL) ? 0u : 1u;
}
total++;
}
}
off += (size_t)ilen;
}
}
free(heap_fr);
free(tbuf);
return total;
}