Skip to content

XWA Decoder and Lifter

M T edited this page Oct 4, 2026 · 1 revision

XWA Decoder and Lifter

The translator is built from two layers. The bottom layer is a vendored, MIT-licensed static-recompilation toolkit originally written for X-Wing Alliance ("XWA tools", third_party/xwa), which provides PE parsing, Capstone-based x86 disassembly and a permissive x86-to-C lifter. On top of it, tools/engine_reuse subclasses and replaces most of that toolkit with a strict decoder and lifter: it never guesses an indirect target, never approximates a flag, and refuses (or traps at run time) any instruction it cannot express exactly. Static Translation Pipeline drives these classes; the C they emit targets EngineReuse Runtime.

Source files

File Role
third_party/xwa/LICENSE MIT license (copyright sp00nznet)
third_party/xwa/tools/pe_analyze.py pefile wrapper: sections, imports, code/data ranges, IAT map, JSON export. Used (through load_pe)
third_party/xwa/tools/disasm.py Instruction, BasicBlock, Function dataclasses and Disassembler. Used as base class and data model
third_party/xwa/tools/lifter.py Upstream Lifter. Used as base class for operand formatting and ten flag-neutral instructions
third_party/xwa/tools/translator.py Upstream pipeline driver. Only load_pe is used
third_party/xwa/tools/generate.py Upstream linear-sweep generator. Not used
third_party/xwa/recomp_types.h Upstream C runtime header (global registers, RECOMP_CALL). Not used
third_party/xwa/tools/__init__.py, __main__.py Package marker; python -m entry that calls translator.main()
tools/engine_reuse/upstream.py Imports the pinned toolkit under a private package name; PE path and PE_SHA256
tools/engine_reuse/decode.py EngineDisassembler and DecodeError
tools/engine_reuse/lift.py EngineLifter: control flow, memory, stack, function skeleton
tools/engine_reuse/flags.py FlagsMixin: exact integer EFLAGS producers and condition consumers
tools/engine_reuse/extra.py ExtraMixin: string ops, one-operand MUL/IMUL, shifts/rotates, bit ops, atomics-shaped ops, CPUID/RDTSC
tools/engine_reuse/fpu.py FPUMixin: x87 instructions (see x87 Floating Point)
tools/engine_reuse/test_decode.py, test_flags.py, test_fpu.py Decoder regression, flags differential, x87 differential

Why two layers

The upstream toolkit is designed to get a large game running quickly: it keeps x86 registers in C globals, discards EFLAGS and instead pattern-matches the most recent cmp/test into the next jcc, models only the common x87 operations, and lets unresolved branches fall into a dispatcher. Its own sources document the limits, for example #define CMP_O(a, b) 0 /* TODO: overflow detection */ and CMP_P in recomp_types.h:255-258, the "test-after-MOV codegen bug" workaround in lifter.py:361-408, and the comments explaining that a per-function double _st[8] made __ftol return 0 (recomp_types.h:41-44).

The Halo translator keeps the parts that are mechanical and safe (PE parsing, Capstone decoding, operand text formatting) and replaces the rest. The module docstring of lift.py states the rule:

Strict reusable static x86-to-C lowering around the original instruction graph. No per-function CPU reset, flag-history heuristic, guessed game calls or no-op fallback. Every unresolved operation is reported before compilation.

Concern Upstream XWA toolkit Halo engine_reuse
Register state C globals g_eax..; ebp once per-function local EngineCPU struct passed to every function; register names are macros over cpu->gpr[]
EFLAGS not stored; condition rebuilt from the last cmp/test/add... operands real eflags word updated by each producer, read by each consumer
Calls RECOMP_CALL: push dummy 0xDEAD0000, call C function, restore ebx/esi/edi/ebp push the real return address, dispatch, check cpu->pc on return
Unknown jump targets goto to undefined label rewritten to RECOMP_ITAIL switch over own leaders, else engine_dispatch; decode never invents targets
Memory MEM32(addr) volatile dereference at addr + g_mem_base engine_read_u32(cpu, addr) through engine_address (flat or checked)
x87 shifting double _st[8], partial instruction set rotating register file with tags, status/control words, environments (x87 Floating Point)
Unsupported instruction /* UNIMPLEMENTED: ... */ comment, execution continues Python exception, or engine_fail trap with --trap-unsupported
Function discovery E8 call scan plus 55 8B EC prologue scan explicit entry lists plus call graph closure plus optional immediate scan

Loading the pinned toolkit

upstream.py creates a synthetic package named _halo_pinned_xwa_tools whose __path__ is third_party/xwa/tools, then imports disasm, lifter and translator from it. This avoids name clashes with any tools package on sys.path and makes the relative imports inside translator.py (from .pe_analyze import ...) resolve against the vendored copy. Its docstring reads "Pinned MIT-licensed decoding/lifting tools; never mutate the proven prototype": fixes go into tools/engine_reuse subclasses, not into third_party/xwa.

Exported names: Disassembler, Instruction, Function, BasicBlock, Lifter, load_pe, plus VISION (repository root), PE and PE_SHA256 (see Static Translation Pipeline).

Note: the vendored third_party/xwa/tools/lifter.py already contains a few Halo-specific edits from an earlier prototype (for example the fcomp/fnstsw/test ah parity conditions marked "Prototype-local Halo fix" at lines 278-287, and the capstone 5 ST register numbering fix at lines 97-102). None of those branches are reached by EngineLifter, which handles all flag consumers and x87 operations itself.

Data model (from disasm.py)

Class Fields used by the Halo tools
Instruction address, size, mnemonic, op_str, bytes, operands (Capstone operand objects); properties is_call, is_ret, is_cond_jump (includes jcxz/jecxz), is_uncond_jump, is_jump, end_address; get_branch_target() returns the immediate target or None for indirect
BasicBlock start, end, instructions, successors, is_exit
Function address, end, name (sub_XXXXXXXX), blocks (leader -> block), calls_to, size; num_instructions. EngineDisassembler adds indirect_edges and tail_calls

The Disassembler constructor builds a Capstone Cs(CS_ARCH_X86, CS_MODE_32) with detail = True and a cache of code-section ranges. is_code_address tests membership in those ranges (raw size, so trailing virtual-only space is not code); read_bytes falls back to data sections, which is how jump tables and selector bytes are read.

EngineDisassembler

decode.py replaces the upstream recursive descent. Upstream disassemble_function re-disassembles up to 8 KiB linearly from every work item, guesses that conditional targets within 1 MiB are part of the same function, and does not track tail calls. The Halo version decodes one instruction at a time and records exactly what it saw.

Instruction cache

instruction_at(address) (lines 15-25) reads 15 bytes (the maximum x86 instruction length), decodes exactly one instruction, wraps it in the upstream Instruction, and caches it in self.instructions. The cache is shared across all functions decoded by one EngineDisassembler; the generator's --discover pass scans it for immediates.

disassemble_function(start_va, iat_map=None, *, end_va=None, extra_entries=(), known_functions=())

Lines 27-130. Parameters:

Parameter Meaning
start_va function entry; must be in a code section or DecodeError("Entry outside code")
end_va optional exclusive upper bound; targets at or beyond it are not internal
extra_entries additional block leaders (jump-table targets)
known_functions other function entries; any of them except start_va is "foreign"

A target is internal when it is in a code section, not foreign, and inside [start_va, end_va) if end_va is set.

Pass 1 walks a worklist of leaders and decodes linearly from each until a terminator:

flowchart TD
    A["pop leader address"] --> B{"already decoded?"}
    B -- yes --> A
    B -- no --> C{"decoded >= budget (50000)?"}
    C -- yes --> X1["DecodeError: exceeds budget"]
    C -- no --> D{"internal(address)?"}
    D -- no --> E{"foreign known entry,<br/>not an explicit leader?"}
    E -- yes --> T["tail_calls.add(address)<br/>(shared epilogue fallthrough)"]
    E -- no --> X2["DecodeError: fallthrough outside function"]
    D -- yes --> F["decode one instruction"]
    F --> G{"call?"}
    G -- yes --> G1["direct: calls_to.add(target)<br/>indirect: indirect_edges += (addr,'call',ops)<br/>return site -> new leader if internal"]
    G1 --> N["continue at next instruction"]
    G -- no --> H{"jump?"}
    H -- yes --> H1["indirect: indirect_edges += (addr,'jump',ops)<br/>internal target: new leader<br/>else tail_calls.add(target)<br/>jcc fallthrough: leader if internal else tail call"]
    H1 --> A
    H -- no --> I{"ret or int3?"}
    I -- yes --> A
    I -- no --> N
    N --> C
Loading

Design points, each justified in the code comments:

  • Call return sites are block leaders. "The return site is a resume point for setjmp/longjmp and SEH unwinding, so make it a basic-block leader (dispatchable)". This is what lets generated code resume at a return address after a non-local return (see EngineReuse Runtime).
  • Falling into another known function is a tail call. "Adjacent CRT entries can share an epilogue by falling directly into the next function instead of using JMP." The same applies to a conditional branch's fallthrough (lines 81-88).
  • Indirect edges are reported, not resolved. Upstream code tried to guess switch tables; here the generator does that separately with proven guards (Static Translation Pipeline).
  • The budget fails closed. A function over 50,000 instructions (instruction_budget, constructor keyword) is rejected instead of being truncated.

Pass 2 validates and builds blocks:

  • Sorted decoded addresses must not overlap ("Every byte must belong to exactly one decoded instruction. Reject accidental entry into the middle of another instruction."); otherwise DecodeError("Overlapping decode at ...").
  • For each leader, instructions are appended until the next leader, a call whose return site is a leader, a jump, a ret or an int3. A leader with no instructions raises DecodeError("Empty block").
  • func.end is the maximum block end; func.size = end - start.

The iat_map argument is accepted for signature compatibility and not used; import calls are indirect memory calls and appear in indirect_edges.

EngineLifter

EngineLifter(ExtraMixin, FlagsMixin, FPUMixin, Lifter). Constructor keywords:

Keyword Default Meaning
trap_unsupported False on a lowering error, append {address, reason} to self.unsupported and emit engine_fail(cpu, "unsupported original instruction"); instead of raising
static_functions True emit static void sub_... (single translation unit) or void sub_... (chunked)
iat_map, ... passed through to upstream Lifter.__init__
classDiagram
    class Lifter {
      <<third_party/xwa>>
      _fmt_read(op)
      _fmt_write(op, value)
      _fmt_mem_addr(mem)
      lift_instruction(insn)
    }
    class ExtraMixin {
      lift_extra_instruction(insn)
      _string_form / _string_lines / _bit_test
    }
    class FlagsMixin {
      lift_flag_instruction(insn)
      flags_condition(mnemonic)
    }
    class FPUMixin {
      lift_fpu_instruction(insn)
      _fp_read / _fp_index
    }
    class EngineLifter {
      trap_unsupported
      static_functions
      unsupported
      _fmt_mem_addr / _fmt_mem_read / _fmt_mem_write
      _branch(target)
      lift_instruction(insn)
      lift_function(function)
    }
    ExtraMixin <|-- EngineLifter
    FlagsMixin <|-- EngineLifter
    FPUMixin <|-- EngineLifter
    Lifter <|-- EngineLifter
Loading

Operand formatting

Inherited from upstream _fmt_read/_fmt_write (lifter.py:140-186), with memory hooks overridden (lift.py:21-34):

Operand Read Write
32-bit register eax eax = v
16-bit register LO16(eax) SET_LO16(eax, v)
low byte LO8(eax) SET_LO8(eax, v)
high byte HI8(eax) SET_HI8(eax, v)
immediate masked to 32 bits; 0x%08Xu above 0xFFFF, 0x%Xu above 9, else decimal n/a
memory, size 1/2/4/8 engine_read_u8/16/32/64(cpu, addr) engine_write_u8/16/32/64(cpu, addr, v)
memory, other size ValueError("Unsupported memory load size") ValueError("Unsupported memory store size")
FS or GS segment override address becomes (ENGINE_FS_BASE + addr) same
segment register read as _seg_cs etc. (constants in engine_registers.h) rejected (Unsupported register operand)

The effective address is base + index * scale + disp, with negative displacements written (-0x...). All arithmetic is on uint32_t, so it wraps like the hardware. GS is folded onto the FS base because Win32 user code only uses FS (the thread information block).

_check_flag_clobber is overridden to do nothing: "EFLAGS is real shared state, not a deferred operand expression." The upstream workaround is unnecessary when every producer writes eflags immediately.

Instruction dispatch order

lift_instruction tries handlers in a fixed order and the first one that returns code wins:

flowchart TD
    I["instruction"] --> F1{"sahf lahf stc clc cmc cld std"}
    F1 -- yes --> O1["direct eflags bit operations"]
    F1 -- no --> F2{"div / idiv"}
    F2 -- yes --> O2["engine_divide(cpu, src, bits, signed)"]
    F2 -- no --> F3["ExtraMixin.lift_extra_instruction"]
    F3 -- "code" --> OUT["emit"]
    F3 -- None --> F4["FlagsMixin.lift_flag_instruction"]
    F4 -- "code" --> OUT
    F4 -- "raises for known unsupported flag writers" --> ERR
    F4 -- None --> F5["FPUMixin.lift_fpu_instruction<br/>(mnemonics starting with f, plus wait)"]
    F5 -- "code" --> OUT
    F5 -- "raises" --> ERR
    F5 -- None --> F6["call / jmp / jecxz / jcc / setcc / cmovcc /<br/>ret / push / pop / leave / not"]
    F6 -- "code" --> OUT
    F6 -- no --> F7{"mov movzx movsx lea nop<br/>cdq cwde cbw bswap cwd"}
    F7 -- yes --> UP["upstream Lifter.lift_instruction"]
    F7 -- no --> ERR["NotImplementedError / ValueError"]
    ERR --> TRAP{"trap_unsupported?"}
    TRAP -- yes --> T["engine_fail(cpu, 'unsupported original instruction')"]
    TRAP -- no --> RAISE["function recorded as unsupported"]
Loading

The final whitelist (mov, movzx, movsx, lea, nop, cdq, cwde, cbw, bswap, cwd) is the only place upstream lowering is reused, because "all other upstream branches include heuristic or partial approximations" (lines 123-129). The result is rejected if the upstream text contains unknown reg, seg reg or ???. (cwd is in the whitelist but ExtraMixin handles it first.)

Control flow lowering

Instruction Generated C
call target (any operand) { uint32_t target = <operand>; engine_push(cpu, <return VA>, 4); engine_dispatch(cpu, target); if (cpu->pc != <return VA>) { if (cpu->pc >= <func start> && cpu->pc < <func end>) goto L_ENTRY; else return; } } (lines 66-72); the generator later turns constant targets into ENGINE_DIRECT
jmp target (direct) goto L_target; if the target is a block of this function, else engine_dispatch(cpu, target); return; (_branch, lines 39-42)
jmp <reg or mem> switch (<operand>) { case <each leader>: goto L_<leader>; ... default: engine_dispatch(cpu, <operand>); return; } (operand must be 32 bits)
jcc if (<flags condition>) { <_branch(target)> }
jecxz / jcxz if (ecx == 0) / if (LO16(ecx) == 0) then _branch
setcc <dst> = (<condition> ? 1u : 0u);
cmovcc { uint32_t value = <src>; if (<condition>) { <dst> = value; } }: the source is read unconditionally, as on hardware
ret / ret n cpu->pc = engine_pop(cpu, 4); esp += n; return; (operand-size-prefixed ret rejected)
fallthrough at block end _branch(next address) unless the block ends in ret or unconditional jmp

The call protocol and indirect dispatch are diagrammed on EngineReuse Runtime.

Stack and simple integer lowering

Instruction Generated C
push x (16/32-bit) engine_push(cpu, x, size);
pop x (16/32-bit) { uint32_t value=engine_pop(cpu,size); <x> = value; } (a memory destination is addressed with the already incremented esp, as on hardware)
leave esp=ebp;ebp=engine_pop(cpu,4);
not x <x> = ~(<x>); (no flags)
div / idiv (8/16/32) engine_divide(cpu, <src>, bits, signed); (traps on zero divisor and quotient overflow)
sahf eflags = (eflags & ~0xd5u) | (HI8(eax) & 0xd5u);
lahf SET_HI8(eax, (eflags & 0xd5u) | 2u);
stc / clc / cmc eflags |= 1u; / eflags &= ~1u; / eflags ^= 1u;
cld / std clear / set 0x400 (DF)
mov, movzx, movsx, lea, bswap, cdq, cwde, cbw, nop upstream text, e.g. eax = engine_read_u32(cpu, esp + 0x4); /* 0x...: mov ... */

FlagsMixin

flags.py owns add sub adc sbb and or xor cmp test, sal sar shl shr, inc dec neg and two/three-operand imul. Its contract:

  • The mnemonic is the last whitespace token, so Capstone's textual prefixes (lock add, repe cmpsb) are normalized; then a lock prefix (byte 0xF0 or lock text) raises ValueError("LOCK flag instruction requires atomic lowering").
  • Operand widths must be 8, 16 or 32 bits and both sides must match (immediates excepted).
  • Each producer becomes one helper call that both computes the result and updates eflags:
Instruction Generated C
add a, b / sub a, b a = engine_flags_add(&eflags, a, b, 0u, W); / engine_flags_sub(...)
adc / sbb same helpers with (eflags & ENGINE_EFLAGS_CF) as carry/borrow in
cmp a, b (void)engine_flags_sub(&eflags, a, b, 0u, W);
test a, b (void)engine_flags_logic(&eflags, (a) & (b), W);
and/or/xor a = engine_flags_logic(&eflags, (a) op (b), W);
inc/dec/neg a = engine_flags_inc/dec/neg(&eflags, a, W);
shl/sal/shr/sar a = engine_flags_shl/shr/sar(&eflags, a, count, W);
imul r, a / imul r, a, imm (16/32) r = engine_flags_imul(&eflags, a, b, W);
  • Instructions that write flags but have no lowering in this mixin are listed in _KNOWN_UNSUPPORTED_FLAG_WRITERS (lines 22-27) and raise NotImplementedError rather than returning None, so the heuristic upstream path can never handle them silently. Most of them are handled earlier by EngineLifter or ExtraMixin (bt*, rol/ror/rcl/rcr, shld/shrd, string compares, xadd, cmpxchg, mul, div, popf, sahf, stc/clc/cmc). The ones that remain unsupported end to end are the BCD instructions (aaa aad aam aas daa das) and cmpxchg8b.
  • flags_condition(mnemonic) (lines 209-239) strips j/set/cmov, resolves aliases (c/nae to b, z to e, nbe to a, pe to p, nge to l, ...) and returns the architectural expression over ENGINE_EFLAGS_* bits, e.g. jg becomes (!ZF && (SF == OF)). Unknown suffixes raise ValueError (including jecxz, which has its own lowering).
  • Architecturally undefined flags are made deterministic in engine_flags.h and summarized in UNDEFINED_FLAG_NOTES; see EngineReuse Runtime.

The mixin finds operand formatters through _read_op(op, insn)/_write_op(op, value, insn) if the host class defines them, else the upstream _fmt_read(op)/_fmt_write(op, value), so it is testable without the upstream lifter (the tests use a stub host).

ExtraMixin

extra.py runs before the flags mixin. "Everything here is lowered exactly (no heuristics). String instructions honor the direction flag."

Group Instructions Lowering
String ops movs stos lods scas cmps in byte/word/dword forms, with optional rep/repe/repne Opcode and prefixes are decoded from the raw bytes (F3 rep/repe, F2 repne, 66 word size). Step is (eflags & 0x400) ? -width : width. scas/cmps set flags through engine_flags_sub. Repeated forms are while (ecx) { body; ecx--; [if (stop) break;] } where the stop test is !ZF for repe and ZF for repne. Address-size prefix 0x67 is rejected
Widening multiply one-operand mul/imul (8/16/32) engine_mul1(cpu, src, bits, signed) writes AX, DX:AX or EDX:EAX and sets CF=OF
Double shifts shld, shrd (16/32) engine_flags_shld/shrd
Rotates rol ror rcl rcr (8/16/32) engine_flags_rol/ror/rcl/rcr
Bit tests bt bts btr btc (16/32) CF from the selected bit; for a memory base with a register offset, the signed offset selects a word outside the operand (`bitaddr = addr + (bitoff >> 5
Bit scans bsf, bsr ZF set and destination unchanged when the source is 0, else engine_bsf/bsr
Exchange xchg swap through temporaries
xadd engine_flags_add, destination gets the sum, source gets the old destination
cmpxchg compare with AL/AX/EAX via engine_flags_sub; ZF selects store or accumulator load
Table lookup xlatb SET_LO8(eax, engine_read_u8(cpu, ebx + LO8(eax)))
pushal/popal eight pushes/pops; popal discards the saved ESP
pushf(d)/popf(d) push eflags | 0x202; pop the whole word
cpuid engine_cpuid(cpu): "a plain CPU with no MMX/SSE/3DNow! so the game selects its x87 code paths"
rdtsc engine_rdtsc(cpu): host monotonic clock
emms/femms comment only (no MMX state modeled)
int3, hlt, ud2 engine_fail(cpu, "...")
lock (bare mnemonic) no code

Because ExtraMixin matches on the last mnemonic token, lock xadd and lock cmpxchg are lowered as ordinary, non-atomic C sequences, whereas lock add/lock or etc. reach FlagsMixin and are rejected. The code does not establish whether Halo executes any lock-prefixed instruction concurrently on shared data.

FPUMixin

Every x87 instruction is lowered to a helper call in engine_cpu.h; the mapping table and semantics are on x87 Floating Point. Unsupported x87 forms raise ValueError.

Function skeleton

lift_function:

  1. Records the function and its range [_flo, _fhi) (_fhi is the maximum block end) for the call-return check.
  2. Emits [static] void sub_XXXXXXXX(EngineCPU *cpu) { and L_ENTRY: switch (cpu->pc) { case <leader>: goto L_<leader>; ... default: engine_fail(cpu, "dispatch to non-leader address"); }. Every block leader, including call return sites and harvested jump-table targets, is an entry point.
  3. For each block in address order: label L_XXXXXXXX:;, then per instruction engine_step(cpu,0x...u); /* mnemonic operands */ followed by the lowered statements.
  4. A block that does not end in ret or an unconditional jmp gets an explicit branch to its fallthrough address.

There is no per-function register reset, no local copies of registers, and no prologue/epilogue synthesis; the guest's own instructions manage the guest stack.

Upstream command-line tools

These are vendored for completeness; none of Master Chef's build paths call them, and their output format (recomp_*.c against recomp_types.h) is not compatible with EngineReuse.

Tool Invocation Options
translator.py main(); __main__.py calls it (its docstring says python -m tools.recomp <args>, the upstream project's layout) pe_file; --output/-o (default src/game/recomp/gen); --split (1000 functions per file); --all (full pipeline); --analyze-only; --functions-json PATH; --stubs PATH (import stub file); --pe-json PATH
generate.py python generate.py [exe] [output_dir] [split] defaults config/xwingalliance_decrypted.exe, src/game/recomp/gen, 500; also writes config/functions.json
pe_analyze.py python pe_analyze.py <pe_file> [--json out.json] prints a section/import summary

The upstream discovery heuristics are worth knowing when reading old notes: find_call_targets scans every E8 byte as a potential relative call, find_functions adds every 55 8B EC (push ebp; mov ebp, esp) prologue, and generate.py additionally accepts 83 EC (sub esp, imm8) after CC/90/C3 padding and caps each function at 64 KiB. The Halo pipeline uses explicit lists instead (Function Address Lists).

Tests

test_decode.py

Two regression cases against the real executable (test_decode.py):

  • With known_functions={0x4D0580, 0x4D05CA, 0x4D05D0}, function 0x4D0580 (a pool reset) has 0x4D05CA in tail_calls, not in blocks, and its lifted C contains engine_dispatch(cpu, 0x004D05CAu); return;. This is the conditional-fallthrough-into-shared-epilogue case.
  • Without 0x4D05CA in the known set, the same bytes keep 0x4D05CA as an internal block.

0x4D05CA is not in the checked-in lists, so the production build uses the second behavior. The test requires halo.exe with the supported hash and uses package-relative imports (from .upstream import ...), so it must be run as a module from tools/.

test_flags.py

A differential test of engine_flags.h and FlagsMixin against Unicorn executing the original x86 encodings (test_flags.py). It compiles a small C library at -O0 and -O2 (with -Wall -Wextra -Werror) and loads it with ctypes. Each test prints a receipt:

Receipt Assertion
FLAGS-001 12 operations x 3 widths x 100 operand pairs (25 directed edge pairs plus 75 seeded random) x 2 builds: result and every defined status flag (AF excluded for logical ops), DF and bit 1 match Unicorn
FLAGS-002 all 16 Jcc conditions over all 32 combinations of CF/PF/ZF/SF/OF match the architectural truth table
FLAGS-003 mixin hook contract, width checks, alias handling, rejection of shld/div/scasd/xadd in the mixin, one-operand IMUL, 8-bit IMUL, 64-bit operands, mismatched widths, LOCK prefix and jecxz
FLAGS-004 a UBSan build sweeping all helpers over valid widths has no undefined behavior
FLAGS-005 flags written by one call are read correctly by a later, separate call (no hidden per-call state)
FLAGS-006 shl/shr/sar with masked counts match Unicorn
FLAGS-007 two- and three-operand imul match Unicorn for CF/OF and result

test_fpu.py

Lifts x87 snippets with FPUMixin and compares them with Unicorn and an x86_64 binary run under Rosetta. Described on x87 Floating Point.

None of the three Python tests is invoked by tools/run_source_checks.py; they need unicorn, a C compiler, and (for test_decode) the executable.

Related pages

Clone this wiki locally