From 1b07d76c039ce347b87f02a8c960da2a7e8661df Mon Sep 17 00:00:00 2001 From: Xusheng Date: Mon, 13 Jul 2026 12:11:05 -0400 Subject: [PATCH 01/29] Add the BNIL (LLIL) emulator plugin Add an experimental plugin under plugins/emulator that emulates Binary Ninja's Low Level IL with full register, flag, and memory state, structured like the debugger: a core engine (emulatorcore) exposing a C ABI, plus C++ (emulatorapi) and Python bindings. Supports cross-function emulation, breakpoints, arguments, built-in libc stubs, user hooks (call/syscall/memory/pre-instruction/intrinsic/ stdio), and JSON state serialization. Includes a self-contained Python unittest suite (plugins/emulator/test), a user guide (docs/guide/emulator.md) wired into the mkdocs nav, and the intx vendor library for wide register values. Co-Authored-By: Claude Opus 4.8 (1M context) --- binaryninjaapi.h | 18 + binaryninjacore.h | 1 + docs/guide/emulator.md | 353 ++ mkdocs.yml | 1 + plugins/emulator/.gitignore | 28 + plugins/emulator/CMakeLists.txt | 17 + plugins/emulator/README.md | 95 + plugins/emulator/api/CMakeLists.txt | 25 + plugins/emulator/api/emulatorapi.h | 155 + plugins/emulator/api/ffi.h | 194 + plugins/emulator/api/ilemulator.cpp | 552 +++ plugins/emulator/api/python/CMakeLists.txt | 51 + plugins/emulator/api/python/__init__.py | 30 + .../api/python/_emulatorcore_template.py | 58 + plugins/emulator/api/python/generator.cpp | 614 +++ plugins/emulator/api/python/ilemulator.py | 626 +++ plugins/emulator/core/CMakeLists.txt | 42 + plugins/emulator/core/ffi_global.h | 44 + plugins/emulator/core/ilemulator.cpp | 821 ++++ plugins/emulator/core/ilemulator.h | 157 + plugins/emulator/core/llilemulator.cpp | 3642 +++++++++++++++++ plugins/emulator/core/llilemulator.h | 189 + plugins/emulator/core/plugin.cpp | 54 + plugins/emulator/core/refcountobject.h | 70 + plugins/emulator/test/emulator_test.py | 357 ++ python/scriptingprovider.py | 8 + vendor/intx/intx.hpp | 1822 +++++++++ 27 files changed, 10024 insertions(+) create mode 100644 docs/guide/emulator.md create mode 100644 plugins/emulator/.gitignore create mode 100644 plugins/emulator/CMakeLists.txt create mode 100644 plugins/emulator/README.md create mode 100644 plugins/emulator/api/CMakeLists.txt create mode 100644 plugins/emulator/api/emulatorapi.h create mode 100644 plugins/emulator/api/ffi.h create mode 100644 plugins/emulator/api/ilemulator.cpp create mode 100644 plugins/emulator/api/python/CMakeLists.txt create mode 100644 plugins/emulator/api/python/__init__.py create mode 100644 plugins/emulator/api/python/_emulatorcore_template.py create mode 100644 plugins/emulator/api/python/generator.cpp create mode 100644 plugins/emulator/api/python/ilemulator.py create mode 100644 plugins/emulator/core/CMakeLists.txt create mode 100644 plugins/emulator/core/ffi_global.h create mode 100644 plugins/emulator/core/ilemulator.cpp create mode 100644 plugins/emulator/core/ilemulator.h create mode 100644 plugins/emulator/core/llilemulator.cpp create mode 100644 plugins/emulator/core/llilemulator.h create mode 100644 plugins/emulator/core/plugin.cpp create mode 100644 plugins/emulator/core/refcountobject.h create mode 100644 plugins/emulator/test/emulator_test.py create mode 100644 vendor/intx/intx.hpp diff --git a/binaryninjaapi.h b/binaryninjaapi.h index d6470686e3..e83a4c3803 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -31,6 +31,23 @@ #include "binaryninjacore.h" #include "exceptions.h" +// intx provides the wide-integer type used by the emulator plugin's API. If a +// translation unit includes before this header (as several debugger +// adapters do), its min/max macros would break intx's numeric_limits<>::min()/max() +// member definitions. Neutralize those macros just for the intx include, then +// restore them so any later code that relies on windows.h min/max is unaffected. +#if defined(_WIN32) + #pragma push_macro("min") + #pragma push_macro("max") + #undef min + #undef max +#endif +#include "vendor/intx/intx.hpp" +#if defined(_WIN32) + #pragma pop_macro("min") + #pragma pop_macro("max") +#endif + #include "json/json.h" #include "rapidjsonwrapper.h" #include "vendor/nlohmann/json.hpp" @@ -23337,6 +23354,7 @@ namespace BinaryNinja { std::optional RecognizeConstantData( const HighLevelILInstruction& instr) override; }; + } // namespace BinaryNinja diff --git a/binaryninjacore.h b/binaryninjacore.h index 26a5b85fa0..4cbb0e8c18 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -9427,6 +9427,7 @@ extern "C" BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size); BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size); + #ifdef __cplusplus } #endif diff --git a/docs/guide/emulator.md b/docs/guide/emulator.md new file mode 100644 index 0000000000..fb1711612a --- /dev/null +++ b/docs/guide/emulator.md @@ -0,0 +1,353 @@ +# BNIL Emulator — Python API Guide + +The emulator plugin executes Binary Ninja's Low Level IL (LLIL) with full register, +flag, and memory state. It is aimed at focused, snippet-level tasks — decrypting +strings, resolving API hashes, evaluating a slice of a function — rather than +full-program or whole-system emulation. See the +[plugin README](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/README.md) +for the scope, accuracy notes, and the list of supported LLIL instructions. + +> **Experimental.** The API and behavior may change. + +Today the emulator works on **LLIL**; we intend to expand it to **MLIL** and **HLIL** +as well. + +## Contents + +- [Getting the class](#getting-the-class) +- [Creating an emulator](#creating-an-emulator) +- [Setting the entry point](#setting-the-entry-point) +- [Running and stepping](#running-and-stepping) +- [Stop reasons](#stop-reasons) +- [Registers, flags, and temporaries](#registers-flags-and-temporaries) +- [Memory](#memory) +- [Function arguments](#function-arguments) +- [Breakpoints](#breakpoints) +- [Hooks](#hooks) +- [Built-in libc stubs](#built-in-libc-stubs) +- [Call stack](#call-stack) +- [State serialization](#state-serialization) +- [Full example: cross-function emulation](#full-example-cross-function-emulation) +- [API reference](#api-reference) + +## Getting the class + +The emulator is a normal importable class — there is no auto-injected console +variable. In the Python console or a headless script: + +```python +from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason +``` + +In the interactive console, `bv` (current view) and `here` (current address) are +already available as built-in magic variables, so the two lines below are all you +need to get going: + +```python +emu = LLILEmulator(bv) +emu.set_entry_point(here) +``` + +If you use the emulator often and want `LLILEmulator` available without importing +each session, add the import to `~/.binaryninja/startup.py`. + +## Creating an emulator + +`LLILEmulator` can be constructed three ways: + +```python +# 1. For a whole view — resolve addresses to functions on demand (most common). +emu = LLILEmulator(bv) + +# 2. For a specific LLIL function. +emu = LLILEmulator(bv, il=func.llil) + +# 3. Wrap an existing core handle (advanced / internal). +emu = LLILEmulator(bv, handle=raw_handle) +``` + +A view can have as many independent emulators as you like; each keeps its own +registers, memory, and breakpoints. + +## Setting the entry point + +`set_entry_point` accepts two forms: + +```python +# Address form: resolve an address to its function and start at its first LLIL +# instruction. Returns False if the address is not inside an analyzed function. +if not emu.set_entry_point(0x401000): + raise ValueError("address is not in an analyzed function") + +# IL form: start at a specific LLIL instruction index of a given LLIL function. +emu.set_entry_point(func.llil, 5) +``` + +## Running and stepping + +```python +emu.set_max_instructions(100000) # safety limit against runaway loops +reason = emu.run() # run until a stop condition + +emu.step() # execute a single instruction +emu.step_n(10) # execute up to 10 instructions +emu.step_over() # step over a call (run through the callee) + +emu.request_stop() # ask a running emulator to stop (thread-safe) +``` + +Progress and position: + +```python +emu.instructions_executed # count since the last reset +emu.current_address # address of the current instruction +emu.instruction_index # LLIL index within the current function (settable) +``` + +## Stop reasons + +`run`, `step`, `step_n`, and `step_over` all return an `ILEmulatorStopReason`, also +available afterward via `emu.stop_reason` with a human-readable `emu.stop_message`: + +| Reason | Meaning | +| --- | --- | +| `ILEmulatorRunning` | Still running (not a terminal state) | +| `ILEmulatorBreakpoint` | Hit a breakpoint | +| `ILEmulatorInstructionLimit` | Reached `set_max_instructions` | +| `ILEmulatorHalt` | Returned from the top-level function / halted normally | +| `ILEmulatorError` | Internal error | +| `ILEmulatorCallHook` | Stopped by a call hook | +| `ILEmulatorSyscallHook` | Stopped by a syscall hook | +| `ILEmulatorUndefinedBehavior` | Executed undefined behavior | +| `ILEmulatorUnimplemented` | Hit an unimplemented LLIL instruction | +| `ILEmulatorUserRequestedStop` | Stopped via `request_stop` | + +```python +reason = emu.run() +if reason != ILEmulatorStopReason.ILEmulatorHalt: + print(f"stopped early: {reason.name} — {emu.stop_message}") +``` + +## Registers, flags, and temporaries + +Registers accept either a name (`'rax'`) or a numeric register ID: + +```python +emu.set_register('rsp', 0x7fff0000) +rax = emu.get_register('rax') + +emu.regs # snapshot dict of every named register -> value + +emu.set_flag('z', 1) # flag by name or ID +emu.get_flag('z') + +emu.set_temp_register(0, 0x1234) # LLIL temporary registers, by index +emu.get_temp_register(0) +``` + +## Memory + +> **The emulator does not inherit memory from the BinaryView.** It starts with an +> empty address space of its own. Execution works because the emulator runs on the +> lifted LLIL, not by fetching bytes from its memory — but any data the code *reads* +> (globals, `.rodata`, strings, tables, the stack) is **not** present unless you put +> it there. Reading an address that holds data in the view returns zeroes in the +> emulator. If you want the view's bytes, copy them in explicitly. + +Map regions before accessing them, then read and write raw bytes: + +```python +emu.map_memory(0x1000, b'\x00' * 0x1000) # map with data +emu.map_memory(0x2000, 0x1000) # map zero-filled +emu.map_memory(0x3000, 0x1000, "stack") # map a named region + +emu.write_memory(0x1000, b'hello') # returns bytes written +emu.read_memory(0x1000, 5) # -> b'hello' + +emu.get_mapped_regions() # [{'start':..., 'size':..., 'name':...}, ...] +``` + +### Copying BinaryView memory into the emulator + +To emulate code that reads existing program data, copy the relevant bytes from the +view into the emulator at the same addresses. Copy whole segments: + +```python +for seg in bv.segments: + data = bv.read(seg.start, seg.length) # bytes actually backed by the file + if data: + emu.map_memory(seg.start, data) +``` + +...or just the region you need (cheaper for large binaries): + +```python +emu.map_memory(table_addr, bv.read(table_addr, table_size)) +``` + +Alternatively, serve reads on demand with a memory-read hook that pulls from the view +(see [Hooks](#hooks)): + +```python +emu.set_memory_read_hook( + lambda emu, addr, size: int.from_bytes(bv.read(addr, size), 'little') + if bv.read(addr, size) else None) +``` + +## Function arguments + +Arguments are placed using the function's default calling convention: + +```python +emu.set_argument(0, 0x1000) # a single argument by index +emu.set_arguments([0x1000, 16, 42]) # several at once +``` + +## Breakpoints + +```python +emu.add_breakpoint(0x401234) # stops *before* executing that address +emu.remove_breakpoint(0x401234) +emu.clear_breakpoints() +``` + +A breakpoint stops the emulator before the target instruction executes, so on stop +`emu.current_address` equals the breakpoint address and its side effects have not yet +occurred. + +## Hooks + +Hooks let embedding code intercept emulation. Pass a callable to install a hook and +`None` to remove it. Exceptions raised inside a hook are swallowed and treated as +"not handled". + +```python +# CALL: return True if handled (advance past the call), False to let the emulator +# try cross-function emulation. +emu.set_call_hook(lambda emu, target: True) # skip all calls + +# SYSCALL: return True if handled, False to stop. +emu.set_syscall_hook(lambda emu: True) + +# Memory read: return the value to use, or None to fall through to real memory. +emu.set_memory_read_hook(lambda emu, addr, size: 0 if addr in mmio else None) + +# Memory write: return True if handled, False to let the write proceed. +emu.set_memory_write_hook(lambda emu, addr, size, value: False) + +# Before each instruction: return True to continue, False to stop. +emu.set_pre_instruction_hook(lambda emu, index: True) + +# INTRINSIC: return a list of (register_id, value) pairs if handled, else None. +emu.set_intrinsic_hook(lambda emu, intrinsic, params: None) + +# stdout from emulated printf/puts/putchar; data is bytes. +emu.set_stdout_callback(lambda emu, data: sys.stdout.write(data.decode('latin1'))) + +# stdin for emulated getchar/fgets/fread; return up to max_len bytes, b'' for EOF. +emu.set_stdin_callback(lambda emu, max_len: b'') +``` + +The memory-read hook fires on **every** load, including implicit reads such as the +stack pop performed by a `ret`, so filter by address when you only want to intercept +specific regions. + +## Built-in libc stubs + +The emulator ships simple stubs for common libc functions so snippets that call +`printf`, `malloc`, etc. can run without a real libc: + +```python +emu.builtin_libc_stubs # bool, default True — enable the built-in stubs +emu.log_libc_calls # bool, default True — log stub calls to the console +emu.nop_unknown_externals # bool, default False — treat unknown external calls + # as no-ops returning 0 instead of stopping +``` + +## Call stack + +While stopped inside a called function: + +```python +emu.call_stack_depth # number of nested calls +emu.get_call_stack() # [{'function_address':..., 'return_address':...}, ...] +``` + +Frame 0 is the current function; later frames are its callers. + +## State serialization + +Emulator state (registers, flags, memory, call stack) can be snapshotted to JSON and +restored — useful for save/restore points or reproducing a state across runs: + +```python +snapshot = emu.save_state() # -> JSON string +emu.load_state(snapshot) # restore, returns True on success + +emu.save_state_to_file("state.json") +emu.load_state_from_file("state.json") + +emu.reset() # clear all state back to initial +``` + +## Full example: cross-function emulation + +Decrypt a string by emulating a decryption routine, letting the emulator run through +the called functions and skipping anything it can't resolve: + +```python +from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason + +emu = LLILEmulator(bv) +emu.nop_unknown_externals = True # don't stop on unresolved externals +emu.set_max_instructions(1_000_000) + +# Give the routine a scratch output buffer and the encrypted input. +emu.map_memory(0x100000, 0x1000, "out") +emu.map_memory(0x101000, encrypted, "in") + +emu.set_entry_point(decrypt_func.start) +emu.set_arguments([0x100000, 0x101000, len(encrypted)]) + +reason = emu.run() +if reason == ILEmulatorStopReason.ILEmulatorHalt: + print(emu.read_memory(0x100000, 0x100).split(b'\x00', 1)[0]) +else: + print(f"stopped: {reason.name} — {emu.stop_message}") +``` + +## API reference + +Everything is on the `LLILEmulator` class. + +**Construction:** `LLILEmulator(view, il=None, handle=None)` + +**Execution:** `run`, `step`, `step_n`, `step_over`, `request_stop`, +`set_max_instructions`, `reset` + +**Entry / arguments:** `set_entry_point`, `set_argument`, `set_arguments` + +**State (properties):** `instruction_index`, `current_address`, `stop_reason`, +`stop_message`, `instructions_executed`, `call_stack_depth`, `regs` + +**Registers / flags:** `get_register`, `set_register`, `get_temp_register`, +`set_temp_register`, `get_flag`, `set_flag` + +**Memory:** `map_memory`, `read_memory`, `write_memory`, `get_mapped_regions` + +**Breakpoints:** `add_breakpoint`, `remove_breakpoint`, `clear_breakpoints` + +**Hooks:** `set_call_hook`, `set_syscall_hook`, `set_memory_read_hook`, +`set_memory_write_hook`, `set_pre_instruction_hook`, `set_intrinsic_hook`, +`set_stdout_callback`, `set_stdin_callback` + +**libc stubs (properties):** `builtin_libc_stubs`, `log_libc_calls`, +`nop_unknown_externals` + +**Call stack:** `get_call_stack` + +**Serialization:** `save_state`, `load_state`, `save_state_to_file`, +`load_state_from_file` + +For runnable, self-contained examples of every feature above, see the +[test suite](https://github.com/Vector35/binaryninja-api/blob/dev/plugins/emulator/test/emulator_test.py). diff --git a/mkdocs.yml b/mkdocs.yml index e67867db8e..2f8013d3f2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -103,6 +103,7 @@ nav: - WARP: 'guide/warp.md' - BinExport / BinDiff: 'guide/binexport.md' - Guided Analysis: 'guide/guided_analysis.md' + - BNIL Emulator: 'guide/emulator.md' - Debugger: - Overview: 'guide/debugger/index.md' - Remote Debugging: 'guide/debugger/remote-debugging.md' diff --git a/plugins/emulator/.gitignore b/plugins/emulator/.gitignore new file mode 100644 index 0000000000..9d48c1925d --- /dev/null +++ b/plugins/emulator/.gitignore @@ -0,0 +1,28 @@ +# Build output +/build +/artifacts +cmake-build-*/ +out/ + +# Compiled objects / libraries +*.o +*.obj +*.so +*.dylib +*.dll +*.a +*.lib +*.pdb +*.ilk + +# Generated Python bindings +/api/python/_emulatorcore.py +/api/python/emulator_enums.py +*/__pycache__/ +*.pyc + +# IDE / OS +.idea/ +.vscode/ +.DS_Store +/.claude/ diff --git a/plugins/emulator/CMakeLists.txt b/plugins/emulator/CMakeLists.txt new file mode 100644 index 0000000000..033664bb77 --- /dev/null +++ b/plugins/emulator/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) + +project(bnil-emulator) + +if(NOT BN_INTERNAL_BUILD) + # Out-of-tree build: locate the Binary Ninja API source. When this plugin is checked + # out inside the api repo (api/plugins/emulator), the api root is two levels up. + find_path( + BN_API_PATH + NAMES binaryninjaapi.h + HINTS ../.. binaryninjaapi $ENV{BN_API_PATH} + REQUIRED + ) +endif() + +add_subdirectory(core) +add_subdirectory(api) diff --git a/plugins/emulator/README.md b/plugins/emulator/README.md new file mode 100644 index 0000000000..c3cc90b4ab --- /dev/null +++ b/plugins/emulator/README.md @@ -0,0 +1,95 @@ +# BNIL Emulator + +A standalone Binary Ninja plugin that emulates Binary Ninja's Low Level Intermediate +Language (LLIL). It is structured like the [debugger](https://github.com/Vector35/debugger): +the engine builds into its own plugin (`emulatorcore`) against the public Binary Ninja +API, exposes a C ABI, and ships C++ (`emulatorapi`) and Python bindings. + +> **Experimental.** This plugin is experimental and under active development; its API and +> behavior may change. + +The [Python API guide](../../docs/guide/emulator.md) walks through the `LLILEmulator` +class with examples; runnable tests live in [`test/emulator_test.py`](test/emulator_test.py). + +## Scope & accuracy + +This plugin emulates the given BNIL instructions — it is **not** meant to match the accuracy +of full CPU emulators like [Unicorn](https://www.unicorn-engine.org/) or +[QEMU](https://www.qemu.org/). Because emulation runs on Binary Ninja's *lifted* IL rather +than the raw machine instructions, the emulated state may deviate from the actual state of +the program during real execution. + +In practice it is aimed at focused tasks — decrypting strings, resolving API hashes, and +similar snippet-level emulation. It is **not** intended for full-program or whole-system +emulation. + +## Layout + +- `core/` — the emulator engine (`ilemulator`, `llilemulator`) and plugin entry point, + built as the `emulatorcore` plugin. Exposes the emulator C ABI (`api/ffi.h`). +- `api/` — C++ wrapper (`emulatorapi`, `BinaryNinja::LLILEmulator`) over the C ABI, plus + Python bindings under `api/python/`. + +## Building + +```sh +export BN_API_PATH=/path/to/binaryninja-api +cmake -S . -B build # -GNinja optional +cmake --build build +``` + +The resulting `emulatorcore` plugin is written to `build/out/plugins/`. + +## Testing + +The Python test suite is self-contained — it assembles tiny `BinaryView`s from raw +machine code rather than shipping test binaries — so it runs headless against any +Binary Ninja install that has the emulator plugin: + +```sh +PYTHONPATH=/python python3 -m pytest test/emulator_test.py +# or, without pytest: +PYTHONPATH=/python python3 test/emulator_test.py +``` + +Inside the Binary Ninja source tree these same tests also run as part of the main +Binary Ninja test suite (`tests/python/test_emulator.py` loads this module), so +`cd tests && pytest python` exercises them alongside everything else. + +## Support status + +Only **LLIL** emulation is supported today. **MLIL and HLIL emulation are planned** for the +future. + +Emulating an unsupported instruction stops the emulator with an `Unimplemented` stop reason. + +### Supported LLIL instructions + +- **Constants:** `CONST`, `CONST_PTR`, `EXTERN_PTR`, `FLOAT_CONST` +- **Registers:** `REG`, `SET_REG`, `REG_SPLIT`, `SET_REG_SPLIT`, `LOW_PART` +- **Memory:** `LOAD`, `STORE`, `PUSH`, `POP` +- **Arithmetic:** `ADD`, `ADC`, `SUB`, `SBB`, `MUL`, `MULU_DP`, `MULS_DP`, `DIVU`, `DIVS`, + `DIVU_DP`, `DIVS_DP`, `MODU`, `MODS`, `MODU_DP`, `MODS_DP`, `NEG`, `ADD_OVERFLOW` +- **Bitwise / shifts:** `AND`, `OR`, `XOR`, `NOT`, `LSL`, `LSR`, `ASR`, `ROL`, `ROR`, `RLC`, + `RRC`, `SX`, `ZX`, `LOW_PART`, `TEST_BIT`, `BOOL_TO_INT` +- **Comparisons:** `CMP_E`, `CMP_NE`, `CMP_SLT`, `CMP_SLE`, `CMP_SGE`, `CMP_SGT`, `CMP_ULT`, + `CMP_ULE`, `CMP_UGE`, `CMP_UGT` +- **Flags:** `FLAG`, `SET_FLAG`, `FLAG_BIT`, `FLAG_COND`, `FLAG_GROUP` +- **Control flow:** `JUMP`, `JUMP_TO`, `GOTO`, `IF`, `CALL`, `CALL_STACK_ADJUST`, `TAILCALL`, + `RET`, `NORET` +- **Other:** `NOP` + +### Not yet supported + +- **Floating point:** `FADD`, `FSUB`, `FMUL`, `FDIV`, `FSQRT`, `FABS`, `FNEG`, `FCMP_*`, + `FLOAT_CONV`, `FLOAT_TO_INT`, `INT_TO_FLOAT`, `ROUND_TO_INT`, `CEIL`, `FLOOR`, `FTRUNC` + (float *constants* are read, but float arithmetic is not evaluated) +- **Register stacks (x87/FPU-style):** `REG_STACK_REL`, `SET_REG_STACK_REL`, `REG_STACK_PUSH`, + `REG_STACK_POP`, `REG_STACK_FREE_REG`, `REG_STACK_FREE_REL` +- **Bit operations:** `BSWAP`, `CLZ`, `CTZ`, `CLS`, `POPCNT`, `RBIT`, `ABS`, `MINS`, `MAXS`, + `MINU`, `MAXU` +- **System / hooks** (no built-in semantics — stop unless the embedding code registers a + hook): `SYSCALL`, `INTRINSIC` +- **Halting / non-representable** (stop the emulator): `BP`, `TRAP`, `UNDEF`, `UNIMPL`, + `UNIMPL_MEM` +- **Other:** `ASSERT`, `FORCE_VER`, `CALL_PARAM` diff --git a/plugins/emulator/api/CMakeLists.txt b/plugins/emulator/api/CMakeLists.txt new file mode 100644 index 0000000000..e262f54310 --- /dev/null +++ b/plugins/emulator/api/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) + +project(emulatorapi) + +file(GLOB BN_EMULATOR_API_SOURCES CONFIGURE_DEPENDS *.cpp *.h) +add_library(emulatorapi STATIC ${BN_EMULATOR_API_SOURCES}) + +target_include_directories(emulatorapi + PUBLIC ${PROJECT_SOURCE_DIR}) + +target_link_libraries(emulatorapi PUBLIC emulatorcore) + +set_target_properties(emulatorapi PROPERTIES + CXX_STANDARD 20 + CXX_VISIBILITY_PRESET hidden + CXX_STANDARD_REQUIRED ON + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/out) + +# Python bindings retarget (load the emulatorcore plugin dylib) is pending; wire the +# subdirectory only once api/python/CMakeLists.txt exists. +if (NOT DEMO AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/python/CMakeLists.txt) + add_subdirectory(python) +endif() diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h new file mode 100644 index 0000000000..352dd27fad --- /dev/null +++ b/plugins/emulator/api/emulatorapi.h @@ -0,0 +1,155 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include "binaryninjaapi.h" +#include "ffi.h" +#include +#include +#include +#include +#include + +namespace BinaryNinja +{ + /*! + \ingroup emulator + */ + class LLILEmulator : + public CoreRefCountObject + { + // Stored hooks (prevent dangling captures) + std::function m_callHook; + std::function m_syscallHook; + std::function m_memoryReadHook; + std::function m_memoryWriteHook; + std::function m_preInstructionHook; + std::function&, + std::vector>&)> m_intrinsicHook; + std::function m_stdoutCallback; + std::function m_stdinCallback; + + // Static C bridge callbacks + static bool CallHookCallback(void* ctxt, BNILEmulator* emu, uint64_t target); + static bool SyscallHookCallback(void* ctxt, BNILEmulator* emu); + static bool MemoryReadHookCallback(void* ctxt, BNILEmulator* emu, + uint64_t addr, size_t size, uint8_t* outBuf, size_t bufLen); + static bool MemoryWriteHookCallback(void* ctxt, BNILEmulator* emu, + uint64_t addr, size_t size, const uint8_t* buf, size_t bufLen); + static bool PreInstructionHookCallback(void* ctxt, BNILEmulator* emu, size_t instrIndex); + static bool IntrinsicHookCallback(void* ctxt, BNLLILEmulator* emu, + uint32_t intrinsic, const uint64_t* params, size_t paramCount, + uint64_t* outValues, uint32_t* outRegs, size_t* outCount); + static void StdoutCallbackBridge(void* ctxt, BNILEmulator* emu, const char* data, size_t len); + static size_t StdinCallbackBridge(void* ctxt, BNILEmulator* emu, char* buf, size_t maxLen); + + public: + LLILEmulator(Ref view); + LLILEmulator(Ref il, Ref view); + LLILEmulator(BNLLILEmulator* emu); + + bool SetEntryPoint(uint64_t addr); + void SetEntryPoint(Ref il, size_t instrIndex); + + // Argument setup (uses default calling convention) + void SetArgument(size_t index, const intx::uint512& value); + void SetArguments(const std::vector& values); + + // Execution + BNILEmulatorStopReason Run(); + BNILEmulatorStopReason Step(); + BNILEmulatorStopReason StepN(size_t n); + BNILEmulatorStopReason StepOver(); + void RequestStop(); + + // State + size_t GetInstructionIndex() const; + void SetInstructionIndex(size_t index); + uint64_t GetCurrentAddress() const; + BNILEmulatorStopReason GetStopReason() const; + std::string GetStopMessage() const; + + // Memory + size_t ReadMemory(void* dest, uint64_t addr, size_t len) const; + size_t WriteMemory(uint64_t addr, const void* src, size_t len); + void MapMemory(uint64_t addr, const void* data, size_t len, const std::string& name = ""); + void MapMemory(uint64_t addr, size_t len, const std::string& name = ""); + + // Breakpoints (by address) + void AddBreakpoint(uint64_t addr); + void RemoveBreakpoint(uint64_t addr); + void ClearBreakpoints(); + + // Limits + void SetMaxInstructions(size_t max); + size_t GetInstructionsExecuted() const; + + // Hooks + void SetCallHook(const std::function& hook); + void SetSyscallHook(const std::function& hook); + void SetMemoryReadHook(const std::function& hook); + void SetMemoryWriteHook(const std::function& hook); + void SetPreInstructionHook(const std::function& hook); + void SetIntrinsicHook(const std::function&, std::vector>&)>& hook); + void SetStdoutCallback(const std::function& cb); + void SetStdinCallback(const std::function& cb); + + // Register / flag / temp access + intx::uint512 GetRegister(uint32_t reg) const; + void SetRegister(uint32_t reg, const intx::uint512& value); + intx::uint512 GetTempRegister(uint32_t index) const; + void SetTempRegister(uint32_t index, const intx::uint512& value); + std::unordered_map GetAllTempRegisters() const; + uint8_t GetFlag(uint32_t flag) const; + void SetFlag(uint32_t flag, uint8_t value); + + // Cross-function state + size_t GetCallStackDepth() const; + + struct CallStackEntry + { + uint64_t functionAddress; + uint64_t returnAddress; + }; + std::vector GetCallStack() const; + + // Memory regions + struct MappedRegion + { + uint64_t start; + uint64_t size; + std::string name; + }; + std::vector GetMappedRegions() const; + + // Built-in libc stub settings + void SetBuiltinLibcStubsEnabled(bool enabled); + bool IsBuiltinLibcStubsEnabled() const; + void SetLogLibcCalls(bool enabled); + bool IsLogLibcCalls() const; + void SetNopUnknownExternals(bool enabled); + bool IsNopUnknownExternals() const; + + // Reset + void Reset(); + + // State serialization + std::string SaveState() const; + bool LoadState(const std::string& json); + }; +} // namespace BinaryNinja diff --git a/plugins/emulator/api/ffi.h b/plugins/emulator/api/ffi.h new file mode 100644 index 0000000000..5603e0b31a --- /dev/null +++ b/plugins/emulator/api/ffi.h @@ -0,0 +1,194 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +// The Binary Ninja type parser (used to generate the Python bindings) defines +// BN_TYPE_PARSER and supplies its own fixed-width integer types, so skip the system +// headers in that mode to avoid depending on the parser's clang include search paths. +#ifndef BN_TYPE_PARSER +#ifdef __cplusplus +#include +#include +#include +#else +#include +#include +#include +#include +#endif +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +#ifdef __GNUC__ + #ifdef EMULATOR_LIBRARY + #define EMULATOR_FFI_API __attribute__((visibility("default"))) + #else // EMULATOR_LIBRARY + #define EMULATOR_FFI_API + #endif // EMULATOR_LIBRARY +#else // __GNUC__ + #ifdef _MSC_VER + #ifdef EMULATOR_LIBRARY + #define EMULATOR_FFI_API __declspec(dllexport) + #else // EMULATOR_LIBRARY + #define EMULATOR_FFI_API __declspec(dllimport) + #endif // EMULATOR_LIBRARY + #else // _MSC_VER + #define EMULATOR_FFI_API + #endif // _MSC_VER +#endif // __GNUC__ + + // Opaque handles owned by this plugin + typedef struct BNILEmulator BNILEmulator; + typedef struct BNLLILEmulator BNLLILEmulator; + + // Binary Ninja core handles used by the emulator API (defined by binaryninjacore.h) + typedef struct BNBinaryView BNBinaryView; + typedef struct BNLowLevelILFunction BNLowLevelILFunction; + + enum BNILEmulatorStopReason + { + ILEmulatorRunning = 0, + ILEmulatorBreakpoint, + ILEmulatorInstructionLimit, + ILEmulatorHalt, + ILEmulatorError, + ILEmulatorCallHook, + ILEmulatorSyscallHook, + ILEmulatorUndefinedBehavior, + ILEmulatorUnimplemented, + ILEmulatorUserRequestedStop + }; + + struct BNEmulatorMemoryRegion + { + uint64_t start; + uint64_t size; + char* name; + }; + + struct BNEmulatorCallStackEntry + { + uint64_t functionAddress; + uint64_t returnAddress; + }; + + // IL Emulator — creation/lifecycle + EMULATOR_FFI_API BNLLILEmulator* BNCreateLLILEmulatorForView(BNBinaryView* view); + EMULATOR_FFI_API BNLLILEmulator* BNCreateLLILEmulator(BNLowLevelILFunction* il, BNBinaryView* view); + EMULATOR_FFI_API BNLLILEmulator* BNNewLLILEmulatorReference(BNLLILEmulator* emu); + EMULATOR_FFI_API void BNFreeLLILEmulator(BNLLILEmulator* emu); + EMULATOR_FFI_API BNILEmulator* BNLLILEmulatorGetBase(BNLLILEmulator* emu); + EMULATOR_FFI_API bool BNLLILEmulatorSetEntryPoint(BNLLILEmulator* emu, uint64_t addr); + EMULATOR_FFI_API void BNLLILEmulatorSetEntryPointForIL(BNLLILEmulator* emu, + BNLowLevelILFunction* il, size_t instrIndex); + EMULATOR_FFI_API void BNLLILEmulatorSetArgument(BNLLILEmulator* emu, size_t index, const uint8_t* buf, size_t bufLen); + EMULATOR_FFI_API void BNLLILEmulatorSetArguments(BNLLILEmulator* emu, const uint64_t* values, size_t count); + + // IL Emulator — execution control (shared) + EMULATOR_FFI_API BNILEmulatorStopReason BNILEmulatorRun(BNILEmulator* emu); + EMULATOR_FFI_API BNILEmulatorStopReason BNILEmulatorStep(BNILEmulator* emu); + EMULATOR_FFI_API BNILEmulatorStopReason BNILEmulatorStepN(BNILEmulator* emu, size_t n); + + // IL Emulator — state (shared) + EMULATOR_FFI_API size_t BNILEmulatorGetInstructionIndex(BNILEmulator* emu); + EMULATOR_FFI_API void BNILEmulatorSetInstructionIndex(BNILEmulator* emu, size_t index); + EMULATOR_FFI_API uint64_t BNILEmulatorGetCurrentAddress(BNILEmulator* emu); + EMULATOR_FFI_API BNILEmulatorStopReason BNILEmulatorGetStopReason(BNILEmulator* emu); + EMULATOR_FFI_API char* BNILEmulatorGetStopMessage(BNILEmulator* emu); + + // IL Emulator — memory (shared) + EMULATOR_FFI_API size_t BNILEmulatorReadMemory(BNILEmulator* emu, void* dest, uint64_t addr, size_t len); + EMULATOR_FFI_API size_t BNILEmulatorWriteMemory(BNILEmulator* emu, uint64_t addr, const void* src, size_t len); + EMULATOR_FFI_API void BNILEmulatorMapMemory(BNILEmulator* emu, uint64_t addr, const void* data, size_t len); + EMULATOR_FFI_API void BNILEmulatorMapMemoryZero(BNILEmulator* emu, uint64_t addr, size_t len); + EMULATOR_FFI_API void BNILEmulatorMapMemoryNamed(BNILEmulator* emu, uint64_t addr, const void* data, size_t len, const char* name); + EMULATOR_FFI_API void BNILEmulatorMapMemoryZeroNamed(BNILEmulator* emu, uint64_t addr, size_t len, const char* name); + + // IL Emulator — breakpoints (shared, by address) + EMULATOR_FFI_API void BNILEmulatorAddBreakpoint(BNILEmulator* emu, uint64_t addr); + EMULATOR_FFI_API void BNILEmulatorRemoveBreakpoint(BNILEmulator* emu, uint64_t addr); + EMULATOR_FFI_API void BNILEmulatorClearBreakpoints(BNILEmulator* emu); + + // IL Emulator — limits (shared) + EMULATOR_FFI_API void BNILEmulatorSetMaxInstructions(BNILEmulator* emu, size_t max); + EMULATOR_FFI_API size_t BNILEmulatorGetInstructionsExecuted(BNILEmulator* emu); + + // IL Emulator — hooks (shared) + EMULATOR_FFI_API void BNILEmulatorSetCallHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNILEmulator* emu, uint64_t target)); + EMULATOR_FFI_API void BNILEmulatorSetSyscallHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNILEmulator* emu)); + EMULATOR_FFI_API void BNILEmulatorSetMemoryReadHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNILEmulator* emu, uint64_t addr, size_t size, uint8_t* outBuf, size_t bufLen)); + EMULATOR_FFI_API void BNILEmulatorSetMemoryWriteHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNILEmulator* emu, uint64_t addr, size_t size, const uint8_t* buf, size_t bufLen)); + EMULATOR_FFI_API void BNILEmulatorSetPreInstructionHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNILEmulator* emu, size_t instrIndex)); + EMULATOR_FFI_API void BNILEmulatorSetStdoutCallback(BNILEmulator* emu, void* ctxt, + void (*callback)(void* ctxt, BNILEmulator* emu, const char* data, size_t len)); + EMULATOR_FFI_API void BNILEmulatorSetStdinCallback(BNILEmulator* emu, void* ctxt, + size_t (*callback)(void* ctxt, BNILEmulator* emu, char* buf, size_t maxLen)); + EMULATOR_FFI_API void BNILEmulatorRequestStop(BNILEmulator* emu); + EMULATOR_FFI_API void BNILEmulatorReset(BNILEmulator* emu); + + // LLIL Emulator — register/flag access (byte-buffer API; values are 64-byte little-endian) + EMULATOR_FFI_API void BNLLILEmulatorGetRegister(BNLLILEmulator* emu, uint32_t reg, uint8_t* outBuf, size_t bufLen); + EMULATOR_FFI_API void BNLLILEmulatorSetRegister(BNLLILEmulator* emu, uint32_t reg, const uint8_t* buf, size_t bufLen); + EMULATOR_FFI_API void BNLLILEmulatorGetTempRegister(BNLLILEmulator* emu, uint32_t index, uint8_t* outBuf, size_t bufLen); + EMULATOR_FFI_API void BNLLILEmulatorSetTempRegister(BNLLILEmulator* emu, uint32_t index, const uint8_t* buf, size_t bufLen); + EMULATOR_FFI_API size_t BNLLILEmulatorGetAllTempRegisters( + BNLLILEmulator* emu, uint32_t* outIndices, uint8_t* outValues, size_t maxCount); + EMULATOR_FFI_API uint8_t BNLLILEmulatorGetFlag(BNLLILEmulator* emu, uint32_t flag); + EMULATOR_FFI_API void BNLLILEmulatorSetFlag(BNLLILEmulator* emu, uint32_t flag, uint8_t value); + + // LLIL Emulator — intrinsic hook + EMULATOR_FFI_API void BNLLILEmulatorSetIntrinsicHook(BNLLILEmulator* emu, void* ctxt, + bool (*callback)(void* ctxt, BNLLILEmulator* emu, uint32_t intrinsic, + const uint64_t* params, size_t paramCount, + uint64_t* outValues, uint32_t* outRegs, size_t* outCount)); + + // LLIL Emulator — call stack + EMULATOR_FFI_API size_t BNLLILEmulatorGetCallStackDepth(BNLLILEmulator* emu); + EMULATOR_FFI_API BNEmulatorCallStackEntry* BNLLILEmulatorGetCallStack(BNLLILEmulator* emu, size_t* count); + EMULATOR_FFI_API void BNLLILEmulatorFreeCallStack(BNEmulatorCallStackEntry* entries); + + // LLIL Emulator — stepping + EMULATOR_FFI_API BNILEmulatorStopReason BNLLILEmulatorStepOver(BNLLILEmulator* emu); + + // IL Emulator — memory regions + EMULATOR_FFI_API BNEmulatorMemoryRegion* BNILEmulatorGetMappedRegions(BNILEmulator* emu, size_t* count); + EMULATOR_FFI_API void BNFreeEmulatorMemoryRegions(BNEmulatorMemoryRegion* regions, size_t count); + + // LLIL Emulator — built-in libc stubs + EMULATOR_FFI_API void BNLLILEmulatorSetBuiltinLibcStubsEnabled(BNLLILEmulator* emu, bool enabled); + EMULATOR_FFI_API bool BNLLILEmulatorIsBuiltinLibcStubsEnabled(BNLLILEmulator* emu); + EMULATOR_FFI_API void BNLLILEmulatorSetLogLibcCalls(BNLLILEmulator* emu, bool enabled); + EMULATOR_FFI_API bool BNLLILEmulatorIsLogLibcCalls(BNLLILEmulator* emu); + EMULATOR_FFI_API void BNLLILEmulatorSetNopUnknownExternals(BNLLILEmulator* emu, bool enabled); + EMULATOR_FFI_API bool BNLLILEmulatorIsNopUnknownExternals(BNLLILEmulator* emu); + + // LLIL Emulator — state serialization + EMULATOR_FFI_API char* BNLLILEmulatorSaveState(BNLLILEmulator* emu); + EMULATOR_FFI_API bool BNLLILEmulatorLoadState(BNLLILEmulator* emu, const char* json); + +#ifdef __cplusplus +} +#endif diff --git a/plugins/emulator/api/ilemulator.cpp b/plugins/emulator/api/ilemulator.cpp new file mode 100644 index 0000000000..58ea709874 --- /dev/null +++ b/plugins/emulator/api/ilemulator.cpp @@ -0,0 +1,552 @@ +// Copyright (c) 2015-2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#include "binaryninjaapi.h" +#include "ffi.h" +#include "emulatorapi.h" + +using namespace BinaryNinja; + + +LLILEmulator::LLILEmulator(Ref view) +{ + m_object = BNCreateLLILEmulatorForView(view->GetObject()); +} + + +LLILEmulator::LLILEmulator(Ref il, Ref view) +{ + m_object = BNCreateLLILEmulator(il->GetObject(), view->GetObject()); +} + + +LLILEmulator::LLILEmulator(BNLLILEmulator* emu) +{ + m_object = emu; +} + + +bool LLILEmulator::SetEntryPoint(uint64_t addr) +{ + return BNLLILEmulatorSetEntryPoint(m_object, addr); +} + + +void LLILEmulator::SetEntryPoint(Ref il, size_t instrIndex) +{ + BNLLILEmulatorSetEntryPointForIL(m_object, il->GetObject(), instrIndex); +} + + +static void ApiUint512ToBytes(const intx::uint512& value, uint8_t* buf, size_t bufLen) +{ + intx::uint512 tmp = value; + size_t n = std::min(bufLen, (size_t)64); + for (size_t i = 0; i < n; i++) + { + buf[i] = static_cast(tmp); + tmp >>= 8; + } + for (size_t i = n; i < bufLen; i++) + buf[i] = 0; +} + + +static intx::uint512 ApiBytesToUint512(const uint8_t* buf, size_t bufLen) +{ + intx::uint512 result = 0; + size_t n = std::min(bufLen, (size_t)64); + for (size_t i = n; i > 0; i--) + result = (result << 8) | buf[i - 1]; + return result; +} + + +void LLILEmulator::SetArgument(size_t index, const intx::uint512& value) +{ + uint8_t buf[64]; + ApiUint512ToBytes(value, buf, sizeof(buf)); + BNLLILEmulatorSetArgument(m_object, index, buf, sizeof(buf)); +} + + +void LLILEmulator::SetArguments(const std::vector& values) +{ + BNLLILEmulatorSetArguments(m_object, values.data(), values.size()); +} + + +// ─── Execution ─────────────────────────────────────────────────────────────── + +BNILEmulatorStopReason LLILEmulator::Run() +{ + return BNILEmulatorRun(BNLLILEmulatorGetBase(m_object)); +} + + +BNILEmulatorStopReason LLILEmulator::Step() +{ + return BNILEmulatorStep(BNLLILEmulatorGetBase(m_object)); +} + + +BNILEmulatorStopReason LLILEmulator::StepN(size_t n) +{ + return BNILEmulatorStepN(BNLLILEmulatorGetBase(m_object), n); +} + + +BNILEmulatorStopReason LLILEmulator::StepOver() +{ + return BNLLILEmulatorStepOver(m_object); +} + + +void LLILEmulator::RequestStop() +{ + BNILEmulatorRequestStop(BNLLILEmulatorGetBase(m_object)); +} + + +// ─── State ─────────────────────────────────────────────────────────────────── + +size_t LLILEmulator::GetInstructionIndex() const +{ + return BNILEmulatorGetInstructionIndex(BNLLILEmulatorGetBase(m_object)); +} + + +void LLILEmulator::SetInstructionIndex(size_t index) +{ + BNILEmulatorSetInstructionIndex(BNLLILEmulatorGetBase(m_object), index); +} + + +uint64_t LLILEmulator::GetCurrentAddress() const +{ + return BNILEmulatorGetCurrentAddress(BNLLILEmulatorGetBase(m_object)); +} + + +BNILEmulatorStopReason LLILEmulator::GetStopReason() const +{ + return BNILEmulatorGetStopReason(BNLLILEmulatorGetBase(m_object)); +} + + +std::string LLILEmulator::GetStopMessage() const +{ + char* msg = BNILEmulatorGetStopMessage(BNLLILEmulatorGetBase(m_object)); + std::string result(msg); + BNFreeString(msg); + return result; +} + + +// ─── Memory ────────────────────────────────────────────────────────────────── + +size_t LLILEmulator::ReadMemory(void* dest, uint64_t addr, size_t len) const +{ + return BNILEmulatorReadMemory(BNLLILEmulatorGetBase(m_object), dest, addr, len); +} + + +size_t LLILEmulator::WriteMemory(uint64_t addr, const void* src, size_t len) +{ + return BNILEmulatorWriteMemory(BNLLILEmulatorGetBase(m_object), addr, src, len); +} + + +void LLILEmulator::MapMemory(uint64_t addr, const void* data, size_t len, const std::string& name) +{ + if (name.empty()) + BNILEmulatorMapMemory(BNLLILEmulatorGetBase(m_object), addr, data, len); + else + BNILEmulatorMapMemoryNamed(BNLLILEmulatorGetBase(m_object), addr, data, len, name.c_str()); +} + + +void LLILEmulator::MapMemory(uint64_t addr, size_t len, const std::string& name) +{ + if (name.empty()) + BNILEmulatorMapMemoryZero(BNLLILEmulatorGetBase(m_object), addr, len); + else + BNILEmulatorMapMemoryZeroNamed(BNLLILEmulatorGetBase(m_object), addr, len, name.c_str()); +} + + +// ─── Breakpoints ───────────────────────────────────────────────────────────── + +void LLILEmulator::AddBreakpoint(uint64_t addr) +{ + BNILEmulatorAddBreakpoint(BNLLILEmulatorGetBase(m_object), addr); +} + + +void LLILEmulator::RemoveBreakpoint(uint64_t addr) +{ + BNILEmulatorRemoveBreakpoint(BNLLILEmulatorGetBase(m_object), addr); +} + + +void LLILEmulator::ClearBreakpoints() +{ + BNILEmulatorClearBreakpoints(BNLLILEmulatorGetBase(m_object)); +} + + +// ─── Limits ────────────────────────────────────────────────────────────────── + +void LLILEmulator::SetMaxInstructions(size_t max) +{ + BNILEmulatorSetMaxInstructions(BNLLILEmulatorGetBase(m_object), max); +} + + +size_t LLILEmulator::GetInstructionsExecuted() const +{ + return BNILEmulatorGetInstructionsExecuted(BNLLILEmulatorGetBase(m_object)); +} + + +// ─── Hook bridge callbacks ─────────────────────────────────────────────────── + +bool LLILEmulator::CallHookCallback(void* ctxt, BNILEmulator*, uint64_t target) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + return self->m_callHook(self, target); +} + + +bool LLILEmulator::SyscallHookCallback(void* ctxt, BNILEmulator*) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + return self->m_syscallHook(self); +} + + +bool LLILEmulator::MemoryReadHookCallback( + void* ctxt, BNILEmulator*, uint64_t addr, size_t size, uint8_t* outBuf, size_t bufLen) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + intx::uint512 value; + if (!self->m_memoryReadHook(self, addr, size, value)) + return false; + ApiUint512ToBytes(value, outBuf, bufLen); + return true; +} + + +bool LLILEmulator::MemoryWriteHookCallback( + void* ctxt, BNILEmulator*, uint64_t addr, size_t size, const uint8_t* buf, size_t bufLen) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + intx::uint512 value = ApiBytesToUint512(buf, bufLen); + return self->m_memoryWriteHook(self, addr, size, value); +} + + +bool LLILEmulator::PreInstructionHookCallback(void* ctxt, BNILEmulator*, size_t instrIndex) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + return self->m_preInstructionHook(self, instrIndex); +} + + +bool LLILEmulator::IntrinsicHookCallback(void* ctxt, BNLLILEmulator*, + uint32_t intrinsic, const uint64_t* params, size_t paramCount, + uint64_t* outValues, uint32_t* outRegs, size_t* outCount) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + std::vector paramVec(params, params + paramCount); + std::vector> outputs; + + bool result = self->m_intrinsicHook(self, intrinsic, paramVec, outputs); + + size_t count = outputs.size(); + if (outCount) + *outCount = count; + for (size_t i = 0; i < count; i++) + { + if (outRegs) + outRegs[i] = outputs[i].first; + if (outValues) + outValues[i] = outputs[i].second; + } + return result; +} + + +void LLILEmulator::StdoutCallbackBridge(void* ctxt, BNILEmulator*, const char* data, size_t len) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + self->m_stdoutCallback(self, std::string(data, len)); +} + + +size_t LLILEmulator::StdinCallbackBridge(void* ctxt, BNILEmulator*, char* buf, size_t maxLen) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + return self->m_stdinCallback(self, buf, maxLen); +} + + +// ─── Hook setters ──────────────────────────────────────────────────────────── + +void LLILEmulator::SetCallHook(const std::function& hook) +{ + m_callHook = hook; + BNILEmulatorSetCallHook(BNLLILEmulatorGetBase(m_object), + hook ? (void*)this : nullptr, + hook ? CallHookCallback : nullptr); +} + + +void LLILEmulator::SetSyscallHook(const std::function& hook) +{ + m_syscallHook = hook; + BNILEmulatorSetSyscallHook(BNLLILEmulatorGetBase(m_object), + hook ? (void*)this : nullptr, + hook ? SyscallHookCallback : nullptr); +} + + +void LLILEmulator::SetMemoryReadHook( + const std::function& hook) +{ + m_memoryReadHook = hook; + BNILEmulatorSetMemoryReadHook(BNLLILEmulatorGetBase(m_object), + hook ? (void*)this : nullptr, + hook ? MemoryReadHookCallback : nullptr); +} + + +void LLILEmulator::SetMemoryWriteHook( + const std::function& hook) +{ + m_memoryWriteHook = hook; + BNILEmulatorSetMemoryWriteHook(BNLLILEmulatorGetBase(m_object), + hook ? (void*)this : nullptr, + hook ? MemoryWriteHookCallback : nullptr); +} + + +void LLILEmulator::SetPreInstructionHook(const std::function& hook) +{ + m_preInstructionHook = hook; + BNILEmulatorSetPreInstructionHook(BNLLILEmulatorGetBase(m_object), + hook ? (void*)this : nullptr, + hook ? PreInstructionHookCallback : nullptr); +} + + +void LLILEmulator::SetIntrinsicHook(const std::function&, std::vector>&)>& hook) +{ + m_intrinsicHook = hook; + BNLLILEmulatorSetIntrinsicHook(m_object, + hook ? (void*)this : nullptr, + hook ? IntrinsicHookCallback : nullptr); +} + + +void LLILEmulator::SetStdoutCallback(const std::function& cb) +{ + m_stdoutCallback = cb; + BNILEmulatorSetStdoutCallback(BNLLILEmulatorGetBase(m_object), + cb ? (void*)this : nullptr, + cb ? StdoutCallbackBridge : nullptr); +} + + +void LLILEmulator::SetStdinCallback(const std::function& cb) +{ + m_stdinCallback = cb; + BNILEmulatorSetStdinCallback(BNLLILEmulatorGetBase(m_object), + cb ? (void*)this : nullptr, + cb ? StdinCallbackBridge : nullptr); +} + + +// ─── Register / flag / temp access ────────────────────────────────────────── + +intx::uint512 LLILEmulator::GetRegister(uint32_t reg) const +{ + uint8_t buf[64] = {}; + BNLLILEmulatorGetRegister(m_object, reg, buf, sizeof(buf)); + return ApiBytesToUint512(buf, sizeof(buf)); +} + + +void LLILEmulator::SetRegister(uint32_t reg, const intx::uint512& value) +{ + uint8_t buf[64]; + ApiUint512ToBytes(value, buf, sizeof(buf)); + BNLLILEmulatorSetRegister(m_object, reg, buf, sizeof(buf)); +} + + +intx::uint512 LLILEmulator::GetTempRegister(uint32_t index) const +{ + uint8_t buf[64] = {}; + BNLLILEmulatorGetTempRegister(m_object, index, buf, sizeof(buf)); + return ApiBytesToUint512(buf, sizeof(buf)); +} + + +void LLILEmulator::SetTempRegister(uint32_t index, const intx::uint512& value) +{ + uint8_t buf[64]; + ApiUint512ToBytes(value, buf, sizeof(buf)); + BNLLILEmulatorSetTempRegister(m_object, index, buf, sizeof(buf)); +} + + +std::unordered_map LLILEmulator::GetAllTempRegisters() const +{ + // Query count first, then fetch + size_t count = BNLLILEmulatorGetAllTempRegisters(m_object, nullptr, nullptr, 0); + std::unordered_map result; + if (count == 0) + return result; + std::vector indices(count); + std::vector values(count * 64); + count = BNLLILEmulatorGetAllTempRegisters(m_object, indices.data(), values.data(), count); + for (size_t i = 0; i < count; i++) + result[indices[i]] = ApiBytesToUint512(values.data() + i * 64, 64); + return result; +} + + +uint8_t LLILEmulator::GetFlag(uint32_t flag) const +{ + return BNLLILEmulatorGetFlag(m_object, flag); +} + + +void LLILEmulator::SetFlag(uint32_t flag, uint8_t value) +{ + BNLLILEmulatorSetFlag(m_object, flag, value); +} + + +// ─── Cross-function state ──────────────────────────────────────────────────── + +size_t LLILEmulator::GetCallStackDepth() const +{ + return BNLLILEmulatorGetCallStackDepth(m_object); +} + + +std::vector LLILEmulator::GetCallStack() const +{ + size_t count = 0; + BNEmulatorCallStackEntry* entries = BNLLILEmulatorGetCallStack(m_object, &count); + std::vector result; + if (entries) + { + result.reserve(count); + for (size_t i = 0; i < count; i++) + result.push_back({entries[i].functionAddress, entries[i].returnAddress}); + BNLLILEmulatorFreeCallStack(entries); + } + return result; +} + + +std::vector LLILEmulator::GetMappedRegions() const +{ + size_t count = 0; + BNEmulatorMemoryRegion* regions = BNILEmulatorGetMappedRegions(BNLLILEmulatorGetBase(m_object), &count); + std::vector result; + if (regions) + { + result.reserve(count); + for (size_t i = 0; i < count; i++) + result.push_back({regions[i].start, regions[i].size, regions[i].name ? regions[i].name : ""}); + BNFreeEmulatorMemoryRegions(regions, count); + } + return result; +} + + +// ─── Built-in libc stub settings ───────────────────────────────────────────── + +void LLILEmulator::SetBuiltinLibcStubsEnabled(bool enabled) +{ + BNLLILEmulatorSetBuiltinLibcStubsEnabled(m_object, enabled); +} + + +bool LLILEmulator::IsBuiltinLibcStubsEnabled() const +{ + return BNLLILEmulatorIsBuiltinLibcStubsEnabled(m_object); +} + + +void LLILEmulator::SetLogLibcCalls(bool enabled) +{ + BNLLILEmulatorSetLogLibcCalls(m_object, enabled); +} + + +bool LLILEmulator::IsLogLibcCalls() const +{ + return BNLLILEmulatorIsLogLibcCalls(m_object); +} + + +void LLILEmulator::SetNopUnknownExternals(bool enabled) +{ + BNLLILEmulatorSetNopUnknownExternals(m_object, enabled); +} + + +bool LLILEmulator::IsNopUnknownExternals() const +{ + return BNLLILEmulatorIsNopUnknownExternals(m_object); +} + + +// ─── Reset ─────────────────────────────────────────────────────────────────── + +void LLILEmulator::Reset() +{ + BNILEmulatorReset(BNLLILEmulatorGetBase(m_object)); +} + + +// ─── State serialization ───────────────────────────────────────────────────── + +std::string LLILEmulator::SaveState() const +{ + char* json = BNLLILEmulatorSaveState(m_object); + if (!json) + return {}; + std::string result(json); + BNFreeString(json); + return result; +} + + +bool LLILEmulator::LoadState(const std::string& json) +{ + return BNLLILEmulatorLoadState(m_object, json.c_str()); +} diff --git a/plugins/emulator/api/python/CMakeLists.txt b/plugins/emulator/api/python/CMakeLists.txt new file mode 100644 index 0000000000..ca19e8c5c6 --- /dev/null +++ b/plugins/emulator/api/python/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(emulator-python-api) + +if(BN_API_PATH) + include(${BN_API_PATH}/cmake/PythonBindings.cmake) +else() + include(${CMAKE_SOURCE_DIR}/api/cmake/PythonBindings.cmake) +endif() + +file(GLOB PYTHON_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/*.py) +list(REMOVE_ITEM PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/_emulatorcore.py) +list(REMOVE_ITEM PYTHON_SOURCES ${PROJECT_SOURCE_DIR}/emulator_enums.py) + +add_executable(emulator_generator + ${PROJECT_SOURCE_DIR}/generator.cpp) +target_link_libraries(emulator_generator binaryninjaapi) + +set_target_properties(emulator_generator PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON + BUILD_WITH_INSTALL_RPATH OFF + RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) + +if(BN_INTERNAL_BUILD) + set(PYTHON_OUTPUT_DIRECTORY ${BN_RESOURCE_DIR}/python/binaryninja/emulator/) +else() + set(PYTHON_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/out/plugins/emulator/) +endif() + +if(WIN32) + if (BN_INTERNAL_BUILD) + add_custom_command(TARGET emulator_generator PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${BN_CORE_OUTPUT_DIR}/binaryninjacore.dll ${PROJECT_BINARY_DIR}/) + else() + add_custom_command(TARGET emulator_generator PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${BN_INSTALL_DIR}/binaryninjacore.dll ${PROJECT_BINARY_DIR}/) + endif() +endif() + +generate_python_bindings( + TARGET_NAME emulator_generator_copy + DISPLAY_NAME "Emulator" + GENERATOR_TARGET emulator_generator + HEADER_FILE ${PROJECT_SOURCE_DIR}/../ffi.h + TEMPLATE_FILE ${PROJECT_SOURCE_DIR}/_emulatorcore_template.py + OUTPUT_DIRECTORY ${PYTHON_OUTPUT_DIRECTORY} + CORE_OUTPUT_FILE _emulatorcore.py + ENUMS_OUTPUT_FILE emulator_enums.py + PYTHON_SOURCES ${PYTHON_SOURCES} +) diff --git a/plugins/emulator/api/python/__init__.py b/plugins/emulator/api/python/__init__.py new file mode 100644 index 0000000000..ee6d29d725 --- /dev/null +++ b/plugins/emulator/api/python/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2020-2026 Vector 35 Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from binaryninja.settings import Settings + +from binaryninja._binaryninjacore import BNGetUserPluginDirectory +user_plugin_dir = os.path.realpath(BNGetUserPluginDirectory()) +current_path = os.path.realpath(__file__) + +# If BN_STANDALONE_EMULATOR is set, only initialize the python module when it is loaded from the user plugin dir +if os.environ.get('BN_STANDALONE_EMULATOR'): + if current_path.startswith(user_plugin_dir): + from .ilemulator import * + from .emulator_enums import * +else: + if Settings().get_bool('corePlugins.emulator') and (os.environ.get('BN_DISABLE_CORE_EMULATOR') is None): + from .ilemulator import * + from .emulator_enums import * diff --git a/plugins/emulator/api/python/_emulatorcore_template.py b/plugins/emulator/api/python/_emulatorcore_template.py new file mode 100644 index 0000000000..327c1521a0 --- /dev/null +++ b/plugins/emulator/api/python/_emulatorcore_template.py @@ -0,0 +1,58 @@ +import binaryninja +import ctypes, os + +from typing import Optional +from . import emulator_enums +# Emulator C ABI strings are allocated by the core allocator, so free them with BNFreeString. +from binaryninja._binaryninjacore import BNFreeString +# Load core module +import platform +core = None +core_platform = platform.system() + +if os.environ.get('BN_STANDALONE_EMULATOR'): + from binaryninja._binaryninjacore import BNGetUserPluginDirectory + if core_platform == "Darwin": + _base_path = BNGetUserPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "libemulatorcore.dylib")) + + elif core_platform == "Linux": + _base_path = BNGetUserPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "libemulatorcore.so")) + + elif (core_platform == "Windows") or (core_platform.find("CYGWIN_NT") == 0): + _base_path = BNGetUserPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "emulatorcore.dll")) + else: + raise Exception("OS not supported") +else: + from binaryninja._binaryninjacore import BNGetBundledPluginDirectory + if core_platform == "Darwin": + _base_path = BNGetBundledPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "libemulatorcore.dylib")) + + elif core_platform == "Linux": + _base_path = BNGetBundledPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "libemulatorcore.so")) + + elif (core_platform == "Windows") or (core_platform.find("CYGWIN_NT") == 0): + _base_path = BNGetBundledPluginDirectory() + core = ctypes.CDLL(os.path.join(_base_path, "emulatorcore.dll")) + else: + raise Exception("OS not supported") + +def cstr(var) -> Optional[ctypes.c_char_p]: + if var is None: + return None + if isinstance(var, bytes): + return var + return var.encode("utf-8") + +def pyNativeStr(arg): + if isinstance(arg, str): + return arg + else: + return arg.decode('utf8') + +def free_string(value:ctypes.c_char_p) -> None: + BNFreeString(ctypes.cast(value, ctypes.POINTER(ctypes.c_byte))) diff --git a/plugins/emulator/api/python/generator.cpp b/plugins/emulator/api/python/generator.cpp new file mode 100644 index 0000000000..883b04dc7b --- /dev/null +++ b/plugins/emulator/api/python/generator.cpp @@ -0,0 +1,614 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + + +#include +#include +#include "binaryninjaapi.h" + +using namespace BinaryNinja; +using namespace std; + + +map g_pythonKeywordReplacements = { + {"False", "False_"}, + {"True", "True_"}, + {"None", "None_"}, + {"and", "and_"}, + {"as", "as_"}, + {"assert", "assert_"}, + {"async", "async_"}, + {"await", "await_"}, + {"break", "break_"}, + {"class", "class_"}, + {"continue", "continue_"}, + {"def", "def_"}, + {"del", "del_"}, + {"elif", "elif_"}, + {"else", "else_"}, + {"except", "except_"}, + {"finally", "finally_"}, + {"for", "for_"}, + {"from", "from_"}, + {"global", "global_"}, + {"if", "if_"}, + {"import", "import_"}, + {"in", "in_"}, + {"is", "is_"}, + {"lambda", "lambda_"}, + {"nonlocal", "nonlocal_"}, + {"not", "not_"}, + {"or", "or_"}, + {"pass", "pass_"}, + {"raise", "raise_"}, + {"return", "return_"}, + {"try", "try_"}, + {"while", "while_"}, + {"with", "with_"}, + {"yield", "yield_"}, +}; + + +void OutputType(FILE* out, Type* type, bool isReturnType = false, bool isCallback = false) +{ + switch (type->GetClass()) + { + case BoolTypeClass: + fprintf(out, "ctypes.c_bool"); + break; + case IntegerTypeClass: + switch (type->GetWidth()) + { + case 1: + if (type->IsSigned()) + fprintf(out, "ctypes.c_byte"); + else + fprintf(out, "ctypes.c_ubyte"); + break; + case 2: + if (type->IsSigned()) + fprintf(out, "ctypes.c_short"); + else + fprintf(out, "ctypes.c_ushort"); + break; + case 4: + if (type->IsSigned()) + fprintf(out, "ctypes.c_int"); + else + fprintf(out, "ctypes.c_uint"); + break; + default: + if (type->IsSigned()) + fprintf(out, "ctypes.c_longlong"); + else + fprintf(out, "ctypes.c_ulonglong"); + break; + } + break; + case FloatTypeClass: + if (type->GetWidth() == 4) + fprintf(out, "ctypes.c_float"); + else + fprintf(out, "ctypes.c_double"); + break; + case NamedTypeReferenceClass: + if (type->GetNamedTypeReference()->GetTypeReferenceClass() == EnumNamedTypeClass) + { + string name = type->GetNamedTypeReference()->GetName().GetString(); + if (name.size() > 16 && name.substr(0, 11) == "_BNDebugger") + name = name.substr(3); + else if (name.size() > 15 && name.substr(0, 10) == "BNDebugger") + name = name.substr(2); + else if (name.size() > 15 && name.substr(0, 7) == "BNDebug") + name = name.substr(2); + else if (name.size() > 2 && name.substr(0, 2) == "BN") + name = name.substr(2); + fprintf(out, "%sEnum", name.c_str()); + } + else + { + fprintf(out, "%s", type->GetNamedTypeReference()->GetName().GetString().c_str()); + } + break; + case PointerTypeClass: + if (isCallback || (type->GetChildType()->GetClass() == VoidTypeClass)) + { + fprintf(out, "ctypes.c_void_p"); + break; + } + else if ((type->GetChildType()->GetClass() == IntegerTypeClass) && + (type->GetChildType()->GetWidth() == 1) && (type->GetChildType()->IsSigned())) + { + if (isReturnType) + fprintf(out, "ctypes.POINTER(ctypes.c_byte)"); + else + fprintf(out, "ctypes.c_char_p"); + break; + } + else if (type->GetChildType()->GetClass() == FunctionTypeClass) + { + fprintf(out, "ctypes.CFUNCTYPE("); + OutputType(out, type->GetChildType()->GetChildType().GetValue(), true, true); + for (auto& i : type->GetChildType()->GetParameters()) + { + fprintf(out, ", "); + OutputType(out, i.type.GetValue()); + } + fprintf(out, ")"); + break; + } + fprintf(out, "ctypes.POINTER("); + OutputType(out, type->GetChildType().GetValue()); + fprintf(out, ")"); + break; + case ArrayTypeClass: + OutputType(out, type->GetChildType().GetValue()); + fprintf(out, " * %" PRId64, type->GetElementCount()); + break; + default: + fprintf(out, "None"); + break; + } +} + + +void OutputSwizzledType(FILE* out, Type* type) +{ + switch (type->GetClass()) + { + case BoolTypeClass: + fprintf(out, "bool"); + break; + case IntegerTypeClass: + fprintf(out, "int"); + break; + case FloatTypeClass: + fprintf(out, "float"); + break; + case NamedTypeReferenceClass: + if (type->GetNamedTypeReference()->GetTypeReferenceClass() == EnumNamedTypeClass) + { + string name = type->GetNamedTypeReference()->GetName().GetString(); + if (name.size() > 16 && name.substr(0, 11) == "_BNDebugger") + name = name.substr(3); + else if (name.size() > 15 && name.substr(0, 10) == "BNDebugger") + name = name.substr(2); + else if (name.size() > 15 && name.substr(0, 7) == "BNDebug") + name = name.substr(2); + else if (name.size() > 2 && name.substr(0, 2) == "BN") + name = name.substr(2); + fprintf(out, "%sEnum", name.c_str()); + } + else + { + fprintf(out, "%s", type->GetNamedTypeReference()->GetName().GetString().c_str()); + } + break; + case PointerTypeClass: + if (type->GetChildType()->GetClass() == VoidTypeClass) + { + fprintf(out, "Optional[ctypes.c_void_p]"); + break; + } + else if ((type->GetChildType()->GetClass() == IntegerTypeClass) && + (type->GetChildType()->GetWidth() == 1) && (type->GetChildType()->IsSigned())) + { + fprintf(out, "Optional[str]"); + break; + } + else if (type->GetChildType()->GetClass() == FunctionTypeClass) + { + fprintf(out, "ctypes.CFUNCTYPE("); + OutputType(out, type->GetChildType()->GetChildType().GetValue(), true, true); + for (auto& i : type->GetChildType()->GetParameters()) + { + fprintf(out, ", "); + OutputType(out, i.type.GetValue()); + } + fprintf(out, ")"); + break; + } + fprintf(out, "ctypes.POINTER("); + OutputType(out, type->GetChildType().GetValue()); + fprintf(out, ")"); + break; + case ArrayTypeClass: + OutputType(out, type->GetChildType().GetValue()); + fprintf(out, " * %" PRId64, type->GetElementCount()); + break; + default: + fprintf(out, "None"); + break; + } +} + + +int main(int argc, char* argv[]) +{ + if (argc < 5) + { + fprintf(stderr, "Usage: generator
\n"); + return 1; + } + + // Parse API header to get type and function information + map> types, vars, funcs; + string errors; + auto arch = new CoreArchitecture(BNGetNativeTypeParserArchitecture()); + + // Enable ephemeral settings + Settings::Instance()->LoadSettingsFile(""); + Settings::Instance()->Set("analysis.types.parserName", "ClangTypeParser"); + bool ok = arch->GetStandalonePlatform()->ParseTypesFromSourceFile(argv[1], types, vars, funcs, errors); + + if (!ok) + { + fprintf(stderr, "Errors: %s\n", errors.c_str()); + return 1; + } + + FILE* out = fopen(argv[2], "w"); + FILE* out_template = fopen(argv[3], "r"); + FILE* enums = fopen(argv[4], "w"); + + fprintf(enums, "import enum\n"); + + // Copy the content of the template to the output file + int c; + while((c = fgetc(out_template)) != EOF) + fputc(c, out); + + // Create type objects + fprintf(out, "# Type definitions\n"); + for (auto& i : types) + { + string name; + if (i.first.size() != 1) + continue; + name = i.first[0]; + if (i.second->GetClass() == StructureTypeClass) + { + fprintf(out, "class %s(ctypes.Structure):\n", name.c_str()); + + // python uses str's, C uses byte-arrays + bool stringField = false; + for (auto& arg : i.second->GetStructure()->GetMembers()) + { + if ((arg.type->GetClass() == PointerTypeClass) && + (arg.type->GetChildType()->GetWidth() == 1) && + (arg.type->GetChildType()->IsSigned())) + { + fprintf(out, "\t@property\n\tdef %s(self):\n\t\treturn pyNativeStr(self._%s)\n", arg.name.c_str(), arg.name.c_str()); + fprintf(out, "\t@%s.setter\n\tdef %s(self, value):\n\t\tself._%s = cstr(value)\n", arg.name.c_str(), arg.name.c_str(), arg.name.c_str()); + stringField = true; + } + } + + if (!stringField) + fprintf(out, "\tpass\n"); + + fprintf(out, "%sHandle = ctypes.POINTER(%s)\n", name.c_str(), name.c_str()); + } + else if (i.second->GetClass() == EnumerationTypeClass) + { + bool isBNAPIEnum = false; + if (name.size() > 16 && name.substr(0, 11) == "_BNDebugger") + name = name.substr(3); + else if (name.size() > 15 && name.substr(0, 10) == "BNDebugger") + name = name.substr(2); + else if (name.size() > 15 && name.substr(0, 7) == "BNDebug") + name = name.substr(2); + else if (name.size() > 12 && name.substr(0, 12) == "BNILEmulator") + name = name.substr(2); // emulator enums are defined by this plugin, not core + else if (name.size() > 2 && name.substr(0, 2) == "BN") + { + name = name.substr(2); + isBNAPIEnum = true; + } + else + continue; + + if (isBNAPIEnum) + { + fprintf(out, "from binaryninja._binaryninjacore import %sEnum\n", name.c_str()); + continue; + } + + fprintf(out, "%sEnum = ctypes.c_int\n", name.c_str()); + + fprintf(enums, "\n\nclass %s(enum.IntEnum):\n", name.c_str()); + for (auto& j : i.second->GetEnumeration()->GetMembers()) + { + fprintf(enums, "\t%s = %" PRId64 "\n", j.name.c_str(), j.value); + } + } + else if ((i.second->GetClass() == BoolTypeClass) || (i.second->GetClass() == IntegerTypeClass) || + (i.second->GetClass() == FloatTypeClass) || (i.second->GetClass() == ArrayTypeClass)) + { + fprintf(out, "%s = ", name.c_str()); + OutputType(out, i.second); + fprintf(out, "\n"); + } + } + + + fprintf(out, "\n# Structure definitions\n"); + set structsToProcess; + set finishedStructs; + for (auto& i : types) + structsToProcess.insert(i.first); + while (structsToProcess.size() != 0) + { + set currentStructList = structsToProcess; + structsToProcess.clear(); + bool processedSome = false; + for (auto& i : currentStructList) + { + string name; + if (i.size() != 1) + continue; + Ref type = types[i]; + name = i[0]; + if ((type->GetClass() == StructureTypeClass) && (type->GetStructure()->GetMembers().size() != 0)) + { + bool requiresDependency = false; + for (auto& j : type->GetStructure()->GetMembers()) + { + if ((j.type->GetClass() == NamedTypeReferenceClass) && + (types[j.type->GetNamedTypeReference()->GetName()]->GetClass() == StructureTypeClass) && + (finishedStructs.count(j.type->GetNamedTypeReference()->GetName()) == 0)) + { + // This structure needs another structure that isn't fully defined yet, need to wait + // for the dependencies to be defined + structsToProcess.insert(i); + requiresDependency = true; + break; + } + } + + if (requiresDependency) + continue; + fprintf(out, "%s._fields_ = [\n", name.c_str()); + for (auto& j : type->GetStructure()->GetMembers()) + { + // To help the python->C wrappers + if ((j.type->GetClass() == PointerTypeClass) && + (j.type->GetChildType()->GetWidth() == 1) && + (j.type->GetChildType()->IsSigned())) + { + fprintf(out, "\t\t(\"_%s\", ", j.name.c_str()); + } + else + fprintf(out, "\t\t(\"%s\", ", j.name.c_str()); + OutputType(out, j.type.GetValue()); + fprintf(out, "),\n"); + } + fprintf(out, "\t]\n"); + finishedStructs.insert(i); + processedSome = true; + } + else if (type->GetClass() == NamedTypeReferenceClass) + { + if (type->GetNamedTypeReference()->GetTypeReferenceClass() == StructNamedTypeClass) + { + fprintf(out, "%s = %s\n", name.c_str(), type->GetNamedTypeReference()->GetName().GetString().c_str()); + fprintf(out, "%sHandle = %sHandle\n", name.c_str(), type->GetNamedTypeReference()->GetName().GetString().c_str()); + } + else if (type->GetNamedTypeReference()->GetTypeReferenceClass() == EnumNamedTypeClass) + { + fprintf(out, "%s = ctypes.c_int\n", name.c_str()); + } + finishedStructs.insert(i); + processedSome = true; + } + } + + if (!processedSome) + { + fprintf(stderr, "Detected dependency cycle in structures\n"); + for (auto& i : structsToProcess) + fprintf(stderr, "%s\n", i.GetString().c_str()); + return 1; + } + } + + fprintf(out, "\n# Function definitions\n"); + for (auto& i : funcs) + { + string name; + if (i.first.size() != 1) + continue; + name = i.first[0]; + + // Check for a string result, these will be automatically wrapped to free the string + // memory and return a Python string + bool stringResult = (i.second->GetChildType()->GetClass() == PointerTypeClass) && + (i.second->GetChildType()->GetChildType()->GetWidth() == 1) && + (i.second->GetChildType()->GetChildType()->IsSigned()); + // Pointer returns will be automatically wrapped to return None on null pointer + bool pointerResult = (i.second->GetChildType()->GetClass() == PointerTypeClass); + + // From python -> C python3 requires str -> str.encode('charmap') + bool swizzleArgs = true; + if (name == "BNDebuggerFreeString") + swizzleArgs = false; + + bool callbackConvention = false; + if (name == "BNDebuggerAllocString") + { + // Don't perform automatic wrapping of string allocation, and return a void + // pointer so that callback functions (which is the only valid use of BNDebuggerAllocString) + // can properly return the result + stringResult = false; + callbackConvention = true; + swizzleArgs = false; + } + + string funcName = string("_") + name; + + fprintf(out, "# -------------------------------------------------------\n"); + fprintf(out, "# %s\n\n", funcName.c_str()); + fprintf(out, "%s = core.%s\n", funcName.c_str(), name.c_str()); + fprintf(out, "%s.restype = ", funcName.c_str()); + OutputType(out, i.second->GetChildType().GetValue(), true, callbackConvention); + fprintf(out, "\n"); + if (!i.second->HasVariableArguments()) + { + fprintf(out, "%s.argtypes = [\n", funcName.c_str()); + for (auto& j : i.second->GetParameters()) + { + fprintf(out, "\t\t"); + if (name == "BNDebuggerFreeString") + { + // BNDebuggerFreeString expects a pointer to a string allocated by the core, so do not use + // a c_char_p here, as that would be allocated by the Python runtime. This can + // be enforced by outputting like a return value. + OutputType(out, j.type.GetValue(), true); + } + else + { + OutputType(out, j.type.GetValue()); + } + fprintf(out, ",\n"); + } + fprintf(out, "\t]"); + } + else + { + // As of writing this, only BNLog's have variable instruction lengths, but in an attempt not to break in the future: + if (funcName.compare(0, 6, "_BNLog") == 0) + { + if (funcName != "_BNLog") + { + fprintf(out, "def %s(*args):\n", name.c_str()); + fprintf(out, "\treturn %s(*[cstr(arg) for arg in args])\n\n", funcName.c_str()); + continue; + } + else + { + fprintf(out, "def %s(level, *args):\n", name.c_str()); + fprintf(out, "\treturn %s(level, *[cstr(arg) for arg in args])\n\n", funcName.c_str()); + continue; + } + } + } + fprintf(out, "\n\n\n# noinspection PyPep8Naming\n"); + fprintf(out, "def %s(", name.c_str()); + if (!i.second->HasVariableArguments()) + { + size_t argN = 0; + for (auto& arg: i.second->GetParameters()) + { + string argName = arg.name; + if (g_pythonKeywordReplacements.find(argName) != g_pythonKeywordReplacements.end()) + argName = g_pythonKeywordReplacements[argName]; + + if (argName.empty()) + argName = "arg" + to_string(argN); + + if (argN > 0) + fprintf(out, ", "); + fprintf(out, "\n\t\t"); + fprintf(out, "%s: ", argName.c_str()); + if (swizzleArgs) + OutputSwizzledType(out, arg.type.GetValue()); + else + OutputType(out, arg.type.GetValue()); + argN ++; + } + } + fprintf(out, "\n\t\t) -> "); + if (swizzleArgs) + { + if (stringResult || pointerResult) + fprintf(out, "Optional["); + OutputSwizzledType(out, i.second->GetChildType().GetValue()); + if (stringResult || pointerResult) + fprintf(out, "]"); + } + else + { + OutputType(out, i.second->GetChildType().GetValue()); + } + fprintf(out, ":\n"); + + string stringArgFuncCall = funcName + "("; + size_t argN = 0; + for (auto& arg : i.second->GetParameters()) + { + string argName = arg.name; + if (g_pythonKeywordReplacements.find(argName) != g_pythonKeywordReplacements.end()) + argName = g_pythonKeywordReplacements[argName]; + + if (argName.empty()) + argName = "arg" + to_string(argN); + + if (swizzleArgs && (arg.type->GetClass() == PointerTypeClass) && + (arg.type->GetChildType()->GetClass() == IntegerTypeClass) && + (arg.type->GetChildType()->GetWidth() == 1) && + (arg.type->GetChildType()->IsSigned())) + { + stringArgFuncCall += string("cstr(") + argName + "), "; + } + else + { + stringArgFuncCall += argName + ", "; + } + argN++; + } + if (argN > 0) + stringArgFuncCall = stringArgFuncCall.substr(0, stringArgFuncCall.size()-2); + stringArgFuncCall += ")"; + + if (stringResult) + { + // Emit wrapper to get Python string and free native memory + fprintf(out, "\tresult = "); + fprintf(out, "%s\n", stringArgFuncCall.c_str()); + fprintf(out, "\tif not result:\n"); + fprintf(out, "\t\treturn None\n"); + fprintf(out, "\tstring = str(pyNativeStr(ctypes.cast(result, ctypes.c_char_p).value))\n"); + fprintf(out, "\tBNFreeString(result)\n"); + fprintf(out, "\treturn string\n"); + } + else if (pointerResult) + { + // Emit wrapper to return None on null pointer + fprintf(out, "\tresult = "); + fprintf(out, "%s\n", stringArgFuncCall.c_str()); + fprintf(out, "\tif not result:\n"); + fprintf(out, "\t\treturn None\n"); + fprintf(out, "\treturn result\n"); + } + else + { + fprintf(out, "\treturn "); + fprintf(out, "%s\n", stringArgFuncCall.c_str()); + } + fprintf(out, "\n\n"); + } + + fprintf(out, "\n# Helper functions\n"); + fprintf(out, "def handle_of_type(value, handle_type):\n"); + fprintf(out, "\tif isinstance(value, ctypes.POINTER(handle_type)) or isinstance(value, ctypes.c_void_p):\n"); + fprintf(out, "\t\treturn ctypes.cast(value, ctypes.POINTER(handle_type))\n"); + fprintf(out, "\traise ValueError('expected pointer to %%s' %% str(handle_type))\n"); + + fclose(out); + fclose(enums); + return 0; +} diff --git a/plugins/emulator/api/python/ilemulator.py b/plugins/emulator/api/python/ilemulator.py new file mode 100644 index 0000000000..0bce146c43 --- /dev/null +++ b/plugins/emulator/api/python/ilemulator.py @@ -0,0 +1,626 @@ +# Copyright (c) 2015-2026 Vector 35 Inc +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import ctypes +from typing import Callable, Dict, List, Optional, Tuple + +import binaryninja +from . import _emulatorcore as core +from .emulator_enums import ILEmulatorStopReason +from binaryninja import binaryview +from binaryninja import lowlevelil + + +class LLILEmulator: + """Concrete emulator for Low Level IL. + + Executes LLIL instructions with full register, flag, and memory state. + Supports cross-function emulation and user-defined hooks for calls, + syscalls, memory access, and intrinsics. + + Example usage:: + + >>> emu = LLILEmulator(bv) + >>> emu.set_entry_point(here) + >>> emu.set_register("rsp", 0x7fff0000) + >>> emu.set_call_hook(lambda emu, target: True) # skip all calls + >>> emu.set_max_instructions(1000) + >>> reason = emu.run() + >>> print(f"Stopped: {reason.name}, rax = {hex(emu.get_register('rax'))}") + """ + + def __init__( + self, + view: 'binaryview.BinaryView', + il: Optional['lowlevelil.LowLevelILFunction'] = None, + handle: Optional[core.BNLLILEmulator] = None, + ): + if handle is not None: + LLILHandle = ctypes.POINTER(core.BNLLILEmulator) + self.handle = ctypes.cast(handle, LLILHandle) + elif il is not None: + self.handle = core.BNCreateLLILEmulator( + ctypes.cast(il.handle, ctypes.POINTER(core.BNLowLevelILFunction)), + ctypes.cast(view.handle, ctypes.POINTER(core.BNBinaryView))) + assert self.handle is not None, "Failed to create LLILEmulator" + else: + self.handle = core.BNCreateLLILEmulatorForView(ctypes.cast(view.handle, ctypes.POINTER(core.BNBinaryView))) + assert self.handle is not None, "Failed to create LLILEmulator" + + self._arch = il.arch if il is not None else view.arch + self._view = view + + # Store callback wrappers to prevent garbage collection + self._call_hook_cb = None + self._syscall_hook_cb = None + self._memory_read_hook_cb = None + self._memory_write_hook_cb = None + self._pre_instruction_hook_cb = None + self._intrinsic_hook_cb = None + self._stdout_cb = None + self._stdin_cb = None + + # Store user callbacks + self._call_hook = None + self._syscall_hook = None + self._memory_read_hook = None + self._memory_write_hook = None + self._pre_instruction_hook = None + self._intrinsic_hook = None + self._stdout_callback = None + self._stdin_callback = None + + def __del__(self): + if core is not None and hasattr(self, 'handle') and self.handle is not None: + core.BNFreeLLILEmulator(self.handle) + + def _get_base(self): + return core.BNLLILEmulatorGetBase(self.handle) + + # ── Execution ───────────────────────────────────────────────────────── + + def run(self) -> ILEmulatorStopReason: + """Run until a stop condition is hit.""" + return ILEmulatorStopReason(core.BNILEmulatorRun(self._get_base())) + + def step(self) -> ILEmulatorStopReason: + """Execute a single instruction.""" + return ILEmulatorStopReason(core.BNILEmulatorStep(self._get_base())) + + def step_n(self, n: int) -> ILEmulatorStopReason: + """Execute up to *n* instructions.""" + return ILEmulatorStopReason(core.BNILEmulatorStepN(self._get_base(), n)) + + def step_over(self) -> ILEmulatorStopReason: + """Step over the current instruction (runs through called functions).""" + return ILEmulatorStopReason(core.BNLLILEmulatorStepOver(self.handle)) + + def request_stop(self): + """Request the emulator to stop at the next opportunity. Thread-safe.""" + core.BNILEmulatorRequestStop(self._get_base()) + + # ── State ───────────────────────────────────────────────────────────── + + @property + def instruction_index(self) -> int: + return core.BNILEmulatorGetInstructionIndex(self._get_base()) + + @instruction_index.setter + def instruction_index(self, index: int): + core.BNILEmulatorSetInstructionIndex(self._get_base(), index) + + @property + def current_address(self) -> int: + return core.BNILEmulatorGetCurrentAddress(self._get_base()) + + @property + def stop_reason(self) -> ILEmulatorStopReason: + return ILEmulatorStopReason(core.BNILEmulatorGetStopReason(self._get_base())) + + @property + def stop_message(self) -> str: + return core.BNILEmulatorGetStopMessage(self._get_base()) + + @property + def instructions_executed(self) -> int: + return core.BNILEmulatorGetInstructionsExecuted(self._get_base()) + + @property + def call_stack_depth(self) -> int: + return core.BNLLILEmulatorGetCallStackDepth(self.handle) + + def get_call_stack(self) -> List[Dict[str, int]]: + """Return the call stack as a list of dicts with 'function_address' and 'return_address'. + + Frame 0 is the current function; subsequent frames are callers. + For frame 0, 'return_address' is the current PC. + """ + count = ctypes.c_ulonglong(0) + entries = core.BNLLILEmulatorGetCallStack(self.handle, count) + result = [] + if entries: + for i in range(count.value): + result.append({ + 'function_address': entries[i].functionAddress, + 'return_address': entries[i].returnAddress, + }) + core.BNLLILEmulatorFreeCallStack(entries) + return result + + def get_mapped_regions(self) -> List[Dict]: + """Return all mapped memory regions as a list of dicts with 'start', 'size', and 'name'.""" + count = ctypes.c_ulonglong(0) + regions = core.BNILEmulatorGetMappedRegions(self._get_base(), count) + result = [] + if regions: + for i in range(count.value): + result.append({ + 'start': regions[i].start, + 'size': regions[i].size, + 'name': regions[i].name, + }) + core.BNFreeEmulatorMemoryRegions(regions, count.value) + return result + + # ── Entry point ─────────────────────────────────────────────────────── + + def set_entry_point(self, addr_or_il, instr_index: Optional[int] = None) -> bool: + """Set the emulation entry point. + + Two forms: + + - ``set_entry_point(0x401000)`` — resolve address to a function and + start at its first LLIL instruction. Returns ``False`` if the + address does not belong to an analyzed function. + + - ``set_entry_point(func.llil, 5)`` — start at LLIL instruction + index 5 of the given LLIL function. Always succeeds. + """ + if instr_index is not None: + # (LowLevelILFunction, index) form + il = addr_or_il + core.BNLLILEmulatorSetEntryPointForIL(self.handle, ctypes.cast(il.handle, ctypes.POINTER(core.BNLowLevelILFunction)), instr_index) + self._arch = il.arch + return True + else: + # address form + result = core.BNLLILEmulatorSetEntryPoint(self.handle, addr_or_il) + return result + + # ── Arguments ───────────────────────────────────────────────────────── + + def set_argument(self, index: int, value: int): + """Set a single function argument by index using the default calling convention.""" + raw = value.to_bytes(64, 'little', signed=(value < 0)) + buf = (ctypes.c_ubyte * 64)(*raw) + core.BNLLILEmulatorSetArgument(self.handle, index, buf, 64) + + def set_arguments(self, values: list): + """Set multiple function arguments using the default calling convention.""" + count = len(values) + arr = (ctypes.c_uint64 * count)(*values) + core.BNLLILEmulatorSetArguments(self.handle, arr, count) + + # ── Memory ──────────────────────────────────────────────────────────── + + def read_memory(self, addr: int, length: int) -> bytes: + """Read *length* bytes from emulator memory at *addr*.""" + buf = (ctypes.c_ubyte * length)() + n = core.BNILEmulatorReadMemory(self._get_base(), buf, addr, length) + return bytes(buf[:n]) + + def write_memory(self, addr: int, data: bytes) -> int: + """Write *data* to emulator memory at *addr*. Returns bytes written.""" + buf = (ctypes.c_ubyte * len(data))(*data) + return core.BNILEmulatorWriteMemory(self._get_base(), addr, buf, len(data)) + + def map_memory(self, addr: int, data_or_size, name: str = ""): + """Map a memory region into the emulator. + + - ``map_memory(0x1000, b'\\x00' * 0x1000)`` — map with data + - ``map_memory(0x1000, 0x1000)`` — map zero-filled + - ``map_memory(0x1000, 0x1000, "my_region")`` — map with a name + """ + base = self._get_base() + if isinstance(data_or_size, (bytes, bytearray)): + buf = (ctypes.c_ubyte * len(data_or_size))(*data_or_size) + if name: + core.BNILEmulatorMapMemoryNamed(base, addr, buf, len(data_or_size), name.encode()) + else: + core.BNILEmulatorMapMemory(base, addr, buf, len(data_or_size)) + else: + size = data_or_size + if name: + core.BNILEmulatorMapMemoryZeroNamed(base, addr, size, name.encode()) + else: + core.BNILEmulatorMapMemoryZero(base, addr, size) + + # ── Breakpoints ─────────────────────────────────────────────────────── + + def add_breakpoint(self, addr: int): + core.BNILEmulatorAddBreakpoint(self._get_base(), addr) + + def remove_breakpoint(self, addr: int): + core.BNILEmulatorRemoveBreakpoint(self._get_base(), addr) + + def clear_breakpoints(self): + core.BNILEmulatorClearBreakpoints(self._get_base()) + + # ── Limits ──────────────────────────────────────────────────────────── + + def set_max_instructions(self, max_count: int): + core.BNILEmulatorSetMaxInstructions(self._get_base(), max_count) + + # ── Register / flag access ──────────────────────────────────────────── + + def _resolve_reg(self, reg) -> int: + """Accept register name (str) or numeric register ID (int).""" + if isinstance(reg, str): + return self._arch.regs[reg].index + return reg + + def get_register(self, reg) -> int: + """Get register value. *reg* can be a name (``'rax'``) or numeric ID.""" + buf = (ctypes.c_ubyte * 64)() + core.BNLLILEmulatorGetRegister(self.handle, self._resolve_reg(reg), buf, 64) + return int.from_bytes(bytes(buf), 'little') + + def set_register(self, reg, value: int): + """Set register value. *reg* can be a name (``'rax'``) or numeric ID.""" + raw = value.to_bytes(64, 'little', signed=(value < 0)) + buf = (ctypes.c_ubyte * 64)(*raw) + core.BNLLILEmulatorSetRegister(self.handle, self._resolve_reg(reg), buf, 64) + + def get_temp_register(self, index: int) -> int: + buf = (ctypes.c_ubyte * 64)() + core.BNLLILEmulatorGetTempRegister(self.handle, index, buf, 64) + return int.from_bytes(bytes(buf), 'little') + + def set_temp_register(self, index: int, value: int): + raw = value.to_bytes(64, 'little', signed=(value < 0)) + buf = (ctypes.c_ubyte * 64)(*raw) + core.BNLLILEmulatorSetTempRegister(self.handle, index, buf, 64) + + def get_flag(self, flag) -> int: + """Get flag value. *flag* can be a name (``'z'``) or numeric ID.""" + if isinstance(flag, str): + flag = self._arch._flags[flag] + return core.BNLLILEmulatorGetFlag(self.handle, flag) + + def set_flag(self, flag, value: int): + """Set flag value. *flag* can be a name (``'z'``) or numeric ID.""" + if isinstance(flag, str): + flag = self._arch._flags[flag] + core.BNLLILEmulatorSetFlag(self.handle, flag, value) + + @property + def regs(self) -> Dict[str, int]: + """Snapshot of all named registers as ``{name: value}``.""" + result = {} + for name in self._arch.regs: + result[name] = self.get_register(name) + return result + + # ── Hooks ───────────────────────────────────────────────────────────── + + def set_call_hook(self, callback: Optional[Callable[['LLILEmulator', int], bool]]): + """Set a hook called on CALL instructions. + + The callback receives ``(emulator, target_address)`` and should return + ``True`` to indicate the call was handled (emulator advances past the + call) or ``False`` to let the emulator try cross-function emulation. + Pass ``None`` to remove the hook. + """ + self._call_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetCallHook(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._call_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu, target): + try: + return self._call_hook(self, target) + except: + return False + + self._call_hook_cb = _cb + core.BNILEmulatorSetCallHook(self._get_base(), None, _cb) + + def set_syscall_hook(self, callback: Optional[Callable[['LLILEmulator'], bool]]): + """Set a hook called on SYSCALL instructions. + + Return ``True`` if handled, ``False`` to stop. + """ + self._syscall_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator)) + if callback is None: + core.BNILEmulatorSetSyscallHook(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._syscall_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu): + try: + return self._syscall_hook(self) + except: + return False + + self._syscall_hook_cb = _cb + core.BNILEmulatorSetSyscallHook(self._get_base(), None, _cb) + + def set_memory_read_hook( + self, + callback: Optional[Callable[['LLILEmulator', int, int], Optional[int]]], + ): + """Set a hook called on every memory read. + + The callback receives ``(emulator, address, size)`` and should return + the value to use, or ``None`` to fall through to normal memory. + """ + self._memory_read_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), + ctypes.c_ulonglong, ctypes.c_ulonglong, + ctypes.POINTER(ctypes.c_ubyte), ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetMemoryReadHook(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._memory_read_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu, addr, size, out_buf, buf_len): + try: + result = self._memory_read_hook(self, addr, size) + if result is not None: + raw = result.to_bytes(buf_len, 'little', signed=(result < 0)) + for i in range(buf_len): + out_buf[i] = raw[i] + return True + return False + except: + return False + + self._memory_read_hook_cb = _cb + core.BNILEmulatorSetMemoryReadHook(self._get_base(), None, _cb) + + def set_memory_write_hook( + self, + callback: Optional[Callable[['LLILEmulator', int, int, int], bool]], + ): + """Set a hook called on every memory write. + + The callback receives ``(emulator, address, size, value)`` and should + return ``True`` if handled, ``False`` to let the write proceed normally. + """ + self._memory_write_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), + ctypes.c_ulonglong, ctypes.c_ulonglong, + ctypes.POINTER(ctypes.c_ubyte), ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetMemoryWriteHook(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._memory_write_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu, addr, size, buf, buf_len): + try: + raw = bytes(buf[:buf_len]) + value = int.from_bytes(raw, 'little') + return self._memory_write_hook(self, addr, size, value) + except: + return False + + self._memory_write_hook_cb = _cb + core.BNILEmulatorSetMemoryWriteHook(self._get_base(), None, _cb) + + def set_pre_instruction_hook( + self, + callback: Optional[Callable[['LLILEmulator', int], bool]], + ): + """Set a hook called before each instruction executes. + + The callback receives ``(emulator, instruction_index)`` and should + return ``True`` to continue or ``False`` to stop. + """ + self._pre_instruction_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetPreInstructionHook(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._pre_instruction_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu, instr_index): + try: + return self._pre_instruction_hook(self, instr_index) + except: + return False + + self._pre_instruction_hook_cb = _cb + core.BNILEmulatorSetPreInstructionHook(self._get_base(), None, _cb) + + def set_intrinsic_hook( + self, + callback: Optional[Callable[ + ['LLILEmulator', int, List[int]], + Optional[List[Tuple[int, int]]], + ]], + ): + """Set a hook called on INTRINSIC instructions. + + The callback receives ``(emulator, intrinsic_id, params)`` and should + return a list of ``(register_id, value)`` pairs if handled, or ``None`` + to stop with Unimplemented. + """ + self._intrinsic_hook = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, + ctypes.POINTER(core.BNLLILEmulator), ctypes.c_uint, + ctypes.POINTER(ctypes.c_ulonglong), ctypes.c_ulonglong, + ctypes.POINTER(ctypes.c_ulonglong), ctypes.POINTER(ctypes.c_uint), + ctypes.POINTER(ctypes.c_ulonglong)) + if callback is None: + core.BNLLILEmulatorSetIntrinsicHook(self.handle, None, ctypes.cast(None, _CB_T)) + self._intrinsic_hook_cb = None + return + + @_CB_T + def _cb(ctxt, emu, intrinsic, params, param_count, out_values, out_regs, out_count): + try: + param_list = [params[i] for i in range(param_count)] + result = self._intrinsic_hook(self, intrinsic, param_list) + if result is not None: + out_count[0] = len(result) + for i, (reg, val) in enumerate(result): + out_regs[i] = reg + out_values[i] = val + return True + return False + except: + return False + + self._intrinsic_hook_cb = _cb + core.BNLLILEmulatorSetIntrinsicHook(self.handle, None, _cb) + + def set_stdout_callback( + self, + callback: Optional[Callable[['LLILEmulator', bytes], None]], + ): + """Set a callback for stdout output from emulated printf/puts/putchar. + + The callback receives ``(emulator, data)`` where *data* is a ``bytes`` + object containing the raw output. Pass ``None`` to remove the callback. + """ + self._stdout_callback = callback + _CB_T = ctypes.CFUNCTYPE(None, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), + ctypes.c_char_p, ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetStdoutCallback(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._stdout_cb = None + return + + @_CB_T + def _cb(ctxt, emu, data, length): + try: + self._stdout_callback(self, data[:length]) + except: + pass + + self._stdout_cb = _cb + core.BNILEmulatorSetStdoutCallback(self._get_base(), None, _cb) + + def set_stdin_callback( + self, + callback: Optional[Callable[['LLILEmulator', int], bytes]], + ): + """Set a callback for stdin reads from emulated getchar/fgets/fread. + + The callback receives ``(emulator, max_len)`` and should return a + ``bytes`` object with the input data (up to *max_len* bytes). + Return ``b''`` for EOF. Pass ``None`` to remove the callback. + """ + self._stdin_callback = callback + _CB_T = ctypes.CFUNCTYPE(ctypes.c_ulonglong, ctypes.c_void_p, + ctypes.POINTER(core.BNILEmulator), + ctypes.c_char_p, ctypes.c_ulonglong) + if callback is None: + core.BNILEmulatorSetStdinCallback(self._get_base(), None, ctypes.cast(None, _CB_T)) + self._stdin_cb = None + return + + @_CB_T + def _cb(ctxt, emu, buf, max_len): + try: + data = self._stdin_callback(self, max_len) + if data: + n = min(len(data), max_len) + ctypes.memmove(buf, data[:n], n) + return n + return 0 + except: + return 0 + + self._stdin_cb = _cb + core.BNILEmulatorSetStdinCallback(self._get_base(), None, _cb) + + # ── Built-in libc stub settings ────────────────────────────────────── + + @property + def builtin_libc_stubs(self) -> bool: + """Whether built-in libc stub emulation is enabled (default: True).""" + return core.BNLLILEmulatorIsBuiltinLibcStubsEnabled(self.handle) + + @builtin_libc_stubs.setter + def builtin_libc_stubs(self, value: bool): + core.BNLLILEmulatorSetBuiltinLibcStubsEnabled(self.handle, value) + + @property + def log_libc_calls(self) -> bool: + """Whether libc stub calls are logged to the console (default: True).""" + return core.BNLLILEmulatorIsLogLibcCalls(self.handle) + + @log_libc_calls.setter + def log_libc_calls(self, value: bool): + core.BNLLILEmulatorSetLogLibcCalls(self.handle, value) + + @property + def nop_unknown_externals(self) -> bool: + """Whether unhandled external calls are treated as no-ops returning 0 (default: False).""" + return core.BNLLILEmulatorIsNopUnknownExternals(self.handle) + + @nop_unknown_externals.setter + def nop_unknown_externals(self, value: bool): + core.BNLLILEmulatorSetNopUnknownExternals(self.handle, value) + + # ── Reset ───────────────────────────────────────────────────────────── + + def reset(self): + """Reset all emulator state (registers, memory, flags, call stack).""" + core.BNILEmulatorReset(self._get_base()) + + # ── State serialization ────────────────────────────────────────────── + + def save_state(self) -> str: + """Serialize emulator state to a JSON string.""" + result = core.BNLLILEmulatorSaveState(self.handle) + if result is None: + return "" + return result + + def load_state(self, json_str: str) -> bool: + """Restore emulator state from a JSON string.""" + return core.BNLLILEmulatorLoadState(self.handle, json_str.encode('utf-8')) + + def save_state_to_file(self, path: str): + """Save emulator state to a file.""" + state = self.save_state() + with open(path, 'w') as f: + f.write(state) + + def load_state_from_file(self, path: str) -> bool: + """Load emulator state from a file.""" + with open(path, 'r') as f: + return self.load_state(f.read()) diff --git a/plugins/emulator/core/CMakeLists.txt b/plugins/emulator/core/CMakeLists.txt new file mode 100644 index 0000000000..89b2828de8 --- /dev/null +++ b/plugins/emulator/core/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.21 FATAL_ERROR) + +project(emulatorcore) + +if(NOT BN_INTERNAL_BUILD) + # BN_API_PATH is resolved by the parent CMakeLists (api/plugins/emulator). + add_subdirectory(${BN_API_PATH} ${PROJECT_BINARY_DIR}/api) +endif() + +file(GLOB SOURCES CONFIGURE_DEPENDS + *.cpp + *.h + ) + +if(DEMO) + add_library(emulatorcore STATIC ${SOURCES}) +else() + add_library(emulatorcore SHARED ${SOURCES}) +endif() + +target_link_libraries(emulatorcore binaryninjaapi) + +set_target_properties(emulatorcore PROPERTIES + CXX_STANDARD 20 + CXX_VISIBILITY_PRESET hidden + CXX_STANDARD_REQUIRED ON + VISIBILITY_INLINES_HIDDEN ON + POSITION_INDEPENDENT_CODE ON +) + +if(BN_INTERNAL_BUILD) + set(LIBRARY_OUTPUT_DIRECTORY_PATH "${BN_CORE_PLUGIN_DIR}") +else() + set(LIBRARY_OUTPUT_DIRECTORY_PATH "${CMAKE_BINARY_DIR}/out/plugins") +endif() + +set_target_properties(emulatorcore PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${LIBRARY_OUTPUT_DIRECTORY_PATH} + RUNTIME_OUTPUT_DIRECTORY ${LIBRARY_OUTPUT_DIRECTORY_PATH} + ) + +target_compile_definitions(emulatorcore PRIVATE EMULATOR_LIBRARY) diff --git a/plugins/emulator/core/ffi_global.h b/plugins/emulator/core/ffi_global.h new file mode 100644 index 0000000000..20dd42e5e5 --- /dev/null +++ b/plugins/emulator/core/ffi_global.h @@ -0,0 +1,44 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +// Plugin-side equivalents of core's DECLARE_CORE_API_OBJECT / IMPLEMENT_CORE_API_OBJECT. +// Each emulator C++ class embeds a small C handle struct whose only member is a pointer +// back to the owning object, so the C ABI can round-trip opaque handles (see api/ffi.h). + +#define DECLARE_EMULATOR_API_OBJECT(handle, cls) \ + namespace BinaryNinjaEmulator \ + { \ + class cls; \ + } \ + struct handle \ + { \ + BinaryNinjaEmulator::cls* object; \ + } + +#define IMPLEMENT_EMULATOR_API_OBJECT(handle) \ +\ +private: \ + handle m_apiObject; \ +\ +public: \ + typedef handle* APIHandle; \ + handle* GetAPIObject() { return &m_apiObject; } \ +\ +private: + +#define INIT_EMULATOR_API_OBJECT() m_apiObject.object = this; diff --git a/plugins/emulator/core/ilemulator.cpp b/plugins/emulator/core/ilemulator.cpp new file mode 100644 index 0000000000..09e55f5fa8 --- /dev/null +++ b/plugins/emulator/core/ilemulator.cpp @@ -0,0 +1,821 @@ +#include "ilemulator.h" +#include "../api/ffi.h" + +using namespace std; +using namespace BinaryNinja; +using namespace BinaryNinjaEmulator; + + +// ============================================================================ +// EmulatorMemory +// ============================================================================ + +EmulatorMemory::Segment* EmulatorMemory::FindSegment(uint64_t addr) +{ + for (auto& seg : m_segments) + if (seg.Contains(addr)) + return &seg; + return nullptr; +} + + +const EmulatorMemory::Segment* EmulatorMemory::FindSegment(uint64_t addr) const +{ + for (auto& seg : m_segments) + if (seg.Contains(addr)) + return &seg; + return nullptr; +} + + +const EmulatorMemory::Segment* EmulatorMemory::FindNextSegment(uint64_t addr) const +{ + const Segment* best = nullptr; + for (auto& seg : m_segments) + { + if (seg.start > addr) + { + if (!best || seg.start < best->start) + best = &seg; + } + } + return best; +} + + +bool EmulatorMemory::ReadValue(uint64_t addr, size_t size, BNEndianness endian, intx::uint512& result) +{ + if (size == 0 || size > 64) + return false; + + uint8_t buf[64]; + if (Read(buf, addr, size) != size) + return false; + + result = 0; + if (endian == LittleEndian) + { + // Build uint512 from little-endian bytes + for (size_t i = size; i > 0; i--) + result = (result << 8) | buf[i - 1]; + } + else + { + for (size_t i = 0; i < size; i++) + result = (result << 8) | buf[i]; + } + return true; +} + + +bool EmulatorMemory::WriteValue(uint64_t addr, const intx::uint512& value, size_t size, BNEndianness endian) +{ + if (size == 0 || size > 64) + return false; + + uint8_t buf[64]; + // Extract bytes from uint512 — work on a copy we can shift + intx::uint512 tmp = value; + if (endian == LittleEndian) + { + for (size_t i = 0; i < size; i++) + { + buf[i] = static_cast(tmp); + tmp >>= 8; + } + } + else + { + for (size_t i = size; i > 0; i--) + { + buf[i - 1] = static_cast(tmp); + tmp >>= 8; + } + } + return Write(addr, buf, size) == size; +} + + +size_t EmulatorMemory::Read(void* dest, uint64_t addr, size_t len) +{ + uint8_t* out = (uint8_t*)dest; + size_t totalRead = 0; + + while (totalRead < len) + { + uint64_t cur = addr + totalRead; + const Segment* seg = FindSegment(cur); + if (seg) + { + size_t offset = (size_t)(cur - seg->start); + size_t avail = seg->data.size() - offset; + size_t chunkSize = std::min(len - totalRead, avail); + memcpy(out + totalRead, seg->data.data() + offset, chunkSize); + totalRead += chunkSize; + } + else + { + // Unmapped region — zero-fill up to the next adjacent segment or end of request. + // On real hardware this would fault, but we silently return zeros. + const Segment* next = FindNextSegment(cur); + size_t remaining = len - totalRead; + size_t skipSize; + if (next && next->start <= addr + len) + skipSize = std::min((size_t)(next->start - cur), remaining); + else + skipSize = remaining; + memset(out + totalRead, 0, skipSize); + totalRead += skipSize; + } + } + return totalRead; +} + + +size_t EmulatorMemory::Write(uint64_t addr, const void* src, size_t len) +{ + const uint8_t* in = (const uint8_t*)src; + size_t totalWritten = 0; + + while (totalWritten < len) + { + uint64_t cur = addr + totalWritten; + Segment* seg = FindSegment(cur); + if (seg) + { + size_t offset = (size_t)(cur - seg->start); + size_t avail = seg->data.size() - offset; + size_t chunkSize = std::min(len - totalWritten, avail); + memcpy(seg->data.data() + offset, in + totalWritten, chunkSize); + totalWritten += chunkSize; + } + else + { + // Unmapped region — skip to the next adjacent segment or end of request. + // On real hardware this would fault, but we silently discard the data. + const Segment* next = FindNextSegment(cur); + size_t remaining = len - totalWritten; + size_t skipSize; + if (next && next->start <= addr + len) + skipSize = std::min((size_t)(next->start - cur), remaining); + else + skipSize = remaining; + totalWritten += skipSize; + } + } + return totalWritten; +} + + +void EmulatorMemory::Map(uint64_t addr, const void* data, size_t len, const std::string& name) +{ + Segment seg; + seg.start = addr; + seg.data.assign((const uint8_t*)data, (const uint8_t*)data + len); + seg.name = name; + m_segments.push_back(std::move(seg)); +} + + +void EmulatorMemory::Map(uint64_t addr, size_t len, const std::string& name) +{ + Segment seg; + seg.start = addr; + seg.data.resize(len, 0); + seg.name = name; + m_segments.push_back(std::move(seg)); +} + + +std::vector EmulatorMemory::GetMappedRegions() const +{ + std::vector result; + result.reserve(m_segments.size()); + for (auto& seg : m_segments) + result.push_back({seg.start, seg.data.size(), seg.name}); + return result; +} + + +void EmulatorMemory::Reset() +{ + m_segments.clear(); +} + + +// ============================================================================ +// ILEmulator +// ============================================================================ + +ILEmulator::ILEmulator() +{ + INIT_EMULATOR_API_OBJECT(); +} + + +ILEmulator::~ILEmulator() {} + + +ILEmulatorStopReason ILEmulator::Run() +{ + m_stopReason = ILEmulatorStopReason::Running; + m_stopMessage.clear(); + m_stopRequested.store(false, std::memory_order_relaxed); + + // Remember the address we're resuming from so we skip the breakpoint on the + // very first instruction (avoids re-hitting the same breakpoint on resume). + // If we loop back to this address later, the breakpoint will fire normally. + uint64_t resumeAddress = GetCurrentInstructionAddress(); + bool firstInstruction = true; + + while (m_stopReason == ILEmulatorStopReason::Running) + { + if (m_stopRequested.load(std::memory_order_relaxed)) + { + SetStopReason(ILEmulatorStopReason::UserRequestedStop, "user requested stop"); + break; + } + + if (m_maxInstructions > 0 && m_instructionsExecuted >= m_maxInstructions) + { + SetStopReason(ILEmulatorStopReason::InstructionLimit, "instruction limit reached"); + break; + } + + if (m_instrIndex >= GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Halt, "ran off end of IL"); + break; + } + + m_currentAddress = GetCurrentInstructionAddress(); + + bool skipBreakpoint = firstInstruction && m_currentAddress == resumeAddress; + firstInstruction = false; + + if (m_breakpoints.count(m_currentAddress) && !skipBreakpoint) + { + SetStopReason(ILEmulatorStopReason::Breakpoint); + break; + } + + if (m_preInstructionHook && !m_preInstructionHook(this, m_instrIndex)) + { + SetStopReason(ILEmulatorStopReason::Halt, "pre-instruction hook stopped execution"); + break; + } + + size_t prevIndex = m_instrIndex; + ExecuteCurrentInstruction(); + m_instructionsExecuted++; + + // If ExecuteCurrentInstruction didn't change the index, advance to next + if (m_instrIndex == prevIndex && m_stopReason == ILEmulatorStopReason::Running) + m_instrIndex++; + } + return m_stopReason; +} + + +ILEmulatorStopReason ILEmulator::Step() +{ + return StepN(1); +} + + +ILEmulatorStopReason ILEmulator::StepN(size_t n) +{ + m_stopReason = ILEmulatorStopReason::Running; + m_stopMessage.clear(); + m_stopRequested.store(false, std::memory_order_relaxed); + + for (size_t i = 0; i < n && m_stopReason == ILEmulatorStopReason::Running; i++) + { + if (m_instrIndex >= GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Halt, "ran off end of IL"); + break; + } + + size_t prevIndex = m_instrIndex; + ExecuteCurrentInstruction(); + m_instructionsExecuted++; + + if (m_instrIndex == prevIndex && m_stopReason == ILEmulatorStopReason::Running) + m_instrIndex++; + } + + m_currentAddress = GetCurrentInstructionAddress(); + + if (m_stopReason == ILEmulatorStopReason::Running) + SetStopReason(ILEmulatorStopReason::Halt); + + return m_stopReason; +} + + +size_t ILEmulator::GetInstructionIndex() const +{ + return m_instrIndex; +} + + +void ILEmulator::SetInstructionIndex(size_t index) +{ + m_instrIndex = index; +} + + +uint64_t ILEmulator::GetCurrentAddress() const +{ + return m_currentAddress; +} + + +ILEmulatorStopReason ILEmulator::GetStopReason() const +{ + return m_stopReason; +} + + +const std::string& ILEmulator::GetStopMessage() const +{ + return m_stopMessage; +} + + +EmulatorMemory& ILEmulator::GetMemory() +{ + return m_memory; +} + + +size_t ILEmulator::ReadMemory(void* dest, uint64_t addr, size_t len) +{ + return m_memory.Read(dest, addr, len); +} + + +size_t ILEmulator::WriteMemory(uint64_t addr, const void* src, size_t len) +{ + return m_memory.Write(addr, src, len); +} + + +void ILEmulator::MapMemory(uint64_t addr, const void* data, size_t len, const std::string& name) +{ + m_memory.Map(addr, data, len, name); +} + + +void ILEmulator::MapMemory(uint64_t addr, size_t len, const std::string& name) +{ + m_memory.Map(addr, len, name); +} + + +std::vector ILEmulator::GetMappedRegions() const +{ + return m_memory.GetMappedRegions(); +} + + +intx::uint512 ILEmulator::ReadMemoryValue(uint64_t addr, size_t size) +{ + if (m_memoryReadHook) + { + intx::uint512 value; + if (m_memoryReadHook(this, addr, size, value)) + return value; + } + + intx::uint512 result = 0; + if (!m_memory.ReadValue(addr, size, GetEndianness(), result)) + { + SetStopReason(ILEmulatorStopReason::Error, + fmt::format("failed to read {} bytes at 0x{:x}", size, addr)); + return 0; + } + return result; +} + + +void ILEmulator::WriteMemoryValue(uint64_t addr, const intx::uint512& value, size_t size) +{ + if (m_memoryWriteHook) + { + if (m_memoryWriteHook(this, addr, size, value)) + return; + } + + if (!m_memory.WriteValue(addr, value, size, GetEndianness())) + { + SetStopReason(ILEmulatorStopReason::Error, + fmt::format("failed to write {} bytes at 0x{:x}", size, addr)); + } +} + + +void ILEmulator::AddBreakpoint(uint64_t addr) +{ + m_breakpoints.insert(addr); +} + + +void ILEmulator::RemoveBreakpoint(uint64_t addr) +{ + m_breakpoints.erase(addr); +} + + +void ILEmulator::ClearBreakpoints() +{ + m_breakpoints.clear(); +} + + +void ILEmulator::SetMaxInstructions(size_t max) +{ + m_maxInstructions = max; +} + + +size_t ILEmulator::GetInstructionsExecuted() const +{ + return m_instructionsExecuted; +} + + +void ILEmulator::SetCallHook(std::function hook) +{ + m_callHook = std::move(hook); +} + + +void ILEmulator::SetSyscallHook(std::function hook) +{ + m_syscallHook = std::move(hook); +} + + +void ILEmulator::SetMemoryReadHook(std::function hook) +{ + m_memoryReadHook = std::move(hook); +} + + +void ILEmulator::SetMemoryWriteHook(std::function hook) +{ + m_memoryWriteHook = std::move(hook); +} + + +void ILEmulator::SetPreInstructionHook(std::function hook) +{ + m_preInstructionHook = std::move(hook); +} + + +void ILEmulator::SetStdoutCallback(std::function cb) +{ + m_stdoutCallback = std::move(cb); +} + + +void ILEmulator::SetStdinCallback(std::function cb) +{ + m_stdinCallback = std::move(cb); +} + + +void ILEmulator::EmitStdout(const std::string& data) +{ + if (m_stdoutCallback && !data.empty()) + m_stdoutCallback(this, data.data(), data.size()); +} + + +void ILEmulator::EmitStdout(const char* data, size_t len) +{ + if (m_stdoutCallback && len > 0) + m_stdoutCallback(this, data, len); +} + + +size_t ILEmulator::RequestStdin(char* buf, size_t maxLen) +{ + if (m_stdinCallback) + return m_stdinCallback(this, buf, maxLen); + return 0; +} + + +void ILEmulator::SetStopReason(ILEmulatorStopReason reason, const std::string& msg) +{ + m_stopReason = reason; + m_stopMessage = msg; +} + + +void ILEmulator::RequestStop() +{ + m_stopRequested.store(true, std::memory_order_relaxed); +} + + +void ILEmulator::Reset() +{ + m_memory.Reset(); + m_breakpoints.clear(); + m_instructionsExecuted = 0; + m_instrIndex = 0; + m_currentAddress = 0; + m_stopReason = ILEmulatorStopReason::Running; + m_stopMessage.clear(); +} + + +// ============================================================================ +// C API — Shared ILEmulator functions +// ============================================================================ + +BNILEmulatorStopReason BNILEmulatorRun(BNILEmulator* emu) +{ + return (BNILEmulatorStopReason)emu->object->Run(); +} + + +BNILEmulatorStopReason BNILEmulatorStep(BNILEmulator* emu) +{ + return (BNILEmulatorStopReason)emu->object->Step(); +} + + +BNILEmulatorStopReason BNILEmulatorStepN(BNILEmulator* emu, size_t n) +{ + return (BNILEmulatorStopReason)emu->object->StepN(n); +} + + +size_t BNILEmulatorGetInstructionIndex(BNILEmulator* emu) +{ + return emu->object->GetInstructionIndex(); +} + + +void BNILEmulatorSetInstructionIndex(BNILEmulator* emu, size_t index) +{ + emu->object->SetInstructionIndex(index); +} + + +uint64_t BNILEmulatorGetCurrentAddress(BNILEmulator* emu) +{ + return emu->object->GetCurrentAddress(); +} + + +BNILEmulatorStopReason BNILEmulatorGetStopReason(BNILEmulator* emu) +{ + return (BNILEmulatorStopReason)emu->object->GetStopReason(); +} + + +char* BNILEmulatorGetStopMessage(BNILEmulator* emu) +{ + return BNAllocString(emu->object->GetStopMessage().c_str()); +} + + +size_t BNILEmulatorReadMemory(BNILEmulator* emu, void* dest, uint64_t addr, size_t len) +{ + return emu->object->ReadMemory(dest, addr, len); +} + + +size_t BNILEmulatorWriteMemory(BNILEmulator* emu, uint64_t addr, const void* src, size_t len) +{ + return emu->object->WriteMemory(addr, src, len); +} + + +void BNILEmulatorAddBreakpoint(BNILEmulator* emu, uint64_t addr) +{ + emu->object->AddBreakpoint(addr); +} + + +void BNILEmulatorRemoveBreakpoint(BNILEmulator* emu, uint64_t addr) +{ + emu->object->RemoveBreakpoint(addr); +} + + +void BNILEmulatorClearBreakpoints(BNILEmulator* emu) +{ + emu->object->ClearBreakpoints(); +} + + +void BNILEmulatorSetMaxInstructions(BNILEmulator* emu, size_t max) +{ + emu->object->SetMaxInstructions(max); +} + + +size_t BNILEmulatorGetInstructionsExecuted(BNILEmulator* emu) +{ + return emu->object->GetInstructionsExecuted(); +} + + +void BNILEmulatorSetCallHook(BNILEmulator* emu, void* ctxt, bool (*callback)(void*, BNILEmulator*, uint64_t)) +{ + if (callback) + { + emu->object->SetCallHook([ctxt, callback](ILEmulator* e, uint64_t target) -> bool { + return callback(ctxt, e->GetAPIObject(), target); + }); + } + else + { + emu->object->SetCallHook(nullptr); + } +} + + +void BNILEmulatorSetSyscallHook(BNILEmulator* emu, void* ctxt, bool (*callback)(void*, BNILEmulator*)) +{ + if (callback) + { + emu->object->SetSyscallHook([ctxt, callback](ILEmulator* e) -> bool { + return callback(ctxt, e->GetAPIObject()); + }); + } + else + { + emu->object->SetSyscallHook(nullptr); + } +} + + +void BNILEmulatorSetMemoryReadHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void*, BNILEmulator*, uint64_t, size_t, uint8_t*, size_t)) +{ + if (callback) + { + emu->object->SetMemoryReadHook( + [ctxt, callback](ILEmulator* e, uint64_t addr, size_t size, intx::uint512& value) -> bool { + uint8_t buf[64] = {}; + if (!callback(ctxt, e->GetAPIObject(), addr, size, buf, sizeof(buf))) + return false; + // Convert little-endian buf to uint512 + value = 0; + for (size_t i = sizeof(buf); i > 0; i--) + value = (value << 8) | buf[i - 1]; + return true; + }); + } + else + { + emu->object->SetMemoryReadHook(nullptr); + } +} + + +void BNILEmulatorSetMemoryWriteHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void*, BNILEmulator*, uint64_t, size_t, const uint8_t*, size_t)) +{ + if (callback) + { + emu->object->SetMemoryWriteHook( + [ctxt, callback](ILEmulator* e, uint64_t addr, size_t size, const intx::uint512& value) -> bool { + // Convert uint512 to little-endian byte buffer + uint8_t buf[64]; + intx::uint512 tmp = value; + for (size_t i = 0; i < 64; i++) + { + buf[i] = static_cast(tmp); + tmp >>= 8; + } + return callback(ctxt, e->GetAPIObject(), addr, size, buf, sizeof(buf)); + }); + } + else + { + emu->object->SetMemoryWriteHook(nullptr); + } +} + + +void BNILEmulatorSetPreInstructionHook(BNILEmulator* emu, void* ctxt, + bool (*callback)(void*, BNILEmulator*, size_t)) +{ + if (callback) + { + emu->object->SetPreInstructionHook( + [ctxt, callback](ILEmulator* e, size_t instrIndex) -> bool { + return callback(ctxt, e->GetAPIObject(), instrIndex); + }); + } + else + { + emu->object->SetPreInstructionHook(nullptr); + } +} + + +void BNILEmulatorSetStdoutCallback(BNILEmulator* emu, void* ctxt, + void (*callback)(void*, BNILEmulator*, const char*, size_t)) +{ + if (callback) + { + emu->object->SetStdoutCallback( + [ctxt, callback](ILEmulator* e, const char* data, size_t len) { + callback(ctxt, e->GetAPIObject(), data, len); + }); + } + else + { + emu->object->SetStdoutCallback(nullptr); + } +} + + +void BNILEmulatorSetStdinCallback(BNILEmulator* emu, void* ctxt, + size_t (*callback)(void*, BNILEmulator*, char*, size_t)) +{ + if (callback) + { + emu->object->SetStdinCallback( + [ctxt, callback](ILEmulator* e, char* buf, size_t maxLen) -> size_t { + return callback(ctxt, e->GetAPIObject(), buf, maxLen); + }); + } + else + { + emu->object->SetStdinCallback(nullptr); + } +} + + +void BNILEmulatorMapMemory(BNILEmulator* emu, uint64_t addr, const void* data, size_t len) +{ + emu->object->MapMemory(addr, data, len); +} + + +void BNILEmulatorMapMemoryZero(BNILEmulator* emu, uint64_t addr, size_t len) +{ + emu->object->MapMemory(addr, len); +} + + +void BNILEmulatorMapMemoryNamed(BNILEmulator* emu, uint64_t addr, const void* data, size_t len, const char* name) +{ + emu->object->MapMemory(addr, data, len, name ? name : ""); +} + + +void BNILEmulatorMapMemoryZeroNamed(BNILEmulator* emu, uint64_t addr, size_t len, const char* name) +{ + emu->object->MapMemory(addr, len, name ? name : ""); +} + + +void BNILEmulatorRequestStop(BNILEmulator* emu) +{ + emu->object->RequestStop(); +} + + +void BNILEmulatorReset(BNILEmulator* emu) +{ + emu->object->Reset(); +} + + +BNEmulatorMemoryRegion* BNILEmulatorGetMappedRegions(BNILEmulator* emu, size_t* count) +{ + auto regions = emu->object->GetMappedRegions(); + *count = regions.size(); + if (regions.empty()) + return nullptr; + + auto* result = new BNEmulatorMemoryRegion[regions.size()]; + for (size_t i = 0; i < regions.size(); i++) + { + result[i].start = regions[i].start; + result[i].size = regions[i].size; + result[i].name = BNAllocString(regions[i].name.c_str()); + } + return result; +} + + +void BNFreeEmulatorMemoryRegions(BNEmulatorMemoryRegion* regions, size_t count) +{ + for (size_t i = 0; i < count; i++) + BNFreeString(regions[i].name); + delete[] regions; +} diff --git a/plugins/emulator/core/ilemulator.h b/plugins/emulator/core/ilemulator.h new file mode 100644 index 0000000000..08b069c901 --- /dev/null +++ b/plugins/emulator/core/ilemulator.h @@ -0,0 +1,157 @@ +#pragma once + +#include "binaryninjaapi.h" +#include "ffi_global.h" +#include "refcountobject.h" +#include +#include +#include +#include +#include + +DECLARE_EMULATOR_API_OBJECT(BNILEmulator, ILEmulator); + +namespace BinaryNinjaEmulator +{ + enum class ILEmulatorStopReason : uint8_t + { + Running = 0, + Breakpoint, + InstructionLimit, + Halt, + Error, + CallHook, + SyscallHook, + UndefinedBehavior, + Unimplemented, + UserRequestedStop + }; + + class EmulatorMemory + { + struct Segment + { + uint64_t start; + std::vector data; + std::string name; + + uint64_t End() const { return start + data.size(); } + bool Contains(uint64_t addr) const { return addr >= start && addr < End(); } + }; + + std::vector m_segments; + + // Find the segment containing addr, or nullptr + Segment* FindSegment(uint64_t addr); + const Segment* FindSegment(uint64_t addr) const; + + // Find the lowest-start segment whose start > addr, or nullptr + const Segment* FindNextSegment(uint64_t addr) const; + + public: + EmulatorMemory() = default; + + bool ReadValue(uint64_t addr, size_t size, BNEndianness endian, intx::uint512& result); + bool WriteValue(uint64_t addr, const intx::uint512& value, size_t size, BNEndianness endian); + size_t Read(void* dest, uint64_t addr, size_t len); + size_t Write(uint64_t addr, const void* src, size_t len); + + // Map a region of memory with the given data + void Map(uint64_t addr, const void* data, size_t len, const std::string& name = ""); + + // Map a zero-filled region + void Map(uint64_t addr, size_t len, const std::string& name = ""); + + struct MappedRegion + { + uint64_t start; + uint64_t size; + std::string name; + }; + std::vector GetMappedRegions() const; + + void Reset(); + }; + + class ILEmulator : public EmuRefCountObject + { + IMPLEMENT_EMULATOR_API_OBJECT(BNILEmulator) + + protected: + EmulatorMemory m_memory; + std::function m_callHook; + std::function m_syscallHook; + std::function m_memoryReadHook; + std::function m_memoryWriteHook; + std::function m_preInstructionHook; + std::function m_stdoutCallback; + std::function m_stdinCallback; + std::set m_breakpoints; + size_t m_maxInstructions = 0; + size_t m_instructionsExecuted = 0; + size_t m_instrIndex = 0; + uint64_t m_currentAddress = 0; + ILEmulatorStopReason m_stopReason = ILEmulatorStopReason::Running; + std::string m_stopMessage; + std::atomic m_stopRequested {false}; + + virtual void ExecuteCurrentInstruction() = 0; + virtual size_t GetInstructionCount() const = 0; + virtual uint64_t GetCurrentInstructionAddress() const = 0; + + intx::uint512 ReadMemoryValue(uint64_t addr, size_t size); + void WriteMemoryValue(uint64_t addr, const intx::uint512& value, size_t size); + virtual BNEndianness GetEndianness() const = 0; + + // Stdout/stdin helpers for stubs + void EmitStdout(const std::string& data); + void EmitStdout(const char* data, size_t len); + size_t RequestStdin(char* buf, size_t maxLen); + + public: + ILEmulator(); + virtual ~ILEmulator(); + + // Execution control + ILEmulatorStopReason Run(); + ILEmulatorStopReason Step(); + ILEmulatorStopReason StepN(size_t n); + + // State + size_t GetInstructionIndex() const; + void SetInstructionIndex(size_t index); + uint64_t GetCurrentAddress() const; + ILEmulatorStopReason GetStopReason() const; + const std::string& GetStopMessage() const; + + // Memory + EmulatorMemory& GetMemory(); + size_t ReadMemory(void* dest, uint64_t addr, size_t len); + size_t WriteMemory(uint64_t addr, const void* src, size_t len); + void MapMemory(uint64_t addr, const void* data, size_t len, const std::string& name = ""); + void MapMemory(uint64_t addr, size_t len, const std::string& name = ""); + std::vector GetMappedRegions() const; + + // Breakpoints (by address) + void AddBreakpoint(uint64_t addr); + void RemoveBreakpoint(uint64_t addr); + void ClearBreakpoints(); + + // Limits + void SetMaxInstructions(size_t max); + size_t GetInstructionsExecuted() const; + + // Hooks + void SetCallHook(std::function hook); + void SetSyscallHook(std::function hook); + void SetMemoryReadHook(std::function hook); + void SetMemoryWriteHook(std::function hook); + void SetPreInstructionHook(std::function hook); + void SetStdoutCallback(std::function cb); + void SetStdinCallback(std::function cb); + + void SetStopReason(ILEmulatorStopReason reason, const std::string& msg = ""); + void RequestStop(); + virtual void Reset(); + }; +} diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp new file mode 100644 index 0000000000..660e67f6ef --- /dev/null +++ b/plugins/emulator/core/llilemulator.cpp @@ -0,0 +1,3642 @@ +#include "llilemulator.h" +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" +#include "../api/ffi.h" + +using namespace std; +using namespace BinaryNinja; +using namespace BinaryNinjaEmulator; + + +// ============================================================================ +// LLILEmulator +// ============================================================================ + +LLILEmulator::LLILEmulator(BinaryView* backingView) : + m_view(backingView), m_il(nullptr), m_arch(nullptr) +{ + if (m_view) + m_arch = m_view->GetDefaultArchitecture(); + Ref settings = Settings::Instance(); + m_builtinLibcStubs = settings->Get("emulator.builtinLibcStubs"); + m_logLibcCalls = settings->Get("emulator.logLibcCalls"); + INIT_EMULATOR_API_OBJECT(); +} + + +LLILEmulator::LLILEmulator(LowLevelILFunction* il, BinaryView* backingView) : + m_view(backingView), m_il(il), m_arch(il->GetArchitecture()) +{ + Ref settings = Settings::Instance(); + m_builtinLibcStubs = settings->Get("emulator.builtinLibcStubs"); + m_logLibcCalls = settings->Get("emulator.logLibcCalls"); + INIT_EMULATOR_API_OBJECT(); +} + + +bool LLILEmulator::SetEntryPoint(uint64_t addr) +{ + if (!m_view) + return false; + + Ref func = m_view->GetAnalysisFunction(m_view->GetDefaultPlatform(), addr); + if (!func) + return false; + + Ref targetIL = func->GetLowLevelIL(); + if (!targetIL || targetIL->GetInstructionCount() == 0) + return false; + + m_il = targetIL; + m_arch = targetIL->GetArchitecture(); + m_instrIndex = 0; + m_currentAddress = GetCurrentInstructionAddress(); + m_callStack.clear(); + return true; +} + + +void LLILEmulator::SetEntryPoint(LowLevelILFunction* il, size_t instrIndex) +{ + m_il = il; + m_arch = il->GetArchitecture(); + m_instrIndex = instrIndex; + m_currentAddress = GetCurrentInstructionAddress(); + m_callStack.clear(); +} + + +void LLILEmulator::SetArgument(size_t index, const intx::uint512& value) +{ + if (!m_view || !m_arch) + return; + + Ref platform = m_view->GetDefaultPlatform(); + if (!platform) + return; + + Ref cc = platform->GetDefaultCallingConvention(); + if (!cc) + return; + + auto argRegs = cc->GetIntegerArgumentRegisters(); + if (index < argRegs.size()) + { + SetRegister(argRegs[index], value); + return; + } + + // Stack argument + size_t addrSize = m_arch->GetAddressSize(); + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + size_t stackIndex = index - argRegs.size(); + WriteMemoryValue(spVal + stackIndex * addrSize, value, addrSize); +} + + +void LLILEmulator::SetArguments(const std::vector& values) +{ + for (size_t i = 0; i < values.size(); i++) + SetArgument(i, values[i]); +} + + +intx::uint512 LLILEmulator::MaskToSize(const intx::uint512& value, size_t size) +{ + if (size >= 64) + return value; + if (size == 0) + return 0; + intx::uint512 mask = (intx::uint512(1) << (size * 8)) - 1; + return value & mask; +} + + +intx::uint512 LLILEmulator::SignExtend(const intx::uint512& value, size_t fromSize, size_t toSize) +{ + if (fromSize >= toSize || fromSize == 0) + return value; + + intx::uint512 signBit = intx::uint512(1) << (fromSize * 8 - 1); + intx::uint512 result = MaskToSize(value, fromSize); + if (result & signBit) + { + // Set all bits from fromSize*8 up to toSize*8 + intx::uint512 fromMask = (intx::uint512(1) << (fromSize * 8)) - 1; + intx::uint512 toMask = (toSize >= 64) ? ~intx::uint512(0) : (intx::uint512(1) << (toSize * 8)) - 1; + result |= toMask & ~fromMask; + } + return MaskToSize(result, toSize); +} + + +BNEndianness LLILEmulator::GetEndianness() const +{ + if (m_arch) + return m_arch->GetEndianness(); + return LittleEndian; +} + + +size_t LLILEmulator::GetInstructionCount() const +{ + if (m_il) + return m_il->GetInstructionCount(); + return 0; +} + + +uint64_t LLILEmulator::GetCurrentInstructionAddress() const +{ + if (m_il && m_instrIndex < m_il->GetInstructionCount()) + return m_il->GetInstruction(m_instrIndex).address; + return m_currentAddress; +} + + +intx::uint512 LLILEmulator::GetRegister(uint32_t reg) const +{ + BNRegisterInfo info = m_arch->GetRegisterInfo(reg); + auto it = m_registers.find(info.fullWidthRegister); + intx::uint512 fullValue = (it != m_registers.end()) ? it->second : intx::uint512(0); + + if (reg == info.fullWidthRegister) + return fullValue; + + intx::uint512 extracted = (fullValue >> (info.offset * 8)); + return MaskToSize(extracted, info.size); +} + + +void LLILEmulator::SetRegister(uint32_t reg, const intx::uint512& value) +{ + BNRegisterInfo info = m_arch->GetRegisterInfo(reg); + + if (reg == info.fullWidthRegister) + { + m_registers[reg] = value; + return; + } + + intx::uint512 existing = 0; + auto it = m_registers.find(info.fullWidthRegister); + if (it != m_registers.end()) + existing = it->second; + + intx::uint512 masked = MaskToSize(value, info.size); + + switch (info.extend) + { + case ZeroExtendToFullWidth: + m_registers[info.fullWidthRegister] = masked << (info.offset * 8); + break; + case SignExtendToFullWidth: + { + intx::uint512 signExtended = SignExtend(masked, info.size, m_arch->GetRegisterInfo(info.fullWidthRegister).size); + m_registers[info.fullWidthRegister] = signExtended; + break; + } + case NoExtend: + default: + { + size_t bitOffset = info.offset * 8; + size_t bitWidth = info.size * 8; + intx::uint512 mask = ((intx::uint512(1) << bitWidth) - 1) << bitOffset; + existing &= ~mask; + existing |= (masked << bitOffset); + m_registers[info.fullWidthRegister] = existing; + break; + } + } +} + + +intx::uint512 LLILEmulator::GetTempRegister(uint32_t index) const +{ + auto it = m_tempRegisters.find(index); + return (it != m_tempRegisters.end()) ? it->second : intx::uint512(0); +} + + +void LLILEmulator::SetTempRegister(uint32_t index, const intx::uint512& value) +{ + m_tempRegisters[index] = value; +} + + +const std::unordered_map& LLILEmulator::GetAllTempRegisters() const +{ + return m_tempRegisters; +} + + +uint8_t LLILEmulator::GetFlag(uint32_t flag) const +{ + auto it = m_flags.find(flag); + return (it != m_flags.end()) ? it->second : 0; +} + + +void LLILEmulator::SetFlag(uint32_t flag, uint8_t value) +{ + m_flags[flag] = value ? 1 : 0; +} + + +void LLILEmulator::SetIntrinsicHook(IntrinsicHook hook) +{ + m_intrinsicHook = std::move(hook); +} + + +LowLevelILFunction* LLILEmulator::GetFunction() const +{ + return m_il; +} + + +Architecture* LLILEmulator::GetArchitecture() const +{ + return m_arch; +} + + +void LLILEmulator::Push(const intx::uint512& value, size_t size) +{ + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + spVal -= size; + SetRegister(sp, intx::uint512(spVal)); + WriteMemoryValue(spVal, value, size); +} + + +intx::uint512 LLILEmulator::Pop(size_t size) +{ + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + intx::uint512 value = ReadMemoryValue(spVal, size); + spVal += size; + SetRegister(sp, intx::uint512(spVal)); + return value; +} + + +size_t LLILEmulator::GetCallStackDepth() const +{ + return m_callStack.size(); +} + + +std::vector LLILEmulator::GetCallStack() const +{ + std::vector result; + + // Frame 0 (current): the function currently executing + if (m_il) + { + Ref curFunc = m_il->GetFunction(); + uint64_t funcAddr = curFunc ? curFunc->GetStart() : 0; + uint64_t pc = GetCurrentInstructionAddress(); + result.push_back({funcAddr, pc}); + } + + // Parent frames: walk the call stack from most recent to oldest + for (auto it = m_callStack.rbegin(); it != m_callStack.rend(); ++it) + { + uint64_t funcAddr = 0; + uint64_t returnAddr = 0; + + if (it->il) + { + Ref func = it->il->GetFunction(); + funcAddr = func ? func->GetStart() : 0; + + if (it->returnIndex < it->il->GetInstructionCount()) + returnAddr = it->il->GetInstruction(it->returnIndex).address; + } + + result.push_back({funcAddr, returnAddr}); + } + + return result; +} + + +ILEmulatorStopReason LLILEmulator::StepOver() +{ + size_t targetDepth = m_callStack.size(); + + m_stopReason = ILEmulatorStopReason::Running; + m_stopMessage.clear(); + m_stopRequested.store(false, std::memory_order_relaxed); + + if (m_instrIndex >= GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Halt, "ran off end of IL"); + return m_stopReason; + } + + // Execute one instruction + size_t prevIndex = m_instrIndex; + ExecuteCurrentInstruction(); + m_instructionsExecuted++; + if (m_instrIndex == prevIndex && m_stopReason == ILEmulatorStopReason::Running) + m_instrIndex++; + + // If we didn't enter a deeper function, we're done + if (m_callStack.size() <= targetDepth || m_stopReason != ILEmulatorStopReason::Running) + { + m_currentAddress = GetCurrentInstructionAddress(); + if (m_stopReason == ILEmulatorStopReason::Running) + SetStopReason(ILEmulatorStopReason::Halt); + return m_stopReason; + } + + // Entered a function — run until we return to the original depth. + // Respect breakpoints and other stop conditions inside the callee, + // matching standard debugger step-over semantics. + while (m_stopReason == ILEmulatorStopReason::Running) + { + if (m_callStack.size() <= targetDepth) + break; + + if (m_stopRequested.load(std::memory_order_relaxed)) + { + SetStopReason(ILEmulatorStopReason::UserRequestedStop, "user requested stop"); + break; + } + + if (m_maxInstructions > 0 && m_instructionsExecuted >= m_maxInstructions) + { + SetStopReason(ILEmulatorStopReason::InstructionLimit, "instruction limit reached"); + break; + } + + if (m_instrIndex >= GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Halt, "ran off end of IL"); + break; + } + + m_currentAddress = GetCurrentInstructionAddress(); + + if (m_breakpoints.count(m_currentAddress)) + { + SetStopReason(ILEmulatorStopReason::Breakpoint); + break; + } + + if (m_preInstructionHook && !m_preInstructionHook(this, m_instrIndex)) + { + SetStopReason(ILEmulatorStopReason::Halt, "pre-instruction hook stopped execution"); + break; + } + + prevIndex = m_instrIndex; + ExecuteCurrentInstruction(); + m_instructionsExecuted++; + if (m_instrIndex == prevIndex && m_stopReason == ILEmulatorStopReason::Running) + m_instrIndex++; + } + + m_currentAddress = GetCurrentInstructionAddress(); + + if (m_stopReason == ILEmulatorStopReason::Running) + SetStopReason(ILEmulatorStopReason::Halt); + return m_stopReason; +} + + +bool LLILEmulator::EnterFunction(uint64_t addr, size_t returnIndex) +{ + if (!m_view) + return false; + + // Resolve address to a Function + Ref func = m_view->GetAnalysisFunction(m_view->GetDefaultPlatform(), addr); + if (!func) + return false; + + Ref targetIL = func->GetLowLevelIL(); + if (!targetIL) + return false; + + size_t entryInstr = targetIL->GetInstructionStart(targetIL->GetArchitecture(), addr); + if (entryInstr >= targetIL->GetInstructionCount()) + return false; + + // Save current context (including temp registers, which are per-function) + m_callStack.push_back({m_il, m_arch, returnIndex, std::move(m_tempRegisters)}); + m_tempRegisters.clear(); + + // Switch to callee + m_il = targetIL; + m_arch = targetIL->GetArchitecture(); + m_instrIndex = entryInstr; + + return true; +} + + +bool LLILEmulator::ReturnToCaller() +{ + if (m_callStack.empty()) + return false; + + CallFrame& frame = m_callStack.back(); + m_il = frame.il; + m_arch = frame.arch; + m_instrIndex = frame.returnIndex; + m_tempRegisters = std::move(frame.tempRegisters); + m_callStack.pop_back(); + + return true; +} + + +uint64_t LLILEmulator::GetNativeReturnAddress() const +{ + if (!m_il || m_instrIndex >= m_il->GetInstructionCount()) + return 0; + + uint64_t callAddr = m_il->GetInstruction(m_instrIndex).address; + + // Use the architecture to get the native instruction length + if (m_view && m_arch) + { + uint8_t buf[16]; + size_t bytesRead = m_view->Read(buf, callAddr, sizeof(buf)); + if (bytesRead > 0) + { + InstructionInfo info; + if (m_arch->GetInstructionInfo(buf, callAddr, bytesRead, info) && info.length > 0) + return callAddr + info.length; + } + } + + // Fallback: use the next LLIL instruction's address + if (m_instrIndex + 1 < m_il->GetInstructionCount()) + return m_il->GetInstruction(m_instrIndex + 1).address; + + return callAddr; +} + + +void LLILEmulator::Reset() +{ + ILEmulator::Reset(); + m_registers.clear(); + m_tempRegisters.clear(); + m_flags.clear(); + m_callStack.clear(); + m_heapBumpPtr = HEAP_BASE; + m_heapMapped = false; + m_heapAllocations.clear(); +} + +// ============================================================================ +// State serialization helpers +// ============================================================================ + +static std::string Uint512ToHexString(const intx::uint512& value) +{ + // Extract 64 bytes little-endian, find last non-zero byte for compact output + uint8_t bytes[64]; + intx::uint512 tmp = value; + for (int i = 0; i < 64; i++) + { + bytes[i] = static_cast(tmp); + tmp >>= 8; + } + // Find last significant byte + int last = 63; + while (last > 0 && bytes[last] == 0) + last--; + + // Build hex string (big-endian display, like normal hex) + std::string result = "0x"; + for (int i = last; i >= 0; i--) + result += fmt::format("{:02x}", bytes[i]); + return result; +} + + +static intx::uint512 HexStringToUint512(const std::string& hex) +{ + intx::uint512 result = 0; + const char* p = hex.c_str(); + if (p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) + p += 2; + + while (*p) + { + uint8_t nibble; + if (*p >= '0' && *p <= '9') + nibble = *p - '0'; + else if (*p >= 'a' && *p <= 'f') + nibble = *p - 'a' + 10; + else if (*p >= 'A' && *p <= 'F') + nibble = *p - 'A' + 10; + else + break; + result = (result << 4) | intx::uint512(nibble); + p++; + } + return result; +} + + +static void AddUint64Member(rapidjson::Value& obj, const char* key, uint64_t value, + rapidjson::Document::AllocatorType& alloc) +{ + auto hexStr = fmt::format("0x{:x}", value); + obj.AddMember(rapidjson::Value(key, alloc), rapidjson::Value(hexStr.c_str(), alloc), alloc); +} + + +std::string LLILEmulator::SaveState() const +{ + rapidjson::Document doc(rapidjson::kObjectType); + auto& alloc = doc.GetAllocator(); + + doc.AddMember("version", 1, alloc); + + // Instruction state + doc.AddMember("instrIndex", (uint64_t)m_instrIndex, alloc); + AddUint64Member(doc, "currentAddress", m_currentAddress, alloc); + doc.AddMember("instructionsExecuted", (uint64_t)m_instructionsExecuted, alloc); + + // Registers + { + rapidjson::Value regs(rapidjson::kObjectType); + for (auto& [regId, value] : m_registers) + { + auto key = fmt::format("{}", regId); + auto hexVal = Uint512ToHexString(value); + regs.AddMember(rapidjson::Value(key.c_str(), alloc), rapidjson::Value(hexVal.c_str(), alloc), alloc); + } + doc.AddMember("registers", regs, alloc); + } + + // Temp registers + { + rapidjson::Value temps(rapidjson::kObjectType); + for (auto& [index, value] : m_tempRegisters) + { + auto key = fmt::format("{}", index); + auto hexVal = Uint512ToHexString(value); + temps.AddMember(rapidjson::Value(key.c_str(), alloc), rapidjson::Value(hexVal.c_str(), alloc), alloc); + } + doc.AddMember("tempRegisters", temps, alloc); + } + + // Flags + { + rapidjson::Value flags(rapidjson::kObjectType); + for (auto& [flagId, value] : m_flags) + { + auto key = fmt::format("{}", flagId); + rapidjson::Value keyVal(key.c_str(), alloc); + rapidjson::Value valVal((int)value); + flags.AddMember(keyVal, valVal, alloc); + } + doc.AddMember("flags", flags, alloc); + } + + // Memory segments + { + rapidjson::Value memory(rapidjson::kArrayType); + auto regions = m_memory.GetMappedRegions(); + for (auto& region : regions) + { + rapidjson::Value seg(rapidjson::kObjectType); + AddUint64Member(seg, "start", region.start, alloc); + AddUint64Member(seg, "size", region.size, alloc); + seg.AddMember("name", rapidjson::Value(region.name.c_str(), alloc), alloc); + + // Read the data and base64 encode + std::vector data(region.size); + const_cast(m_memory).Read(data.data(), region.start, region.size); + DataBuffer buf(data.data(), data.size()); + auto b64 = buf.ToBase64(); + seg.AddMember("data", rapidjson::Value(b64.c_str(), alloc), alloc); + + memory.PushBack(seg, alloc); + } + doc.AddMember("memory", memory, alloc); + } + + // Breakpoints + { + rapidjson::Value bps(rapidjson::kArrayType); + for (auto addr : m_breakpoints) + { + auto hexAddr = fmt::format("0x{:x}", addr); + bps.PushBack(rapidjson::Value(hexAddr.c_str(), alloc), alloc); + } + doc.AddMember("breakpoints", bps, alloc); + } + + // Heap state + { + rapidjson::Value heap(rapidjson::kObjectType); + AddUint64Member(heap, "bumpPtr", m_heapBumpPtr, alloc); + heap.AddMember("mapped", m_heapMapped, alloc); + + rapidjson::Value allocations(rapidjson::kObjectType); + for (auto& [addr, size] : m_heapAllocations) + { + auto key = fmt::format("0x{:x}", addr); + rapidjson::Value keyVal(key.c_str(), alloc); + rapidjson::Value sizeVal((uint64_t)size); + allocations.AddMember(keyVal, sizeVal, alloc); + } + heap.AddMember("allocations", allocations, alloc); + doc.AddMember("heap", heap, alloc); + } + + // Call stack + { + rapidjson::Value stack(rapidjson::kArrayType); + for (auto& frame : m_callStack) + { + rapidjson::Value f(rapidjson::kObjectType); + // Store function start address (resolve back on load) + uint64_t funcAddr = 0; + if (frame.il) + { + auto func = frame.il->GetFunction(); + if (func) + funcAddr = func->GetStart(); + } + AddUint64Member(f, "funcAddr", funcAddr, alloc); + f.AddMember("returnIndex", (uint64_t)frame.returnIndex, alloc); + + rapidjson::Value temps(rapidjson::kObjectType); + for (auto& [index, value] : frame.tempRegisters) + { + auto key = fmt::format("{}", index); + auto hexVal = Uint512ToHexString(value); + temps.AddMember(rapidjson::Value(key.c_str(), alloc), rapidjson::Value(hexVal.c_str(), alloc), alloc); + } + f.AddMember("temps", temps, alloc); + stack.PushBack(f, alloc); + } + doc.AddMember("callStack", stack, alloc); + } + + // Settings + { + rapidjson::Value settings(rapidjson::kObjectType); + settings.AddMember("builtinLibcStubs", m_builtinLibcStubs, alloc); + settings.AddMember("logLibcCalls", m_logLibcCalls, alloc); + settings.AddMember("nopUnknownExternals", m_nopUnknownExternals, alloc); + doc.AddMember("settings", settings, alloc); + } + + // Stop state + doc.AddMember("stopReason", (int)m_stopReason, alloc); + doc.AddMember("stopMessage", rapidjson::Value(m_stopMessage.c_str(), alloc), alloc); + + // Serialize to string + rapidjson::StringBuffer sb; + rapidjson::Writer writer(sb); + doc.Accept(writer); + return std::string(sb.GetString(), sb.GetSize()); +} + + +bool LLILEmulator::LoadState(const std::string& json, BinaryView* view) +{ + rapidjson::Document doc; + doc.Parse(json.c_str()); + if (doc.HasParseError() || !doc.IsObject()) + return false; + + if (!doc.HasMember("version") || doc["version"].GetInt() != 1) + return false; + + // Instruction state + if (doc.HasMember("instrIndex")) + m_instrIndex = doc["instrIndex"].GetUint64(); + if (doc.HasMember("currentAddress")) + m_currentAddress = strtoull(doc["currentAddress"].GetString() + 2, nullptr, 16); + if (doc.HasMember("instructionsExecuted")) + m_instructionsExecuted = doc["instructionsExecuted"].GetUint64(); + + // Registers + m_registers.clear(); + if (doc.HasMember("registers") && doc["registers"].IsObject()) + { + for (auto it = doc["registers"].MemberBegin(); it != doc["registers"].MemberEnd(); ++it) + { + uint32_t regId = std::stoul(it->name.GetString()); + m_registers[regId] = HexStringToUint512(it->value.GetString()); + } + } + + // Temp registers + m_tempRegisters.clear(); + if (doc.HasMember("tempRegisters") && doc["tempRegisters"].IsObject()) + { + for (auto it = doc["tempRegisters"].MemberBegin(); it != doc["tempRegisters"].MemberEnd(); ++it) + { + uint32_t index = std::stoul(it->name.GetString()); + m_tempRegisters[index] = HexStringToUint512(it->value.GetString()); + } + } + + // Flags + m_flags.clear(); + if (doc.HasMember("flags") && doc["flags"].IsObject()) + { + for (auto it = doc["flags"].MemberBegin(); it != doc["flags"].MemberEnd(); ++it) + { + uint32_t flagId = std::stoul(it->name.GetString()); + m_flags[flagId] = (uint8_t)it->value.GetInt(); + } + } + + // Memory + m_memory.Reset(); + if (doc.HasMember("memory") && doc["memory"].IsArray()) + { + for (auto& seg : doc["memory"].GetArray()) + { + if (!seg.IsObject() || !seg.HasMember("start") || !seg.HasMember("data")) + continue; + + uint64_t start = strtoull(seg["start"].GetString() + 2, nullptr, 16); + std::string name = seg.HasMember("name") ? seg["name"].GetString() : ""; + string b64 = seg["data"].GetString(); + DataBuffer data = DataBuffer::FromBase64(b64); + m_memory.Map(start, data.GetData(), data.GetLength(), name); + } + } + + // Breakpoints + m_breakpoints.clear(); + if (doc.HasMember("breakpoints") && doc["breakpoints"].IsArray()) + { + for (auto& bp : doc["breakpoints"].GetArray()) + { + if (bp.IsString()) + m_breakpoints.insert(strtoull(bp.GetString() + 2, nullptr, 16)); + } + } + + // Heap state + if (doc.HasMember("heap") && doc["heap"].IsObject()) + { + auto& heap = doc["heap"]; + if (heap.HasMember("bumpPtr")) + m_heapBumpPtr = strtoull(heap["bumpPtr"].GetString() + 2, nullptr, 16); + if (heap.HasMember("mapped")) + m_heapMapped = heap["mapped"].GetBool(); + + m_heapAllocations.clear(); + if (heap.HasMember("allocations") && heap["allocations"].IsObject()) + { + for (auto it = heap["allocations"].MemberBegin(); it != heap["allocations"].MemberEnd(); ++it) + { + uint64_t addr = strtoull(it->name.GetString() + 2, nullptr, 16); + m_heapAllocations[addr] = it->value.GetUint64(); + } + } + } + + // Call stack + m_callStack.clear(); + if (doc.HasMember("callStack") && doc["callStack"].IsArray() && view) + { + for (auto& frame : doc["callStack"].GetArray()) + { + if (!frame.IsObject()) + continue; + + CallFrame cf; + cf.arch = nullptr; + + if (frame.HasMember("funcAddr")) + { + uint64_t funcAddr = strtoull(frame["funcAddr"].GetString() + 2, nullptr, 16); + auto func = view->GetAnalysisFunction(view->GetDefaultPlatform(), funcAddr); + if (func) + { + cf.il = func->GetLowLevelIL(); + cf.arch = func->GetArchitecture(); + } + } + + cf.returnIndex = frame.HasMember("returnIndex") ? frame["returnIndex"].GetUint64() : 0; + + if (frame.HasMember("temps") && frame["temps"].IsObject()) + { + for (auto it = frame["temps"].MemberBegin(); it != frame["temps"].MemberEnd(); ++it) + { + uint32_t index = std::stoul(it->name.GetString()); + cf.tempRegisters[index] = HexStringToUint512(it->value.GetString()); + } + } + + m_callStack.push_back(std::move(cf)); + } + } + + // Resolve current function IL from the entry in the call stack or from the current address + if (view) + { + auto func = view->GetAnalysisFunction(view->GetDefaultPlatform(), m_currentAddress); + if (func) + { + m_il = func->GetLowLevelIL(); + m_arch = func->GetArchitecture(); + } + } + + // Settings + if (doc.HasMember("settings") && doc["settings"].IsObject()) + { + auto& settings = doc["settings"]; + if (settings.HasMember("builtinLibcStubs")) + m_builtinLibcStubs = settings["builtinLibcStubs"].GetBool(); + if (settings.HasMember("logLibcCalls")) + m_logLibcCalls = settings["logLibcCalls"].GetBool(); + if (settings.HasMember("nopUnknownExternals")) + m_nopUnknownExternals = settings["nopUnknownExternals"].GetBool(); + } + + // Stop state + if (doc.HasMember("stopReason")) + m_stopReason = (ILEmulatorStopReason)doc["stopReason"].GetInt(); + if (doc.HasMember("stopMessage")) + m_stopMessage = doc["stopMessage"].GetString(); + + m_view = view; + + return true; +} + + +// ============================================================================ +// EvalExpr — Expression evaluation +// ============================================================================ + +intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) +{ + if (m_stopReason != ILEmulatorStopReason::Running) + return 0; + + size_t sz = expr.size; + + switch (expr.operation) + { + // --- Constants --- + case LLIL_CONST: + case LLIL_CONST_PTR: + return MaskToSize(intx::uint512((uint64_t)expr.GetRawOperandAsInteger(0)), sz); + + case LLIL_EXTERN_PTR: + return MaskToSize(intx::uint512((uint64_t)(expr.GetRawOperandAsInteger(0) + expr.GetRawOperandAsInteger(1))), sz); + + case LLIL_FLOAT_CONST: + return intx::uint512((uint64_t)expr.GetRawOperandAsInteger(0)); + + // --- Registers --- + case LLIL_REG: + { + uint32_t reg = expr.GetRawOperandAsRegister(0); + if (LLIL_REG_IS_TEMP(reg)) + return MaskToSize(GetTempRegister(LLIL_GET_TEMP_REG_INDEX(reg)), sz); + return MaskToSize(GetRegister(reg), sz); + } + + case LLIL_REG_SPLIT: + { + uint32_t hi = expr.GetRawOperandAsRegister(0); + uint32_t lo = expr.GetRawOperandAsRegister(1); + intx::uint512 hiVal = LLIL_REG_IS_TEMP(hi) + ? GetTempRegister(LLIL_GET_TEMP_REG_INDEX(hi)) : GetRegister(hi); + intx::uint512 loVal = LLIL_REG_IS_TEMP(lo) + ? GetTempRegister(LLIL_GET_TEMP_REG_INDEX(lo)) : GetRegister(lo); + size_t loSize = LLIL_REG_IS_TEMP(lo) ? sz / 2 : m_arch->GetRegisterInfo(lo).size; + return MaskToSize((hiVal << (loSize * 8)) | loVal, sz); + } + + case LLIL_FLAG: + return intx::uint512(GetFlag(expr.GetRawOperandAsRegister(0))); + + case LLIL_FLAG_BIT: + { + uint8_t flagVal = GetFlag(expr.GetRawOperandAsRegister(0)); + size_t bitIndex = expr.GetRawOperandAsIndex(1); + return intx::uint512((flagVal >> bitIndex) & 1); + } + + // --- Two-operand arithmetic --- + case LLIL_ADD: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left + right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_ADD, true}; + return result; + } + + case LLIL_SUB: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left - right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_SUB, true}; + return result; + } + + case LLIL_AND: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left & right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_AND, true}; + return result; + } + + case LLIL_OR: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left | right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_OR, true}; + return result; + } + + case LLIL_XOR: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left ^ right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_XOR, true}; + return result; + } + + case LLIL_LSL: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + size_t shiftAmt = static_cast(right) & (sz * 8 - 1); + intx::uint512 result = MaskToSize(left << shiftAmt, sz); + m_lastArithmetic = {MaskToSize(left, sz), right, result, sz, LLIL_LSL, true}; + return result; + } + + case LLIL_LSR: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + size_t shiftAmt = static_cast(right) & (sz * 8 - 1); + intx::uint512 result = MaskToSize(left >> shiftAmt, sz); + m_lastArithmetic = {left, right, result, sz, LLIL_LSR, true}; + return result; + } + + case LLIL_ASR: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 maskedLeft = MaskToSize(left, sz); + // Sign-extend to full 512 bits, then arithmetic shift right + intx::uint512 extended = SignExtend(maskedLeft, sz, 64); + size_t shiftAmt = static_cast(right) & (sz * 8 - 1); + // Logical shift on the sign-extended value, then mask back + intx::uint512 result = MaskToSize(extended >> shiftAmt, sz); + m_lastArithmetic = {maskedLeft, right, result, sz, LLIL_ASR, true}; + return result; + } + + case LLIL_ROL: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + size_t bits = sz * 8; + size_t shift = static_cast(static_cast(right) % bits); + intx::uint512 result = (shift == 0) ? left : MaskToSize((left << shift) | (left >> (bits - shift)), sz); + m_lastArithmetic = {left, right, result, sz, LLIL_ROL, true}; + return result; + } + + case LLIL_ROR: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + size_t bits = sz * 8; + size_t shift = static_cast(static_cast(right) % bits); + intx::uint512 result = (shift == 0) ? left : MaskToSize((left >> shift) | (left << (bits - shift)), sz); + m_lastArithmetic = {left, right, result, sz, LLIL_ROR, true}; + return result; + } + + case LLIL_MUL: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 result = MaskToSize(left * right, sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_MUL, true}; + return result; + } + + // --- With carry --- + case LLIL_ADC: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)); + intx::uint512 result = MaskToSize(left + right + (carry & 1), sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right + (carry & 1), sz), result, sz, LLIL_ADC, true}; + return result; + } + + case LLIL_SBB: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)); + intx::uint512 result = MaskToSize(left - right - (carry & 1), sz); + m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right + (carry & 1), sz), result, sz, LLIL_SBB, true}; + return result; + } + + case LLIL_RLC: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; + size_t bits = sz * 8; + size_t shift = static_cast(static_cast(right) % (bits + 1)); + intx::uint512 extended = (left << 1) | carry; + return MaskToSize((extended << shift) | (left >> (bits - shift)), sz); + } + + case LLIL_RRC: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; + size_t bits = sz * 8; + size_t shift = static_cast(static_cast(right) % (bits + 1)); + intx::uint512 extended = left | (carry << bits); + return MaskToSize(extended >> shift, sz); + } + + // --- Division --- + case LLIL_DIVU: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return MaskToSize(left / right, sz); + } + + case LLIL_DIVS: + { + // For signed division, work with 64-bit values since intx doesn't provide signed types + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return MaskToSize(intx::uint512(static_cast(left / right)), sz); + } + + case LLIL_MODU: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return MaskToSize(left % right, sz); + } + + case LLIL_MODS: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return MaskToSize(intx::uint512(static_cast(left % right)), sz); + } + + // --- Double-precision --- + case LLIL_MULU_DP: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); + return MaskToSize(left * right, sz); + } + + case LLIL_MULS_DP: + { + // Signed multiply double-precision: sign extend operands, multiply, mask + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); + // Sign-extend both operands to full width as two's-complement intx values and + // multiply; the low bits of the product are correct regardless of sign. This + // avoids __int128, which MSVC does not support. + intx::uint512 l(static_cast(left)); + intx::uint512 r(static_cast(right)); + if (left < 0) + l |= ~intx::uint512(0) << 64; + if (right < 0) + r |= ~intx::uint512(0) << 64; + return MaskToSize(l * r, sz); + } + + case LLIL_DIVU_DP: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return MaskToSize(left / right, sz / 2); + } + + case LLIL_DIVS_DP: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return MaskToSize(intx::uint512(static_cast(left / right)), sz / 2); + } + + case LLIL_MODU_DP: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return MaskToSize(left % right, sz / 2); + } + + case LLIL_MODS_DP: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); + if (right == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return MaskToSize(intx::uint512(static_cast(left % right)), sz / 2); + } + + // --- Unary --- + case LLIL_NEG: + { + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 result = MaskToSize(~val + 1, sz); + m_lastArithmetic = {intx::uint512(0), val, result, sz, LLIL_NEG, true}; + return result; + } + + case LLIL_NOT: + { + intx::uint512 val = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 result = MaskToSize(~val, sz); + m_lastArithmetic = {val, intx::uint512(0), result, sz, LLIL_NOT, true}; + return result; + } + + case LLIL_SX: + { + LowLevelILInstruction src = expr.GetRawOperandAsExpr(0); + intx::uint512 val = EvalExpr(src); + return MaskToSize(SignExtend(val, src.size, sz), sz); + } + + case LLIL_ZX: + { + LowLevelILInstruction src = expr.GetRawOperandAsExpr(0); + return MaskToSize(EvalExpr(src), sz); + } + + case LLIL_LOW_PART: + return MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + + // --- Comparisons --- + case LLIL_CMP_E: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return MaskToSize(left, sz) == MaskToSize(right, sz) ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_NE: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return MaskToSize(left, sz) != MaskToSize(right, sz) ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_SLT: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + return left < right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_ULT: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + return left < right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_SLE: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + return left <= right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_ULE: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + return left <= right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_SGE: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + return left >= right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_UGE: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + return left >= right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_SGT: + { + int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); + int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); + return left > right ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_CMP_UGT: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + return left > right ? intx::uint512(1) : intx::uint512(0); + } + + // --- Memory --- + case LLIL_LOAD: + { + uint64_t addr = static_cast(EvalExpr(expr.GetRawOperandAsExpr(0))); + return ReadMemoryValue(addr, sz); + } + + case LLIL_POP: + return Pop(sz); + + // --- Misc --- + case LLIL_BOOL_TO_INT: + return EvalExpr(expr.GetRawOperandAsExpr(0)) != 0 ? intx::uint512(1) : intx::uint512(0); + + case LLIL_TEST_BIT: + { + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return (left & right) != 0 ? intx::uint512(1) : intx::uint512(0); + } + + case LLIL_ADD_OVERFLOW: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 result = left + right; + return MaskToSize(result, sz) < left ? intx::uint512(1) : intx::uint512(0); + } + + // --- Flag conditions (Lifted IL) --- + case LLIL_FLAG_COND: + { + BNLowLevelILFlagCondition cond = expr.GetRawOperandAsFlagCondition(0); + uint32_t semClass = expr.GetRawOperandAsRegister(1); + return EvalFlagCondition(cond, semClass); + } + + case LLIL_FLAG_GROUP: + { + uint32_t semGroup = expr.GetRawOperandAsRegister(0); + auto condMap = m_arch->GetFlagConditionsForSemanticFlagGroup(semGroup); + if (!condMap.empty()) + return EvalFlagCondition(condMap.begin()->second, condMap.begin()->first); + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unknown flag group {}", semGroup)); + return 0; + } + + case LLIL_UNDEF: + return 0; + + case LLIL_UNIMPL: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unimplemented IL expression at 0x{:x}", expr.address)); + return 0; + + case LLIL_UNIMPL_MEM: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unimplemented memory IL expression at 0x{:x}", expr.address)); + return 0; + + default: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unhandled IL expression operation {} at 0x{:x}", + (int)expr.operation, expr.address)); + return 0; + } +} + + +// ============================================================================ +// Flag condition evaluation +// ============================================================================ + +intx::uint512 LLILEmulator::EvalFlagCondition(BNLowLevelILFlagCondition cond, uint32_t semClass) +{ + auto requiredFlags = m_arch->GetFlagsRequiredForFlagCondition(cond, semClass); + + uint8_t z = 0, s = 0, c = 0, o = 0; + for (uint32_t flag : requiredFlags) + { + uint8_t val = GetFlag(flag); + BNFlagRole role = m_arch->GetFlagRole(flag, semClass); + switch (role) + { + case ZeroFlagRole: + z = val; + break; + case NegativeSignFlagRole: + s = val; + break; + case PositiveSignFlagRole: + s = val ? 0 : 1; + break; + case CarryFlagRole: + case CarryFlagWithInvertedSubtractRole: + c = val; + break; + case OverflowFlagRole: + o = val; + break; + default: + break; + } + } + + switch (cond) + { + case LLFC_E: return z ? 1 : 0; + case LLFC_NE: return z ? 0 : 1; + case LLFC_SLT: return (s != o) ? 1 : 0; + case LLFC_ULT: return c ? 1 : 0; + case LLFC_SLE: return (z || (s != o)) ? 1 : 0; + case LLFC_ULE: return (c || z) ? 1 : 0; + case LLFC_SGE: return (s == o) ? 1 : 0; + case LLFC_UGE: return c ? 0 : 1; + case LLFC_SGT: return (!z && (s == o)) ? 1 : 0; + case LLFC_UGT: return (!c && !z) ? 1 : 0; + case LLFC_NEG: return s ? 1 : 0; + case LLFC_POS: return s ? 0 : 1; + case LLFC_O: return o ? 1 : 0; + case LLFC_NO: return o ? 0 : 1; + default: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unhandled flag condition {}", (int)cond)); + return 0; + } +} + + +// ============================================================================ +// Flag computation for non-lifted LLIL (flag write types on SET_REG) +// ============================================================================ + +uint8_t LLILEmulator::ComputeFlagForRole(BNFlagRole role) const +{ + const auto& ctx = m_lastArithmetic; + intx::uint512 signBit = (ctx.size > 0 && ctx.size <= 64) ? (intx::uint512(1) << (ctx.size * 8 - 1)) : intx::uint512(0); + bool isAdd = (ctx.operation == LLIL_ADD || ctx.operation == LLIL_ADC); + bool isSub = (ctx.operation == LLIL_SUB || ctx.operation == LLIL_SBB || ctx.operation == LLIL_NEG); + + switch (role) + { + case ZeroFlagRole: + return ctx.result == 0 ? 1 : 0; + + case NegativeSignFlagRole: + return (ctx.result & signBit) != 0 ? 1 : 0; + + case PositiveSignFlagRole: + return (ctx.result & signBit) != 0 ? 0 : 1; + + case CarryFlagRole: + if (isAdd) + return ctx.result < ctx.left ? 1 : 0; + if (isSub) + return ctx.left < ctx.right ? 1 : 0; + return 0; + + case CarryFlagWithInvertedSubtractRole: + if (isSub) + return ctx.left >= ctx.right ? 1 : 0; + if (isAdd) + return ctx.result < ctx.left ? 1 : 0; + return 0; + + case OverflowFlagRole: + if (isAdd) + { + // Overflow if both operands same sign but result different sign + return (uint8_t)(((~(ctx.left ^ ctx.right)) & (ctx.left ^ ctx.result) & signBit) != 0 ? 1 : 0); + } + if (isSub) + { + // Overflow if operands different sign and result differs from left + return (uint8_t)((((ctx.left ^ ctx.right)) & (ctx.left ^ ctx.result) & signBit) != 0 ? 1 : 0); + } + return 0; + + case HalfCarryFlagRole: + if (isAdd) + return ((ctx.left & intx::uint512(0xf)) + (ctx.right & intx::uint512(0xf))) > intx::uint512(0xf) ? 1 : 0; + if (isSub) + return (ctx.left & intx::uint512(0xf)) < (ctx.right & intx::uint512(0xf)) ? 1 : 0; + return 0; + + case EvenParityFlagRole: + { + uint8_t byte = static_cast(ctx.result); + byte ^= byte >> 4; + byte ^= byte >> 2; + byte ^= byte >> 1; + return (byte & 1) ? 0 : 1; + } + + case OddParityFlagRole: + { + uint8_t byte = static_cast(ctx.result); + byte ^= byte >> 4; + byte ^= byte >> 2; + byte ^= byte >> 1; + return byte & 1; + } + + default: + return 0; + } +} + + +void LLILEmulator::WriteFlags(uint32_t flagWriteType) +{ + if (!m_lastArithmetic.valid) + return; + + auto flagsWritten = m_arch->GetFlagsWrittenByFlagWriteType(flagWriteType); + uint32_t semClass = m_arch->GetSemanticClassForFlagWriteType(flagWriteType); + + for (uint32_t flag : flagsWritten) + { + BNFlagRole role = m_arch->GetFlagRole(flag, semClass); + SetFlag(flag, ComputeFlagForRole(role)); + } +} + + +// ============================================================================ +// ExecInstruction — Side effects +// ============================================================================ + +void LLILEmulator::ExecuteCurrentInstruction() +{ + LowLevelILInstruction instr = m_il->GetInstruction(m_instrIndex); + m_currentAddress = instr.address; + + switch (instr.operation) + { + case LLIL_NOP: + break; + + case LLIL_SET_REG: + { + uint32_t reg = instr.GetRawOperandAsRegister(0); + m_lastArithmetic.valid = false; + intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + if (LLIL_REG_IS_TEMP(reg)) + SetTempRegister(LLIL_GET_TEMP_REG_INDEX(reg), val); + else + SetRegister(reg, val); + if (instr.flags != 0) + WriteFlags(instr.flags); + break; + } + + case LLIL_SET_REG_SPLIT: + { + uint32_t hi = instr.GetRawOperandAsRegister(0); + uint32_t lo = instr.GetRawOperandAsRegister(1); + m_lastArithmetic.valid = false; + intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(2)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + size_t loSize; + if (LLIL_REG_IS_TEMP(lo)) + { + loSize = instr.size / 2; + SetTempRegister(LLIL_GET_TEMP_REG_INDEX(lo), MaskToSize(val, loSize)); + } + else + { + BNRegisterInfo loInfo = m_arch->GetRegisterInfo(lo); + loSize = loInfo.size; + SetRegister(lo, MaskToSize(val, loSize)); + } + + intx::uint512 hiVal = val >> (loSize * 8); + if (LLIL_REG_IS_TEMP(hi)) + SetTempRegister(LLIL_GET_TEMP_REG_INDEX(hi), hiVal); + else + SetRegister(hi, hiVal); + + if (instr.flags != 0) + WriteFlags(instr.flags); + break; + } + + case LLIL_SET_FLAG: + { + uint32_t flag = instr.GetRawOperandAsRegister(0); + intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + SetFlag(flag, static_cast(static_cast(val) & 1)); + break; + } + + case LLIL_STORE: + { + uint64_t addr = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + WriteMemoryValue(addr, val, instr.size); + break; + } + + case LLIL_PUSH: + { + intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(0)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + Push(val, instr.size); + break; + } + + case LLIL_GOTO: + { + size_t target = instr.GetRawOperandAsIndex(0); + m_instrIndex = target; + return; + } + + case LLIL_IF: + { + intx::uint512 cond = EvalExpr(instr.GetRawOperandAsExpr(0)); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + size_t target = (cond != 0) ? instr.GetRawOperandAsIndex(1) : instr.GetRawOperandAsIndex(2); + m_instrIndex = target; + return; + } + + case LLIL_JUMP: + { + uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + size_t target = m_il->GetInstructionStart(m_arch, dest); + if (target >= m_il->GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Error, + fmt::format("jump to unmapped address 0x{:x}", dest)); + return; + } + m_instrIndex = target; + return; + } + + case LLIL_JUMP_TO: + { + uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + size_t target = m_il->GetInstructionStart(m_arch, dest); + if (target >= m_il->GetInstructionCount()) + { + SetStopReason(ILEmulatorStopReason::Error, + fmt::format("jump_to unmapped address 0x{:x}", dest)); + return; + } + m_instrIndex = target; + return; + } + + case LLIL_CALL: + { + uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + // 1) Try built-in stubs first + if (m_builtinLibcStubs && HandleBuiltinCall(dest)) + break; + + // 2) Let the hook handle it (e.g., stub a library call) + if (m_callHook && m_callHook(this, dest)) + break; + + // 3) Try to enter the callee's LLIL + { + // Compute return address before EnterFunction changes m_il/m_arch + uint64_t returnAddr = GetNativeReturnAddress(); + size_t addrSize = m_arch->GetAddressSize(); + uint32_t lr = m_arch->GetLinkRegister(); + + if (EnterFunction(dest, m_instrIndex + 1)) + { + if (lr != BN_INVALID_REGISTER) + SetRegister(lr, intx::uint512(returnAddr)); + else + Push(intx::uint512(returnAddr), addrSize); + return; + } + } + + // 4) Treat unknown external calls as no-op if enabled + if (HandleUnknownCall(dest)) + break; + + // 5) Cannot resolve — stop + SetStopReason(ILEmulatorStopReason::CallHook, + fmt::format("call to 0x{:x}", dest)); + return; + } + + case LLIL_CALL_STACK_ADJUST: + { + uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + if (m_builtinLibcStubs && HandleBuiltinCall(dest)) + { + int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); + break; + } + + if (m_callHook && m_callHook(this, dest)) + { + int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); + break; + } + + { + uint64_t returnAddr = GetNativeReturnAddress(); + size_t addrSize = m_arch->GetAddressSize(); + uint32_t lr = m_arch->GetLinkRegister(); + + if (EnterFunction(dest, m_instrIndex + 1)) + { + if (lr != BN_INVALID_REGISTER) + SetRegister(lr, intx::uint512(returnAddr)); + else + Push(intx::uint512(returnAddr), addrSize); + return; + } + } + + if (HandleUnknownCall(dest)) + { + int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); + break; + } + + SetStopReason(ILEmulatorStopReason::CallHook, + fmt::format("call to 0x{:x}", dest)); + return; + } + + case LLIL_TAILCALL: + { + uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + if (m_builtinLibcStubs && HandleBuiltinCall(dest)) + break; + + if (m_callHook && m_callHook(this, dest)) + break; + + // Tailcall: don't push a frame, replace current function + if (m_view) + { + Ref func = m_view->GetAnalysisFunction( + m_view->GetDefaultPlatform(), dest); + if (func) + { + Ref targetIL = func->GetLowLevelIL(); + if (targetIL) + { + size_t entry = targetIL->GetInstructionStart( + targetIL->GetArchitecture(), dest); + if (entry < targetIL->GetInstructionCount()) + { + m_il = targetIL; + m_arch = targetIL->GetArchitecture(); + m_instrIndex = entry; + return; + } + } + } + } + + if (HandleUnknownCall(dest)) + break; + + SetStopReason(ILEmulatorStopReason::CallHook, + fmt::format("tailcall to 0x{:x}", dest)); + return; + } + + case LLIL_RET: + { + // Evaluate the return address expression for side effects + // (pops RA from stack on x86, reads LR on ARM — no-op) + EvalExpr(instr.GetRawOperandAsExpr(0)); + + if (ReturnToCaller()) + return; + + // Top-level return — halt + SetStopReason(ILEmulatorStopReason::Halt, "return"); + return; + } + + case LLIL_NORET: + SetStopReason(ILEmulatorStopReason::Halt, "noreturn"); + return; + + case LLIL_SYSCALL: + { + if (m_syscallHook) + { + if (m_syscallHook(this)) + break; + } + SetStopReason(ILEmulatorStopReason::SyscallHook, "syscall"); + return; + } + + case LLIL_BP: + SetStopReason(ILEmulatorStopReason::Halt, "breakpoint instruction"); + return; + + case LLIL_TRAP: + SetStopReason(ILEmulatorStopReason::Halt, + fmt::format("trap {}", instr.GetRawOperandAsInteger(0))); + return; + + case LLIL_INTRINSIC: + { + if (m_intrinsicHook) + { + uint32_t intrinsic = instr.GetRawOperandAsRegister(2); + LowLevelILInstruction paramExpr = instr.GetRawOperandAsExpr(3); + LowLevelILInstructionList paramList = paramExpr.GetRawOperandAsExprList(0); + std::vector params; + for (auto& p : paramList) + { + params.push_back(static_cast(EvalExpr(p))); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + } + + std::vector> outputs; + if (m_intrinsicHook(this, intrinsic, params, outputs)) + { + for (auto& [reg, val] : outputs) + SetRegister(reg, intx::uint512(val)); + break; + } + } + + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("intrinsic at 0x{:x}", instr.address)); + return; + } + + case LLIL_UNDEF: + SetStopReason(ILEmulatorStopReason::UndefinedBehavior, + fmt::format("undefined behavior at 0x{:x}", instr.address)); + return; + + case LLIL_UNIMPL: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unimplemented instruction at 0x{:x}", instr.address)); + return; + + case LLIL_UNIMPL_MEM: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unimplemented memory instruction at 0x{:x}", instr.address)); + return; + + default: + SetStopReason(ILEmulatorStopReason::Unimplemented, + fmt::format("unhandled IL instruction operation {} at 0x{:x}", + (int)instr.operation, instr.address)); + return; + } +} + + +// ============================================================================ +// Built-in libc stub settings +// ============================================================================ + +void LLILEmulator::SetBuiltinLibcStubsEnabled(bool enabled) +{ + m_builtinLibcStubs = enabled; +} + + +bool LLILEmulator::IsBuiltinLibcStubsEnabled() const +{ + return m_builtinLibcStubs; +} + + +void LLILEmulator::SetLogLibcCalls(bool enabled) +{ + m_logLibcCalls = enabled; +} + + +bool LLILEmulator::IsLogLibcCalls() const +{ + return m_logLibcCalls; +} + + +void LLILEmulator::SetNopUnknownExternals(bool enabled) +{ + m_nopUnknownExternals = enabled; +} + + +bool LLILEmulator::IsNopUnknownExternals() const +{ + return m_nopUnknownExternals; +} + + +// ============================================================================ +// Built-in libc stub helpers +// ============================================================================ + +void LLILEmulator::EnsureHeapMapped() +{ + if (!m_heapMapped) + { + MapMemory(HEAP_BASE, HEAP_SIZE, "heap"); + m_heapMapped = true; + } +} + + +uint64_t LLILEmulator::ReadArgument(size_t index) +{ + if (!m_view) + return 0; + + Ref platform = m_view->GetDefaultPlatform(); + if (!platform) + return 0; + + Ref cc = platform->GetDefaultCallingConvention(); + if (!cc) + return 0; + + auto argRegs = cc->GetIntegerArgumentRegisters(); + size_t addrSize = m_arch->GetAddressSize(); + + if (index < argRegs.size()) + return static_cast(GetRegister(argRegs[index])); + + // Stack argument: read from SP + (index - argRegs.size()) * addrSize + uint32_t sp = m_arch->GetStackPointerRegister(); + uint64_t spVal = static_cast(GetRegister(sp)); + size_t stackIndex = index - argRegs.size(); + return static_cast(ReadMemoryValue(spVal + stackIndex * addrSize, addrSize)); +} + + +void LLILEmulator::WriteReturnValue(uint64_t value) +{ + if (!m_view) + return; + + Ref platform = m_view->GetDefaultPlatform(); + if (!platform) + return; + + Ref cc = platform->GetDefaultCallingConvention(); + if (!cc) + return; + + uint32_t retReg = cc->GetIntegerReturnValueRegister(); + SetRegister(retReg, intx::uint512(value)); +} + + +std::string LLILEmulator::ResolveCallTargetName(uint64_t addr) +{ + if (!m_view) + return ""; + + Ref sym = m_view->GetSymbolByAddress(addr); + if (!sym) + return ""; + + BNSymbolType type = sym->GetType(); + if (type == ImportedFunctionSymbol || type == FunctionSymbol + || type == LibraryFunctionSymbol || type == ExternalSymbol) + return std::string(sym->GetShortName()); + + return ""; +} + + +std::string LLILEmulator::NormalizeLibcName(const std::string& name) +{ + std::string result = name; + + // Strip leading underscore (macOS/Mach-O convention) + if (result.size() > 1 && result[0] == '_') + result = result.substr(1); + + // Strip j_ prefix (PLT/thunk convention) + if (result.size() > 2 && result.substr(0, 2) == "j_") + result = result.substr(2); + + return result; +} + + +const std::unordered_map& LLILEmulator::GetStubTable() +{ + static const std::unordered_map table = { + // C library memory functions + {"memcpy", &LLILEmulator::StubMemcpy}, + {"memset", &LLILEmulator::StubMemset}, + {"memmove", &LLILEmulator::StubMemmove}, + // C library string functions + {"strlen", &LLILEmulator::StubStrlen}, + {"strcmp", &LLILEmulator::StubStrcmp}, + {"strncmp", &LLILEmulator::StubStrncmp}, + {"strcpy", &LLILEmulator::StubStrcpy}, + {"strncpy", &LLILEmulator::StubStrncpy}, + // C library allocation + {"malloc", &LLILEmulator::StubMalloc}, + {"free", &LLILEmulator::StubFree}, + {"calloc", &LLILEmulator::StubCalloc}, + {"realloc", &LLILEmulator::StubRealloc}, + // Windows API - virtual memory + {"VirtualAlloc", &LLILEmulator::StubVirtualAlloc}, + {"VirtualFree", &LLILEmulator::StubVirtualFree}, + {"VirtualProtect", &LLILEmulator::StubVirtualProtect}, + // Windows API - heap + {"HeapAlloc", &LLILEmulator::StubHeapAlloc}, + {"HeapFree", &LLILEmulator::StubHeapFree}, + // Windows API - string + {"lstrcpyA", &LLILEmulator::StubLstrcpyA}, + {"lstrcpyW", &LLILEmulator::StubLstrcpyW}, + {"lstrcmpA", &LLILEmulator::StubLstrcmpA}, + {"lstrcmpW", &LLILEmulator::StubLstrcmpW}, + {"lstrlenA", &LLILEmulator::StubLstrlenA}, + {"lstrlenW", &LLILEmulator::StubLstrlenW}, + // Windows API - conversion + {"MultiByteToWideChar", &LLILEmulator::StubMultiByteToWideChar}, + {"WideCharToMultiByte", &LLILEmulator::StubWideCharToMultiByte}, + // C library printf family + {"putchar", &LLILEmulator::StubPutchar}, + {"puts", &LLILEmulator::StubPuts}, + {"printf", &LLILEmulator::StubPrintf}, + {"sprintf", &LLILEmulator::StubSprintf}, + {"snprintf", &LLILEmulator::StubSnprintf}, + {"fprintf", &LLILEmulator::StubFprintf}, + // glibc fortified variants + {"__printf_chk", &LLILEmulator::StubPrintf}, + {"__sprintf_chk", &LLILEmulator::StubSprintf}, + {"__snprintf_chk", &LLILEmulator::StubSnprintf}, + {"__fprintf_chk", &LLILEmulator::StubFprintf}, + // C library stdin + {"getchar", &LLILEmulator::StubGetchar}, + {"gets", &LLILEmulator::StubGets}, + {"fgets", &LLILEmulator::StubFgets}, + {"fread", &LLILEmulator::StubFread}, + }; + return table; +} + + +bool LLILEmulator::HandleBuiltinCall(uint64_t dest) +{ + std::string name = ResolveCallTargetName(dest); + if (name.empty()) + return false; + + std::string normalized = NormalizeLibcName(name); + + auto& table = GetStubTable(); + auto it = table.find(normalized); + if (it == table.end()) + return false; + + return (this->*(it->second))(dest); +} + + +bool LLILEmulator::HandleUnknownCall(uint64_t dest) +{ + if (!m_nopUnknownExternals) + return false; + + std::string name = ResolveCallTargetName(dest); + if (name.empty()) + return false; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: unhandled call to %s (0x%" PRIx64 "), returning 0", name.c_str(), dest); + + WriteReturnValue(0); + return true; +} + + +// ============================================================================ +// Built-in libc stub implementations +// ============================================================================ + +static constexpr size_t MEM_OP_LIMIT = 16 * 1024 * 1024; // 16MB +static constexpr size_t STR_OP_LIMIT = 1 * 1024 * 1024; // 1MB + + +bool LLILEmulator::StubMemcpy(uint64_t) +{ + uint64_t destAddr = ReadArgument(0); + uint64_t srcAddr = ReadArgument(1); + uint64_t n = ReadArgument(2); + + if (n > MEM_OP_LIMIT) + n = MEM_OP_LIMIT; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub memcpy(dest=0x%" PRIx64 ", src=0x%" PRIx64 ", n=0x%" PRIx64 ")", destAddr, srcAddr, n); + + if (n > 0) + { + std::vector buf(n); + m_memory.Read(buf.data(), srcAddr, n); + m_memory.Write(destAddr, buf.data(), n); + } + + WriteReturnValue(destAddr); + return true; +} + + +bool LLILEmulator::StubMemset(uint64_t) +{ + uint64_t s = ReadArgument(0); + uint64_t c = ReadArgument(1); + uint64_t n = ReadArgument(2); + + if (n > MEM_OP_LIMIT) + n = MEM_OP_LIMIT; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub memset(s=0x%" PRIx64 ", c=0x%" PRIx64 ", n=0x%" PRIx64 ")", s, c, n); + + if (n > 0) + { + std::vector buf(n, (uint8_t)(c & 0xFF)); + m_memory.Write(s, buf.data(), n); + } + + WriteReturnValue(s); + return true; +} + + +bool LLILEmulator::StubMemmove(uint64_t) +{ + uint64_t destAddr = ReadArgument(0); + uint64_t srcAddr = ReadArgument(1); + uint64_t n = ReadArgument(2); + + if (n > MEM_OP_LIMIT) + n = MEM_OP_LIMIT; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub memmove(dest=0x%" PRIx64 ", src=0x%" PRIx64 ", n=0x%" PRIx64 ")", destAddr, srcAddr, n); + + if (n > 0) + { + std::vector buf(n); + m_memory.Read(buf.data(), srcAddr, n); + m_memory.Write(destAddr, buf.data(), n); + } + + WriteReturnValue(destAddr); + return true; +} + + +bool LLILEmulator::StubStrlen(uint64_t) +{ + uint64_t s = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub strlen(s=0x%" PRIx64 ")", s); + + uint64_t len = 0; + uint8_t byte; + while (len < STR_OP_LIMIT) + { + if (m_memory.Read(&byte, s + len, 1) != 1 || byte == 0) + break; + len++; + } + + WriteReturnValue(len); + return true; +} + + +bool LLILEmulator::StubStrcmp(uint64_t) +{ + uint64_t s1 = ReadArgument(0); + uint64_t s2 = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub strcmp(s1=0x%" PRIx64 ", s2=0x%" PRIx64 ")", s1, s2); + + int result = 0; + for (size_t i = 0; i < STR_OP_LIMIT; i++) + { + uint8_t c1 = 0, c2 = 0; + m_memory.Read(&c1, s1 + i, 1); + m_memory.Read(&c2, s2 + i, 1); + if (c1 != c2) + { + result = (int)c1 - (int)c2; + break; + } + if (c1 == 0) + break; + } + + WriteReturnValue((uint64_t)(int64_t)result); + return true; +} + + +bool LLILEmulator::StubStrncmp(uint64_t) +{ + uint64_t s1 = ReadArgument(0); + uint64_t s2 = ReadArgument(1); + uint64_t n = ReadArgument(2); + + if (n > STR_OP_LIMIT) + n = STR_OP_LIMIT; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub strncmp(s1=0x%" PRIx64 ", s2=0x%" PRIx64 ", n=0x%" PRIx64 ")", s1, s2, n); + + int result = 0; + for (uint64_t i = 0; i < n; i++) + { + uint8_t c1 = 0, c2 = 0; + m_memory.Read(&c1, s1 + i, 1); + m_memory.Read(&c2, s2 + i, 1); + if (c1 != c2) + { + result = (int)c1 - (int)c2; + break; + } + if (c1 == 0) + break; + } + + WriteReturnValue((uint64_t)(int64_t)result); + return true; +} + + +bool LLILEmulator::StubStrcpy(uint64_t) +{ + uint64_t destAddr = ReadArgument(0); + uint64_t srcAddr = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub strcpy(dest=0x%" PRIx64 ", src=0x%" PRIx64 ")", destAddr, srcAddr); + + for (size_t i = 0; i < STR_OP_LIMIT; i++) + { + uint8_t byte = 0; + m_memory.Read(&byte, srcAddr + i, 1); + m_memory.Write(destAddr + i, &byte, 1); + if (byte == 0) + break; + } + + WriteReturnValue(destAddr); + return true; +} + + +bool LLILEmulator::StubStrncpy(uint64_t) +{ + uint64_t destAddr = ReadArgument(0); + uint64_t srcAddr = ReadArgument(1); + uint64_t n = ReadArgument(2); + + if (n > STR_OP_LIMIT) + n = STR_OP_LIMIT; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub strncpy(dest=0x%" PRIx64 ", src=0x%" PRIx64 ", n=0x%" PRIx64 ")", destAddr, srcAddr, n); + + bool hitNull = false; + for (uint64_t i = 0; i < n; i++) + { + uint8_t byte = 0; + if (!hitNull) + { + m_memory.Read(&byte, srcAddr + i, 1); + if (byte == 0) + hitNull = true; + } + m_memory.Write(destAddr + i, &byte, 1); + } + + WriteReturnValue(destAddr); + return true; +} + + +bool LLILEmulator::StubMalloc(uint64_t) +{ + uint64_t size = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub malloc(size=0x%" PRIx64 ")", size); + + EnsureHeapMapped(); + + // 16-byte align + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + size > HEAP_BASE + HEAP_SIZE || size > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + + m_heapBumpPtr = aligned + size; + m_heapAllocations[aligned] = (size_t)size; + WriteReturnValue(aligned); + return true; +} + + +bool LLILEmulator::StubFree(uint64_t) +{ + uint64_t ptr = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub free(ptr=0x%" PRIx64 ")", ptr); + + m_heapAllocations.erase(ptr); + WriteReturnValue(0); + return true; +} + + +bool LLILEmulator::StubCalloc(uint64_t) +{ + uint64_t nmemb = ReadArgument(0); + uint64_t size = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub calloc(nmemb=0x%" PRIx64 ", size=0x%" PRIx64 ")", nmemb, size); + + // Check for multiplication overflow + if (size != 0 && nmemb > UINT64_MAX / size) + { + WriteReturnValue(0); + return true; + } + uint64_t total = nmemb * size; + + EnsureHeapMapped(); + + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + total > HEAP_BASE + HEAP_SIZE || total > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + + // Zero-fill (heap is already zero-mapped, but be explicit) + std::vector zeros(total, 0); + m_memory.Write(aligned, zeros.data(), total); + + m_heapBumpPtr = aligned + total; + m_heapAllocations[aligned] = (size_t)total; + WriteReturnValue(aligned); + return true; +} + + +bool LLILEmulator::StubRealloc(uint64_t) +{ + uint64_t ptr = ReadArgument(0); + uint64_t newSize = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub realloc(ptr=0x%" PRIx64 ", size=0x%" PRIx64 ")", ptr, newSize); + + if (ptr == 0) + { + // realloc(NULL, size) == malloc(size) + EnsureHeapMapped(); + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + newSize > HEAP_BASE + HEAP_SIZE || newSize > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + m_heapBumpPtr = aligned + newSize; + m_heapAllocations[aligned] = (size_t)newSize; + WriteReturnValue(aligned); + return true; + } + + EnsureHeapMapped(); + + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + newSize > HEAP_BASE + HEAP_SIZE || newSize > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + + // Copy old data + size_t oldSize = 0; + auto it = m_heapAllocations.find(ptr); + if (it != m_heapAllocations.end()) + oldSize = it->second; + + size_t copySize = (oldSize < (size_t)newSize) ? oldSize : (size_t)newSize; + if (copySize > 0) + { + std::vector buf(copySize); + m_memory.Read(buf.data(), ptr, copySize); + m_memory.Write(aligned, buf.data(), copySize); + } + + m_heapAllocations.erase(ptr); + m_heapBumpPtr = aligned + newSize; + m_heapAllocations[aligned] = (size_t)newSize; + WriteReturnValue(aligned); + return true; +} + + +// ============================================================================ +// Windows API stubs +// ============================================================================ + +bool LLILEmulator::StubVirtualAlloc(uint64_t) +{ + uint64_t addr = ReadArgument(0); + uint64_t size = ReadArgument(1); + // uint64_t type = ReadArgument(2); // MEM_COMMIT, etc. + // uint64_t protect = ReadArgument(3); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub VirtualAlloc(addr=0x%" PRIx64 ", size=0x%" PRIx64 ")", addr, size); + + EnsureHeapMapped(); + + (void)addr; // Ignore address hint + + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + size > HEAP_BASE + HEAP_SIZE || size > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + + // Zero-fill + std::vector zeros(size, 0); + m_memory.Write(aligned, zeros.data(), size); + + m_heapBumpPtr = aligned + size; + m_heapAllocations[aligned] = (size_t)size; + WriteReturnValue(aligned); + return true; +} + + +bool LLILEmulator::StubVirtualFree(uint64_t) +{ + uint64_t addr = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub VirtualFree(addr=0x%" PRIx64 ")", addr); + + m_heapAllocations.erase(addr); + WriteReturnValue(1); // TRUE + return true; +} + + +bool LLILEmulator::StubVirtualProtect(uint64_t) +{ + uint64_t addr = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub VirtualProtect(addr=0x%" PRIx64 ")", addr); + + WriteReturnValue(1); // TRUE + return true; +} + + +bool LLILEmulator::StubHeapAlloc(uint64_t) +{ + // uint64_t hHeap = ReadArgument(0); + uint64_t flags = ReadArgument(1); + uint64_t size = ReadArgument(2); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub HeapAlloc(flags=0x%" PRIx64 ", size=0x%" PRIx64 ")", flags, size); + + EnsureHeapMapped(); + + uint64_t aligned = (m_heapBumpPtr + 15) & ~(uint64_t)15; + if (aligned + size > HEAP_BASE + HEAP_SIZE || size > HEAP_SIZE) + { + WriteReturnValue(0); + return true; + } + + // HEAP_ZERO_MEMORY = 0x00000008 + if (flags & 0x8) + { + std::vector zeros(size, 0); + m_memory.Write(aligned, zeros.data(), size); + } + + m_heapBumpPtr = aligned + size; + m_heapAllocations[aligned] = (size_t)size; + WriteReturnValue(aligned); + return true; +} + + +bool LLILEmulator::StubHeapFree(uint64_t) +{ + // uint64_t hHeap = ReadArgument(0); + // uint64_t flags = ReadArgument(1); + uint64_t ptr = ReadArgument(2); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub HeapFree(ptr=0x%" PRIx64 ")", ptr); + + m_heapAllocations.erase(ptr); + WriteReturnValue(1); // TRUE + return true; +} + + +bool LLILEmulator::StubLstrcpyA(uint64_t dest) +{ + if (m_logLibcCalls) + { + uint64_t d = ReadArgument(0); + uint64_t s = ReadArgument(1); + LogInfo("BNIL Emulator: stub lstrcpyA(dest=0x%" PRIx64 ", src=0x%" PRIx64 ")", d, s); + } + bool savedLog = m_logLibcCalls; + m_logLibcCalls = false; + bool result = StubStrcpy(dest); + m_logLibcCalls = savedLog; + return result; +} + + +bool LLILEmulator::StubLstrcpyW(uint64_t) +{ + uint64_t destAddr = ReadArgument(0); + uint64_t srcAddr = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub lstrcpyW(dest=0x%" PRIx64 ", src=0x%" PRIx64 ")", destAddr, srcAddr); + + for (size_t i = 0; i < STR_OP_LIMIT; i++) + { + uint8_t wchar[2] = {0, 0}; + m_memory.Read(wchar, srcAddr + i * 2, 2); + m_memory.Write(destAddr + i * 2, wchar, 2); + if (wchar[0] == 0 && wchar[1] == 0) + break; + } + + WriteReturnValue(destAddr); + return true; +} + + +bool LLILEmulator::StubLstrcmpA(uint64_t dest) +{ + if (m_logLibcCalls) + { + uint64_t s1 = ReadArgument(0); + uint64_t s2 = ReadArgument(1); + LogInfo("BNIL Emulator: stub lstrcmpA(s1=0x%" PRIx64 ", s2=0x%" PRIx64 ")", s1, s2); + } + bool savedLog = m_logLibcCalls; + m_logLibcCalls = false; + bool result = StubStrcmp(dest); + m_logLibcCalls = savedLog; + return result; +} + + +bool LLILEmulator::StubLstrcmpW(uint64_t) +{ + uint64_t s1 = ReadArgument(0); + uint64_t s2 = ReadArgument(1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub lstrcmpW(s1=0x%" PRIx64 ", s2=0x%" PRIx64 ")", s1, s2); + + int result = 0; + for (size_t i = 0; i < STR_OP_LIMIT; i++) + { + uint8_t w1[2] = {0, 0}, w2[2] = {0, 0}; + m_memory.Read(w1, s1 + i * 2, 2); + m_memory.Read(w2, s2 + i * 2, 2); + uint16_t c1 = (uint16_t)w1[0] | ((uint16_t)w1[1] << 8); + uint16_t c2 = (uint16_t)w2[0] | ((uint16_t)w2[1] << 8); + if (c1 != c2) + { + result = (int)c1 - (int)c2; + break; + } + if (c1 == 0) + break; + } + + WriteReturnValue((uint64_t)(int64_t)result); + return true; +} + + +bool LLILEmulator::StubLstrlenA(uint64_t dest) +{ + if (m_logLibcCalls) + { + uint64_t s = ReadArgument(0); + LogInfo("BNIL Emulator: stub lstrlenA(s=0x%" PRIx64 ")", s); + } + bool savedLog = m_logLibcCalls; + m_logLibcCalls = false; + bool result = StubStrlen(dest); + m_logLibcCalls = savedLog; + return result; +} + + +bool LLILEmulator::StubLstrlenW(uint64_t) +{ + uint64_t s = ReadArgument(0); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub lstrlenW(s=0x%" PRIx64 ")", s); + + uint64_t len = 0; + while (len < STR_OP_LIMIT) + { + uint8_t wchar[2] = {0, 0}; + if (m_memory.Read(wchar, s + len * 2, 2) != 2) + break; + if (wchar[0] == 0 && wchar[1] == 0) + break; + len++; + } + + WriteReturnValue(len); + return true; +} + + +bool LLILEmulator::StubMultiByteToWideChar(uint64_t) +{ + // uint64_t codePage = ReadArgument(0); + // uint64_t flags = ReadArgument(1); + uint64_t mbStr = ReadArgument(2); + int64_t mbLen = (int64_t)ReadArgument(3); + uint64_t wcStr = ReadArgument(4); + int64_t wcLen = (int64_t)ReadArgument(5); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub MultiByteToWideChar(mb=0x%" PRIx64 ", mbLen=%" PRId64 ", wc=0x%" PRIx64 ", wcLen=%" PRId64 ")", + mbStr, mbLen, wcStr, wcLen); + + // Determine source length + size_t srcLen; + if (mbLen == -1) + { + // null-terminated + srcLen = 0; + uint8_t byte; + while (srcLen < STR_OP_LIMIT) + { + if (m_memory.Read(&byte, mbStr + srcLen, 1) != 1 || byte == 0) + { + srcLen++; // include null + break; + } + srcLen++; + } + } + else + { + srcLen = (size_t)mbLen; + if (srcLen > STR_OP_LIMIT) + srcLen = STR_OP_LIMIT; + } + + if (wcLen == 0) + { + // Query mode: return required size + WriteReturnValue((uint64_t)srcLen); + return true; + } + + size_t copyLen = srcLen; + if (copyLen > (size_t)wcLen) + copyLen = (size_t)wcLen; + + // Simple ASCII -> UTF16LE copy + for (size_t i = 0; i < copyLen; i++) + { + uint8_t byte = 0; + m_memory.Read(&byte, mbStr + i, 1); + uint8_t wchar[2] = {byte, 0}; + m_memory.Write(wcStr + i * 2, wchar, 2); + } + + WriteReturnValue((uint64_t)copyLen); + return true; +} + + +bool LLILEmulator::StubWideCharToMultiByte(uint64_t) +{ + // uint64_t codePage = ReadArgument(0); + // uint64_t flags = ReadArgument(1); + uint64_t wcStr = ReadArgument(2); + int64_t wcLen = (int64_t)ReadArgument(3); + uint64_t mbStr = ReadArgument(4); + int64_t mbLen = (int64_t)ReadArgument(5); + // uint64_t defaultChar = ReadArgument(6); + // uint64_t usedDefaultChar = ReadArgument(7); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub WideCharToMultiByte(wc=0x%" PRIx64 ", wcLen=%" PRId64 ", mb=0x%" PRIx64 ", mbLen=%" PRId64 ")", + wcStr, wcLen, mbStr, mbLen); + + // Determine source length (in wide chars) + size_t srcLen; + if (wcLen == -1) + { + srcLen = 0; + while (srcLen < STR_OP_LIMIT) + { + uint8_t wchar[2] = {0, 0}; + if (m_memory.Read(wchar, wcStr + srcLen * 2, 2) != 2) + break; + srcLen++; + if (wchar[0] == 0 && wchar[1] == 0) + break; + } + } + else + { + srcLen = (size_t)wcLen; + if (srcLen > STR_OP_LIMIT) + srcLen = STR_OP_LIMIT; + } + + if (mbLen == 0) + { + // Query mode + WriteReturnValue((uint64_t)srcLen); + return true; + } + + size_t copyLen = srcLen; + if (copyLen > (size_t)mbLen) + copyLen = (size_t)mbLen; + + // Simple UTF16LE -> ASCII copy (take low byte) + for (size_t i = 0; i < copyLen; i++) + { + uint8_t wchar[2] = {0, 0}; + m_memory.Read(wchar, wcStr + i * 2, 2); + uint8_t byte = wchar[0]; // Take low byte (ASCII) + m_memory.Write(mbStr + i, &byte, 1); + } + + WriteReturnValue((uint64_t)copyLen); + return true; +} + + +// ============================================================================ +// printf family stubs +// ============================================================================ + +// Escape a string for log display: \n, \r, \t, \0, and non-printable bytes shown as \xNN. +static std::string EscapeForLog(const std::string& s) +{ + std::string out; + out.reserve(s.size()); + for (unsigned char c : s) + { + switch (c) + { + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + case '\0': out += "\\0"; break; + case '\\': out += "\\\\"; break; + default: + if (c >= 0x20 && c < 0x7f) + out += (char)c; + else + { + char buf[5]; + snprintf(buf, sizeof(buf), "\\x%02x", c); + out += buf; + } + break; + } + } + return out; +} + + +static constexpr size_t PRINTF_FMT_LIMIT = 4096; +static constexpr size_t PRINTF_OUTPUT_LIMIT = 65536; +static constexpr int PRINTF_WIDTH_PRECISION_MAX = 256; +static constexpr size_t PRINTF_SPECIFIER_BUF = 512; + + +std::string LLILEmulator::FormatPrintf(size_t fmtArgIndex, size_t firstVarArgIndex) +{ + uint64_t fmtPtr = ReadArgument(fmtArgIndex); + + // Read format string from emulated memory + std::string fmt; + fmt.reserve(256); + for (size_t i = 0; i < PRINTF_FMT_LIMIT; i++) + { + uint8_t byte = 0; + if (m_memory.Read(&byte, fmtPtr + i, 1) != 1 || byte == 0) + break; + fmt.push_back((char)byte); + } + + std::string output; + output.reserve(256); + size_t argIndex = firstVarArgIndex; + size_t pos = 0; + + while (pos < fmt.size() && output.size() < PRINTF_OUTPUT_LIMIT) + { + if (fmt[pos] != '%') + { + output.push_back(fmt[pos++]); + continue; + } + pos++; // skip '%' + + if (pos >= fmt.size()) + break; + + // Handle %% + if (fmt[pos] == '%') + { + output.push_back('%'); + pos++; + continue; + } + + // Parse specifier: % [flags] [width] [.precision] [length] conversion + std::string specFmt = "%"; + + // Flags + while (pos < fmt.size()) + { + char c = fmt[pos]; + if (c == '-' || c == '+' || c == ' ' || c == '#' || c == '0') + { + specFmt.push_back(c); + pos++; + } + else + break; + } + + // Width + int width = 0; + if (pos < fmt.size() && fmt[pos] == '*') + { + width = (int)(int64_t)ReadArgument(argIndex++); + if (width < 0) + width = -width; // negative width means left-justify + if (width > PRINTF_WIDTH_PRECISION_MAX) + width = PRINTF_WIDTH_PRECISION_MAX; + specFmt += std::to_string(width); + pos++; + } + else + { + while (pos < fmt.size() && fmt[pos] >= '0' && fmt[pos] <= '9') + { + width = width * 10 + (fmt[pos] - '0'); + pos++; + } + if (width > PRINTF_WIDTH_PRECISION_MAX) + width = PRINTF_WIDTH_PRECISION_MAX; + if (width > 0) + specFmt += std::to_string(width); + } + + // Precision + int precision = -1; + if (pos < fmt.size() && fmt[pos] == '.') + { + pos++; + precision = 0; + if (pos < fmt.size() && fmt[pos] == '*') + { + precision = (int)(int64_t)ReadArgument(argIndex++); + if (precision < 0) + precision = 0; + if (precision > PRINTF_WIDTH_PRECISION_MAX) + precision = PRINTF_WIDTH_PRECISION_MAX; + pos++; + } + else + { + while (pos < fmt.size() && fmt[pos] >= '0' && fmt[pos] <= '9') + { + precision = precision * 10 + (fmt[pos] - '0'); + pos++; + } + if (precision > PRINTF_WIDTH_PRECISION_MAX) + precision = PRINTF_WIDTH_PRECISION_MAX; + } + specFmt += "." + std::to_string(precision); + } + + // Length modifier + std::string lengthMod; + if (pos < fmt.size()) + { + char c = fmt[pos]; + if (c == 'h') + { + pos++; + if (pos < fmt.size() && fmt[pos] == 'h') + { + lengthMod = "hh"; + pos++; + } + else + lengthMod = "h"; + } + else if (c == 'l') + { + pos++; + if (pos < fmt.size() && fmt[pos] == 'l') + { + lengthMod = "ll"; + pos++; + } + else + lengthMod = "l"; + } + else if (c == 'z' || c == 'j' || c == 't' || c == 'L') + { + lengthMod.push_back(c); + pos++; + } + } + + // Conversion + if (pos >= fmt.size()) + break; + + char conv = fmt[pos++]; + + // %n: reject + if (conv == 'n') + { + LogWarn("BNIL Emulator: printf %%n rejected (attempted write via format string)"); + argIndex++; // consume the argument + continue; + } + + // Read the argument value + uint64_t argVal = ReadArgument(argIndex++); + + char specBuf[PRINTF_SPECIFIER_BUF]; + + switch (conv) + { + case 'd': + case 'i': + { + // Use ll to handle all integer sizes uniformly + std::string finalFmt = specFmt + "ll" + conv; + snprintf(specBuf, sizeof(specBuf), finalFmt.c_str(), (long long)(int64_t)argVal); + output += specBuf; + break; + } + case 'u': + case 'o': + case 'x': + case 'X': + { + std::string finalFmt = specFmt + "ll" + conv; + snprintf(specBuf, sizeof(specBuf), finalFmt.c_str(), (unsigned long long)argVal); + output += specBuf; + break; + } + case 'c': + { + std::string finalFmt = specFmt + "c"; + snprintf(specBuf, sizeof(specBuf), finalFmt.c_str(), (int)(argVal & 0xFF)); + output += specBuf; + break; + } + case 's': + { + // Read string from emulated memory + size_t maxRead = STR_OP_LIMIT; + if (precision >= 0 && (size_t)precision < maxRead) + maxRead = (size_t)precision; + + std::string strVal; + strVal.reserve(256); + for (size_t i = 0; i < maxRead; i++) + { + uint8_t byte = 0; + if (m_memory.Read(&byte, argVal + i, 1) != 1 || byte == 0) + break; + strVal.push_back((char)byte); + } + + // Format with %.*s using known length for safety + std::string finalFmt = specFmt + ".*s"; + snprintf(specBuf, sizeof(specBuf), finalFmt.c_str(), (int)strVal.size(), strVal.c_str()); + output += specBuf; + break; + } + case 'p': + { + snprintf(specBuf, sizeof(specBuf), "0x%llx", (unsigned long long)argVal); + output += specBuf; + break; + } + default: + // Unsupported conversion + output += ""; + break; + } + + // Check total output limit + if (output.size() >= PRINTF_OUTPUT_LIMIT) + { + output.resize(PRINTF_OUTPUT_LIMIT); + output += "[...]"; + break; + } + } + + return output; +} + + +bool LLILEmulator::StubPutchar(uint64_t) +{ + // putchar(c) — writes a single character, returns the character written + uint64_t c = ReadArgument(0); + char ch = (char)(c & 0xFF); + + EmitStdout(&ch, 1); + + if (m_logLibcCalls) + { + std::string s(1, ch); + LogInfo("BNIL Emulator: stub putchar -> '%s'", EscapeForLog(s).c_str()); + } + + WriteReturnValue(c & 0xFF); + return true; +} + + +bool LLILEmulator::StubPuts(uint64_t) +{ + // puts(s) — prints string followed by newline, returns non-negative on success + uint64_t sPtr = ReadArgument(0); + std::string s; + char c; + while (m_memory.Read(&c, sPtr + s.size(), 1) == 1 && c != '\0' && s.size() < PRINTF_OUTPUT_LIMIT) + s += c; + + EmitStdout(s + "\n"); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub puts -> \"%s\"", EscapeForLog(s).c_str()); + + WriteReturnValue(1); // non-negative = success + return true; +} + + +bool LLILEmulator::StubPrintf(uint64_t) +{ + // printf(fmt, ...) + std::string result = FormatPrintf(0, 1); + + EmitStdout(result); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub printf -> \"%s\"", EscapeForLog(result).c_str()); + + WriteReturnValue((uint64_t)result.size()); + return true; +} + + +bool LLILEmulator::StubSprintf(uint64_t) +{ + // sprintf(dest, fmt, ...) + uint64_t destAddr = ReadArgument(0); + std::string result = FormatPrintf(1, 2); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub sprintf(dest=0x%" PRIx64 ") -> \"%s\"", destAddr, EscapeForLog(result).c_str()); + + // Write result + null terminator to emulated memory + m_memory.Write(destAddr, result.data(), result.size()); + uint8_t nul = 0; + m_memory.Write(destAddr + result.size(), &nul, 1); + + WriteReturnValue((uint64_t)result.size()); + return true; +} + + +bool LLILEmulator::StubSnprintf(uint64_t) +{ + // snprintf(dest, n, fmt, ...) + uint64_t destAddr = ReadArgument(0); + uint64_t n = ReadArgument(1); + std::string result = FormatPrintf(2, 3); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub snprintf(dest=0x%" PRIx64 ", n=%" PRIu64 ") -> \"%s\"", destAddr, n, EscapeForLog(result).c_str()); + + // Write min(len, n-1) bytes + null terminator + if (n > 0) + { + size_t writeLen = result.size(); + if (writeLen >= (size_t)n) + writeLen = (size_t)n - 1; + if (writeLen > 0) + m_memory.Write(destAddr, result.data(), writeLen); + uint8_t nul = 0; + m_memory.Write(destAddr + writeLen, &nul, 1); + } + + // Return untruncated length (per C standard) + WriteReturnValue((uint64_t)result.size()); + return true; +} + + +bool LLILEmulator::StubFprintf(uint64_t) +{ + // fprintf(stream, fmt, ...) — stream is ignored, treated as stdout + std::string result = FormatPrintf(1, 2); + + EmitStdout(result); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub fprintf -> \"%s\"", EscapeForLog(result).c_str()); + + WriteReturnValue((uint64_t)result.size()); + return true; +} + + +// ─── Stdin stubs ───────────────────────────────────────────────────────────── + +bool LLILEmulator::StubGetchar(uint64_t) +{ + // getchar() — reads one character from stdin, returns char or EOF (-1) + char ch; + size_t n = RequestStdin(&ch, 1); + if (n == 0) + { + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub getchar -> EOF"); + WriteReturnValue((uint64_t)-1); // EOF + return true; + } + + if (m_logLibcCalls) + { + std::string s(1, ch); + LogInfo("BNIL Emulator: stub getchar -> '%s'", EscapeForLog(s).c_str()); + } + + WriteReturnValue((uint64_t)(unsigned char)ch); + return true; +} + + +bool LLILEmulator::StubGets(uint64_t) +{ + // gets(buf) — reads until newline or EOF into buf (deprecated, no size limit) + uint64_t bufAddr = ReadArgument(0); + std::string line; + char ch; + bool gotAny = false; + + while (line.size() < PRINTF_OUTPUT_LIMIT) + { + size_t n = RequestStdin(&ch, 1); + if (n == 0) + break; // EOF + gotAny = true; + if (ch == '\n') + break; + line += ch; + } + + if (!gotAny) + { + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub gets -> NULL (EOF)"); + WriteReturnValue(0); + return true; + } + + // Write to buffer + null terminator + if (!line.empty()) + m_memory.Write(bufAddr, line.data(), line.size()); + uint8_t nul = 0; + m_memory.Write(bufAddr + line.size(), &nul, 1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub gets -> \"%s\"", EscapeForLog(line).c_str()); + + WriteReturnValue(bufAddr); + return true; +} + + +bool LLILEmulator::StubFgets(uint64_t) +{ + // fgets(buf, n, stream) — reads up to n-1 chars until newline or EOF + uint64_t bufAddr = ReadArgument(0); + uint64_t n = ReadArgument(1); + // arg 2 (stream) is ignored — always treated as stdin + + if (n <= 1) + { + if (n == 1) + { + uint8_t nul = 0; + m_memory.Write(bufAddr, &nul, 1); + } + WriteReturnValue(bufAddr); + return true; + } + + std::string line; + char ch; + size_t maxRead = (size_t)(n - 1); + + while (line.size() < maxRead) + { + size_t nr = RequestStdin(&ch, 1); + if (nr == 0) + break; // EOF + line += ch; + if (ch == '\n') + break; + } + + if (line.empty()) + { + // EOF with no data — return NULL + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub fgets -> NULL (EOF)"); + WriteReturnValue(0); + return true; + } + + // Write to buffer + null terminator + m_memory.Write(bufAddr, line.data(), line.size()); + uint8_t nul = 0; + m_memory.Write(bufAddr + line.size(), &nul, 1); + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub fgets -> \"%s\"", EscapeForLog(line).c_str()); + + WriteReturnValue(bufAddr); + return true; +} + + +bool LLILEmulator::StubFread(uint64_t) +{ + // fread(buf, size, count, stream) — reads size*count bytes + uint64_t bufAddr = ReadArgument(0); + uint64_t elemSize = ReadArgument(1); + uint64_t elemCount = ReadArgument(2); + // arg 3 (stream) is ignored — always treated as stdin + + size_t totalBytes = (size_t)(elemSize * elemCount); + if (totalBytes == 0) + { + WriteReturnValue(0); + return true; + } + + // Read in chunks to avoid huge stack allocations + size_t totalRead = 0; + constexpr size_t CHUNK = 4096; + char chunk[CHUNK]; + + while (totalRead < totalBytes) + { + size_t want = std::min(CHUNK, totalBytes - totalRead); + size_t got = RequestStdin(chunk, want); + if (got == 0) + break; // EOF + m_memory.Write(bufAddr + totalRead, chunk, got); + totalRead += got; + if (got < want) + break; // short read + } + + size_t itemsRead = elemSize > 0 ? totalRead / (size_t)elemSize : 0; + + if (m_logLibcCalls) + LogInfo("BNIL Emulator: stub fread(size=%" PRIu64 ", count=%" PRIu64 ") -> %zu items", + elemSize, elemCount, itemsRead); + + WriteReturnValue((uint64_t)itemsRead); + return true; +} + + +// ============================================================================ +// C API — LLIL Emulator +// ============================================================================ + +BNLLILEmulator* BNCreateLLILEmulatorForView(BNBinaryView* view) +{ + return EMU_API_OBJECT_REF(new LLILEmulator(new BinaryView(BNNewViewReference(view)))); +} + + +BNLLILEmulator* BNCreateLLILEmulator(BNLowLevelILFunction* il, BNBinaryView* view) +{ + return EMU_API_OBJECT_REF(new LLILEmulator(new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), new BinaryView(BNNewViewReference(view)))); +} + + +BNLLILEmulator* BNNewLLILEmulatorReference(BNLLILEmulator* emu) +{ + return EMU_API_OBJECT_NEW_REF(emu); +} + + +void BNFreeLLILEmulator(BNLLILEmulator* emu) +{ + EMU_API_OBJECT_FREE(emu); +} + + +BNILEmulator* BNLLILEmulatorGetBase(BNLLILEmulator* emu) +{ + return emu->object->ILEmulator::GetAPIObject(); +} + + +static void Uint512ToBytes(const intx::uint512& value, uint8_t* buf, size_t bufLen) +{ + intx::uint512 tmp = value; + size_t n = std::min(bufLen, (size_t)64); + for (size_t i = 0; i < n; i++) + { + buf[i] = static_cast(tmp); + tmp >>= 8; + } + for (size_t i = n; i < bufLen; i++) + buf[i] = 0; +} + + +static intx::uint512 BytesToUint512(const uint8_t* buf, size_t bufLen) +{ + intx::uint512 result = 0; + size_t n = std::min(bufLen, (size_t)64); + for (size_t i = n; i > 0; i--) + result = (result << 8) | buf[i - 1]; + return result; +} + + +void BNLLILEmulatorGetRegister(BNLLILEmulator* emu, uint32_t reg, uint8_t* outBuf, size_t bufLen) +{ + intx::uint512 value = emu->object->GetRegister(reg); + Uint512ToBytes(value, outBuf, bufLen); +} + + +void BNLLILEmulatorSetRegister(BNLLILEmulator* emu, uint32_t reg, const uint8_t* buf, size_t bufLen) +{ + emu->object->SetRegister(reg, BytesToUint512(buf, bufLen)); +} + + +void BNLLILEmulatorGetTempRegister(BNLLILEmulator* emu, uint32_t index, uint8_t* outBuf, size_t bufLen) +{ + intx::uint512 value = emu->object->GetTempRegister(index); + Uint512ToBytes(value, outBuf, bufLen); +} + + +void BNLLILEmulatorSetTempRegister(BNLLILEmulator* emu, uint32_t index, const uint8_t* buf, size_t bufLen) +{ + emu->object->SetTempRegister(index, BytesToUint512(buf, bufLen)); +} + + +size_t BNLLILEmulatorGetAllTempRegisters( + BNLLILEmulator* emu, uint32_t* outIndices, uint8_t* outValues, size_t maxCount) +{ + auto& temps = emu->object->GetAllTempRegisters(); + if (!outIndices || !outValues) + return temps.size(); + size_t count = 0; + for (auto& [index, value] : temps) + { + if (count >= maxCount) + break; + outIndices[count] = index; + Uint512ToBytes(value, outValues + count * 64, 64); + count++; + } + return count; +} + + +uint8_t BNLLILEmulatorGetFlag(BNLLILEmulator* emu, uint32_t flag) +{ + return emu->object->GetFlag(flag); +} + + +void BNLLILEmulatorSetFlag(BNLLILEmulator* emu, uint32_t flag, uint8_t value) +{ + emu->object->SetFlag(flag, value); +} + + +void BNLLILEmulatorSetIntrinsicHook(BNLLILEmulator* emu, void* ctxt, + bool (*callback)(void*, BNLLILEmulator*, uint32_t, const uint64_t*, size_t, uint64_t*, uint32_t*, size_t*)) +{ + if (callback) + { + emu->object->SetIntrinsicHook( + [ctxt, callback, emu](LLILEmulator* e, uint32_t intrinsic, + const std::vector& params, + std::vector>& outputs) -> bool { + size_t outCount = 0; + uint64_t outValues[64]; + uint32_t outRegs[64]; + bool result = callback(ctxt, emu, intrinsic, + params.data(), params.size(), outValues, outRegs, &outCount); + if (result) + { + for (size_t i = 0; i < outCount && i < 64; i++) + outputs.push_back({outRegs[i], outValues[i]}); + } + return result; + }); + } + else + { + emu->object->SetIntrinsicHook(nullptr); + } +} + + +bool BNLLILEmulatorSetEntryPoint(BNLLILEmulator* emu, uint64_t addr) +{ + return emu->object->SetEntryPoint(addr); +} + + +void BNLLILEmulatorSetEntryPointForIL(BNLLILEmulator* emu, BNLowLevelILFunction* il, size_t instrIndex) +{ + emu->object->SetEntryPoint(new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), instrIndex); +} + + +void BNLLILEmulatorSetArgument(BNLLILEmulator* emu, size_t index, const uint8_t* buf, size_t bufLen) +{ + emu->object->SetArgument(index, BytesToUint512(buf, bufLen)); +} + + +void BNLLILEmulatorSetArguments(BNLLILEmulator* emu, const uint64_t* values, size_t count) +{ + std::vector v(values, values + count); + emu->object->SetArguments(v); +} + + +size_t BNLLILEmulatorGetCallStackDepth(BNLLILEmulator* emu) +{ + return emu->object->GetCallStackDepth(); +} + + +BNILEmulatorStopReason BNLLILEmulatorStepOver(BNLLILEmulator* emu) +{ + return (BNILEmulatorStopReason)emu->object->StepOver(); +} + + +void BNLLILEmulatorSetBuiltinLibcStubsEnabled(BNLLILEmulator* emu, bool enabled) +{ + emu->object->SetBuiltinLibcStubsEnabled(enabled); +} + + +bool BNLLILEmulatorIsBuiltinLibcStubsEnabled(BNLLILEmulator* emu) +{ + return emu->object->IsBuiltinLibcStubsEnabled(); +} + + +void BNLLILEmulatorSetLogLibcCalls(BNLLILEmulator* emu, bool enabled) +{ + emu->object->SetLogLibcCalls(enabled); +} + + +bool BNLLILEmulatorIsLogLibcCalls(BNLLILEmulator* emu) +{ + return emu->object->IsLogLibcCalls(); +} + + +void BNLLILEmulatorSetNopUnknownExternals(BNLLILEmulator* emu, bool enabled) +{ + emu->object->SetNopUnknownExternals(enabled); +} + + +bool BNLLILEmulatorIsNopUnknownExternals(BNLLILEmulator* emu) +{ + return emu->object->IsNopUnknownExternals(); +} + + +BNEmulatorCallStackEntry* BNLLILEmulatorGetCallStack(BNLLILEmulator* emu, size_t* count) +{ + auto frames = emu->object->GetCallStack(); + *count = frames.size(); + if (frames.empty()) + return nullptr; + + auto* result = new BNEmulatorCallStackEntry[frames.size()]; + for (size_t i = 0; i < frames.size(); i++) + { + result[i].functionAddress = frames[i].functionAddress; + result[i].returnAddress = frames[i].returnAddress; + } + return result; +} + + +void BNLLILEmulatorFreeCallStack(BNEmulatorCallStackEntry* entries) +{ + delete[] entries; +} + + +char* BNLLILEmulatorSaveState(BNLLILEmulator* emu) +{ + auto json = emu->object->SaveState(); + return BNAllocString(json.c_str()); +} + + +bool BNLLILEmulatorLoadState(BNLLILEmulator* emu, const char* json) +{ + return emu->object->LoadState(json, emu->object->GetView()); +} diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h new file mode 100644 index 0000000000..23728d0c69 --- /dev/null +++ b/plugins/emulator/core/llilemulator.h @@ -0,0 +1,189 @@ +#pragma once + +#include "ilemulator.h" +#include "lowlevelilinstruction.h" + +DECLARE_EMULATOR_API_OBJECT(BNLLILEmulator, LLILEmulator); + +namespace BinaryNinjaEmulator +{ + class LLILEmulator : public ILEmulator + { + IMPLEMENT_EMULATOR_API_OBJECT(BNLLILEmulator) + + BinaryNinja::Ref m_view; + BinaryNinja::Ref m_il; + BinaryNinja::Ref m_arch; + + // LLIL-specific state + std::unordered_map m_registers; + std::unordered_map m_tempRegisters; + std::unordered_map m_flags; + + // Call stack for cross-function emulation + struct CallFrame + { + BinaryNinja::Ref il; + BinaryNinja::Ref arch; + size_t returnIndex; // instruction index to resume in caller + std::unordered_map tempRegisters; // saved caller temps + }; + std::vector m_callStack; + + using IntrinsicHook = std::function& params, + std::vector>& outputs)>; + IntrinsicHook m_intrinsicHook; + + // Arithmetic context for flag computation (non-lifted LLIL) + struct ArithmeticContext + { + intx::uint512 left; + intx::uint512 right; + intx::uint512 result; + size_t size; + BNLowLevelILOperation operation; + bool valid; + }; + ArithmeticContext m_lastArithmetic {}; + + // Built-in libc stub settings + bool m_builtinLibcStubs = true; + bool m_logLibcCalls = true; + bool m_nopUnknownExternals = false; + + // Heap allocator state for malloc/free/etc. + static constexpr uint64_t HEAP_BASE = 0x40000000; + static constexpr size_t HEAP_SIZE = 16 * 1024 * 1024; // 16MB + uint64_t m_heapBumpPtr = HEAP_BASE; + bool m_heapMapped = false; + std::unordered_map m_heapAllocations; + + // Core dispatch + intx::uint512 EvalExpr(const BinaryNinja::LowLevelILInstruction& expr); + void ExecuteCurrentInstruction() override; + size_t GetInstructionCount() const override; + uint64_t GetCurrentInstructionAddress() const override; + BNEndianness GetEndianness() const override; + + // Helpers + static intx::uint512 MaskToSize(const intx::uint512& value, size_t size); + static intx::uint512 SignExtend(const intx::uint512& value, size_t fromSize, size_t toSize); + void Push(const intx::uint512& value, size_t size); + intx::uint512 Pop(size_t size); + intx::uint512 EvalFlagCondition(BNLowLevelILFlagCondition cond, uint32_t semClass); + void WriteFlags(uint32_t flagWriteType); + uint8_t ComputeFlagForRole(BNFlagRole role) const; + + // Cross-function call support + bool EnterFunction(uint64_t addr, size_t returnIndex); + bool ReturnToCaller(); + uint64_t GetNativeReturnAddress() const; + + // Built-in libc stub helpers + uint64_t ReadArgument(size_t index); + void WriteReturnValue(uint64_t value); + intx::uint512 ReadArgumentWide(size_t index); + void WriteReturnValueWide(const intx::uint512& value); + std::string ResolveCallTargetName(uint64_t addr); + static std::string NormalizeLibcName(const std::string& name); + bool HandleBuiltinCall(uint64_t dest); + bool HandleUnknownCall(uint64_t dest); + void EnsureHeapMapped(); + + // Stub implementations + using StubFn = bool (LLILEmulator::*)(uint64_t dest); + static const std::unordered_map& GetStubTable(); + bool StubMemcpy(uint64_t dest); + bool StubMemset(uint64_t dest); + bool StubMemmove(uint64_t dest); + bool StubStrlen(uint64_t dest); + bool StubStrcmp(uint64_t dest); + bool StubStrncmp(uint64_t dest); + bool StubStrcpy(uint64_t dest); + bool StubStrncpy(uint64_t dest); + bool StubMalloc(uint64_t dest); + bool StubFree(uint64_t dest); + bool StubCalloc(uint64_t dest); + bool StubRealloc(uint64_t dest); + bool StubVirtualAlloc(uint64_t dest); + bool StubVirtualFree(uint64_t dest); + bool StubVirtualProtect(uint64_t dest); + bool StubHeapAlloc(uint64_t dest); + bool StubHeapFree(uint64_t dest); + bool StubLstrcpyA(uint64_t dest); + bool StubLstrcpyW(uint64_t dest); + bool StubLstrcmpA(uint64_t dest); + bool StubLstrcmpW(uint64_t dest); + bool StubLstrlenA(uint64_t dest); + bool StubLstrlenW(uint64_t dest); + bool StubMultiByteToWideChar(uint64_t dest); + bool StubWideCharToMultiByte(uint64_t dest); + + // printf family stubs + std::string FormatPrintf(size_t fmtArgIndex, size_t firstVarArgIndex); + bool StubPutchar(uint64_t dest); + bool StubPuts(uint64_t dest); + bool StubPrintf(uint64_t dest); + bool StubSprintf(uint64_t dest); + bool StubSnprintf(uint64_t dest); + bool StubFprintf(uint64_t dest); + + // stdin stubs + bool StubGetchar(uint64_t dest); + bool StubGets(uint64_t dest); + bool StubFgets(uint64_t dest); + bool StubFread(uint64_t dest); + + public: + LLILEmulator(BinaryNinja::BinaryView* backingView); + LLILEmulator(BinaryNinja::LowLevelILFunction* il, BinaryNinja::BinaryView* backingView); + + bool SetEntryPoint(uint64_t addr); + void SetEntryPoint(BinaryNinja::LowLevelILFunction* il, size_t instrIndex); + + // Argument setup (uses default calling convention) + void SetArgument(size_t index, const intx::uint512& value); + void SetArguments(const std::vector& values); + + // LLIL-specific state access + intx::uint512 GetRegister(uint32_t reg) const; + void SetRegister(uint32_t reg, const intx::uint512& value); + intx::uint512 GetTempRegister(uint32_t index) const; + void SetTempRegister(uint32_t index, const intx::uint512& value); + const std::unordered_map& GetAllTempRegisters() const; + uint8_t GetFlag(uint32_t flag) const; + void SetFlag(uint32_t flag, uint8_t value); + + void SetIntrinsicHook(IntrinsicHook hook); + + BinaryNinja::LowLevelILFunction* GetFunction() const; + BinaryNinja::Architecture* GetArchitecture() const; + size_t GetCallStackDepth() const; + + struct CallStackEntry + { + uint64_t functionAddress; + uint64_t returnAddress; + }; + std::vector GetCallStack() const; + + ILEmulatorStopReason StepOver(); + + // Built-in libc stub settings + void SetBuiltinLibcStubsEnabled(bool enabled); + bool IsBuiltinLibcStubsEnabled() const; + void SetLogLibcCalls(bool enabled); + bool IsLogLibcCalls() const; + void SetNopUnknownExternals(bool enabled); + bool IsNopUnknownExternals() const; + + void Reset() override; + + // State serialization + std::string SaveState() const; + bool LoadState(const std::string& json, BinaryNinja::BinaryView* view); + BinaryNinja::BinaryView* GetView() const { return m_view; } + }; +} diff --git a/plugins/emulator/core/plugin.cpp b/plugins/emulator/core/plugin.cpp new file mode 100644 index 0000000000..7f97a94398 --- /dev/null +++ b/plugins/emulator/core/plugin.cpp @@ -0,0 +1,54 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "binaryninjaapi.h" + +using namespace BinaryNinja; + +static void RegisterSettings() +{ + Ref settings = Settings::Instance(); + + settings->RegisterSetting("emulator.builtinLibcStubs", + R"~({ + "title" : "Built-in Libc Stubs", + "type" : "boolean", + "default" : true, + "description" : "Enable built-in stub implementations for common C library and Windows API functions (memcpy, strlen, malloc, VirtualAlloc, etc.) during LLIL emulation. When enabled, the emulator can continue through calls to these functions without stopping." + })~"); + + settings->RegisterSetting("emulator.logLibcCalls", + R"~({ + "title" : "Log Libc Stub Calls", + "type" : "boolean", + "default" : true, + "description" : "Log built-in libc stub invocations to the console during LLIL emulation. Messages include function name and argument values." + })~"); +} + +extern "C" +{ + BN_DECLARE_CORE_ABI_VERSION + + BINARYNINJAPLUGIN void CorePluginDependencies() {} + + BINARYNINJAPLUGIN bool CorePluginInit() + { + LogDebug("BNIL emulator plugin loaded!"); + RegisterSettings(); + return true; + } +} diff --git a/plugins/emulator/core/refcountobject.h b/plugins/emulator/core/refcountobject.h new file mode 100644 index 0000000000..a35dd6deaa --- /dev/null +++ b/plugins/emulator/core/refcountobject.h @@ -0,0 +1,70 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +#include +#include + +namespace BinaryNinjaEmulator +{ + // Plugin-side reference-counted base, mirroring core's RefCountObject. Objects handed + // across the C ABI keep an API-side reference; the last Release() deletes the object. + class EmuRefCountObject + { + public: + std::atomic m_refs; + EmuRefCountObject() : m_refs(0) {} + virtual ~EmuRefCountObject() {} + + void AddRef() { m_refs.fetch_add(1); } + + void Release() + { + if (m_refs.fetch_sub(1) == 1) + delete this; + } + + void AddAPIRef() { AddRef(); } + void ReleaseAPIRef() { Release(); } + }; + + + // Macro-like helpers to hand referenced objects across the external C ABI. + template + static typename T::APIHandle EMU_API_OBJECT_REF(T* obj) + { + if (obj == nullptr) + return nullptr; + obj->AddAPIRef(); + return obj->GetAPIObject(); + } + + template + static T* EMU_API_OBJECT_NEW_REF(T* obj) + { + if (obj) + obj->object->AddAPIRef(); + return obj; + } + + template + static void EMU_API_OBJECT_FREE(T* obj) + { + if (obj) + obj->object->ReleaseAPIRef(); + } +} // namespace BinaryNinjaEmulator diff --git a/plugins/emulator/test/emulator_test.py b/plugins/emulator/test/emulator_test.py new file mode 100644 index 0000000000..db24de5fb6 --- /dev/null +++ b/plugins/emulator/test/emulator_test.py @@ -0,0 +1,357 @@ +#!/usr/bin/env python3 +# +# Unit tests for the BNIL (LLIL) emulator plugin. +# +# These tests are self-contained: instead of shipping test binaries, each test +# assembles a tiny BinaryView from raw x86-64 machine code, lets Binary Ninja +# lift it to LLIL, and emulates the result. Run headless with, e.g.: +# +# PYTHONPATH=/python python3 -m pytest emulator_test.py +# PYTHONPATH=/python python3 emulator_test.py +# +# Requires the `emulator` core plugin to be present in the Binary Ninja install. + +import os +import tempfile +import unittest + +from binaryninja import BinaryView, Architecture + +try: + from emulator import LLILEmulator, ILEmulatorStopReason +except ImportError: + from binaryninja.emulator import LLILEmulator, ILEmulatorStopReason + + +def make_bv(code: bytes, entries=(0,), arch: str = 'x86_64') -> BinaryView: + """Build an analyzed BinaryView from raw machine code with a function at each entry.""" + bv = BinaryView.new(code) + bv.platform = Architecture[arch].standalone_platform + for entry in entries: + bv.add_function(entry) + bv.update_analysis_and_wait() + return bv + + +# Common x86-64 snippets used across tests. +# mov eax, 5 (0..4); mov ebx, 3 (5..9); add eax, ebx (0xa..0xb); ret (0xc) +ADD_CONSTS = b'\xb8\x05\x00\x00\x00\xbb\x03\x00\x00\x00\x01\xd8\xc3' +# mov eax, edi; add eax, esi; ret -> returns arg0 + arg1 +ADD_ARGS = b'\x89\xf8\x01\xf0\xc3' +# mov ecx, 0x100000; dec ecx; jnz -3; ret -> long-running loop +BUSY_LOOP = b'\xb9\x00\x00\x10\x00\xff\xc9\x75\xfc\xc3' +# call +6 (target 0xb); ret; ; ret@0xb +CALL_SNIPPET = b'\xe8\x06\x00\x00\x00\xc3\x90\x90\x90\x90\x90\xc3' +# mov eax, [rdi]; ret +LOAD_RDI = b'\x8b\x07\xc3' +# mov [rdi], eax; ret +STORE_RDI = b'\x89\x07\xc3' + + +class EmulatorTestBase(unittest.TestCase): + def emulator_for(self, code: bytes, entries=(0,), entry_point=0, max_instructions=1000): + bv = make_bv(code, entries=entries) + emu = LLILEmulator(bv) + self.assertTrue(emu.set_entry_point(entry_point)) + emu.set_max_instructions(max_instructions) + # Keep the view alive for the lifetime of the test. + emu._test_bv = bv + return emu + + +class ExecutionTests(EmulatorTestBase): + def test_basic_arithmetic(self): + emu = self.emulator_for(ADD_CONSTS) + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorHalt) + self.assertEqual(emu.get_register('rax'), 8) + + def test_step_advances_one_instruction(self): + emu = self.emulator_for(ADD_CONSTS) + self.assertEqual(emu.instructions_executed, 0) + emu.step() + self.assertEqual(emu.instructions_executed, 1) + self.assertEqual(emu.get_register('rax'), 5) + + def test_step_n(self): + emu = self.emulator_for(ADD_CONSTS) + emu.step_n(2) + self.assertEqual(emu.instructions_executed, 2) + self.assertEqual(emu.get_register('rax'), 5) + self.assertEqual(emu.get_register('rbx'), 3) + + def test_instruction_index_property(self): + emu = self.emulator_for(ADD_CONSTS) + emu.step() + self.assertGreater(emu.instruction_index, 0) + emu.instruction_index = 0 + self.assertEqual(emu.instruction_index, 0) + + def test_current_address_and_stop_message(self): + emu = self.emulator_for(ADD_CONSTS) + emu.step() + self.assertIsInstance(emu.current_address, int) + emu.run() + self.assertIsInstance(emu.stop_message, str) + self.assertEqual(emu.stop_reason, ILEmulatorStopReason.ILEmulatorHalt) + + def test_instruction_limit(self): + emu = self.emulator_for(BUSY_LOOP, max_instructions=50) + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorInstructionLimit) + self.assertEqual(emu.instructions_executed, 50) + + def test_reset_clears_state(self): + emu = self.emulator_for(ADD_CONSTS) + emu.run() + self.assertEqual(emu.get_register('rax'), 8) + emu.reset() + self.assertEqual(emu.get_register('rax'), 0) + self.assertEqual(emu.instructions_executed, 0) + + +class EntryPointTests(EmulatorTestBase): + def test_entry_point_by_address(self): + bv = make_bv(ADD_CONSTS) + emu = LLILEmulator(bv) + self.assertTrue(emu.set_entry_point(0)) + + def test_entry_point_invalid_address(self): + bv = make_bv(ADD_CONSTS) + emu = LLILEmulator(bv) + self.assertFalse(emu.set_entry_point(0xdeadbeef)) + + def test_entry_point_by_il_index(self): + bv = make_bv(ADD_CONSTS) + llil = bv.get_function_at(0).llil + emu = LLILEmulator(bv, il=llil) + self.assertTrue(emu.set_entry_point(llil, 0)) + emu.set_max_instructions(100) + emu.run() + self.assertEqual(emu.get_register('rax'), 8) + + +class RegisterFlagTests(EmulatorTestBase): + def test_register_get_set_by_name(self): + emu = self.emulator_for(ADD_CONSTS) + emu.set_register('rax', 0x1122334455667788) + self.assertEqual(emu.get_register('rax'), 0x1122334455667788) + + def test_register_get_set_by_id(self): + emu = self.emulator_for(ADD_CONSTS) + reg_id = emu._arch.regs['rax'].index + emu.set_register(reg_id, 0xabcd) + self.assertEqual(emu.get_register(reg_id), 0xabcd) + self.assertEqual(emu.get_register('rax'), 0xabcd) + + def test_regs_snapshot(self): + emu = self.emulator_for(ADD_CONSTS) + emu.set_register('rax', 0x42) + regs = emu.regs + self.assertIn('rax', regs) + self.assertEqual(regs['rax'], 0x42) + + def test_temp_registers(self): + emu = self.emulator_for(ADD_CONSTS) + emu.set_temp_register(0, 0x1234) + self.assertEqual(emu.get_temp_register(0), 0x1234) + + def test_flags(self): + emu = self.emulator_for(ADD_CONSTS) + emu.set_flag('z', 1) + self.assertEqual(emu.get_flag('z'), 1) + emu.set_flag('z', 0) + self.assertEqual(emu.get_flag('z'), 0) + + +class MemoryTests(EmulatorTestBase): + def test_map_write_read(self): + emu = self.emulator_for(ADD_CONSTS) + emu.map_memory(0x200000, 0x1000) + n = emu.write_memory(0x200000, b'\xde\xad\xbe\xef') + self.assertEqual(n, 4) + self.assertEqual(emu.read_memory(0x200000, 4), b'\xde\xad\xbe\xef') + + def test_map_with_data(self): + emu = self.emulator_for(ADD_CONSTS) + emu.map_memory(0x300000, b'\x01\x02\x03\x04') + self.assertEqual(emu.read_memory(0x300000, 4), b'\x01\x02\x03\x04') + + def test_map_named_region_listed(self): + emu = self.emulator_for(ADD_CONSTS) + emu.map_memory(0x400000, 0x1000, "scratch") + regions = emu.get_mapped_regions() + self.assertTrue(any(r['name'] == 'scratch' for r in regions)) + scratch = next(r for r in regions if r['name'] == 'scratch') + self.assertEqual(scratch['start'], 0x400000) + self.assertEqual(scratch['size'], 0x1000) + + def test_memory_not_inherited_from_view(self): + # The emulator has its own empty address space; it does not read the + # BinaryView's bytes. Data must be copied in explicitly. + code = ADD_CONSTS + b'\xAA\xBB\xCC\xDD' # data appended after the code + data_addr = len(ADD_CONSTS) + bv = make_bv(code) + self.assertEqual(bv.read(data_addr, 4), b'\xAA\xBB\xCC\xDD') + emu = LLILEmulator(bv) + # Not present in the emulator until mapped. + self.assertEqual(emu.read_memory(data_addr, 4), b'\x00\x00\x00\x00') + # After copying it in, the emulator sees it. + emu.map_memory(data_addr, bv.read(data_addr, 4)) + self.assertEqual(emu.read_memory(data_addr, 4), b'\xAA\xBB\xCC\xDD') + + +class BreakpointTests(EmulatorTestBase): + def test_breakpoint_stops_before_instruction(self): + emu = self.emulator_for(ADD_CONSTS) + emu.add_breakpoint(0xa) # the `add eax, ebx` instruction + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorBreakpoint) + self.assertEqual(emu.current_address, 0xa) + self.assertEqual(emu.get_register('rax'), 5) # add has not executed yet + + def test_remove_breakpoint(self): + emu = self.emulator_for(ADD_CONSTS) + emu.add_breakpoint(0xa) + emu.remove_breakpoint(0xa) + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorHalt) + self.assertEqual(emu.get_register('rax'), 8) + + def test_clear_breakpoints(self): + emu = self.emulator_for(ADD_CONSTS) + emu.add_breakpoint(0x5) + emu.add_breakpoint(0xa) + emu.clear_breakpoints() + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorHalt) + + +class ArgumentTests(EmulatorTestBase): + def test_set_arguments(self): + emu = self.emulator_for(ADD_ARGS) + emu.set_arguments([10, 20]) + emu.run() + self.assertEqual(emu.get_register('rax'), 30) + + def test_set_single_argument(self): + emu = self.emulator_for(ADD_ARGS) + emu.set_argument(0, 7) + emu.set_argument(1, 35) + emu.run() + self.assertEqual(emu.get_register('rax'), 42) + + +class CallStackTests(EmulatorTestBase): + def test_call_stack_inside_callee(self): + emu = self.emulator_for(CALL_SNIPPET, entries=(0, 0xb)) + emu.add_breakpoint(0xb) + emu.run() + self.assertEqual(emu.call_stack_depth, 1) + stack = emu.get_call_stack() + self.assertEqual(len(stack), 2) + # The caller frame returns to the instruction after the call (offset 5). + self.assertEqual(stack[-1]['return_address'], 0x5) + + +class HookTests(EmulatorTestBase): + def test_call_hook_receives_target_and_skips(self): + emu = self.emulator_for(CALL_SNIPPET, entries=(0, 0xb)) + seen = [] + emu.set_call_hook(lambda e, target: (seen.append(target), True)[1]) + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorHalt) + self.assertIn(0xb, seen) + + def test_memory_read_hook_overrides_value(self): + emu = self.emulator_for(LOAD_RDI) + emu.set_register('rdi', 0x400000) + seen = [] + emu.set_memory_read_hook(lambda e, addr, size: (seen.append((addr, size)), 0xcafe)[1]) + emu.run() + self.assertEqual(emu.get_register('rax'), 0xcafe) + self.assertIn((0x400000, 4), seen) + + def test_memory_write_hook_intercepts(self): + emu = self.emulator_for(STORE_RDI) + emu.set_register('rdi', 0x400000) + emu.set_register('rax', 0x11223344) + writes = [] + emu.set_memory_write_hook( + lambda e, addr, size, value: (writes.append((addr, size, value)), True)[1]) + emu.run() + self.assertIn((0x400000, 4, 0x11223344), writes) + + def test_pre_instruction_hook_stops_early(self): + emu = self.emulator_for(ADD_CONSTS) + calls = {'n': 0} + + def pre(e, idx): + calls['n'] += 1 + return calls['n'] < 2 # allow one instruction, then stop + + emu.set_pre_instruction_hook(pre) + emu.run() + self.assertEqual(emu.instructions_executed, 1) + self.assertEqual(calls['n'], 2) + + def test_all_hooks_can_be_removed(self): + # Regression test: removing a hook must pass a NULL callback to the core + # binding rather than Python None (which raised ctypes.ArgumentError). + emu = self.emulator_for(ADD_CONSTS) + setters = [ + (emu.set_call_hook, lambda *a: True), + (emu.set_syscall_hook, lambda *a: True), + (emu.set_memory_read_hook, lambda *a: None), + (emu.set_memory_write_hook, lambda *a: False), + (emu.set_pre_instruction_hook, lambda *a: True), + (emu.set_intrinsic_hook, lambda *a: None), + (emu.set_stdout_callback, lambda *a: None), + (emu.set_stdin_callback, lambda *a: b''), + ] + for setter, fn in setters: + setter(fn) + setter(None) # must not raise + + +class StateSerializationTests(EmulatorTestBase): + def test_save_load_round_trip(self): + emu = self.emulator_for(ADD_CONSTS) + emu.step() + emu.step() + rax_mid = emu.get_register('rax') + snapshot = emu.save_state() + self.assertIsInstance(snapshot, str) + self.assertTrue(snapshot) + emu.run() # mutate state + self.assertTrue(emu.load_state(snapshot)) + self.assertEqual(emu.get_register('rax'), rax_mid) + + def test_save_load_file_round_trip(self): + emu = self.emulator_for(ADD_CONSTS) + emu.step() + rax_mid = emu.get_register('rax') + fd, path = tempfile.mkstemp(suffix='.json') + os.close(fd) + try: + emu.save_state_to_file(path) + emu.run() + self.assertTrue(emu.load_state_from_file(path)) + self.assertEqual(emu.get_register('rax'), rax_mid) + finally: + os.remove(path) + + +class LibcStubSettingTests(EmulatorTestBase): + def test_toggle_settings(self): + emu = self.emulator_for(ADD_CONSTS) + for prop in ('builtin_libc_stubs', 'log_libc_calls', 'nop_unknown_externals'): + original = getattr(emu, prop) + setattr(emu, prop, not original) + self.assertEqual(getattr(emu, prop), not original) + setattr(emu, prop, original) + self.assertEqual(getattr(emu, prop), original) + + +if __name__ == '__main__': + unittest.main() diff --git a/python/scriptingprovider.py b/python/scriptingprovider.py index b1f83ec451..dea0dae569 100644 --- a/python/scriptingprovider.py +++ b/python/scriptingprovider.py @@ -969,6 +969,14 @@ def __init__(self, provider): (os.environ.get('BN_DISABLE_CORE_DEBUGGER') is None): from .debugger import DebuggerController self.DebuggerController = DebuggerController + if os.environ.get('BN_STANDALONE_EMULATOR'): + from emulator import LLILEmulator + self.LLILEmulator = LLILEmulator + else: + if settings.contains('corePlugins.emulator') and settings.get_bool('corePlugins.emulator') and \ + (os.environ.get('BN_DISABLE_CORE_EMULATOR') is None): + from .emulator import LLILEmulator + self.LLILEmulator = LLILEmulator @abc.abstractmethod def perform_stop(self): diff --git a/vendor/intx/intx.hpp b/vendor/intx/intx.hpp new file mode 100644 index 0000000000..8bacda2922 --- /dev/null +++ b/vendor/intx/intx.hpp @@ -0,0 +1,1822 @@ +// intx: extended precision integer library. +// Copyright 2019 Pawel Bylica. +// Licensed under the Apache License, Version 2.0. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // fputs +#include // abort +#include +#include +#include +#include +#include +#include + +#ifdef _MSC_VER + #pragma warning(push) + #pragma warning(disable : 5030) // Allow unknown attributes. +#endif + + +#ifndef __has_builtin + #define __has_builtin(NAME) 0 +#endif + +#ifdef _MSC_VER + #include +#endif + +#if !defined(__has_builtin) + #define __has_builtin(NAME) 0 +#endif + +#if !defined(__has_feature) + #define __has_feature(NAME) 0 +#endif + +#if __has_builtin(__builtin_expect) + #define INTX_UNLIKELY(EXPR) __builtin_expect(bool{EXPR}, false) +#else + #define INTX_UNLIKELY(EXPR) (bool{EXPR}) +#endif + +#if !defined(NDEBUG) + #define INTX_REQUIRE assert +#else + #define INTX_REQUIRE(X) (X) ? (void)0 : intx::unreachable() +#endif + + +// Detect compiler support for 128-bit integer __int128 +#if defined(__SIZEOF_INT128__) + #define INTX_HAS_BUILTIN_INT128 1 +#else + #define INTX_HAS_BUILTIN_INT128 0 +#endif + +namespace intx +{ +/// Mark a possible code path as unreachable (invokes undefined behavior). +/// TODO(C++23): Use std::unreachable(). +[[noreturn]] inline void unreachable() noexcept +{ +#if __has_builtin(__builtin_unreachable) + __builtin_unreachable(); +#elif defined(_MSC_VER) + __assume(false); +#endif +} + +#if INTX_HAS_BUILTIN_INT128 + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wpedantic" // Usage of __int128 triggers a pedantic warning. + +/// Alias for the compiler supported unsigned __int128 type. +using builtin_uint128 = unsigned __int128; + + #pragma GCC diagnostic pop +#endif + + +template +struct uint; + +/// Contains result of add/sub/etc with a carry flag. +template +struct result_with_carry +{ + T value; + bool carry; + + /// Conversion to tuple of references, to allow usage with std::tie(). + constexpr explicit(false) operator std::tuple() noexcept { return {value, carry}; } +}; + +template +struct div_result +{ + QuotT quot; + RemT rem; + + bool operator==(const div_result&) const = default; + + /// Conversion to tuple of references, to allow usage with std::tie(). + constexpr explicit(false) operator std::tuple() noexcept { return {quot, rem}; } +}; + +/// Addition with carry. +constexpr result_with_carry addc(uint64_t x, uint64_t y, bool carry = false) noexcept +{ +#if __has_builtin(__builtin_addcll) + if (!std::is_constant_evaluated()) + { + unsigned long long carryout = 0; // NOLINT(google-runtime-int) + const auto s = __builtin_addcll(x, y, carry, &carryout); + static_assert(sizeof(s) == sizeof(uint64_t)); + return {s, static_cast(carryout)}; + } +#elif __has_builtin(__builtin_ia32_addcarryx_u64) + if (!std::is_constant_evaluated()) + { + unsigned long long s = 0; // NOLINT(google-runtime-int) + static_assert(sizeof(s) == sizeof(uint64_t)); + const auto carryout = __builtin_ia32_addcarryx_u64(carry, x, y, &s); + return {s, static_cast(carryout)}; + } +#endif + + const auto s = x + y; + const auto carry1 = s < x; + const auto t = s + carry; + const auto carry2 = t < s; + return {t, carry1 || carry2}; +} + +/// Subtraction with carry (borrow). +constexpr result_with_carry subc(uint64_t x, uint64_t y, bool carry = false) noexcept +{ +// Use __builtin_subcll if available (except buggy Xcode 14.3.1 on arm64). +#if __has_builtin(__builtin_subcll) && __apple_build_version__ != 14030022 + if (!std::is_constant_evaluated()) + { + unsigned long long carryout = 0; // NOLINT(google-runtime-int) + const auto d = __builtin_subcll(x, y, carry, &carryout); + static_assert(sizeof(d) == sizeof(uint64_t)); + return {d, static_cast(carryout)}; + } +#elif __has_builtin(__builtin_ia32_sbb_u64) + if (!std::is_constant_evaluated()) + { + unsigned long long d = 0; // NOLINT(google-runtime-int) + static_assert(sizeof(d) == sizeof(uint64_t)); + const auto carryout = __builtin_ia32_sbb_u64(carry, x, y, &d); + return {d, static_cast(carryout)}; + } +#endif + + const auto d = x - y; + const auto carry1 = x < y; + const auto e = d - carry; + const auto carry2 = d < uint64_t{carry}; + return {e, carry1 || carry2}; +} + +/// Addition with carry. +template +constexpr result_with_carry> addc( + const uint& x, const uint& y, bool carry = false) noexcept +{ + uint s; + bool k = carry; + for (size_t i = 0; i < uint::num_words; ++i) + { + auto t = addc(x[i], y[i], k); + s[i] = t.value; + k = t.carry; + } + return {s, k}; +} + +/// Performs subtraction of two unsigned numbers and returns the difference +/// and the carry bit (aka borrow, overflow). +template +constexpr result_with_carry> subc( + const uint& x, const uint& y, bool carry = false) noexcept +{ + uint z; + bool k = carry; + for (size_t i = 0; i < uint::num_words; ++i) + { + auto t = subc(x[i], y[i], k); + z[i] = t.value; + k = t.carry; + } + return {z, k}; +} + +constexpr uint<128> umul(uint64_t x, uint64_t y) noexcept; + + +/// The 128-bit unsigned integer. +/// +/// This type is defined as a specialization of uint<> to easier integration with full intx package, +/// however, uint128 may be used independently. +template <> +struct uint<128> +{ + using word_type = uint64_t; + static constexpr auto word_num_bits = sizeof(word_type) * 8; + static constexpr unsigned num_bits = 128; + static constexpr auto num_words = num_bits / word_num_bits; + +private: + uint64_t words_[2]{}; + +public: + constexpr uint() noexcept = default; + + constexpr uint(uint64_t low, uint64_t high) noexcept : words_{low, high} {} + + template + constexpr explicit(false) uint(T x) noexcept + requires std::is_convertible_v + : words_{static_cast(x), 0} + {} + +#if INTX_HAS_BUILTIN_INT128 + constexpr explicit(false) uint(builtin_uint128 x) noexcept + : words_{uint64_t(x), uint64_t(x >> 64)} + {} + + constexpr explicit operator builtin_uint128() const noexcept + { + return (builtin_uint128{words_[1]} << 64) | words_[0]; + } +#endif + + constexpr uint64_t& operator[](size_t i) noexcept { return words_[i]; } + constexpr const uint64_t& operator[](size_t i) const noexcept { return words_[i]; } + + constexpr explicit operator bool() const noexcept { return (words_[0] | words_[1]) != 0; } + + /// Explicit converting operator for all builtin integral types. + template + constexpr explicit operator Int() const noexcept + requires std::is_integral_v + { + return static_cast(words_[0]); + } + + friend constexpr uint operator+(uint x, uint y) noexcept { return addc(x, y).value; } + + constexpr uint operator+() const noexcept { return *this; } + + friend constexpr uint operator-(uint x, uint y) noexcept { return subc(x, y).value; } + + constexpr uint operator-() const noexcept + { + // Implementing as subtraction is better than ~x + 1. + // Clang9: Perfect. + // GCC8: Does something weird. + return 0 - *this; + } + + constexpr uint& operator+=(uint y) noexcept { return *this = *this + y; } + + constexpr uint& operator-=(uint y) noexcept { return *this = *this - y; } + + constexpr uint& operator++() noexcept { return *this += 1; } + + constexpr uint& operator--() noexcept { return *this -= 1; } + + constexpr const uint operator++(int) noexcept // NOLINT(*-const-return-type) + { + const auto ret = *this; + *this += 1; + return ret; + } + + constexpr const uint operator--(int) noexcept // NOLINT(*-const-return-type) + { + const auto ret = *this; + *this -= 1; + return ret; + } + + friend constexpr bool operator==(uint x, uint y) noexcept + { + return ((x[0] ^ y[0]) | (x[1] ^ y[1])) == 0; + } + + friend constexpr bool operator<(uint x, uint y) noexcept + { + // OPT: This should be implemented by checking the borrow of x - y, + // but compilers (GCC8, Clang7) + // have problem with properly optimizing subtraction. + +#if INTX_HAS_BUILTIN_INT128 + return builtin_uint128{x} < builtin_uint128{y}; +#else + return (unsigned{x[1] < y[1]} | (unsigned{x[1] == y[1]} & unsigned{x[0] < y[0]})) != 0; +#endif + } + friend constexpr bool operator<=(uint x, uint y) noexcept { return !(y < x); } + friend constexpr bool operator>(uint x, uint y) noexcept { return y < x; } + friend constexpr bool operator>=(uint x, uint y) noexcept { return !(x < y); } + + friend constexpr std::strong_ordering operator<=>(uint x, uint y) noexcept + { + if (x == y) + return std::strong_ordering::equal; + + return (x < y) ? std::strong_ordering::less : std::strong_ordering::greater; + } + + friend constexpr uint operator~(uint x) noexcept { return {~x[0], ~x[1]}; } + friend constexpr uint operator|(uint x, uint y) noexcept { return {x[0] | y[0], x[1] | y[1]}; } + friend constexpr uint operator&(uint x, uint y) noexcept { return {x[0] & y[0], x[1] & y[1]}; } + friend constexpr uint operator^(uint x, uint y) noexcept { return {x[0] ^ y[0], x[1] ^ y[1]}; } + + friend constexpr uint operator<<(uint x, uint64_t shift) noexcept + { + if (shift < 64) + { + // Find the part moved from lo to hi. + // For shift == 0 right shift by (64 - shift) is invalid so + // split it into 2 shifts by 1 and (63 - shift). + return {x[0] << shift, (x[1] << shift) | ((x[0] >> 1) >> (63 - shift))}; + } + if (shift < 128) + { + // The lo part becomes the shifted hi part. + return {0, x[0] << (shift - 64)}; + } + + // Guarantee "defined" behavior for shifts larger than 128. + return 0; + } + + friend constexpr uint operator<<(uint x, std::integral auto shift) noexcept + { + static_assert(sizeof(shift) <= sizeof(uint64_t)); + return x << static_cast(shift); + } + + friend constexpr uint operator<<(uint x, uint shift) noexcept + { + if (shift[1] != 0) [[unlikely]] + return 0; + + return x << shift[0]; + } + + friend constexpr uint operator>>(uint x, uint64_t shift) noexcept + { + if (shift < 64) + { + // Find the part moved from lo to hi. + // For shift == 0 left shift by (64 - shift) is invalid so + // split it into 2 shifts by 1 and (63 - shift). + return {(x[0] >> shift) | ((x[1] << 1) << (63 - shift)), x[1] >> shift}; + } + if (shift < 128) + { + // The lo part becomes the shifted hi part. + return {x[1] >> (shift - 64), 0}; + } + + // Guarantee "defined" behavior for shifts larger than 128. + return 0; + } + + friend constexpr uint operator>>(uint x, std::integral auto shift) noexcept + { + static_assert(sizeof(shift) <= sizeof(uint64_t)); + return x >> static_cast(shift); + } + + friend constexpr uint operator>>(uint x, uint shift) noexcept + { + if (shift[1] != 0) [[unlikely]] + return 0; + + return x >> shift[0]; + } + + friend constexpr uint operator*(uint x, uint y) noexcept + { + auto p = umul(x[0], y[0]); + p[1] += (x[0] * y[1]) + (x[1] * y[0]); + return {p[0], p[1]}; + } + + friend constexpr div_result udivrem(uint x, uint y) noexcept; + friend constexpr uint operator/(uint x, uint y) noexcept { return udivrem(x, y).quot; } + friend constexpr uint operator%(uint x, uint y) noexcept { return udivrem(x, y).rem; } + + constexpr uint& operator*=(uint y) noexcept { return *this = *this * y; } + constexpr uint& operator|=(uint y) noexcept { return *this = *this | y; } + constexpr uint& operator&=(uint y) noexcept { return *this = *this & y; } + constexpr uint& operator^=(uint y) noexcept { return *this = *this ^ y; } + constexpr uint& operator<<=(uint shift) noexcept { return *this = *this << shift; } + constexpr uint& operator>>=(uint shift) noexcept { return *this = *this >> shift; } + constexpr uint& operator/=(uint y) noexcept { return *this = *this / y; } + constexpr uint& operator%=(uint y) noexcept { return *this = *this % y; } +}; + +using uint128 = uint<128>; + + +/// Optimized addition. +/// +/// This keeps the multiprecision addition until CodeGen so the pattern is not +/// broken during other optimizations. +constexpr uint128 fast_add(uint128 x, uint128 y) noexcept +{ +#if INTX_HAS_BUILTIN_INT128 + return builtin_uint128{x} + builtin_uint128{y}; +#else + return x + y; // Fallback to generic addition. +#endif +} + +/// Full unsigned multiplication 64 x 64 -> 128. +constexpr uint128 umul(uint64_t x, uint64_t y) noexcept +{ +#if INTX_HAS_BUILTIN_INT128 + return builtin_uint128{x} * builtin_uint128{y}; +#elif defined(_MSC_VER) && _MSC_VER >= 1925 && defined(_M_X64) + if (!std::is_constant_evaluated()) + { + unsigned __int64 hi = 0; + const auto lo = _umul128(x, y, &hi); + return {lo, hi}; + } + // For constexpr fallback to portable variant. +#endif + + // Portable full unsigned multiplication 64 x 64 -> 128. + uint64_t xl = x & 0xffffffff; + uint64_t xh = x >> 32; + uint64_t yl = y & 0xffffffff; + uint64_t yh = y >> 32; + + uint64_t t0 = xl * yl; + uint64_t t1 = xh * yl; + uint64_t t2 = xl * yh; + uint64_t t3 = xh * yh; + + uint64_t u1 = t1 + (t0 >> 32); + uint64_t u2 = t2 + (u1 & 0xffffffff); + + uint64_t lo = (u2 << 32) | (t0 & 0xffffffff); + uint64_t hi = t3 + (u2 >> 32) + (u1 >> 32); + return {lo, hi}; +} + +constexpr unsigned clz(std::unsigned_integral auto x) noexcept +{ + return static_cast(std::countl_zero(x)); +} + +constexpr unsigned clz(uint128 x) noexcept +{ + // In this order `h == 0` we get less instructions than in case of `h != 0`. + return x[1] == 0 ? clz(x[0]) + 64 : clz(x[1]); +} + +template +T bswap(T x) noexcept = delete; // Disable type auto promotion + +constexpr uint8_t bswap(uint8_t x) noexcept +{ + return x; +} + +constexpr uint16_t bswap(uint16_t x) noexcept +{ +#if __has_builtin(__builtin_bswap16) + return __builtin_bswap16(x); +#else + #ifdef _MSC_VER + if (!std::is_constant_evaluated()) + return _byteswap_ushort(x); + #endif + return static_cast((x << 8) | (x >> 8)); +#endif +} + +constexpr uint32_t bswap(uint32_t x) noexcept +{ +#if __has_builtin(__builtin_bswap32) + return __builtin_bswap32(x); +#else + #ifdef _MSC_VER + if (!std::is_constant_evaluated()) + return _byteswap_ulong(x); + #endif + const auto a = ((x << 8) & 0xFF00FF00) | ((x >> 8) & 0x00FF00FF); + return (a << 16) | (a >> 16); +#endif +} + +constexpr uint64_t bswap(uint64_t x) noexcept +{ +#if __has_builtin(__builtin_bswap64) + return __builtin_bswap64(x); +#else + #ifdef _MSC_VER + if (!std::is_constant_evaluated()) + return _byteswap_uint64(x); + #endif + const auto a = ((x << 8) & 0xFF00FF00FF00FF00) | ((x >> 8) & 0x00FF00FF00FF00FF); + const auto b = ((a << 16) & 0xFFFF0000FFFF0000) | ((a >> 16) & 0x0000FFFF0000FFFF); + return (b << 32) | (b >> 32); +#endif +} + +constexpr uint128 bswap(uint128 x) noexcept +{ + return {bswap(x[1]), bswap(x[0])}; +} + + +/// Division. +/// @{ + +namespace internal +{ +/// Reciprocal lookup table. +constexpr auto reciprocal_table = []() noexcept { + std::array table{}; + for (size_t i = 0; i < table.size(); ++i) + table[i] = static_cast(0x7fd00 / (i + 256)); + return table; +}(); +} // namespace internal + +/// Computes the reciprocal (2^128 - 1) / d - 2^64 for normalized d. +/// +/// Based on Algorithm 2 from "Improved division by invariant integers". +constexpr uint64_t reciprocal_2by1(uint64_t d) noexcept +{ + INTX_REQUIRE(d & 0x8000000000000000); // Must be normalized. + + const uint64_t d9 = d >> 55; + const uint32_t v0 = internal::reciprocal_table[static_cast(d9 - 256)]; + + const uint64_t d40 = (d >> 24) + 1; + const uint64_t v1 = (v0 << 11) - uint32_t(uint32_t{v0 * v0} * d40 >> 40) - 1; + + const uint64_t v2 = (v1 << 13) + (v1 * (0x1000000000000000 - v1 * d40) >> 47); + + const uint64_t d0 = d & 1; + const uint64_t d63 = (d >> 1) + d0; // ceil(d/2) + const uint64_t e = ((v2 >> 1) & (0 - d0)) - (v2 * d63); + const uint64_t v3 = (umul(v2, e)[1] >> 1) + (v2 << 31); + + const uint64_t v4 = v3 - (umul(v3, d) + d)[1] - d; + return v4; +} + +constexpr uint64_t reciprocal_3by2(uint128 d) noexcept +{ + auto v = reciprocal_2by1(d[1]); + auto p = d[1] * v; + p += d[0]; + if (p < d[0]) + { + --v; + if (p >= d[1]) + { + --v; + p -= d[1]; + } + p -= d[1]; + } + + const auto t = umul(v, d[0]); + + p += t[1]; + if (p < t[1]) + { + --v; + if (p >= d[1]) + { + if (p > d[1] || t[0] >= d[0]) + --v; + } + } + return v; +} + +constexpr div_result udivrem_2by1(uint128 u, uint64_t d, uint64_t v) noexcept +{ + auto q = umul(v, u[1]); + q = fast_add(q, u); + + ++q[1]; + + auto r = u[0] - (q[1] * d); + + if (r > q[0]) + { + --q[1]; + r += d; + } + + if (r >= d) + { + ++q[1]; + r -= d; + } + + return {q[1], r}; +} + +constexpr div_result udivrem_3by2( + uint64_t u2, uint64_t u1, uint64_t u0, uint128 d, uint64_t v) noexcept +{ + auto q = umul(v, u2); + q = fast_add(q, {u1, u2}); + + auto r1 = u1 - (q[1] * d[1]); + + auto t = umul(d[0], q[1]); + + auto r = uint128{u0, r1} - t - d; + r1 = r[1]; + + ++q[1]; + + if (r1 >= q[0]) + { + --q[1]; + r += d; + } + + if (r >= d) + { + ++q[1]; + r -= d; + } + + return {q[1], r}; +} + +constexpr div_result udivrem(uint128 x, uint128 y) noexcept +{ + if (y[1] == 0) + { + INTX_REQUIRE(y[0] != 0); // Division by 0. + + const auto lsh = clz(y[0]); + const auto rsh = (64 - lsh) % 64; + const auto rsh_mask = uint64_t{lsh == 0} - 1; + + const auto yn = y[0] << lsh; + const auto xn_lo = x[0] << lsh; + const auto xn_hi = (x[1] << lsh) | ((x[0] >> rsh) & rsh_mask); + const auto xn_ex = (x[1] >> rsh) & rsh_mask; + + const auto v = reciprocal_2by1(yn); + const auto res1 = udivrem_2by1({xn_hi, xn_ex}, yn, v); + const auto res2 = udivrem_2by1({xn_lo, res1.rem}, yn, v); + return {{res2.quot, res1.quot}, res2.rem >> lsh}; + } + + if (y[1] > x[1]) + return {0, x}; + + const auto lsh = clz(y[1]); + if (lsh == 0) + { + const auto q = unsigned{y[1] < x[1]} | unsigned{y[0] <= x[0]}; + return {q, x - (q ? y : 0)}; + } + + const auto rsh = 64 - lsh; + + const auto yn_lo = y[0] << lsh; + const auto yn_hi = (y[1] << lsh) | (y[0] >> rsh); + const auto xn_lo = x[0] << lsh; + const auto xn_hi = (x[1] << lsh) | (x[0] >> rsh); + const auto xn_ex = x[1] >> rsh; + + const auto v = reciprocal_3by2({yn_lo, yn_hi}); + const auto res = udivrem_3by2(xn_ex, xn_hi, xn_lo, {yn_lo, yn_hi}, v); + + return {res.quot, res.rem >> lsh}; +} + +constexpr div_result sdivrem(uint128 x, uint128 y) noexcept +{ + constexpr auto sign_mask = uint128{1} << 127; + const auto x_is_neg = (x & sign_mask) != 0; + const auto y_is_neg = (y & sign_mask) != 0; + + const auto x_abs = x_is_neg ? -x : x; + const auto y_abs = y_is_neg ? -y : y; + + const auto q_is_neg = x_is_neg ^ y_is_neg; + + const auto res = udivrem(x_abs, y_abs); + + return {q_is_neg ? -res.quot : res.quot, x_is_neg ? -res.rem : res.rem}; +} + +/// @} + +} // namespace intx + + +namespace std +{ +template +struct numeric_limits> // NOLINT(cert-dcl58-cpp) +{ + using type = intx::uint; + + static constexpr bool is_specialized = true; + static constexpr bool is_integer = true; + static constexpr bool is_signed = false; + static constexpr bool is_exact = true; + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + static constexpr float_round_style round_style = round_toward_zero; + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = true; + static constexpr int digits = CHAR_BIT * sizeof(type); + static constexpr int digits10 = int(0.3010299956639812 * digits); + static constexpr int max_digits10 = 0; + static constexpr int radix = 2; + static constexpr int min_exponent = 0; + static constexpr int min_exponent10 = 0; + static constexpr int max_exponent = 0; + static constexpr int max_exponent10 = 0; + static constexpr bool traps = std::numeric_limits::traps; + static constexpr bool tinyness_before = false; + + static constexpr type min() noexcept { return 0; } + static constexpr type lowest() noexcept { return min(); } + static constexpr type max() noexcept { return ~type{0}; } + static constexpr type epsilon() noexcept { return 0; } + static constexpr type round_error() noexcept { return 0; } + static constexpr type infinity() noexcept { return 0; } + static constexpr type quiet_NaN() noexcept { return 0; } + static constexpr type signaling_NaN() noexcept { return 0; } + static constexpr type denorm_min() noexcept { return 0; } +}; +} // namespace std + +namespace intx +{ +template +[[noreturn]] inline void throw_(const char* what) +{ +#if __cpp_exceptions + throw T{what}; +#else + std::fputs(what, stderr); + std::abort(); +#endif +} + +constexpr int from_dec_digit(char c) +{ + if (c < '0' || c > '9') + throw_("invalid digit"); + return c - '0'; +} + +constexpr int from_hex_digit(char c) +{ + if (c >= 'a' && c <= 'f') + return c - ('a' - 10); + if (c >= 'A' && c <= 'F') + return c - ('A' - 10); + return from_dec_digit(c); +} + +template +constexpr Int from_string(const char* str) +{ + auto s = str; + auto x = Int{}; + int num_digits = 0; + + if (s[0] == '0' && s[1] == 'x') + { + s += 2; + while (const auto c = *s++) + { + if (++num_digits > int{sizeof(x) * 2}) + throw_(str); + x = (x << uint64_t{4}) | from_hex_digit(c); + } + return x; + } + + while (const auto c = *s++) + { + if (num_digits++ > std::numeric_limits::digits10) + throw_(str); + + const auto d = from_dec_digit(c); + x = x * Int{10} + d; + if (x < d) + throw_(str); + } + return x; +} + +template +constexpr Int from_string(const std::string& s) +{ + return from_string(s.c_str()); +} + +template +inline std::string to_string(uint x, int base = 10) +{ + if (base < 2 || base > 36) + throw_("invalid base"); + + if (x == 0) + return "0"; + + auto s = std::string{}; + while (x != 0) + { + // TODO: Use constexpr udivrem_1? + const auto res = udivrem(x, uint{base}); + const auto d = int(res.rem); + const auto c = d < 10 ? '0' + d : 'a' + d - 10; + s.push_back(char(c)); + x = res.quot; + } + std::ranges::reverse(s); + return s; +} + +template +inline std::string hex(uint x) +{ + return to_string(x, 16); +} + +template +struct uint +{ + using word_type = uint64_t; + static constexpr auto word_num_bits = sizeof(word_type) * 8; + static constexpr auto num_bits = N; + static constexpr auto num_words = num_bits / word_num_bits; + + static_assert(N >= 2 * word_num_bits, "Number of bits must be at lest 128"); + static_assert(N % word_num_bits == 0, "Number of bits must be a multiply of 64"); + +private: + uint64_t words_[num_words]{}; + +public: + constexpr uint() noexcept = default; + + /// Implicit converting constructor for any smaller uint type. + template + constexpr explicit(false) uint(const uint& x) noexcept + requires(M < N) + { + for (size_t i = 0; i < uint::num_words; ++i) + words_[i] = x[i]; + } + + template + constexpr explicit(false) uint(T... v) noexcept + requires std::conjunction_v...> + : words_{static_cast(v)...} + {} + + constexpr uint64_t& operator[](size_t i) noexcept { return words_[i]; } + + constexpr const uint64_t& operator[](size_t i) const noexcept { return words_[i]; } + + constexpr explicit operator bool() const noexcept { return *this != uint{}; } + + /// Explicit converting operator to smaller uint types. + template + constexpr explicit operator uint() const noexcept + requires(M < N) + { + uint r; + for (size_t i = 0; i < uint::num_words; ++i) + r[i] = words_[i]; + return r; + } + + /// Explicit converting operator for all builtin integral types. + template + constexpr explicit operator Int() const noexcept + requires(std::is_integral_v) + { + static_assert(sizeof(Int) <= sizeof(uint64_t)); + return static_cast(words_[0]); + } + + friend constexpr uint operator+(const uint& x, const uint& y) noexcept + { + return addc(x, y).value; + } + + constexpr uint& operator+=(const uint& y) noexcept { return *this = *this + y; } + + constexpr uint operator-() const noexcept { return ~*this + uint{1}; } + + friend constexpr uint operator-(const uint& x, const uint& y) noexcept + { + return subc(x, y).value; + } + + constexpr uint& operator-=(const uint& y) noexcept { return *this = *this - y; } + + /// Multiplication implementation using word access + /// and discarding the high part of the result product. + friend constexpr uint operator*(const uint& x, const uint& y) noexcept + { + uint p; + for (size_t j = 0; j < num_words; j++) + { + uint64_t k = 0; + for (size_t i = 0; i < (num_words - j - 1); i++) + { + auto a = addc(p[i + j], k); + auto t = umul(x[i], y[j]) + uint128{a.value, a.carry}; + p[i + j] = t[0]; + k = t[1]; + } + p[num_words - 1] += x[num_words - j - 1] * y[j] + k; + } + return p; + } + + constexpr uint& operator*=(const uint& y) noexcept { return *this = *this * y; } + + friend constexpr uint operator/(const uint& x, const uint& y) noexcept + { + return udivrem(x, y).quot; + } + + friend constexpr uint operator%(const uint& x, const uint& y) noexcept + { + return udivrem(x, y).rem; + } + + constexpr uint& operator/=(const uint& y) noexcept { return *this = *this / y; } + + constexpr uint& operator%=(const uint& y) noexcept { return *this = *this % y; } + + + constexpr uint operator~() const noexcept + { + uint z; + for (size_t i = 0; i < num_words; ++i) + z[i] = ~words_[i]; + return z; + } + + friend constexpr uint operator|(const uint& x, const uint& y) noexcept + { + uint z; + for (size_t i = 0; i < num_words; ++i) + z[i] = x[i] | y[i]; + return z; + } + + constexpr uint& operator|=(const uint& y) noexcept { return *this = *this | y; } + + friend constexpr uint operator&(const uint& x, const uint& y) noexcept + { + uint z; + for (size_t i = 0; i < num_words; ++i) + z[i] = x[i] & y[i]; + return z; + } + + constexpr uint& operator&=(const uint& y) noexcept { return *this = *this & y; } + + friend constexpr uint operator^(const uint& x, const uint& y) noexcept + { + uint z; + for (size_t i = 0; i < num_words; ++i) + z[i] = x[i] ^ y[i]; + return z; + } + + constexpr uint& operator^=(const uint& y) noexcept { return *this = *this ^ y; } + + friend constexpr bool operator==(const uint& x, const uint& y) noexcept + { + uint64_t folded = 0; + for (size_t i = 0; i < num_words; ++i) + folded |= (x[i] ^ y[i]); + return folded == 0; + } + + friend constexpr bool operator<(const uint& x, const uint& y) noexcept + { + if constexpr (N == 256) + { + auto xp = uint128{x[2], x[3]}; + auto yp = uint128{y[2], y[3]}; + if (xp == yp) + { + xp = uint128{x[0], x[1]}; + yp = uint128{y[0], y[1]}; + } + return xp < yp; + } + else + return subc(x, y).carry; + } + friend constexpr bool operator>(const uint& x, const uint& y) noexcept { return y < x; } + friend constexpr bool operator>=(const uint& x, const uint& y) noexcept { return !(x < y); } + friend constexpr bool operator<=(const uint& x, const uint& y) noexcept { return !(y < x); } + + friend constexpr std::strong_ordering operator<=>(const uint& x, const uint& y) noexcept + { + if (x == y) + return std::strong_ordering::equal; + + return (x < y) ? std::strong_ordering::less : std::strong_ordering::greater; + } + + friend constexpr uint operator<<(const uint& x, uint64_t shift) noexcept + { + if (shift >= num_bits) [[unlikely]] + return 0; + + if constexpr (N == 256) + { + constexpr auto half_bits = num_bits / 2; + + const auto xlo = uint128{x[0], x[1]}; + + if (shift < half_bits) + { + const auto lo = xlo << shift; + + const auto xhi = uint128{x[2], x[3]}; + + // Find the part moved from lo to hi. + // The shift right here can be invalid: + // for shift == 0 => rshift == half_bits. + // Split it into 2 valid shifts by (rshift - 1) and 1. + const auto rshift = half_bits - shift; + const auto lo_overflow = (xlo >> (rshift - 1)) >> 1; + const auto hi = (xhi << shift) | lo_overflow; + return {lo[0], lo[1], hi[0], hi[1]}; + } + + const auto hi = xlo << (shift - half_bits); + return {0, 0, hi[0], hi[1]}; + } + else + { + constexpr auto word_bits = sizeof(uint64_t) * 8; + + const auto s = shift % word_bits; + const auto skip = static_cast(shift / word_bits); + + uint r; + uint64_t carry = 0; + for (size_t i = 0; i < (num_words - skip); ++i) + { + r[i + skip] = (x[i] << s) | carry; + carry = (x[i] >> (word_bits - s - 1)) >> 1; + } + return r; + } + } + + friend constexpr uint operator<<(const uint& x, std::integral auto shift) noexcept + { + static_assert(sizeof(shift) <= sizeof(uint64_t)); + return x << static_cast(shift); + } + + friend constexpr uint operator<<(const uint& x, const uint& shift) noexcept + { + // TODO: This optimisation should be handled by operator<. + uint64_t high_words_fold = 0; + for (size_t i = 1; i < num_words; ++i) + high_words_fold |= shift[i]; + + if (high_words_fold != 0) [[unlikely]] + return 0; + + return x << shift[0]; + } + + friend constexpr uint operator>>(const uint& x, uint64_t shift) noexcept + { + if (shift >= num_bits) [[unlikely]] + return 0; + + if constexpr (N == 256) + { + constexpr auto half_bits = num_bits / 2; + + const auto xhi = uint128{x[2], x[3]}; + + if (shift < half_bits) + { + const auto hi = xhi >> shift; + + const auto xlo = uint128{x[0], x[1]}; + + // Find the part moved from hi to lo. + // The shift left here can be invalid: + // for shift == 0 => lshift == half_bits. + // Split it into 2 valid shifts by (lshift - 1) and 1. + const auto lshift = half_bits - shift; + const auto hi_overflow = (xhi << (lshift - 1)) << 1; + const auto lo = (xlo >> shift) | hi_overflow; + return {lo[0], lo[1], hi[0], hi[1]}; + } + + const auto lo = xhi >> (shift - half_bits); + return {lo[0], lo[1], 0, 0}; + } + else + { + constexpr auto word_bits = sizeof(uint64_t) * 8; + + const auto s = shift % word_bits; + const auto skip = static_cast(shift / word_bits); + + uint r; + uint64_t carry = 0; + for (size_t i = 0; i < (num_words - skip); ++i) + { + r[num_words - 1 - i - skip] = (x[num_words - 1 - i] >> s) | carry; + carry = (x[num_words - 1 - i] << (word_bits - s - 1)) << 1; + } + return r; + } + } + + friend constexpr uint operator>>(const uint& x, std::integral auto shift) noexcept + { + static_assert(sizeof(shift) <= sizeof(uint64_t)); + return x >> static_cast(shift); + } + + friend constexpr uint operator>>(const uint& x, const uint& shift) noexcept + { + uint64_t high_words_fold = 0; + for (size_t i = 1; i < num_words; ++i) + high_words_fold |= shift[i]; + + if (high_words_fold != 0) [[unlikely]] + return 0; + + return x >> shift[0]; + } + + constexpr uint& operator<<=(uint shift) noexcept { return *this = *this << shift; } + constexpr uint& operator>>=(uint shift) noexcept { return *this = *this >> shift; } +}; + +using uint256 = uint<256>; + + +/// Signed less than comparison. +/// +/// Interprets the arguments as two's complement signed integers +/// and checks the "less than" relation. +template +constexpr bool slt(const uint& x, const uint& y) noexcept +{ + constexpr auto top_word_idx = uint::num_words - 1; + const auto x_neg = static_cast(x[top_word_idx]) < 0; + const auto y_neg = static_cast(y[top_word_idx]) < 0; + return ((x_neg ^ y_neg) != 0) ? x_neg : x < y; +} + + +constexpr uint64_t* as_words(uint128& x) noexcept +{ + return &x[0]; +} + +constexpr const uint64_t* as_words(const uint128& x) noexcept +{ + return &x[0]; +} + +template +constexpr uint64_t* as_words(uint& x) noexcept +{ + return &x[0]; +} + +template +constexpr const uint64_t* as_words(const uint& x) noexcept +{ + return &x[0]; +} + +template +inline uint8_t* as_bytes(T& x) noexcept +{ + static_assert(std::is_trivially_copyable_v); // As in bit_cast. + return reinterpret_cast(&x); +} + +template +inline const uint8_t* as_bytes(const T& x) noexcept +{ + static_assert(std::is_trivially_copyable_v); // As in bit_cast. + return reinterpret_cast(&x); +} + +template +constexpr uint<2 * N> umul(const uint& x, const uint& y) noexcept +{ + constexpr auto num_words = uint::num_words; + + uint<2 * N> p; + for (size_t j = 0; j < num_words; ++j) + { + uint64_t k = 0; + for (size_t i = 0; i < num_words; ++i) + { + auto a = addc(p[i + j], k); + auto t = umul(x[i], y[j]) + uint128{a.value, a.carry}; + p[i + j] = t[0]; + k = t[1]; + } + p[j + num_words] = k; + } + return p; +} + +template +constexpr uint exp(uint base, uint exponent) noexcept +{ + auto result = uint{1}; + if (base == 2) + return result << exponent; + + while (exponent != 0) + { + if ((exponent & 1) != 0) + result *= base; + base *= base; + exponent >>= 1; + } + return result; +} + +template +constexpr unsigned count_significant_words(const uint& x) noexcept +{ + for (size_t i = uint::num_words; i > 0; --i) + { + if (x[i - 1] != 0) + return static_cast(i); + } + return 0; +} + +constexpr unsigned count_significant_bytes(uint64_t x) noexcept +{ + return (64 - clz(x) + 7) / 8; +} + +template +constexpr unsigned count_significant_bytes(const uint& x) noexcept +{ + const auto w = count_significant_words(x); + return (w != 0) ? count_significant_bytes(x[w - 1]) + (w - 1) * 8 : 0; +} + +template +constexpr unsigned clz(const uint& x) noexcept +{ + constexpr unsigned num_words = uint::num_words; + const auto s = count_significant_words(x); + if (s == 0) + return num_words * 64; + return clz(x[s - 1]) + (num_words - s) * 64; +} + +namespace internal +{ +/// Counts the number of zero leading bits in nonzero argument x. +constexpr unsigned clz_nonzero(uint64_t x) noexcept +{ + INTX_REQUIRE(x != 0); + return static_cast(std::countl_zero(x)); +} + +template +struct normalized_div_args // NOLINT(cppcoreguidelines-pro-type-member-init) +{ + uint divisor; + uint numerator; + int num_divisor_words; + int num_numerator_words; + unsigned shift; +}; + +template +[[gnu::always_inline]] constexpr normalized_div_args normalize( + const uint& numerator, const uint& denominator) noexcept +{ + constexpr auto num_numerator_words = uint::num_words; + constexpr auto num_denominator_words = uint::num_words; + + auto* u = as_words(numerator); + auto* v = as_words(denominator); + + normalized_div_args na; + auto* un = as_words(na.numerator); + auto* vn = as_words(na.divisor); + + auto& m = na.num_numerator_words; + for (m = num_numerator_words; m > 0 && u[m - 1] == 0; --m) + ; + + auto& n = na.num_divisor_words; + for (n = num_denominator_words; n > 0 && v[n - 1] == 0; --n) + ; + + na.shift = clz_nonzero(v[n - 1]); // Use clz_nonzero() to avoid clang analyzer's warning. + if (na.shift) + { + for (int i = num_denominator_words - 1; i > 0; --i) + vn[i] = (v[i] << na.shift) | (v[i - 1] >> (64 - na.shift)); + vn[0] = v[0] << na.shift; + + un[num_numerator_words] = u[num_numerator_words - 1] >> (64 - na.shift); + for (int i = num_numerator_words - 1; i > 0; --i) + un[i] = (u[i] << na.shift) | (u[i - 1] >> (64 - na.shift)); + un[0] = u[0] << na.shift; + } + else + { + na.numerator = numerator; + na.divisor = denominator; + } + + // Add the highest word of the normalized numerator if significant. + if (m != 0 && (un[m] != 0 || un[m - 1] >= vn[n - 1])) + ++m; + + return na; +} + +/// Divides arbitrary long unsigned integer by 64-bit unsigned integer (1 word). +/// @param u The array of a normalized numerator words. It will contain +/// the quotient after execution. +/// @param len The number of numerator words. +/// @param d The normalized divisor. +/// @return The remainder. +constexpr uint64_t udivrem_by1(uint64_t u[], int len, uint64_t d) noexcept +{ + INTX_REQUIRE(len >= 2); + + const auto reciprocal = reciprocal_2by1(d); + + auto rem = u[len - 1]; // Set the top word as remainder. + u[len - 1] = 0; // Reset the word being a part of the result quotient. + + auto it = &u[len - 2]; + while (true) + { + std::tie(*it, rem) = udivrem_2by1({*it, rem}, d, reciprocal); + if (it == &u[0]) + break; + --it; + } + + return rem; +} + +/// Divides arbitrary long unsigned integer by 128-bit unsigned integer (2 words). +/// @param u The array of a normalized numerator words. It will contain the +/// quotient after execution. +/// @param len The number of numerator words. +/// @param d The normalized divisor. +/// @return The remainder. +constexpr uint128 udivrem_by2(uint64_t u[], int len, uint128 d) noexcept +{ + INTX_REQUIRE(len >= 3); + + const auto reciprocal = reciprocal_3by2(d); + + auto rem = uint128{u[len - 2], u[len - 1]}; // Set the 2 top words as remainder. + u[len - 1] = u[len - 2] = 0; // Reset these words being a part of the result quotient. + + auto it = &u[len - 3]; + while (true) + { + std::tie(*it, rem) = udivrem_3by2(rem[1], rem[0], *it, d, reciprocal); + if (it == &u[0]) + break; + --it; + } + + return rem; +} + +/// s = x + y. +constexpr bool add(uint64_t s[], const uint64_t x[], const uint64_t y[], int len) noexcept +{ + // OPT: Add MinLen template parameter and unroll first loop iterations. + INTX_REQUIRE(len >= 2); + + bool carry = false; + for (int i = 0; i < len; ++i) + std::tie(s[i], carry) = addc(x[i], y[i], carry); + return carry; +} + +/// r = x - multiplier * y. +constexpr uint64_t submul( + uint64_t r[], const uint64_t x[], const uint64_t y[], int len, uint64_t multiplier) noexcept +{ + // OPT: Add MinLen template parameter and unroll first loop iterations. + INTX_REQUIRE(len >= 1); + + uint64_t borrow = 0; + for (int i = 0; i < len; ++i) + { + const auto s = x[i] - borrow; + const auto p = umul(y[i], multiplier); + borrow = p[1] + (x[i] < s); + r[i] = s - p[0]; + borrow += (s < r[i]); + } + return borrow; +} + +constexpr void udivrem_knuth( + uint64_t q[], uint64_t u[], int ulen, const uint64_t d[], int dlen) noexcept +{ + INTX_REQUIRE(dlen >= 3); + INTX_REQUIRE(ulen >= dlen); + + const auto divisor = uint128{d[dlen - 2], d[dlen - 1]}; + const auto reciprocal = reciprocal_3by2(divisor); + for (int j = ulen - dlen - 1; j >= 0; --j) + { + const auto u2 = u[j + dlen]; + const auto u1 = u[j + dlen - 1]; + const auto u0 = u[j + dlen - 2]; + + uint64_t qhat{}; + if (INTX_UNLIKELY((uint128{u1, u2}) == divisor)) // Division overflows. + { + qhat = ~uint64_t{0}; + + u[j + dlen] = u2 - submul(&u[j], &u[j], d, dlen, qhat); + } + else + { + uint128 rhat; + std::tie(qhat, rhat) = udivrem_3by2(u2, u1, u0, divisor, reciprocal); + + bool carry{}; + const auto overflow = submul(&u[j], &u[j], d, dlen - 2, qhat); + std::tie(u[j + dlen - 2], carry) = subc(rhat[0], overflow); + std::tie(u[j + dlen - 1], carry) = subc(rhat[1], carry); + + if (INTX_UNLIKELY(carry)) + { + --qhat; + u[j + dlen - 1] += divisor[1] + add(&u[j], &u[j], d, dlen - 1); + } + } + + q[j] = qhat; // Store quotient digit. + } +} + +} // namespace internal + +template +constexpr div_result, uint> udivrem(const uint& u, const uint& v) noexcept +{ + auto na = internal::normalize(u, v); + INTX_REQUIRE(na.num_divisor_words > 0); + INTX_REQUIRE(na.num_numerator_words >= 0); + + if (na.num_numerator_words <= na.num_divisor_words) + return {0, static_cast>(u)}; + + if (na.num_divisor_words == 1) + { + const auto r = internal::udivrem_by1( + as_words(na.numerator), na.num_numerator_words, as_words(na.divisor)[0]); + return {static_cast>(na.numerator), r >> na.shift}; + } + + if (na.num_divisor_words == 2) + { + const auto d = as_words(na.divisor); + const auto r = + internal::udivrem_by2(as_words(na.numerator), na.num_numerator_words, {d[0], d[1]}); + return {static_cast>(na.numerator), r >> na.shift}; + } + + auto un = as_words(na.numerator); // Will be modified. + + uint q; + internal::udivrem_knuth( + as_words(q), &un[0], na.num_numerator_words, as_words(na.divisor), na.num_divisor_words); + + uint r; + auto rw = as_words(r); + for (int i = 0; i < na.num_divisor_words - 1; ++i) + rw[i] = na.shift ? (un[i] >> na.shift) | (un[i + 1] << (64 - na.shift)) : un[i]; + rw[na.num_divisor_words - 1] = un[na.num_divisor_words - 1] >> na.shift; + + return {q, r}; +} + +template +constexpr div_result> sdivrem(const uint& u, const uint& v) noexcept +{ + const auto sign_mask = uint{1} << (uint::num_bits - 1); + auto u_is_neg = (u & sign_mask) != 0; + auto v_is_neg = (v & sign_mask) != 0; + + auto u_abs = u_is_neg ? -u : u; + auto v_abs = v_is_neg ? -v : v; + + auto q_is_neg = u_is_neg ^ v_is_neg; + + auto res = udivrem(u_abs, v_abs); + + return {q_is_neg ? -res.quot : res.quot, u_is_neg ? -res.rem : res.rem}; +} + +constexpr uint256 bswap(const uint256& x) noexcept +{ + return {bswap(x[3]), bswap(x[2]), bswap(x[1]), bswap(x[0])}; +} + +template +constexpr uint bswap(const uint& x) noexcept +{ + constexpr auto num_words = uint::num_words; + uint z; + for (size_t i = 0; i < num_words; ++i) + z[num_words - 1 - i] = bswap(x[i]); + return z; +} + + +constexpr uint256 addmod(const uint256& x, const uint256& y, const uint256& mod) noexcept +{ + // Fast path for mod >= 2^192, with x and y at most slightly bigger than mod. + // This is always the case when x and y are already reduced modulo mod. + // Based on https://github.com/holiman/uint256/pull/86. + if ((mod[3] != 0) && (x[3] <= mod[3]) && (y[3] <= mod[3])) + { + // Normalize x in case it is bigger than mod. + auto xn = x; + auto xd = subc(x, mod); + if (!xd.carry) + xn = xd.value; + + // Normalize y in case it is bigger than mod. + auto yn = y; + auto yd = subc(y, mod); + if (!yd.carry) + yn = yd.value; + + auto a = addc(xn, yn); + auto av = a.value; + auto b = subc(av, mod); + auto bv = b.value; + if (a.carry || !b.carry) + return bv; + return av; + } + + auto s = addc(x, y); + uint<256 + 64> n = s.value; + n[4] = s.carry; + return udivrem(n, mod).rem; +} + +constexpr uint256 mulmod(const uint256& x, const uint256& y, const uint256& mod) noexcept +{ + return udivrem(umul(x, y), mod).rem; +} + +#define INTX_JOIN(X, Y) X##Y +/// Define type alias uintN = uint and the matching literal ""_uN. +/// The literal operators are defined in the intx::literals namespace. +#define DEFINE_ALIAS_AND_LITERAL(N) \ + using uint##N = uint; \ + namespace literals \ + { \ + consteval uint##N INTX_JOIN(operator"", _u##N)(const char* s) \ + { \ + return from_string(s); \ + } \ + } +DEFINE_ALIAS_AND_LITERAL(128); +DEFINE_ALIAS_AND_LITERAL(192); +DEFINE_ALIAS_AND_LITERAL(256); +DEFINE_ALIAS_AND_LITERAL(320); +DEFINE_ALIAS_AND_LITERAL(384); +DEFINE_ALIAS_AND_LITERAL(448); +DEFINE_ALIAS_AND_LITERAL(512); +#undef DEFINE_ALIAS_AND_LITERAL +#undef INTX_JOIN + +using namespace literals; + +/// Convert native representation to/from little-endian byte order. +/// intx and built-in integral types are supported. +template +constexpr T to_little_endian(const T& x) noexcept +{ + if constexpr (std::endian::native == std::endian::little) + return x; + else if constexpr (std::is_integral_v) + return bswap(x); + else // Wordwise bswap. + { + T r; + for (size_t i = 0; i < T::num_words; ++i) + r[i] = bswap(x[i]); + return r; + } +} + +/// Convert native representation to/from big-endian byte order. +/// intx and built-in integral types are supported. +template +constexpr T to_big_endian(const T& x) noexcept +{ + if constexpr (std::endian::native == std::endian::little) + return bswap(x); + else if constexpr (std::is_integral_v) + return x; + else // Swap words. + { + T r; + for (size_t i = 0; i < T::num_words; ++i) + r[T::num_words - 1 - i] = x[i]; + return r; + } +} + +namespace le // Conversions to/from LE bytes. +{ +template +inline T load(const uint8_t (&src)[M]) noexcept +{ + static_assert( + M == sizeof(T), "the size of source bytes must match the size of the destination uint"); + T x; + std::memcpy(&x, src, sizeof(x)); + return to_little_endian(x); +} + +template +inline void store(uint8_t (&dst)[sizeof(T)], const T& x) noexcept +{ + const auto d = to_little_endian(x); + std::memcpy(dst, &d, sizeof(d)); +} + +namespace unsafe +{ +template +inline T load(const uint8_t* src) noexcept +{ + T x; + std::memcpy(&x, src, sizeof(x)); + return to_little_endian(x); +} + +template +inline void store(uint8_t* dst, const T& x) noexcept +{ + const auto d = to_little_endian(x); + std::memcpy(dst, &d, sizeof(d)); +} +} // namespace unsafe +} // namespace le + + +namespace be // Conversions to/from BE bytes. +{ +/// Loads an integer value from bytes of big-endian order. +/// If the size of bytes is smaller than the result, the value is zero-extended. +template +inline T load(const uint8_t (&src)[M]) noexcept +{ + static_assert(M <= sizeof(T), + "the size of source bytes must not exceed the size of the destination uint"); + T x{}; + std::memcpy(&as_bytes(x)[sizeof(T) - M], src, M); + x = to_big_endian(x); + return x; +} + +template +inline IntT load(const T& t) noexcept +{ + return load(t.bytes); +} + +/// Stores an integer value in a bytes array in big-endian order. +template +inline void store(uint8_t (&dst)[sizeof(T)], const T& x) noexcept +{ + const auto d = to_big_endian(x); + std::memcpy(dst, &d, sizeof(d)); +} + +/// Stores an SrcT value in .bytes field of type DstT. The .bytes must be an array of uint8_t +/// of the size matching the size of uint. +template +inline DstT store(const SrcT& x) noexcept +{ + DstT r{}; + store(r.bytes, x); + return r; +} + +/// Stores the truncated value of an uint in a bytes array. +/// Only the least significant bytes from big-endian representation of the uint +/// are stored in the result bytes array up to array's size. +template +inline void trunc(uint8_t (&dst)[M], const uint& x) noexcept +{ + static_assert(M < N / 8, "destination must be smaller than the source value"); + const auto d = to_big_endian(x); + std::memcpy(dst, &as_bytes(d)[sizeof(d) - M], M); +} + +/// Stores the truncated value of an uint in the .bytes field of an object of type T. +template +inline T trunc(const uint& x) noexcept +{ + T r{}; + trunc(r.bytes, x); + return r; +} + +namespace unsafe +{ +/// Loads an uint value from a buffer. The user must make sure +/// that the provided buffer is big enough. Therefore, marked "unsafe". +template +inline IntT load(const uint8_t* src) noexcept +{ + // Align bytes. + // TODO: Using memcpy() directly triggers this optimization bug in GCC: + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107837 + alignas(IntT) std::byte aligned_storage[sizeof(IntT)]; + std::memcpy(&aligned_storage, src, sizeof(IntT)); + // TODO(C++23): Use std::start_lifetime_as(). + return to_big_endian(*reinterpret_cast(&aligned_storage)); +} + +/// Stores an integer value at the provided pointer in big-endian order. The user must make sure +/// that the provided buffer is big enough to fit the value. Therefore, marked "unsafe". +template +inline void store(uint8_t* dst, const T& x) noexcept +{ + const auto d = to_big_endian(x); + std::memcpy(dst, &d, sizeof(d)); +} + +/// Specialization for uint256. +inline void store(uint8_t* dst, const uint256& x) noexcept +{ + // Store byte-swapped words in primitive temporaries. This helps with memory aliasing + // and GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107837 + // TODO: Use std::byte instead of uint8_t. + const auto v0 = to_big_endian(x[0]); + const auto v1 = to_big_endian(x[1]); + const auto v2 = to_big_endian(x[2]); + const auto v3 = to_big_endian(x[3]); + + // Store words in reverse (big-endian) order, write addresses are ascending. + std::memcpy(dst, &v3, sizeof(v3)); + std::memcpy(dst + 8, &v2, sizeof(v2)); + std::memcpy(dst + 16, &v1, sizeof(v1)); + std::memcpy(dst + 24, &v0, sizeof(v0)); +} + +} // namespace unsafe + +} // namespace be + +} // namespace intx + +#ifdef _MSC_VER + #pragma warning(pop) +#endif From bdd488450df4d40da62e20c4b8c3f024eea05dc1 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Mon, 13 Jul 2026 12:35:33 -0400 Subject: [PATCH 02/29] Add coverage for newly added IL instructions --- plugins/emulator/README.md | 4 +- plugins/emulator/core/llilemulator.cpp | 111 +++++++++++++++++++++++++ plugins/emulator/test/emulator_test.py | 47 ++++++++++- 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/plugins/emulator/README.md b/plugins/emulator/README.md index c3cc90b4ab..349922e491 100644 --- a/plugins/emulator/README.md +++ b/plugins/emulator/README.md @@ -72,6 +72,8 @@ Emulating an unsupported instruction stops the emulator with an `Unimplemented` `DIVU_DP`, `DIVS_DP`, `MODU`, `MODS`, `MODU_DP`, `MODS_DP`, `NEG`, `ADD_OVERFLOW` - **Bitwise / shifts:** `AND`, `OR`, `XOR`, `NOT`, `LSL`, `LSR`, `ASR`, `ROL`, `ROR`, `RLC`, `RRC`, `SX`, `ZX`, `LOW_PART`, `TEST_BIT`, `BOOL_TO_INT` +- **Bit operations:** `BSWAP`, `POPCNT`, `CLZ`, `CTZ`, `RBIT`, `CLS`, `ABS`, `MINS`, `MAXS`, + `MINU`, `MAXU` - **Comparisons:** `CMP_E`, `CMP_NE`, `CMP_SLT`, `CMP_SLE`, `CMP_SGE`, `CMP_SGT`, `CMP_ULT`, `CMP_ULE`, `CMP_UGE`, `CMP_UGT` - **Flags:** `FLAG`, `SET_FLAG`, `FLAG_BIT`, `FLAG_COND`, `FLAG_GROUP` @@ -86,8 +88,6 @@ Emulating an unsupported instruction stops the emulator with an `Unimplemented` (float *constants* are read, but float arithmetic is not evaluated) - **Register stacks (x87/FPU-style):** `REG_STACK_REL`, `SET_REG_STACK_REL`, `REG_STACK_PUSH`, `REG_STACK_POP`, `REG_STACK_FREE_REG`, `REG_STACK_FREE_REL` -- **Bit operations:** `BSWAP`, `CLZ`, `CTZ`, `CLS`, `POPCNT`, `RBIT`, `ABS`, `MINS`, `MAXS`, - `MINU`, `MAXU` - **System / hooks** (no built-in semantics — stop unless the embedding code registers a hook): `SYSCALL`, `INTRINSIC` - **Halting / non-representable** (stop the emulator): `BP`, `TRAP`, `UNDEF`, `UNIMPL`, diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 660e67f6ef..c439002c9f 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1246,6 +1246,117 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_LOW_PART: return MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + // --- Bit operations --- + case LLIL_BSWAP: + { + // Reverse the byte order of the sz-byte value. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 result(0); + for (size_t i = 0; i < sz; i++) + { + intx::uint512 byte = (val >> (8 * i)) & intx::uint512(0xff); + result |= byte << (8 * (sz - 1 - i)); + } + return result; + } + + case LLIL_POPCNT: + { + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + uint64_t count = 0; + for (size_t i = 0; i < sz * 8; i++) + if (((val >> i) & intx::uint512(1)) != 0) + count++; + return intx::uint512(count); + } + + case LLIL_CLZ: + { + // Count leading zero bits; clz(0) == 8 * size. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + size_t bits = sz * 8; + uint64_t count = 0; + for (size_t i = bits; i-- > 0;) + { + if (((val >> i) & intx::uint512(1)) != 0) + break; + count++; + } + return intx::uint512(count); + } + + case LLIL_CTZ: + { + // Count trailing zero bits; ctz(0) == 8 * size. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + size_t bits = sz * 8; + uint64_t count = 0; + for (size_t i = 0; i < bits; i++) + { + if (((val >> i) & intx::uint512(1)) != 0) + break; + count++; + } + return intx::uint512(count); + } + + case LLIL_RBIT: + { + // Reverse the bit order of the sz-byte value. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + size_t bits = sz * 8; + intx::uint512 result(0); + for (size_t i = 0; i < bits; i++) + if (((val >> i) & intx::uint512(1)) != 0) + result |= intx::uint512(1) << (bits - 1 - i); + return result; + } + + case LLIL_CLS: + { + // Count leading sign bits: number of bits below the sign bit that match it. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + size_t bits = sz * 8; + intx::uint512 sign = (val >> (bits - 1)) & intx::uint512(1); + uint64_t count = 0; + for (size_t i = bits - 1; i-- > 0;) + { + if (((val >> i) & intx::uint512(1)) != sign) + break; + count++; + } + return intx::uint512(count); + } + + case LLIL_ABS: + { + // Signed absolute value; abs(INT_MIN) == INT_MIN. + intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + size_t bits = sz * 8; + bool neg = ((val >> (bits - 1)) & intx::uint512(1)) != 0; + return neg ? MaskToSize(~val + 1, sz) : val; + } + + case LLIL_MINS: + case LLIL_MAXS: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + int64_t sl = static_cast(static_cast(SignExtend(left, sz, 8))); + int64_t sr = static_cast(static_cast(SignExtend(right, sz, 8))); + bool leftWins = (expr.operation == LLIL_MINS) ? (sl <= sr) : (sl >= sr); + return leftWins ? left : right; + } + + case LLIL_MINU: + case LLIL_MAXU: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + bool leftWins = (expr.operation == LLIL_MINU) ? (left <= right) : (left >= right); + return leftWins ? left : right; + } + // --- Comparisons --- case LLIL_CMP_E: { diff --git a/plugins/emulator/test/emulator_test.py b/plugins/emulator/test/emulator_test.py index db24de5fb6..83c65022a2 100644 --- a/plugins/emulator/test/emulator_test.py +++ b/plugins/emulator/test/emulator_test.py @@ -49,8 +49,9 @@ def make_bv(code: bytes, entries=(0,), arch: str = 'x86_64') -> BinaryView: class EmulatorTestBase(unittest.TestCase): - def emulator_for(self, code: bytes, entries=(0,), entry_point=0, max_instructions=1000): - bv = make_bv(code, entries=entries) + def emulator_for(self, code: bytes, entries=(0,), entry_point=0, max_instructions=1000, + arch='x86_64'): + bv = make_bv(code, entries=entries, arch=arch) emu = LLILEmulator(bv) self.assertTrue(emu.set_entry_point(entry_point)) emu.set_max_instructions(max_instructions) @@ -314,6 +315,48 @@ def test_all_hooks_can_be_removed(self): setter(None) # must not raise +class BitOpTests(EmulatorTestBase): + # x86-64 snippets whose lifted LLIL exercises the bit ops (BSWAP/POPCNT/CLZ/CTZ), + # and aarch64 snippets for the ops with direct A64 instructions (RBIT/CLS). + def test_bswap(self): + # mov eax, 0x12345678 ; bswap eax ; ret + emu = self.emulator_for(b'\xb8\x78\x56\x34\x12\x0f\xc8\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 0x78563412) + + def test_popcnt(self): + # mov ecx, 0xff ; popcnt eax, ecx ; ret + emu = self.emulator_for(b'\xb9\xff\x00\x00\x00\xf3\x0f\xb8\xc1\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 8) + + def test_clz(self): + # mov ecx, 1 ; lzcnt eax, ecx ; ret -> 31 leading zeros in a 32-bit 1 + emu = self.emulator_for(b'\xb9\x01\x00\x00\x00\xf3\x0f\xbd\xc1\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 31) + + def test_ctz(self): + # mov ecx, 8 ; tzcnt eax, ecx ; ret -> 3 trailing zeros + emu = self.emulator_for(b'\xb9\x08\x00\x00\x00\xf3\x0f\xbc\xc1\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 3) + + def test_rbit(self): + # aarch64: movz w0, #1 ; rbit w0, w0 ; ret -> bit 0 -> bit 31 + emu = self.emulator_for( + b'\x20\x00\x80\x52\x00\x00\xc0\x5a\xc0\x03\x5f\xd6', arch='aarch64') + emu.run() + self.assertEqual(emu.get_register('x0'), 0x80000000) + + def test_cls(self): + # aarch64: movz w0, #1 ; cls w0, w0 ; ret -> 30 leading sign (zero) bits below MSB + emu = self.emulator_for( + b'\x20\x00\x80\x52\x00\x14\xc0\x5a\xc0\x03\x5f\xd6', arch='aarch64') + emu.run() + self.assertEqual(emu.get_register('x0'), 30) + + class StateSerializationTests(EmulatorTestBase): def test_save_load_round_trip(self): emu = self.emulator_for(ADD_CONSTS) From 9b0180cf53d5f1d0d64c8f70f119287d6a09eba0 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:05:25 -0400 Subject: [PATCH 03/29] [emulator] Remove stray blank line in binaryninjacore.h Addresses review feedback (erroneous formatting) on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- binaryninjacore.h | 1 - 1 file changed, 1 deletion(-) diff --git a/binaryninjacore.h b/binaryninjacore.h index 4cbb0e8c18..26a5b85fa0 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -9427,7 +9427,6 @@ extern "C" BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size); BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size); - #ifdef __cplusplus } #endif From 0d1183db42cd68375f14d1ad22b8b95d72825e7b Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:05:25 -0400 Subject: [PATCH 04/29] [emulator] Remove stray blank line before namespace close in binaryninjaapi.h Addresses review feedback (erroneous formatting) on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- binaryninjaapi.h | 1 - 1 file changed, 1 deletion(-) diff --git a/binaryninjaapi.h b/binaryninjaapi.h index e83a4c3803..b257d62e97 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -23354,7 +23354,6 @@ namespace BinaryNinja { std::optional RecognizeConstantData( const HighLevelILInstruction& instr) override; }; - } // namespace BinaryNinja From 9805adc9c2b27b174d13223ce2041fe2439e6c63 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:08:23 -0400 Subject: [PATCH 05/29] [emulator] Move intx include out of binaryninjaapi.h into emulator headers binaryninjaapi.h does not provide the emulator API, so the wide-integer type it pulls in for the plugin does not belong there. Move the (windows.h min/max-guarded) intx include into a shared emulator_intx.h included by the emulator's own core and API roots instead. Addresses review feedback on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- binaryninjaapi.h | 17 ------------- plugins/emulator/api/emulator_intx.h | 36 ++++++++++++++++++++++++++++ plugins/emulator/api/emulatorapi.h | 1 + plugins/emulator/core/ilemulator.h | 1 + 4 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 plugins/emulator/api/emulator_intx.h diff --git a/binaryninjaapi.h b/binaryninjaapi.h index b257d62e97..d6470686e3 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -31,23 +31,6 @@ #include "binaryninjacore.h" #include "exceptions.h" -// intx provides the wide-integer type used by the emulator plugin's API. If a -// translation unit includes before this header (as several debugger -// adapters do), its min/max macros would break intx's numeric_limits<>::min()/max() -// member definitions. Neutralize those macros just for the intx include, then -// restore them so any later code that relies on windows.h min/max is unaffected. -#if defined(_WIN32) - #pragma push_macro("min") - #pragma push_macro("max") - #undef min - #undef max -#endif -#include "vendor/intx/intx.hpp" -#if defined(_WIN32) - #pragma pop_macro("min") - #pragma pop_macro("max") -#endif - #include "json/json.h" #include "rapidjsonwrapper.h" #include "vendor/nlohmann/json.hpp" diff --git a/plugins/emulator/api/emulator_intx.h b/plugins/emulator/api/emulator_intx.h new file mode 100644 index 0000000000..d057fbc58c --- /dev/null +++ b/plugins/emulator/api/emulator_intx.h @@ -0,0 +1,36 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once + +// intx provides the wide-integer type used throughout the emulator plugin's core and +// API. It lives here (rather than in binaryninjaapi.h) so it is only pulled in by the +// emulator's own headers. If a translation unit includes before this header +// (as several debugger adapters do), its min/max macros would break intx's +// numeric_limits<>::min()/max() member definitions. Neutralize those macros just for the +// intx include, then restore them so any later code relying on windows.h min/max is +// unaffected. +#if defined(_WIN32) + #pragma push_macro("min") + #pragma push_macro("max") + #undef min + #undef max +#endif +#include "vendor/intx/intx.hpp" +#if defined(_WIN32) + #pragma pop_macro("min") + #pragma pop_macro("max") +#endif diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h index 352dd27fad..44bb55611d 100644 --- a/plugins/emulator/api/emulatorapi.h +++ b/plugins/emulator/api/emulatorapi.h @@ -17,6 +17,7 @@ limitations under the License. #pragma once #include "binaryninjaapi.h" +#include "emulator_intx.h" #include "ffi.h" #include #include diff --git a/plugins/emulator/core/ilemulator.h b/plugins/emulator/core/ilemulator.h index 08b069c901..ddf7674a8f 100644 --- a/plugins/emulator/core/ilemulator.h +++ b/plugins/emulator/core/ilemulator.h @@ -1,6 +1,7 @@ #pragma once #include "binaryninjaapi.h" +#include "../api/emulator_intx.h" #include "ffi_global.h" #include "refcountobject.h" #include From 8d069d98752ab34482c4dea872a0affa74613088 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:11:22 -0400 Subject: [PATCH 06/29] [emulator] Fix signed IL ops for operands wider than 8 bytes and INT_MIN/-1 DIVS/MODS/MULS_DP/CMP_S*/MINS/MAXS funneled operands through int64_t via SignExtend(v, sz, 8), which silently truncated any operand wider than 8 bytes to its low 64 bits, and native signed division of INT_MIN by -1 is undefined behavior (SIGFPE on x86). Add wide-signed helpers (IsNegative/SignedLess/SignedDivideOrModulo) that operate directly on the 512-bit value: comparisons use sign-aware unsigned compares, and division works on magnitudes then reapplies the sign, so INT_MIN / -1 wraps to INT_MIN (and % to 0) instead of trapping. Addresses bdash review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 104 ++++++++++++++++--------- plugins/emulator/core/llilemulator.h | 7 ++ 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index c439002c9f..82a7463537 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -132,6 +132,46 @@ intx::uint512 LLILEmulator::SignExtend(const intx::uint512& value, size_t fromSi } +bool LLILEmulator::IsNegative(const intx::uint512& value, size_t size) +{ + if (size == 0 || size > 64) + return false; + intx::uint512 signBit = intx::uint512(1) << (size * 8 - 1); + return (MaskToSize(value, size) & signBit) != 0; +} + + +bool LLILEmulator::SignedLess(const intx::uint512& a, const intx::uint512& b, size_t size) +{ + bool an = IsNegative(a, size); + bool bn = IsNegative(b, size); + if (an != bn) + return an; // negative < non-negative + // Same sign: the unsigned comparison of the masked values gives the correct order. + return MaskToSize(a, size) < MaskToSize(b, size); +} + + +intx::uint512 LLILEmulator::SignedDivideOrModulo( + const intx::uint512& a, const intx::uint512& b, size_t size, bool modulo) +{ + // Work with magnitudes as unsigned wide integers, then reapply the sign. This is correct + // for operands of any width up to 64 bytes and, unlike native signed division, does not + // trap on INT_MIN / -1 (which wraps to INT_MIN for divide and 0 for modulo). + intx::uint512 am = MaskToSize(a, size); + intx::uint512 bm = MaskToSize(b, size); + bool an = IsNegative(am, size); + bool bn = IsNegative(bm, size); + intx::uint512 au = an ? MaskToSize(~am + 1, size) : am; + intx::uint512 bu = bn ? MaskToSize(~bm + 1, size) : bm; + intx::uint512 result = modulo ? (au % bu) : (au / bu); + bool resultNeg = modulo ? an : (an != bn); + if (resultNeg) + result = MaskToSize(~result + 1, size); + return MaskToSize(result, size); +} + + BNEndianness LLILEmulator::GetEndianness() const { if (m_arch) @@ -1105,15 +1145,14 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_DIVS: { - // For signed division, work with 64-bit values since intx doesn't provide signed types - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - if (right == 0) + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + if (MaskToSize(right, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); return 0; } - return MaskToSize(intx::uint512(static_cast(left / right)), sz); + return SignedDivideOrModulo(left, right, sz, false); } case LLIL_MODU: @@ -1130,14 +1169,14 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MODS: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - if (right == 0) + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + if (MaskToSize(right, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); return 0; } - return MaskToSize(intx::uint512(static_cast(left % right)), sz); + return SignedDivideOrModulo(left, right, sz, true); } // --- Double-precision --- @@ -1150,18 +1189,11 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MULS_DP: { - // Signed multiply double-precision: sign extend operands, multiply, mask - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); - // Sign-extend both operands to full width as two's-complement intx values and - // multiply; the low bits of the product are correct regardless of sign. This - // avoids __int128, which MSVC does not support. - intx::uint512 l(static_cast(left)); - intx::uint512 r(static_cast(right)); - if (left < 0) - l |= ~intx::uint512(0) << 64; - if (right < 0) - r |= ~intx::uint512(0) << 64; + // Signed multiply double-precision: sign-extend each sz/2-byte operand to the full + // wide value, then multiply as two's-complement. The low sz bytes of the product are + // correct regardless of sign, and this handles operands wider than 8 bytes. + intx::uint512 l = SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2, 64); + intx::uint512 r = SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 64); return MaskToSize(l * r, sz); } @@ -1342,9 +1374,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) { intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); - int64_t sl = static_cast(static_cast(SignExtend(left, sz, 8))); - int64_t sr = static_cast(static_cast(SignExtend(right, sz, 8))); - bool leftWins = (expr.operation == LLIL_MINS) ? (sl <= sr) : (sl >= sr); + // leftWins for MINS when left <= right, for MAXS when left >= right (i.e. !(left < right)) + bool leftWins = (expr.operation == LLIL_MINS) ? !SignedLess(right, left, sz) + : !SignedLess(left, right, sz); return leftWins ? left : right; } @@ -1374,9 +1406,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CMP_SLT: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - return left < right ? intx::uint512(1) : intx::uint512(0); + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return SignedLess(left, right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_ULT: @@ -1388,9 +1420,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CMP_SLE: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - return left <= right ? intx::uint512(1) : intx::uint512(0); + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return !SignedLess(right, left, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_ULE: @@ -1402,9 +1434,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CMP_SGE: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - return left >= right ? intx::uint512(1) : intx::uint512(0); + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return !SignedLess(left, right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_UGE: @@ -1416,9 +1448,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CMP_SGT: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz, 8))); - return left > right ? intx::uint512(1) : intx::uint512(0); + intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + return SignedLess(right, left, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_UGT: diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h index 23728d0c69..f56baf93d5 100644 --- a/plugins/emulator/core/llilemulator.h +++ b/plugins/emulator/core/llilemulator.h @@ -70,6 +70,13 @@ namespace BinaryNinjaEmulator // Helpers static intx::uint512 MaskToSize(const intx::uint512& value, size_t size); static intx::uint512 SignExtend(const intx::uint512& value, size_t fromSize, size_t toSize); + // Signed-integer helpers that operate directly on the wide value, so operands wider + // than 8 bytes are handled correctly (and INT_MIN / -1 does not trap as it would on + // native signed division). + static bool IsNegative(const intx::uint512& value, size_t size); + static bool SignedLess(const intx::uint512& a, const intx::uint512& b, size_t size); + static intx::uint512 SignedDivideOrModulo( + const intx::uint512& a, const intx::uint512& b, size_t size, bool modulo); void Push(const intx::uint512& value, size_t size); intx::uint512 Pop(size_t size); intx::uint512 EvalFlagCondition(BNLowLevelILFlagCondition cond, uint32_t semClass); From 2d427a757343ae0229f955f177f6330adc8746c2 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:13:26 -0400 Subject: [PATCH 07/29] [emulator] Fix LLIL_ADD_OVERFLOW to compute signed overflow, not unsigned carry ADD_OVERFLOW returned (result < left), i.e. the unsigned carry-out, but the operation denotes the signed overflow flag. Compute it as: both operands share a sign and the result's sign differs. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 82a7463537..0647ba4f35 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1483,10 +1483,14 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_ADD_OVERFLOW: { + // Signed overflow flag: set when both operands share a sign and the result's sign + // differs from them (not the unsigned carry-out). intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); - intx::uint512 result = left + right; - return MaskToSize(result, sz) < left ? intx::uint512(1) : intx::uint512(0); + intx::uint512 result = MaskToSize(left + right, sz); + intx::uint512 signBit = intx::uint512(1) << (sz * 8 - 1); + bool overflow = ((left ^ result) & (right ^ result) & signBit) != 0; + return overflow ? intx::uint512(1) : intx::uint512(0); } // --- Flag conditions (Lifted IL) --- From 2e6f68fc78022dadb31ba75d328fd3cac5799ca3 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:13:26 -0400 Subject: [PATCH 08/29] [emulator] Fix RLC/RRC rotate-through-carry semantics The rotate-through-carry implementations mixed a pre-shifted value with an un-rotated operand (RLC) and dropped the low bits instead of wrapping them (RRC), producing wrong results (e.g. 1-byte RLC(0x80, carry=0, count=1) gave 0x01 instead of 0x00). Implement both as a true rotate of the (bits+1)-bit {carry:value} quantity, and record m_lastArithmetic like ROL/ROR so a later flag read is not stale. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 0647ba4f35..b49bb2cb3d 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1110,24 +1110,36 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_RLC: { + // Rotate left through carry: rotate the (bits + 1)-bit quantity {carry : value}, + // with carry as the most-significant bit. intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; size_t bits = sz * 8; - size_t shift = static_cast(static_cast(right) % (bits + 1)); - intx::uint512 extended = (left << 1) | carry; - return MaskToSize((extended << shift) | (left >> (bits - shift)), sz); + size_t width = bits + 1; + size_t shift = static_cast(static_cast(right) % width); + intx::uint512 extended = left | (carry << bits); + intx::uint512 rotated = (shift == 0) ? extended : ((extended << shift) | (extended >> (width - shift))); + intx::uint512 result = MaskToSize(rotated, sz); + m_lastArithmetic = {left, right, result, sz, LLIL_RLC, true}; + return result; } case LLIL_RRC: { + // Rotate right through carry: rotate the (bits + 1)-bit quantity {carry : value}, + // with carry as the most-significant bit. intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; size_t bits = sz * 8; - size_t shift = static_cast(static_cast(right) % (bits + 1)); + size_t width = bits + 1; + size_t shift = static_cast(static_cast(right) % width); intx::uint512 extended = left | (carry << bits); - return MaskToSize(extended >> shift, sz); + intx::uint512 rotated = (shift == 0) ? extended : ((extended >> shift) | (extended << (width - shift))); + intx::uint512 result = MaskToSize(rotated, sz); + m_lastArithmetic = {left, right, result, sz, LLIL_RRC, true}; + return result; } // --- Division --- From 868ce1bf94457d86889eda1617f537bffa0648ca Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:15:43 -0400 Subject: [PATCH 09/29] [emulator] Mask arithmetic operands consistently up front The basic two-operand arithmetic cases masked operands only when recording the flag context, while evaluating the operation on the raw operands, and the double-precision cases mask to sz/2 for unrelated reasons. This was noted as confusing in review. Mask both operands to sz once at the top of each case and use those masked values for both the computation and m_lastArithmetic, and add a comment explaining why the double-precision ops mask differently. No behavior change. Addresses bdash review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 44 ++++++++++++++------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index b49bb2cb3d..fc854dc406 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -977,58 +977,62 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) } // --- Two-operand arithmetic --- + // Convention: operands are masked to the operation size `sz` up front, and the same + // masked values are used both for the computation and for the flag context recorded in + // m_lastArithmetic. (Double-precision ops below intentionally use sz/2-wide operands + // producing an sz-wide result, which is why they mask differently.) case LLIL_ADD: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left + right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_ADD, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_ADD, true}; return result; } case LLIL_SUB: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left - right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_SUB, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_SUB, true}; return result; } case LLIL_AND: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left & right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_AND, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_AND, true}; return result; } case LLIL_OR: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left | right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_OR, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_OR, true}; return result; } case LLIL_XOR: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left ^ right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_XOR, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_XOR, true}; return result; } case LLIL_LSL: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); size_t shiftAmt = static_cast(right) & (sz * 8 - 1); intx::uint512 result = MaskToSize(left << shiftAmt, sz); - m_lastArithmetic = {MaskToSize(left, sz), right, result, sz, LLIL_LSL, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_LSL, true}; return result; } @@ -1080,10 +1084,10 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MUL: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); intx::uint512 result = MaskToSize(left * right, sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_MUL, true}; + m_lastArithmetic = {left, right, result, sz, LLIL_MUL, true}; return result; } From 1ba16fd86dd1a1010c54955a5c7dde958c84183c Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:17:26 -0400 Subject: [PATCH 10/29] [emulator] Fix carry/borrow flag for ADC/SBB at the operand-size boundary ADC/SBB folded the carry-in into the stored right operand as MaskToSize(right + carry, sz), which wraps to 0 when right is all-ones and carry is 1, so the carry/borrow flag was then computed against the wrong value (e.g. SBB(0, 0xFFFFFFFF, 1) reported no borrow though one occurs). Track the carry-in explicitly in the flag context and compute carry-out/borrow-out from the full-width sum/difference so it no longer wraps. Half-carry now also accounts for the carry-in. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 43 ++++++++++++++++---------- plugins/emulator/core/llilemulator.h | 4 +++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index fc854dc406..432ef6c02b 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1094,21 +1094,21 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- With carry --- case LLIL_ADC: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); - intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)); - intx::uint512 result = MaskToSize(left + right + (carry & 1), sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right + (carry & 1), sz), result, sz, LLIL_ADC, true}; + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + uint8_t carryIn = static_cast(EvalExpr(expr.GetRawOperandAsExpr(2)) & 1); + intx::uint512 result = MaskToSize(left + right + intx::uint512(carryIn), sz); + m_lastArithmetic = {left, right, result, sz, LLIL_ADC, true, carryIn}; return result; } case LLIL_SBB: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); - intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)); - intx::uint512 result = MaskToSize(left - right - (carry & 1), sz); - m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right + (carry & 1), sz), result, sz, LLIL_SBB, true}; + intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + uint8_t carryIn = static_cast(EvalExpr(expr.GetRawOperandAsExpr(2)) & 1); + intx::uint512 result = MaskToSize(left - right - intx::uint512(carryIn), sz); + m_lastArithmetic = {left, right, result, sz, LLIL_SBB, true, carryIn}; return result; } @@ -1634,16 +1634,25 @@ uint8_t LLILEmulator::ComputeFlagForRole(BNFlagRole role) const case CarryFlagRole: if (isAdd) - return ctx.result < ctx.left ? 1 : 0; + { + // Carry-out = bit `size*8` of the full-width sum left + right + carryIn. + intx::uint512 fullSum = ctx.left + ctx.right + intx::uint512(ctx.carryIn); + return ((fullSum >> (ctx.size * 8)) != 0) ? 1 : 0; + } if (isSub) - return ctx.left < ctx.right ? 1 : 0; + // Borrow-out = left < right + borrowIn, computed at full width so right + borrowIn + // does not wrap at the operand-size boundary. + return (ctx.left < ctx.right + intx::uint512(ctx.carryIn)) ? 1 : 0; return 0; case CarryFlagWithInvertedSubtractRole: if (isSub) - return ctx.left >= ctx.right ? 1 : 0; + return (ctx.left >= ctx.right + intx::uint512(ctx.carryIn)) ? 1 : 0; if (isAdd) - return ctx.result < ctx.left ? 1 : 0; + { + intx::uint512 fullSum = ctx.left + ctx.right + intx::uint512(ctx.carryIn); + return ((fullSum >> (ctx.size * 8)) != 0) ? 1 : 0; + } return 0; case OverflowFlagRole: @@ -1661,9 +1670,11 @@ uint8_t LLILEmulator::ComputeFlagForRole(BNFlagRole role) const case HalfCarryFlagRole: if (isAdd) - return ((ctx.left & intx::uint512(0xf)) + (ctx.right & intx::uint512(0xf))) > intx::uint512(0xf) ? 1 : 0; + return ((ctx.left & intx::uint512(0xf)) + (ctx.right & intx::uint512(0xf)) + intx::uint512(ctx.carryIn)) + > intx::uint512(0xf) ? 1 : 0; if (isSub) - return (ctx.left & intx::uint512(0xf)) < (ctx.right & intx::uint512(0xf)) ? 1 : 0; + return (ctx.left & intx::uint512(0xf)) < ((ctx.right & intx::uint512(0xf)) + intx::uint512(ctx.carryIn)) + ? 1 : 0; return 0; case EvenParityFlagRole: diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h index f56baf93d5..cf7834cb8e 100644 --- a/plugins/emulator/core/llilemulator.h +++ b/plugins/emulator/core/llilemulator.h @@ -45,6 +45,10 @@ namespace BinaryNinjaEmulator size_t size; BNLowLevelILOperation operation; bool valid; + // Carry/borrow input for ADC/SBB (0 otherwise). Kept separate from `right` so the + // carry-out flag can be computed from the full-width sum without the +carry wrapping + // at the operand-size boundary. + uint8_t carryIn; }; ArithmeticContext m_lastArithmetic {}; From 7864ee30716910af7e5ac7697e4821799c18e703 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:20:35 -0400 Subject: [PATCH 11/29] [emulator] Fix double-precision divide/modulo operand widths and signedness For DIVU_DP/DIVS_DP/MODU_DP/MODS_DP the instruction size is the result (quotient) size N: the dividend is a double-width 2N value and the divisor is N. The code instead masked the dividend to N, the divisor to N/2 and the result to N/2, truncating the high half of any true double-width dividend, and the signed variants additionally funneled operands through int64_t. Mask the dividend to 2*sz and the divisor/result to sz, and compute the signed variants via a width-aware helper (also fixing INT_MIN/-1). The *_DP multiplies are unchanged; a comment now explains why their width convention differs. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 68 ++++++++++++++++---------- plugins/emulator/core/llilemulator.h | 6 +++ 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 432ef6c02b..3195be7460 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -152,23 +152,34 @@ bool LLILEmulator::SignedLess(const intx::uint512& a, const intx::uint512& b, si } -intx::uint512 LLILEmulator::SignedDivideOrModulo( - const intx::uint512& a, const intx::uint512& b, size_t size, bool modulo) +intx::uint512 LLILEmulator::SignedDivideOrModuloWide( + const intx::uint512& dividend, size_t dividendSize, + const intx::uint512& divisor, size_t divisorSize, + size_t resultSize, bool modulo) { // Work with magnitudes as unsigned wide integers, then reapply the sign. This is correct // for operands of any width up to 64 bytes and, unlike native signed division, does not - // trap on INT_MIN / -1 (which wraps to INT_MIN for divide and 0 for modulo). - intx::uint512 am = MaskToSize(a, size); - intx::uint512 bm = MaskToSize(b, size); - bool an = IsNegative(am, size); - bool bn = IsNegative(bm, size); - intx::uint512 au = an ? MaskToSize(~am + 1, size) : am; - intx::uint512 bu = bn ? MaskToSize(~bm + 1, size) : bm; + // trap on INT_MIN / -1 (which wraps to INT_MIN for divide and 0 for modulo). The dividend, + // divisor and result may have different widths (the double-precision ops divide a + // double-width dividend by a single-width divisor to produce a single-width result). + intx::uint512 am = MaskToSize(dividend, dividendSize); + intx::uint512 bm = MaskToSize(divisor, divisorSize); + bool an = IsNegative(am, dividendSize); + bool bn = IsNegative(bm, divisorSize); + intx::uint512 au = an ? MaskToSize(~am + 1, dividendSize) : am; + intx::uint512 bu = bn ? MaskToSize(~bm + 1, divisorSize) : bm; intx::uint512 result = modulo ? (au % bu) : (au / bu); bool resultNeg = modulo ? an : (an != bn); if (resultNeg) - result = MaskToSize(~result + 1, size); - return MaskToSize(result, size); + result = MaskToSize(~result + 1, resultSize); + return MaskToSize(result, resultSize); +} + + +intx::uint512 LLILEmulator::SignedDivideOrModulo( + const intx::uint512& a, const intx::uint512& b, size_t size, bool modulo) +{ + return SignedDivideOrModuloWide(a, size, b, size, size, modulo); } @@ -1213,52 +1224,55 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) return MaskToSize(l * r, sz); } + // The double-precision divide/modulo ops divide a double-width (2*sz) dividend by an + // sz-wide divisor, producing an sz-wide result. (Contrast with the *_DP multiplies above, + // whose `sz` is the double-width product.) case LLIL_DIVU_DP: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); - if (right == 0) + intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz * 2); + intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + if (divisor == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); return 0; } - return MaskToSize(left / right, sz / 2); + return MaskToSize(dividend / divisor, sz); } case LLIL_DIVS_DP: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); - if (right == 0) + intx::uint512 dividend = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 divisor = EvalExpr(expr.GetRawOperandAsExpr(1)); + if (MaskToSize(divisor, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); return 0; } - return MaskToSize(intx::uint512(static_cast(left / right)), sz / 2); + return SignedDivideOrModuloWide(dividend, sz * 2, divisor, sz, sz, false); } case LLIL_MODU_DP: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); - if (right == 0) + intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz * 2); + intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + if (divisor == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); return 0; } - return MaskToSize(left % right, sz / 2); + return MaskToSize(dividend % divisor, sz); } case LLIL_MODS_DP: { - int64_t left = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8))); - int64_t right = static_cast(static_cast(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 8))); - if (right == 0) + intx::uint512 dividend = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 divisor = EvalExpr(expr.GetRawOperandAsExpr(1)); + if (MaskToSize(divisor, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); return 0; } - return MaskToSize(intx::uint512(static_cast(left % right)), sz / 2); + return SignedDivideOrModuloWide(dividend, sz * 2, divisor, sz, sz, true); } // --- Unary --- diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h index cf7834cb8e..05ff25b849 100644 --- a/plugins/emulator/core/llilemulator.h +++ b/plugins/emulator/core/llilemulator.h @@ -81,6 +81,12 @@ namespace BinaryNinjaEmulator static bool SignedLess(const intx::uint512& a, const intx::uint512& b, size_t size); static intx::uint512 SignedDivideOrModulo( const intx::uint512& a, const intx::uint512& b, size_t size, bool modulo); + // Generalized signed divide/modulo where the dividend, divisor and result may have + // different widths (used by the double-precision divide/modulo ops). + static intx::uint512 SignedDivideOrModuloWide( + const intx::uint512& dividend, size_t dividendSize, + const intx::uint512& divisor, size_t divisorSize, + size_t resultSize, bool modulo); void Push(const intx::uint512& value, size_t size); intx::uint512 Pop(size_t size); intx::uint512 EvalFlagCondition(BNLowLevelILFlagCondition cond, uint32_t semClass); From ccf4ea5f816641fe81365d4616e168d384f59256 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:21:40 -0400 Subject: [PATCH 12/29] [emulator] Fix cross-segment memory read/write boundary computation The unmapped-region skip logic compared the next segment's start against addr + len (the original request end, which can overflow uint64_t for high addresses) instead of the running cursor. Compute the gap relative to cur so a mapped segment following an unmapped hole is not skipped past, and high addresses do not misbehave. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/ilemulator.cpp | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/plugins/emulator/core/ilemulator.cpp b/plugins/emulator/core/ilemulator.cpp index 09e55f5fa8..c8b10a00fa 100644 --- a/plugins/emulator/core/ilemulator.cpp +++ b/plugins/emulator/core/ilemulator.cpp @@ -119,11 +119,16 @@ size_t EmulatorMemory::Read(void* dest, uint64_t addr, size_t len) // On real hardware this would fault, but we silently return zeros. const Segment* next = FindNextSegment(cur); size_t remaining = len - totalRead; - size_t skipSize; - if (next && next->start <= addr + len) - skipSize = std::min((size_t)(next->start - cur), remaining); - else - skipSize = remaining; + // Distance from the cursor to the next mapped segment; if it falls within the + // bytes still to read, only skip up to it. Computed relative to `cur` (not + // addr + len, which can overflow for high addresses). + size_t skipSize = remaining; + if (next && next->start > cur) + { + uint64_t gap = next->start - cur; + if (gap < remaining) + skipSize = static_cast(gap); + } memset(out + totalRead, 0, skipSize); totalRead += skipSize; } @@ -155,11 +160,16 @@ size_t EmulatorMemory::Write(uint64_t addr, const void* src, size_t len) // On real hardware this would fault, but we silently discard the data. const Segment* next = FindNextSegment(cur); size_t remaining = len - totalWritten; - size_t skipSize; - if (next && next->start <= addr + len) - skipSize = std::min((size_t)(next->start - cur), remaining); - else - skipSize = remaining; + // Distance from the cursor to the next mapped segment; if it falls within the + // bytes still to write, only skip up to it. Computed relative to `cur` (not + // addr + len, which can overflow for high addresses). + size_t skipSize = remaining; + if (next && next->start > cur) + { + uint64_t gap = next->start - cur; + if (gap < remaining) + skipSize = static_cast(gap); + } totalWritten += skipSize; } } From a00c70c93258b1d1af5884f6d6ce3b075d68ecf1 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:23:06 -0400 Subject: [PATCH 13/29] [emulator] Reject overlapping memory maps EmulatorMemory::Map appended a new segment without checking for overlap with existing ones. Since FindSegment returns the first containing segment, an overlapping map left part of the address range shadowed, so reads returned stale/zero bytes and writes could miss a region. Refuse an overlapping map with a logged warning, preserving the no-overlap invariant the read/write paths rely on. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/ilemulator.cpp | 31 ++++++++++++++++++++++++++++ plugins/emulator/core/ilemulator.h | 3 +++ 2 files changed, 34 insertions(+) diff --git a/plugins/emulator/core/ilemulator.cpp b/plugins/emulator/core/ilemulator.cpp index c8b10a00fa..05ebc62327 100644 --- a/plugins/emulator/core/ilemulator.cpp +++ b/plugins/emulator/core/ilemulator.cpp @@ -177,8 +177,31 @@ size_t EmulatorMemory::Write(uint64_t addr, const void* src, size_t len) } +bool EmulatorMemory::OverlapsExisting(uint64_t addr, size_t len) const +{ + if (len == 0) + return false; + uint64_t end = addr + len; + for (auto& seg : m_segments) + { + // Intersection of [addr, end) and [seg.start, seg.End()); handles end wraparound. + if (addr < seg.End() && seg.start < end) + return true; + } + return false; +} + + void EmulatorMemory::Map(uint64_t addr, const void* data, size_t len, const std::string& name) { + if (len == 0) + return; + if (OverlapsExisting(addr, len)) + { + LogWarn("Emulator: refusing to map region '%s' at 0x%llx (size 0x%zx): " + "overlaps an already-mapped region", name.c_str(), (unsigned long long)addr, len); + return; + } Segment seg; seg.start = addr; seg.data.assign((const uint8_t*)data, (const uint8_t*)data + len); @@ -189,6 +212,14 @@ void EmulatorMemory::Map(uint64_t addr, const void* data, size_t len, const std: void EmulatorMemory::Map(uint64_t addr, size_t len, const std::string& name) { + if (len == 0) + return; + if (OverlapsExisting(addr, len)) + { + LogWarn("Emulator: refusing to map region '%s' at 0x%llx (size 0x%zx): " + "overlaps an already-mapped region", name.c_str(), (unsigned long long)addr, len); + return; + } Segment seg; seg.start = addr; seg.data.resize(len, 0); diff --git a/plugins/emulator/core/ilemulator.h b/plugins/emulator/core/ilemulator.h index ddf7674a8f..aca8d6ac47 100644 --- a/plugins/emulator/core/ilemulator.h +++ b/plugins/emulator/core/ilemulator.h @@ -49,6 +49,9 @@ namespace BinaryNinjaEmulator // Find the lowest-start segment whose start > addr, or nullptr const Segment* FindNextSegment(uint64_t addr) const; + // True if [addr, addr + len) intersects any existing segment. + bool OverlapsExisting(uint64_t addr, size_t len) const; + public: EmulatorMemory() = default; From 3b559d84cc0a9e08ab8f38b5eb19ef8ea74faa6c Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:25:13 -0400 Subject: [PATCH 14/29] [emulator] Bound intrinsic-hook output writes to the buffer capacity The intrinsic-hook FFI passed output buffers (outValues/outRegs) with no capacity parameter, and both the C++ and Python bridges wrote one entry per pair the user hook returned. A hook returning more pairs than the fixed 64-entry buffers the trampoline allocates overran them (stack corruption). Add a maxCount parameter to the callback contract and clamp writes to it on both the C++ and Python sides; the Python side also masks values to their field widths. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/api/emulatorapi.h | 2 +- plugins/emulator/api/ffi.h | 6 ++++-- plugins/emulator/api/ilemulator.cpp | 6 ++++-- plugins/emulator/api/python/ilemulator.py | 16 ++++++++++------ plugins/emulator/core/llilemulator.cpp | 11 ++++++----- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h index 44bb55611d..e55878697d 100644 --- a/plugins/emulator/api/emulatorapi.h +++ b/plugins/emulator/api/emulatorapi.h @@ -54,7 +54,7 @@ namespace BinaryNinja static bool PreInstructionHookCallback(void* ctxt, BNILEmulator* emu, size_t instrIndex); static bool IntrinsicHookCallback(void* ctxt, BNLLILEmulator* emu, uint32_t intrinsic, const uint64_t* params, size_t paramCount, - uint64_t* outValues, uint32_t* outRegs, size_t* outCount); + uint64_t* outValues, uint32_t* outRegs, size_t maxCount, size_t* outCount); static void StdoutCallbackBridge(void* ctxt, BNILEmulator* emu, const char* data, size_t len); static size_t StdinCallbackBridge(void* ctxt, BNILEmulator* emu, char* buf, size_t maxLen); diff --git a/plugins/emulator/api/ffi.h b/plugins/emulator/api/ffi.h index 5603e0b31a..5957a5e2d6 100644 --- a/plugins/emulator/api/ffi.h +++ b/plugins/emulator/api/ffi.h @@ -159,11 +159,13 @@ extern "C" EMULATOR_FFI_API uint8_t BNLLILEmulatorGetFlag(BNLLILEmulator* emu, uint32_t flag); EMULATOR_FFI_API void BNLLILEmulatorSetFlag(BNLLILEmulator* emu, uint32_t flag, uint8_t value); - // LLIL Emulator — intrinsic hook + // LLIL Emulator — intrinsic hook. + // The callback receives output buffers outValues/outRegs with capacity maxCount entries; + // it must write at most maxCount pairs and set *outCount to the number written. EMULATOR_FFI_API void BNLLILEmulatorSetIntrinsicHook(BNLLILEmulator* emu, void* ctxt, bool (*callback)(void* ctxt, BNLLILEmulator* emu, uint32_t intrinsic, const uint64_t* params, size_t paramCount, - uint64_t* outValues, uint32_t* outRegs, size_t* outCount)); + uint64_t* outValues, uint32_t* outRegs, size_t maxCount, size_t* outCount)); // LLIL Emulator — call stack EMULATOR_FFI_API size_t BNLLILEmulatorGetCallStackDepth(BNLLILEmulator* emu); diff --git a/plugins/emulator/api/ilemulator.cpp b/plugins/emulator/api/ilemulator.cpp index 58ea709874..b8b9f9b6c1 100644 --- a/plugins/emulator/api/ilemulator.cpp +++ b/plugins/emulator/api/ilemulator.cpp @@ -272,7 +272,7 @@ bool LLILEmulator::PreInstructionHookCallback(void* ctxt, BNILEmulator*, size_t bool LLILEmulator::IntrinsicHookCallback(void* ctxt, BNLLILEmulator*, uint32_t intrinsic, const uint64_t* params, size_t paramCount, - uint64_t* outValues, uint32_t* outRegs, size_t* outCount) + uint64_t* outValues, uint32_t* outRegs, size_t maxCount, size_t* outCount) { LLILEmulator* self = (LLILEmulator*)ctxt; std::vector paramVec(params, params + paramCount); @@ -280,7 +280,9 @@ bool LLILEmulator::IntrinsicHookCallback(void* ctxt, BNLLILEmulator*, bool result = self->m_intrinsicHook(self, intrinsic, paramVec, outputs); - size_t count = outputs.size(); + // Never write past the caller-provided capacity, even if the user hook returns more + // outputs than the core allocated space for. + size_t count = std::min(outputs.size(), maxCount); if (outCount) *outCount = count; for (size_t i = 0; i < count; i++) diff --git a/plugins/emulator/api/python/ilemulator.py b/plugins/emulator/api/python/ilemulator.py index 0bce146c43..f50cb32b70 100644 --- a/plugins/emulator/api/python/ilemulator.py +++ b/plugins/emulator/api/python/ilemulator.py @@ -480,22 +480,26 @@ def set_intrinsic_hook( ctypes.POINTER(core.BNLLILEmulator), ctypes.c_uint, ctypes.POINTER(ctypes.c_ulonglong), ctypes.c_ulonglong, ctypes.POINTER(ctypes.c_ulonglong), ctypes.POINTER(ctypes.c_uint), - ctypes.POINTER(ctypes.c_ulonglong)) + ctypes.c_ulonglong, ctypes.POINTER(ctypes.c_ulonglong)) if callback is None: core.BNLLILEmulatorSetIntrinsicHook(self.handle, None, ctypes.cast(None, _CB_T)) self._intrinsic_hook_cb = None return @_CB_T - def _cb(ctxt, emu, intrinsic, params, param_count, out_values, out_regs, out_count): + def _cb(ctxt, emu, intrinsic, params, param_count, out_values, out_regs, max_count, out_count): try: param_list = [params[i] for i in range(param_count)] result = self._intrinsic_hook(self, intrinsic, param_list) if result is not None: - out_count[0] = len(result) - for i, (reg, val) in enumerate(result): - out_regs[i] = reg - out_values[i] = val + # Never write past the buffers the core provided (max_count entries). + n = min(len(result), max_count) + out_count[0] = n + mask = (1 << 64) - 1 + for i in range(n): + reg, val = result[i] + out_regs[i] = reg & 0xffffffff + out_values[i] = val & mask return True return False except: diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 3195be7460..b8ee6a3ec0 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -3693,7 +3693,7 @@ void BNLLILEmulatorSetFlag(BNLLILEmulator* emu, uint32_t flag, uint8_t value) void BNLLILEmulatorSetIntrinsicHook(BNLLILEmulator* emu, void* ctxt, - bool (*callback)(void*, BNLLILEmulator*, uint32_t, const uint64_t*, size_t, uint64_t*, uint32_t*, size_t*)) + bool (*callback)(void*, BNLLILEmulator*, uint32_t, const uint64_t*, size_t, uint64_t*, uint32_t*, size_t, size_t*)) { if (callback) { @@ -3701,14 +3701,15 @@ void BNLLILEmulatorSetIntrinsicHook(BNLLILEmulator* emu, void* ctxt, [ctxt, callback, emu](LLILEmulator* e, uint32_t intrinsic, const std::vector& params, std::vector>& outputs) -> bool { + constexpr size_t kMaxOutputs = 64; size_t outCount = 0; - uint64_t outValues[64]; - uint32_t outRegs[64]; + uint64_t outValues[kMaxOutputs]; + uint32_t outRegs[kMaxOutputs]; bool result = callback(ctxt, emu, intrinsic, - params.data(), params.size(), outValues, outRegs, &outCount); + params.data(), params.size(), outValues, outRegs, kMaxOutputs, &outCount); if (result) { - for (size_t i = 0; i < outCount && i < 64; i++) + for (size_t i = 0; i < outCount && i < kMaxOutputs; i++) outputs.push_back({outRegs[i], outValues[i]}); } return result; From 8103cc2fcf0eb76edcd8021a838ab4a6061e1dbe Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:26:49 -0400 Subject: [PATCH 15/29] [emulator] Start EmuRefCountObject at a reference count of 1 The reference-counted base started at 0 and relied on the create helper's AddAPIRef to reach 1, leaving the object destructible by any transient AddRef/Release during construction. Start at 1 (the birth reference handed to the creator) like core's RefCountObject, and have the create helper hand off that reference without an extra AddRef. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 4 ++-- plugins/emulator/core/refcountobject.h | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index b8ee6a3ec0..608db5cccb 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -3583,13 +3583,13 @@ bool LLILEmulator::StubFread(uint64_t) BNLLILEmulator* BNCreateLLILEmulatorForView(BNBinaryView* view) { - return EMU_API_OBJECT_REF(new LLILEmulator(new BinaryView(BNNewViewReference(view)))); + return EMU_API_OBJECT_CREATE(new LLILEmulator(new BinaryView(BNNewViewReference(view)))); } BNLLILEmulator* BNCreateLLILEmulator(BNLowLevelILFunction* il, BNBinaryView* view) { - return EMU_API_OBJECT_REF(new LLILEmulator(new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), new BinaryView(BNNewViewReference(view)))); + return EMU_API_OBJECT_CREATE(new LLILEmulator(new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), new BinaryView(BNNewViewReference(view)))); } diff --git a/plugins/emulator/core/refcountobject.h b/plugins/emulator/core/refcountobject.h index a35dd6deaa..bf200b2764 100644 --- a/plugins/emulator/core/refcountobject.h +++ b/plugins/emulator/core/refcountobject.h @@ -27,7 +27,10 @@ namespace BinaryNinjaEmulator { public: std::atomic m_refs; - EmuRefCountObject() : m_refs(0) {} + // Born with one reference (the one handed to whoever creates the object), mirroring + // core's RefCountObject. Starting at 0 would leave the object destructible by any + // transient AddRef/Release that happens before the creator takes its reference. + EmuRefCountObject() : m_refs(1) {} virtual ~EmuRefCountObject() {} void AddRef() { m_refs.fetch_add(1); } @@ -44,12 +47,13 @@ namespace BinaryNinjaEmulator // Macro-like helpers to hand referenced objects across the external C ABI. + // Hand a freshly-created object's birth reference to the caller (no extra AddRef; the + // object is constructed with a reference count of 1). template - static typename T::APIHandle EMU_API_OBJECT_REF(T* obj) + static typename T::APIHandle EMU_API_OBJECT_CREATE(T* obj) { if (obj == nullptr) return nullptr; - obj->AddAPIRef(); return obj->GetAPIObject(); } From 3beaa124984a500065ff3cac4a1ee54d7a1378b8 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:27:59 -0400 Subject: [PATCH 16/29] [emulator] Don't let map allocation failures cross the C ABI boundary MapMemory allocates a buffer of a caller-controlled length; a std::bad_alloc / length_error from a huge or garbage length would propagate out of the extern "C" map functions, which is undefined behavior. Catch allocation failures in EmulatorMemory::Map (the single point the four map FFI entry points funnel through) and turn them into a logged no-op. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/ilemulator.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/plugins/emulator/core/ilemulator.cpp b/plugins/emulator/core/ilemulator.cpp index 05ebc62327..aa5686fdfc 100644 --- a/plugins/emulator/core/ilemulator.cpp +++ b/plugins/emulator/core/ilemulator.cpp @@ -204,7 +204,18 @@ void EmulatorMemory::Map(uint64_t addr, const void* data, size_t len, const std: } Segment seg; seg.start = addr; - seg.data.assign((const uint8_t*)data, (const uint8_t*)data + len); + try + { + seg.data.assign((const uint8_t*)data, (const uint8_t*)data + len); + } + catch (const std::exception& e) + { + // Do not let an allocation failure for a caller-controlled length escape across the + // extern "C" ABI boundary (that would be undefined behavior). + LogWarn("Emulator: failed to map region '%s' at 0x%llx (size 0x%zx): %s", + name.c_str(), (unsigned long long)addr, len, e.what()); + return; + } seg.name = name; m_segments.push_back(std::move(seg)); } @@ -222,7 +233,18 @@ void EmulatorMemory::Map(uint64_t addr, size_t len, const std::string& name) } Segment seg; seg.start = addr; - seg.data.resize(len, 0); + try + { + seg.data.resize(len, 0); + } + catch (const std::exception& e) + { + // Do not let an allocation failure for a caller-controlled length escape across the + // extern "C" ABI boundary (that would be undefined behavior). + LogWarn("Emulator: failed to map region '%s' at 0x%llx (size 0x%zx): %s", + name.c_str(), (unsigned long long)addr, len, e.what()); + return; + } seg.name = name; m_segments.push_back(std::move(seg)); } From dbbd4be83fff549b115028bddc41810ac72c6429 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:30:25 -0400 Subject: [PATCH 17/29] [emulator] Stop after a call stub/hook requests a stop In the CALL/CALL_STACK_ADJUST/TAILCALL cases, a builtin stub or the call hook can request a stop (e.g. on an error) while returning false, but the code only checked the boolean return and fell through into the call/tailcall path, executing further despite the pending stop. Check m_stopReason after the stub and hook handling and return if a stop was requested. Addresses bdash review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 608db5cccb..6f4436be91 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1888,6 +1888,11 @@ void LLILEmulator::ExecuteCurrentInstruction() if (m_callHook && m_callHook(this, dest)) break; + // A builtin stub or call hook may have requested a stop (e.g. an error) without + // returning true; don't fall through into the call in that case. + if (m_stopReason != ILEmulatorStopReason::Running) + return; + // 3) Try to enter the callee's LLIL { // Compute return address before EnterFunction changes m_il/m_arch @@ -1939,6 +1944,11 @@ void LLILEmulator::ExecuteCurrentInstruction() break; } + // A builtin stub or call hook may have requested a stop (e.g. an error) without + // returning true; don't fall through into the call in that case. + if (m_stopReason != ILEmulatorStopReason::Running) + return; + { uint64_t returnAddr = GetNativeReturnAddress(); size_t addrSize = m_arch->GetAddressSize(); @@ -1980,6 +1990,11 @@ void LLILEmulator::ExecuteCurrentInstruction() if (m_callHook && m_callHook(this, dest)) break; + // A builtin stub or call hook may have requested a stop (e.g. an error) without + // returning true; don't fall through into the tailcall in that case. + if (m_stopReason != ILEmulatorStopReason::Running) + return; + // Tailcall: don't push a frame, replace current function if (m_view) { From c6500c29a960cd8a50e3bd77206284244e3824a5 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:33:29 -0400 Subject: [PATCH 18/29] [emulator] Mask memory-read-hook value to the buffer width in Python The Python memory-read hook did result.to_bytes(buf_len, ...), which raises OverflowError if the hook returns a value that does not fit in buf_len bytes; the bare except then swallowed it and silently returned False, dropping a valid hook result. Mask the value to the buffer width first (matching the C++ bridge's truncating behavior) so it is stored instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/api/python/ilemulator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/emulator/api/python/ilemulator.py b/plugins/emulator/api/python/ilemulator.py index f50cb32b70..acf04c8f5e 100644 --- a/plugins/emulator/api/python/ilemulator.py +++ b/plugins/emulator/api/python/ilemulator.py @@ -393,7 +393,11 @@ def _cb(ctxt, emu, addr, size, out_buf, buf_len): try: result = self._memory_read_hook(self, addr, size) if result is not None: - raw = result.to_bytes(buf_len, 'little', signed=(result < 0)) + # Mask to the buffer width so a value that does not fit (or is negative) + # is truncated to its low bytes instead of raising OverflowError and + # silently dropping the hook result. + mask = (1 << (buf_len * 8)) - 1 + raw = (result & mask).to_bytes(buf_len, 'little') for i in range(buf_len): out_buf[i] = raw[i] return True From 067252aa25d9ea7446e33aba002cf7124429f3d8 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:37:14 -0400 Subject: [PATCH 19/29] [emulator] Move the public C++ API out of the BinaryNinja namespace The plugin's LLILEmulator wrapper lived in namespace BinaryNinja, which is reserved for the core API. Move it to its own BinaryNinjaEmulatorAPI namespace, mirroring the debugger's BinaryNinjaDebuggerAPI. Addresses plafosse review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/api/emulatorapi.h | 8 ++++++-- plugins/emulator/api/ilemulator.cpp | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h index e55878697d..d75db9646a 100644 --- a/plugins/emulator/api/emulatorapi.h +++ b/plugins/emulator/api/emulatorapi.h @@ -25,7 +25,11 @@ limitations under the License. #include #include -namespace BinaryNinja +using namespace BinaryNinja; + +// The plugin's public C++ API lives in its own namespace (mirroring +// BinaryNinjaDebuggerAPI) rather than in BinaryNinja, which is reserved for the core API. +namespace BinaryNinjaEmulatorAPI { /*! \ingroup emulator @@ -153,4 +157,4 @@ namespace BinaryNinja std::string SaveState() const; bool LoadState(const std::string& json); }; -} // namespace BinaryNinja +} // namespace BinaryNinjaEmulatorAPI diff --git a/plugins/emulator/api/ilemulator.cpp b/plugins/emulator/api/ilemulator.cpp index b8b9f9b6c1..ffe19e2e42 100644 --- a/plugins/emulator/api/ilemulator.cpp +++ b/plugins/emulator/api/ilemulator.cpp @@ -23,6 +23,7 @@ #include "emulatorapi.h" using namespace BinaryNinja; +using namespace BinaryNinjaEmulatorAPI; LLILEmulator::LLILEmulator(Ref view) From 02fd0ac4a2e5db1f79106503161854b96666feca Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:37:57 -0400 Subject: [PATCH 20/29] [emulator] Return a reference, not a raw pointer, from LLILEmulator::GetView The view member is reference counted; returning a raw BinaryView* from a public getter invites use-after-free if the emulator outlives the caller's assumptions. Return Ref so callers hold their own reference. Addresses emesare review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h index 05ff25b849..5e94f16245 100644 --- a/plugins/emulator/core/llilemulator.h +++ b/plugins/emulator/core/llilemulator.h @@ -201,6 +201,6 @@ namespace BinaryNinjaEmulator // State serialization std::string SaveState() const; bool LoadState(const std::string& json, BinaryNinja::BinaryView* view); - BinaryNinja::BinaryView* GetView() const { return m_view; } + BinaryNinja::Ref GetView() const { return m_view; } }; } From 53fab4b83c787215e083c3d2c97b869fbac590cb Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:40:17 -0400 Subject: [PATCH 21/29] [emulator] Require a valid architecture as an emulator invariant The emulator half-supported a null architecture with scattered guards that silently no-op'd or fell back to little-endian. Instead reject creation of an emulator whose view/IL has no architecture (BNCreateLLILEmulator* returns null with a logged error) and drop the now-unnecessary null-arch guards, so m_arch is a hard invariant everywhere it is used. Addresses emesare review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 6f4436be91..1472594a02 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -69,7 +69,7 @@ void LLILEmulator::SetEntryPoint(LowLevelILFunction* il, size_t instrIndex) void LLILEmulator::SetArgument(size_t index, const intx::uint512& value) { - if (!m_view || !m_arch) + if (!m_view) return; Ref platform = m_view->GetDefaultPlatform(); @@ -185,9 +185,8 @@ intx::uint512 LLILEmulator::SignedDivideOrModulo( BNEndianness LLILEmulator::GetEndianness() const { - if (m_arch) - return m_arch->GetEndianness(); - return LittleEndian; + // A valid architecture is an invariant established at construction. + return m_arch->GetEndianness(); } @@ -516,7 +515,7 @@ uint64_t LLILEmulator::GetNativeReturnAddress() const uint64_t callAddr = m_il->GetInstruction(m_instrIndex).address; // Use the architecture to get the native instruction length - if (m_view && m_arch) + if (m_view) { uint8_t buf[16]; size_t bytesRead = m_view->Read(buf, callAddr, sizeof(buf)); @@ -3598,13 +3597,30 @@ bool LLILEmulator::StubFread(uint64_t) BNLLILEmulator* BNCreateLLILEmulatorForView(BNBinaryView* view) { - return EMU_API_OBJECT_CREATE(new LLILEmulator(new BinaryView(BNNewViewReference(view)))); + LLILEmulator* emu = new LLILEmulator(new BinaryView(BNNewViewReference(view))); + // A valid architecture is an invariant of the emulator; reject creation without one + // rather than half-supporting a null architecture throughout. + if (!emu->GetArchitecture()) + { + LogError("Cannot create LLIL emulator: the binary view has no default architecture"); + emu->ReleaseAPIRef(); + return nullptr; + } + return EMU_API_OBJECT_CREATE(emu); } BNLLILEmulator* BNCreateLLILEmulator(BNLowLevelILFunction* il, BNBinaryView* view) { - return EMU_API_OBJECT_CREATE(new LLILEmulator(new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), new BinaryView(BNNewViewReference(view)))); + LLILEmulator* emu = new LLILEmulator( + new LowLevelILFunction(BNNewLowLevelILFunctionReference(il)), new BinaryView(BNNewViewReference(view))); + if (!emu->GetArchitecture()) + { + LogError("Cannot create LLIL emulator: the IL function has no architecture"); + emu->ReleaseAPIRef(); + return nullptr; + } + return EMU_API_OBJECT_CREATE(emu); } From 8a42cba00bd44db8f035063733e6b0b2bb6bd0e4 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:41:21 -0400 Subject: [PATCH 22/29] [emulator] Place call arguments using the emulated function's calling convention SetArgument always used the platform's default calling convention, contradicting the documented behavior of using the function's own convention. Derive the calling convention from the function being emulated (via its LLIL function), falling back to the platform default only when the function has none. Also note the address-size-as-stack-slot-size assumption for exotic ABIs. Addresses emesare review comments on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 22 ++++++++++++++++------ plugins/emulator/core/llilemulator.h | 3 ++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 1472594a02..34005f4bbb 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -72,11 +72,19 @@ void LLILEmulator::SetArgument(size_t index, const intx::uint512& value) if (!m_view) return; - Ref platform = m_view->GetDefaultPlatform(); - if (!platform) - return; - - Ref cc = platform->GetDefaultCallingConvention(); + // Place arguments using the calling convention of the function being emulated, falling + // back to the platform default only when the function has none. + Ref cc; + if (m_il) + { + if (Ref func = m_il->GetFunction()) + cc = func->GetCallingConvention().GetValue(); + } + if (!cc) + { + if (Ref platform = m_view->GetDefaultPlatform()) + cc = platform->GetDefaultCallingConvention(); + } if (!cc) return; @@ -87,7 +95,9 @@ void LLILEmulator::SetArgument(size_t index, const intx::uint512& value) return; } - // Stack argument + // Stack argument. Note: the stack slot size is taken to be the address size, which holds + // for the common ABIs but not exotic ones like the x86-64 x32 ABI (4-byte address size, + // 8-byte stack slots). size_t addrSize = m_arch->GetAddressSize(); uint32_t sp = m_arch->GetStackPointerRegister(); uint64_t spVal = static_cast(GetRegister(sp)); diff --git a/plugins/emulator/core/llilemulator.h b/plugins/emulator/core/llilemulator.h index 5e94f16245..bdc6aaad83 100644 --- a/plugins/emulator/core/llilemulator.h +++ b/plugins/emulator/core/llilemulator.h @@ -160,7 +160,8 @@ namespace BinaryNinjaEmulator bool SetEntryPoint(uint64_t addr); void SetEntryPoint(BinaryNinja::LowLevelILFunction* il, size_t instrIndex); - // Argument setup (uses default calling convention) + // Argument setup (uses the emulated function's calling convention, falling back to + // the platform default) void SetArgument(size_t index, const intx::uint512& value); void SetArguments(const std::vector& values); From 5234b600ee63ad75be6486b6b957141b8888f63e Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:44:40 -0400 Subject: [PATCH 23/29] [emulator] Drop false claim that tests run in the main suite The README stated tests/python/test_emulator.py loads this module so the tests run as part of the main Binary Ninja test suite, but that file does not exist and nothing under tests/ references the emulator. Remove the inaccurate paragraph. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/emulator/README.md b/plugins/emulator/README.md index 349922e491..cdc6dfc5ea 100644 --- a/plugins/emulator/README.md +++ b/plugins/emulator/README.md @@ -52,10 +52,6 @@ PYTHONPATH=/python python3 -m pytest test/emulator_test.py PYTHONPATH=/python python3 test/emulator_test.py ``` -Inside the Binary Ninja source tree these same tests also run as part of the main -Binary Ninja test suite (`tests/python/test_emulator.py` loads this module), so -`cd tests && pytest python` exercises them alongside everything else. - ## Support status Only **LLIL** emulation is supported today. **MLIL and HLIL emulation are planned** for the From a2ae96ee4a0c33cf5e7910ee4f4cf0d79fe9260f Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:45:51 -0400 Subject: [PATCH 24/29] [emulator] Fix docs stdout example and Python-bindings cmake fallback path - docs/guide/emulator.md: the stdout callback example used sys.stdout without importing sys; use print(..., end='') so the snippet runs as shown. - api/python/CMakeLists.txt: the non-BN_API_PATH include fell back to ${CMAKE_SOURCE_DIR}/api/cmake/..., which does not exist for an out-of-tree build; point it at the api root via a correct relative path (matching warp). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guide/emulator.md | 2 +- plugins/emulator/api/python/CMakeLists.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/guide/emulator.md b/docs/guide/emulator.md index fb1711612a..ed2a4964b4 100644 --- a/docs/guide/emulator.md +++ b/docs/guide/emulator.md @@ -242,7 +242,7 @@ emu.set_pre_instruction_hook(lambda emu, index: True) emu.set_intrinsic_hook(lambda emu, intrinsic, params: None) # stdout from emulated printf/puts/putchar; data is bytes. -emu.set_stdout_callback(lambda emu, data: sys.stdout.write(data.decode('latin1'))) +emu.set_stdout_callback(lambda emu, data: print(data.decode('latin1'), end='')) # stdin for emulated getchar/fgets/fread; return up to max_len bytes, b'' for EOF. emu.set_stdin_callback(lambda emu, max_len: b'') diff --git a/plugins/emulator/api/python/CMakeLists.txt b/plugins/emulator/api/python/CMakeLists.txt index ca19e8c5c6..c83111ff61 100644 --- a/plugins/emulator/api/python/CMakeLists.txt +++ b/plugins/emulator/api/python/CMakeLists.txt @@ -5,7 +5,8 @@ project(emulator-python-api) if(BN_API_PATH) include(${BN_API_PATH}/cmake/PythonBindings.cmake) else() - include(${CMAKE_SOURCE_DIR}/api/cmake/PythonBindings.cmake) + # api/plugins/emulator/api/python -> api root is four levels up + include(${PROJECT_SOURCE_DIR}/../../../../cmake/PythonBindings.cmake) endif() file(GLOB PYTHON_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/*.py) From be67e81a64b2d16c9dfe706d3f58314bad0ba0ec Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:57:07 -0400 Subject: [PATCH 25/29] [emulator] Type the stdin callback buffer as void* for a writable binding The stdin callback's output buffer was a char* in the FFI, so the generated Python binding exposed it as c_char_p and ctypes handed the callback an immutable bytes copy, making the memmove into it write to the wrong memory. Type the buffer void* across the FFI/C++ bridge so the generated binding uses a writable c_void_p pointer; the Python callback now receives the real buffer address to write into. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/api/emulatorapi.h | 2 +- plugins/emulator/api/ffi.h | 4 +++- plugins/emulator/api/ilemulator.cpp | 4 ++-- plugins/emulator/api/python/ilemulator.py | 5 ++++- plugins/emulator/core/ilemulator.cpp | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h index d75db9646a..32a77d8e8e 100644 --- a/plugins/emulator/api/emulatorapi.h +++ b/plugins/emulator/api/emulatorapi.h @@ -60,7 +60,7 @@ namespace BinaryNinjaEmulatorAPI uint32_t intrinsic, const uint64_t* params, size_t paramCount, uint64_t* outValues, uint32_t* outRegs, size_t maxCount, size_t* outCount); static void StdoutCallbackBridge(void* ctxt, BNILEmulator* emu, const char* data, size_t len); - static size_t StdinCallbackBridge(void* ctxt, BNILEmulator* emu, char* buf, size_t maxLen); + static size_t StdinCallbackBridge(void* ctxt, BNILEmulator* emu, void* buf, size_t maxLen); public: LLILEmulator(Ref view); diff --git a/plugins/emulator/api/ffi.h b/plugins/emulator/api/ffi.h index 5957a5e2d6..23951328b0 100644 --- a/plugins/emulator/api/ffi.h +++ b/plugins/emulator/api/ffi.h @@ -144,8 +144,10 @@ extern "C" bool (*callback)(void* ctxt, BNILEmulator* emu, size_t instrIndex)); EMULATOR_FFI_API void BNILEmulatorSetStdoutCallback(BNILEmulator* emu, void* ctxt, void (*callback)(void* ctxt, BNILEmulator* emu, const char* data, size_t len)); + // buf is a writable output buffer of maxLen bytes (typed void* so the generated bindings + // expose it as a writable pointer rather than an immutable string). EMULATOR_FFI_API void BNILEmulatorSetStdinCallback(BNILEmulator* emu, void* ctxt, - size_t (*callback)(void* ctxt, BNILEmulator* emu, char* buf, size_t maxLen)); + size_t (*callback)(void* ctxt, BNILEmulator* emu, void* buf, size_t maxLen)); EMULATOR_FFI_API void BNILEmulatorRequestStop(BNILEmulator* emu); EMULATOR_FFI_API void BNILEmulatorReset(BNILEmulator* emu); diff --git a/plugins/emulator/api/ilemulator.cpp b/plugins/emulator/api/ilemulator.cpp index ffe19e2e42..d52ae5394f 100644 --- a/plugins/emulator/api/ilemulator.cpp +++ b/plugins/emulator/api/ilemulator.cpp @@ -304,10 +304,10 @@ void LLILEmulator::StdoutCallbackBridge(void* ctxt, BNILEmulator*, const char* d } -size_t LLILEmulator::StdinCallbackBridge(void* ctxt, BNILEmulator*, char* buf, size_t maxLen) +size_t LLILEmulator::StdinCallbackBridge(void* ctxt, BNILEmulator*, void* buf, size_t maxLen) { LLILEmulator* self = (LLILEmulator*)ctxt; - return self->m_stdinCallback(self, buf, maxLen); + return self->m_stdinCallback(self, static_cast(buf), maxLen); } diff --git a/plugins/emulator/api/python/ilemulator.py b/plugins/emulator/api/python/ilemulator.py index acf04c8f5e..e83ba7fd0a 100644 --- a/plugins/emulator/api/python/ilemulator.py +++ b/plugins/emulator/api/python/ilemulator.py @@ -551,9 +551,12 @@ def set_stdin_callback( Return ``b''`` for EOF. Pass ``None`` to remove the callback. """ self._stdin_callback = callback + # buf is a writable, non-null-terminated output buffer. It is typed void* in the FFI + # (hence c_void_p here) so the callback receives the raw address to memmove into, + # rather than the immutable bytes copy ctypes produces for a c_char_p parameter. _CB_T = ctypes.CFUNCTYPE(ctypes.c_ulonglong, ctypes.c_void_p, ctypes.POINTER(core.BNILEmulator), - ctypes.c_char_p, ctypes.c_ulonglong) + ctypes.c_void_p, ctypes.c_ulonglong) if callback is None: core.BNILEmulatorSetStdinCallback(self._get_base(), None, ctypes.cast(None, _CB_T)) self._stdin_cb = None diff --git a/plugins/emulator/core/ilemulator.cpp b/plugins/emulator/core/ilemulator.cpp index aa5686fdfc..2a6582e088 100644 --- a/plugins/emulator/core/ilemulator.cpp +++ b/plugins/emulator/core/ilemulator.cpp @@ -806,7 +806,7 @@ void BNILEmulatorSetStdoutCallback(BNILEmulator* emu, void* ctxt, void BNILEmulatorSetStdinCallback(BNILEmulator* emu, void* ctxt, - size_t (*callback)(void*, BNILEmulator*, char*, size_t)) + size_t (*callback)(void*, BNILEmulator*, void*, size_t)) { if (callback) { From 75fc5039f496967ad3d383b01759f4f35daacd1a Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:57:34 -0400 Subject: [PATCH 26/29] [emulator] Use typed LLIL operand accessors instead of GetRawOperandAs* Replace ~130 raw operand accesses in EvalExpr/ExecuteCurrentInstruction with the typed LowLevelILInstruction accessors (GetLeftExpr/GetRightExpr/GetCarryExpr, GetSourceExpr, GetDestRegister, GetHighRegister/GetLowRegister, GetConstant, GetDestExpr, GetParameterExprs, GetTarget/GetTrueTarget/GetFalseTarget, etc.), which are self-documenting and avoid the operand-index mixups raw indexing invites. Each mapping was verified against the LowLevelILInstructionAccessor specializations to read the same operand index; LLIL_EXTERN_PTR is intentionally left raw (its constant+offset has no equivalent typed pair). No behavior change; the full emulator test suite passes. Addresses bdash review comment on PR #8314. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 267 +++++++++++++------------ 1 file changed, 134 insertions(+), 133 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index 34005f4bbb..b91954c249 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -957,18 +957,20 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Constants --- case LLIL_CONST: case LLIL_CONST_PTR: - return MaskToSize(intx::uint512((uint64_t)expr.GetRawOperandAsInteger(0)), sz); + return MaskToSize(intx::uint512((uint64_t)expr.GetConstant()), sz); case LLIL_EXTERN_PTR: + // EXTERN_PTR's typed GetConstant()/GetOffset() read the operands via GetRawOperandAsIndex, + // which is not equivalent to the GetRawOperandAsInteger reads used here; keep raw access. return MaskToSize(intx::uint512((uint64_t)(expr.GetRawOperandAsInteger(0) + expr.GetRawOperandAsInteger(1))), sz); case LLIL_FLOAT_CONST: - return intx::uint512((uint64_t)expr.GetRawOperandAsInteger(0)); + return intx::uint512((uint64_t)expr.GetConstant()); // --- Registers --- case LLIL_REG: { - uint32_t reg = expr.GetRawOperandAsRegister(0); + uint32_t reg = expr.GetSourceRegister(); if (LLIL_REG_IS_TEMP(reg)) return MaskToSize(GetTempRegister(LLIL_GET_TEMP_REG_INDEX(reg)), sz); return MaskToSize(GetRegister(reg), sz); @@ -976,8 +978,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_REG_SPLIT: { - uint32_t hi = expr.GetRawOperandAsRegister(0); - uint32_t lo = expr.GetRawOperandAsRegister(1); + uint32_t hi = expr.GetHighRegister(); + uint32_t lo = expr.GetLowRegister(); intx::uint512 hiVal = LLIL_REG_IS_TEMP(hi) ? GetTempRegister(LLIL_GET_TEMP_REG_INDEX(hi)) : GetRegister(hi); intx::uint512 loVal = LLIL_REG_IS_TEMP(lo) @@ -987,12 +989,12 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) } case LLIL_FLAG: - return intx::uint512(GetFlag(expr.GetRawOperandAsRegister(0))); + return intx::uint512(GetFlag(expr.GetSourceFlag())); case LLIL_FLAG_BIT: { - uint8_t flagVal = GetFlag(expr.GetRawOperandAsRegister(0)); - size_t bitIndex = expr.GetRawOperandAsIndex(1); + uint8_t flagVal = GetFlag(expr.GetSourceFlag()); + size_t bitIndex = expr.GetBitIndex(); return intx::uint512((flagVal >> bitIndex) & 1); } @@ -1003,8 +1005,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // producing an sz-wide result, which is why they mask differently.) case LLIL_ADD: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left + right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_ADD, true}; return result; @@ -1012,8 +1014,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_SUB: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left - right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_SUB, true}; return result; @@ -1021,8 +1023,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_AND: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left & right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_AND, true}; return result; @@ -1030,8 +1032,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_OR: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left | right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_OR, true}; return result; @@ -1039,8 +1041,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_XOR: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left ^ right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_XOR, true}; return result; @@ -1048,8 +1050,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_LSL: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); size_t shiftAmt = static_cast(right) & (sz * 8 - 1); intx::uint512 result = MaskToSize(left << shiftAmt, sz); m_lastArithmetic = {left, right, result, sz, LLIL_LSL, true}; @@ -1058,8 +1060,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_LSR: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); size_t shiftAmt = static_cast(right) & (sz * 8 - 1); intx::uint512 result = MaskToSize(left >> shiftAmt, sz); m_lastArithmetic = {left, right, result, sz, LLIL_LSR, true}; @@ -1068,8 +1070,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_ASR: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); intx::uint512 maskedLeft = MaskToSize(left, sz); // Sign-extend to full 512 bits, then arithmetic shift right intx::uint512 extended = SignExtend(maskedLeft, sz, 64); @@ -1082,8 +1084,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_ROL: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); size_t bits = sz * 8; size_t shift = static_cast(static_cast(right) % bits); intx::uint512 result = (shift == 0) ? left : MaskToSize((left << shift) | (left >> (bits - shift)), sz); @@ -1093,8 +1095,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_ROR: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); size_t bits = sz * 8; size_t shift = static_cast(static_cast(right) % bits); intx::uint512 result = (shift == 0) ? left : MaskToSize((left >> shift) | (left << (bits - shift)), sz); @@ -1104,8 +1106,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MUL: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left * right, sz); m_lastArithmetic = {left, right, result, sz, LLIL_MUL, true}; return result; @@ -1114,9 +1116,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- With carry --- case LLIL_ADC: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); - uint8_t carryIn = static_cast(EvalExpr(expr.GetRawOperandAsExpr(2)) & 1); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); + uint8_t carryIn = static_cast(EvalExpr(expr.GetCarryExpr()) & 1); intx::uint512 result = MaskToSize(left + right + intx::uint512(carryIn), sz); m_lastArithmetic = {left, right, result, sz, LLIL_ADC, true, carryIn}; return result; @@ -1124,9 +1126,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_SBB: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); - uint8_t carryIn = static_cast(EvalExpr(expr.GetRawOperandAsExpr(2)) & 1); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); + uint8_t carryIn = static_cast(EvalExpr(expr.GetCarryExpr()) & 1); intx::uint512 result = MaskToSize(left - right - intx::uint512(carryIn), sz); m_lastArithmetic = {left, right, result, sz, LLIL_SBB, true, carryIn}; return result; @@ -1136,9 +1138,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) { // Rotate left through carry: rotate the (bits + 1)-bit quantity {carry : value}, // with carry as the most-significant bit. - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); - intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + intx::uint512 carry = EvalExpr(expr.GetCarryExpr()) & 1; size_t bits = sz * 8; size_t width = bits + 1; size_t shift = static_cast(static_cast(right) % width); @@ -1153,9 +1155,9 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) { // Rotate right through carry: rotate the (bits + 1)-bit quantity {carry : value}, // with carry as the most-significant bit. - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); - intx::uint512 carry = EvalExpr(expr.GetRawOperandAsExpr(2)) & 1; + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + intx::uint512 carry = EvalExpr(expr.GetCarryExpr()) & 1; size_t bits = sz * 8; size_t width = bits + 1; size_t shift = static_cast(static_cast(right) % width); @@ -1169,8 +1171,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Division --- case LLIL_DIVU: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); if (right == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); @@ -1181,8 +1183,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_DIVS: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); if (MaskToSize(right, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); @@ -1193,8 +1195,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MODU: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); if (right == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); @@ -1205,8 +1207,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MODS: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); if (MaskToSize(right, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); @@ -1218,8 +1220,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Double-precision --- case LLIL_MULU_DP: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz / 2); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz / 2); return MaskToSize(left * right, sz); } @@ -1228,8 +1230,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // Signed multiply double-precision: sign-extend each sz/2-byte operand to the full // wide value, then multiply as two's-complement. The low sz bytes of the product are // correct regardless of sign, and this handles operands wider than 8 bytes. - intx::uint512 l = SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz / 2, 64); - intx::uint512 r = SignExtend(EvalExpr(expr.GetRawOperandAsExpr(1)), sz / 2, 64); + intx::uint512 l = SignExtend(EvalExpr(expr.GetLeftExpr()), sz / 2, 64); + intx::uint512 r = SignExtend(EvalExpr(expr.GetRightExpr()), sz / 2, 64); return MaskToSize(l * r, sz); } @@ -1238,8 +1240,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // whose `sz` is the double-width product.) case LLIL_DIVU_DP: { - intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz * 2); - intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz * 2); + intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); if (divisor == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); @@ -1250,8 +1252,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_DIVS_DP: { - intx::uint512 dividend = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 divisor = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 dividend = EvalExpr(expr.GetLeftExpr()); + intx::uint512 divisor = EvalExpr(expr.GetRightExpr()); if (MaskToSize(divisor, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "division by zero"); @@ -1262,8 +1264,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MODU_DP: { - intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz * 2); - intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 dividend = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz * 2); + intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); if (divisor == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); @@ -1274,8 +1276,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MODS_DP: { - intx::uint512 dividend = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 divisor = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 dividend = EvalExpr(expr.GetLeftExpr()); + intx::uint512 divisor = EvalExpr(expr.GetRightExpr()); if (MaskToSize(divisor, sz) == 0) { SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); @@ -1287,7 +1289,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Unary --- case LLIL_NEG: { - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); intx::uint512 result = MaskToSize(~val + 1, sz); m_lastArithmetic = {intx::uint512(0), val, result, sz, LLIL_NEG, true}; return result; @@ -1295,7 +1297,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_NOT: { - intx::uint512 val = EvalExpr(expr.GetRawOperandAsExpr(0)); + intx::uint512 val = EvalExpr(expr.GetSourceExpr()); intx::uint512 result = MaskToSize(~val, sz); m_lastArithmetic = {val, intx::uint512(0), result, sz, LLIL_NOT, true}; return result; @@ -1303,25 +1305,25 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_SX: { - LowLevelILInstruction src = expr.GetRawOperandAsExpr(0); + LowLevelILInstruction src = expr.GetSourceExpr(); intx::uint512 val = EvalExpr(src); return MaskToSize(SignExtend(val, src.size, sz), sz); } case LLIL_ZX: { - LowLevelILInstruction src = expr.GetRawOperandAsExpr(0); + LowLevelILInstruction src = expr.GetSourceExpr(); return MaskToSize(EvalExpr(src), sz); } case LLIL_LOW_PART: - return MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + return MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); // --- Bit operations --- case LLIL_BSWAP: { // Reverse the byte order of the sz-byte value. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); intx::uint512 result(0); for (size_t i = 0; i < sz; i++) { @@ -1333,7 +1335,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_POPCNT: { - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); uint64_t count = 0; for (size_t i = 0; i < sz * 8; i++) if (((val >> i) & intx::uint512(1)) != 0) @@ -1344,7 +1346,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CLZ: { // Count leading zero bits; clz(0) == 8 * size. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); size_t bits = sz * 8; uint64_t count = 0; for (size_t i = bits; i-- > 0;) @@ -1359,7 +1361,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CTZ: { // Count trailing zero bits; ctz(0) == 8 * size. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); size_t bits = sz * 8; uint64_t count = 0; for (size_t i = 0; i < bits; i++) @@ -1374,7 +1376,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_RBIT: { // Reverse the bit order of the sz-byte value. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); size_t bits = sz * 8; intx::uint512 result(0); for (size_t i = 0; i < bits; i++) @@ -1386,7 +1388,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_CLS: { // Count leading sign bits: number of bits below the sign bit that match it. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); size_t bits = sz * 8; intx::uint512 sign = (val >> (bits - 1)) & intx::uint512(1); uint64_t count = 0; @@ -1402,7 +1404,7 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_ABS: { // Signed absolute value; abs(INT_MIN) == INT_MIN. - intx::uint512 val = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); + intx::uint512 val = MaskToSize(EvalExpr(expr.GetSourceExpr()), sz); size_t bits = sz * 8; bool neg = ((val >> (bits - 1)) & intx::uint512(1)) != 0; return neg ? MaskToSize(~val + 1, sz) : val; @@ -1411,8 +1413,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MINS: case LLIL_MAXS: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); // leftWins for MINS when left <= right, for MAXS when left >= right (i.e. !(left < right)) bool leftWins = (expr.operation == LLIL_MINS) ? !SignedLess(right, left, sz) : !SignedLess(left, right, sz); @@ -1422,8 +1424,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) case LLIL_MINU: case LLIL_MAXU: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); bool leftWins = (expr.operation == LLIL_MINU) ? (left <= right) : (left >= right); return leftWins ? left : right; } @@ -1431,78 +1433,78 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Comparisons --- case LLIL_CMP_E: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return MaskToSize(left, sz) == MaskToSize(right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_NE: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return MaskToSize(left, sz) != MaskToSize(right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_SLT: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return SignedLess(left, right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_ULT: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); return left < right ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_SLE: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return !SignedLess(right, left, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_ULE: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); return left <= right ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_SGE: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return !SignedLess(left, right, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_UGE: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); return left >= right ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_SGT: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return SignedLess(right, left, sz) ? intx::uint512(1) : intx::uint512(0); } case LLIL_CMP_UGT: { - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); return left > right ? intx::uint512(1) : intx::uint512(0); } // --- Memory --- case LLIL_LOAD: { - uint64_t addr = static_cast(EvalExpr(expr.GetRawOperandAsExpr(0))); + uint64_t addr = static_cast(EvalExpr(expr.GetSourceExpr())); return ReadMemoryValue(addr, sz); } @@ -1511,12 +1513,12 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Misc --- case LLIL_BOOL_TO_INT: - return EvalExpr(expr.GetRawOperandAsExpr(0)) != 0 ? intx::uint512(1) : intx::uint512(0); + return EvalExpr(expr.GetSourceExpr()) != 0 ? intx::uint512(1) : intx::uint512(0); case LLIL_TEST_BIT: { - intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0)); - intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1)); + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); return (left & right) != 0 ? intx::uint512(1) : intx::uint512(0); } @@ -1524,8 +1526,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) { // Signed overflow flag: set when both operands share a sign and the result's sign // differs from them (not the unsigned carry-out). - intx::uint512 left = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(0)), sz); - intx::uint512 right = MaskToSize(EvalExpr(expr.GetRawOperandAsExpr(1)), sz); + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); intx::uint512 result = MaskToSize(left + right, sz); intx::uint512 signBit = intx::uint512(1) << (sz * 8 - 1); bool overflow = ((left ^ result) & (right ^ result) & signBit) != 0; @@ -1535,14 +1537,14 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) // --- Flag conditions (Lifted IL) --- case LLIL_FLAG_COND: { - BNLowLevelILFlagCondition cond = expr.GetRawOperandAsFlagCondition(0); - uint32_t semClass = expr.GetRawOperandAsRegister(1); + BNLowLevelILFlagCondition cond = expr.GetFlagCondition(); + uint32_t semClass = expr.GetSemanticFlagClass(); return EvalFlagCondition(cond, semClass); } case LLIL_FLAG_GROUP: { - uint32_t semGroup = expr.GetRawOperandAsRegister(0); + uint32_t semGroup = expr.GetSemanticFlagGroup(); auto condMap = m_arch->GetFlagConditionsForSemanticFlagGroup(semGroup); if (!condMap.empty()) return EvalFlagCondition(condMap.begin()->second, condMap.begin()->first); @@ -1756,9 +1758,9 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_SET_REG: { - uint32_t reg = instr.GetRawOperandAsRegister(0); + uint32_t reg = instr.GetDestRegister(); m_lastArithmetic.valid = false; - intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; if (LLIL_REG_IS_TEMP(reg)) @@ -1772,10 +1774,10 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_SET_REG_SPLIT: { - uint32_t hi = instr.GetRawOperandAsRegister(0); - uint32_t lo = instr.GetRawOperandAsRegister(1); + uint32_t hi = instr.GetHighRegister(); + uint32_t lo = instr.GetLowRegister(); m_lastArithmetic.valid = false; - intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(2)); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; @@ -1805,8 +1807,8 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_SET_FLAG: { - uint32_t flag = instr.GetRawOperandAsRegister(0); - intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + uint32_t flag = instr.GetDestFlag(); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; SetFlag(flag, static_cast(static_cast(val) & 1)); @@ -1815,8 +1817,8 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_STORE: { - uint64_t addr = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); - intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(1)); + uint64_t addr = static_cast(EvalExpr(instr.GetDestExpr())); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; WriteMemoryValue(addr, val, instr.size); @@ -1825,7 +1827,7 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_PUSH: { - intx::uint512 val = EvalExpr(instr.GetRawOperandAsExpr(0)); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; Push(val, instr.size); @@ -1834,24 +1836,24 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_GOTO: { - size_t target = instr.GetRawOperandAsIndex(0); + size_t target = instr.GetTarget(); m_instrIndex = target; return; } case LLIL_IF: { - intx::uint512 cond = EvalExpr(instr.GetRawOperandAsExpr(0)); + intx::uint512 cond = EvalExpr(instr.GetConditionExpr()); if (m_stopReason != ILEmulatorStopReason::Running) return; - size_t target = (cond != 0) ? instr.GetRawOperandAsIndex(1) : instr.GetRawOperandAsIndex(2); + size_t target = (cond != 0) ? instr.GetTrueTarget() : instr.GetFalseTarget(); m_instrIndex = target; return; } case LLIL_JUMP: { - uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); if (m_stopReason != ILEmulatorStopReason::Running) return; @@ -1868,7 +1870,7 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_JUMP_TO: { - uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); if (m_stopReason != ILEmulatorStopReason::Running) return; @@ -1885,7 +1887,7 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_CALL: { - uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); if (m_stopReason != ILEmulatorStopReason::Running) return; @@ -1931,13 +1933,13 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_CALL_STACK_ADJUST: { - uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); if (m_stopReason != ILEmulatorStopReason::Running) return; if (m_builtinLibcStubs && HandleBuiltinCall(dest)) { - int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + int64_t adj = (int64_t)instr.GetStackAdjustment(); uint32_t sp = m_arch->GetStackPointerRegister(); uint64_t spVal = static_cast(GetRegister(sp)); SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); @@ -1946,7 +1948,7 @@ void LLILEmulator::ExecuteCurrentInstruction() if (m_callHook && m_callHook(this, dest)) { - int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + int64_t adj = (int64_t)instr.GetStackAdjustment(); uint32_t sp = m_arch->GetStackPointerRegister(); uint64_t spVal = static_cast(GetRegister(sp)); SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); @@ -1975,7 +1977,7 @@ void LLILEmulator::ExecuteCurrentInstruction() if (HandleUnknownCall(dest)) { - int64_t adj = (int64_t)instr.GetRawOperandAsInteger(1); + int64_t adj = (int64_t)instr.GetStackAdjustment(); uint32_t sp = m_arch->GetStackPointerRegister(); uint64_t spVal = static_cast(GetRegister(sp)); SetRegister(sp, intx::uint512(spVal + (uint64_t)adj)); @@ -1989,7 +1991,7 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_TAILCALL: { - uint64_t dest = static_cast(EvalExpr(instr.GetRawOperandAsExpr(0))); + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); if (m_stopReason != ILEmulatorStopReason::Running) return; @@ -2039,7 +2041,7 @@ void LLILEmulator::ExecuteCurrentInstruction() { // Evaluate the return address expression for side effects // (pops RA from stack on x86, reads LR on ARM — no-op) - EvalExpr(instr.GetRawOperandAsExpr(0)); + EvalExpr(instr.GetDestExpr()); if (ReturnToCaller()) return; @@ -2070,16 +2072,15 @@ void LLILEmulator::ExecuteCurrentInstruction() case LLIL_TRAP: SetStopReason(ILEmulatorStopReason::Halt, - fmt::format("trap {}", instr.GetRawOperandAsInteger(0))); + fmt::format("trap {}", instr.GetVector())); return; case LLIL_INTRINSIC: { if (m_intrinsicHook) { - uint32_t intrinsic = instr.GetRawOperandAsRegister(2); - LowLevelILInstruction paramExpr = instr.GetRawOperandAsExpr(3); - LowLevelILInstructionList paramList = paramExpr.GetRawOperandAsExprList(0); + uint32_t intrinsic = instr.GetIntrinsic(); + LowLevelILInstructionList paramList = instr.GetParameterExprs(); std::vector params; for (auto& p : paramList) { From cc94bd6ab9b1ad3202187b4be797481f259ae6e9 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 13:57:42 -0400 Subject: [PATCH 27/29] [emulator] Add tests for flags, shifts/rotates, extension, and division/memory edges Cover the previously-untested behavior that recent fixes touch: flag computation (overflow/sign/carry/zero via setcc, signed compare via setl), sign/zero extension (movsx/movzx), shifts and rotate-through-carry (shl/shr/sar/rcl), signed division of INT_MIN by -1 (must wrap, not trap), division by zero (stops with Error), cross-segment memory reads, and rejection of overlapping maps. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/test/emulator_test.py | 110 +++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/plugins/emulator/test/emulator_test.py b/plugins/emulator/test/emulator_test.py index 83c65022a2..91657cac3c 100644 --- a/plugins/emulator/test/emulator_test.py +++ b/plugins/emulator/test/emulator_test.py @@ -396,5 +396,115 @@ def test_toggle_settings(self): self.assertEqual(getattr(emu, prop), original) +class FlagTests(EmulatorTestBase): + # Flags are read back through setcc: Binary Ninja elides dead flag writes when lifting, + # so a flag is only computed when a later instruction actually consumes it. + def test_add_sets_overflow_and_sign(self): + # mov eax, 0x7fffffff ; add eax, 1 ; seto cl ; sets dl ; ret + # 0x7fffffff + 1 -> 0x80000000: signed overflow and negative. + emu = self.emulator_for(b'\xb8\xff\xff\xff\x7f\x83\xc0\x01\x0f\x90\xc1\x0f\x98\xc2\xc3') + emu.run() + self.assertEqual(emu.get_register('rcx') & 0xff, 1) # overflow + self.assertEqual(emu.get_register('rdx') & 0xff, 1) # sign + + def test_add_sets_carry_and_zero(self): + # mov eax, 0xffffffff ; add eax, 1 ; setc cl ; setz dl ; ret + # 0xffffffff + 1 -> wraps to 0 with a carry out. + emu = self.emulator_for(b'\xb8\xff\xff\xff\xff\x83\xc0\x01\x0f\x92\xc1\x0f\x94\xc2\xc3') + emu.run() + self.assertEqual(emu.get_register('rcx') & 0xff, 1) # carry + self.assertEqual(emu.get_register('rdx') & 0xff, 1) # zero + + def test_cmp_equal_sets_zero(self): + # mov eax, 5 ; cmp eax, 5 ; setz al ; ret + emu = self.emulator_for(b'\xb8\x05\x00\x00\x00\x83\xf8\x05\x0f\x94\xc0\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xff, 1) + + def test_signed_less_via_setl(self): + # mov eax, -1 ; cmp eax, 1 ; setl al ; ret -> -1 < 1 signed, so al = 1 + emu = self.emulator_for(b'\xb8\xff\xff\xff\xff\x83\xf8\x01\x0f\x9c\xc0\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xff, 1) + + +class ExtendTests(EmulatorTestBase): + def test_movsx_sign_extends(self): + # mov bl, 0xff ; movsx eax, bl ; ret -> 0xffffffff + emu = self.emulator_for(b'\xb3\xff\x0f\xbe\xc3\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xffffffff, 0xffffffff) + + def test_movzx_zero_extends(self): + # mov bl, 0xff ; movzx eax, bl ; ret -> 0xff + emu = self.emulator_for(b'\xb3\xff\x0f\xb6\xc3\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xffffffff, 0xff) + + +class ShiftRotateTests(EmulatorTestBase): + def test_shl(self): + # mov eax, 1 ; shl eax, 4 ; ret -> 16 + emu = self.emulator_for(b'\xb8\x01\x00\x00\x00\xc1\xe0\x04\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 16) + + def test_shr(self): + # mov eax, 0xff ; shr eax, 4 ; ret -> 0x0f + emu = self.emulator_for(b'\xb8\xff\x00\x00\x00\xc1\xe8\x04\xc3') + emu.run() + self.assertEqual(emu.get_register('rax'), 0x0f) + + def test_sar_sign_fill(self): + # mov eax, 0x80000000 ; sar eax, 31 ; ret -> 0xffffffff (arithmetic shift) + emu = self.emulator_for(b'\xb8\x00\x00\x00\x80\xc1\xf8\x1f\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xffffffff, 0xffffffff) + + def test_rcl_rotates_through_carry(self): + # stc ; mov al, 1 ; rcl al, 1 ; ret + # The 9-bit value {carry=1, al=0x01} rotated left by 1 -> al = 0x03. + # (mov does not affect flags, so stc's carry survives to rcl.) + emu = self.emulator_for(b'\xf9\xb0\x01\xd0\xd0\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xff, 0x03) + + +class DivisionTests(EmulatorTestBase): + def test_signed_div_intmin_by_neg1_does_not_crash(self): + # mov eax, 0x80000000 ; cdq ; mov ecx, -1 ; idiv ecx ; ret + # edx:eax is INT32_MIN sign-extended; dividing by -1 overflows on hardware but the + # emulator must wrap (to INT32_MIN) rather than trap (SIGFPE) on INT_MIN / -1. + emu = self.emulator_for( + b'\xb8\x00\x00\x00\x80\x99\xb9\xff\xff\xff\xff\xf7\xf9\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xffffffff, 0x80000000) + + def test_div_by_zero_stops_with_error(self): + # xor edx,edx ; mov eax, 10 ; mov ecx, 0 ; div ecx ; ret + emu = self.emulator_for( + b'\x31\xd2\xb8\x0a\x00\x00\x00\xb9\x00\x00\x00\x00\xf7\xf1\xc3') + reason = emu.run() + self.assertEqual(reason, ILEmulatorStopReason.ILEmulatorError) + + +class MemoryBoundaryTests(EmulatorTestBase): + def test_read_spans_gap_between_segments(self): + # Two mapped segments with an unmapped gap between them; a read across the gap + # zero-fills the hole and still returns the second segment's bytes. + emu = self.emulator_for(ADD_CONSTS) + emu.map_memory(0x500000, b'\xAA\xBB') + emu.map_memory(0x500004, b'\xCC\xDD') # gap at 0x500002..0x500003 + self.assertEqual(emu.read_memory(0x500000, 6), b'\xAA\xBB\x00\x00\xCC\xDD') + + def test_overlapping_map_is_rejected(self): + emu = self.emulator_for(ADD_CONSTS) + emu.map_memory(0x600000, 0x1000) + emu.map_memory(0x600800, 0x1000) # overlaps the first region + starts = [r['start'] for r in emu.get_mapped_regions()] + self.assertIn(0x600000, starts) + self.assertNotIn(0x600800, starts) + + if __name__ == '__main__': unittest.main() From d79cabb36ecbefa399ee182d7eae626a80a11302 Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 14:05:48 -0400 Subject: [PATCH 28/29] [emulator] Don't re-mask shift counts; execute the IL shift literally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LSL/LSR/ASR masked the shift count with & (sz*8-1), imposing x86-style masking on every architecture. But the architecture's own count masking is already encoded in the lifted IL (x86 emits e.g. `al u>> (cl & 0x1f)` and pre-masks immediates; aarch64 emits a raw `w0 u>> w1`), so re-masking was wrong: it made 8/16-bit x86 shifts by >= the operand width a no-op instead of 0, and imposed x86 semantics on other arches. Perform the literal shift instead — a count >= the operand width yields 0 for the logical shifts and a full sign-fill for the arithmetic shift. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/core/llilemulator.cpp | 28 +++++++++++++++++--------- plugins/emulator/test/emulator_test.py | 8 ++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/plugins/emulator/core/llilemulator.cpp b/plugins/emulator/core/llilemulator.cpp index b91954c249..3e05bbfb9c 100644 --- a/plugins/emulator/core/llilemulator.cpp +++ b/plugins/emulator/core/llilemulator.cpp @@ -1048,12 +1048,16 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) return result; } + // Shifts perform a literal shift by the (unmasked) count: a shift of >= the operand width + // yields 0 for the logical shifts and a full sign-fill for the arithmetic shift. Any + // architecture-specific masking of the count (e.g. x86's `& 0x1f` / `& 0x3f`) is already + // encoded in the lifted IL by the architecture, so the emulator must not re-apply it. case LLIL_LSL: { intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); intx::uint512 right = EvalExpr(expr.GetRightExpr()); - size_t shiftAmt = static_cast(right) & (sz * 8 - 1); - intx::uint512 result = MaskToSize(left << shiftAmt, sz); + uint64_t shiftAmt = static_cast(right); + intx::uint512 result = (shiftAmt >= sz * 8) ? intx::uint512(0) : MaskToSize(left << shiftAmt, sz); m_lastArithmetic = {left, right, result, sz, LLIL_LSL, true}; return result; } @@ -1062,8 +1066,8 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) { intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); intx::uint512 right = EvalExpr(expr.GetRightExpr()); - size_t shiftAmt = static_cast(right) & (sz * 8 - 1); - intx::uint512 result = MaskToSize(left >> shiftAmt, sz); + uint64_t shiftAmt = static_cast(right); + intx::uint512 result = (shiftAmt >= sz * 8) ? intx::uint512(0) : MaskToSize(left >> shiftAmt, sz); m_lastArithmetic = {left, right, result, sz, LLIL_LSR, true}; return result; } @@ -1073,11 +1077,17 @@ intx::uint512 LLILEmulator::EvalExpr(const LowLevelILInstruction& expr) intx::uint512 left = EvalExpr(expr.GetLeftExpr()); intx::uint512 right = EvalExpr(expr.GetRightExpr()); intx::uint512 maskedLeft = MaskToSize(left, sz); - // Sign-extend to full 512 bits, then arithmetic shift right - intx::uint512 extended = SignExtend(maskedLeft, sz, 64); - size_t shiftAmt = static_cast(right) & (sz * 8 - 1); - // Logical shift on the sign-extended value, then mask back - intx::uint512 result = MaskToSize(extended >> shiftAmt, sz); + uint64_t shiftAmt = static_cast(right); + intx::uint512 result; + if (shiftAmt >= sz * 8) + // Shifted out entirely: fill with the sign bit. + result = IsNegative(maskedLeft, sz) ? MaskToSize(~intx::uint512(0), sz) : intx::uint512(0); + else + { + // Sign-extend to full width, logical-shift, then mask back. + intx::uint512 extended = SignExtend(maskedLeft, sz, 64); + result = MaskToSize(extended >> shiftAmt, sz); + } m_lastArithmetic = {maskedLeft, right, result, sz, LLIL_ASR, true}; return result; } diff --git a/plugins/emulator/test/emulator_test.py b/plugins/emulator/test/emulator_test.py index 91657cac3c..495010defe 100644 --- a/plugins/emulator/test/emulator_test.py +++ b/plugins/emulator/test/emulator_test.py @@ -461,6 +461,14 @@ def test_sar_sign_fill(self): emu.run() self.assertEqual(emu.get_register('rax') & 0xffffffff, 0xffffffff) + def test_shr_by_full_operand_width_is_zero(self): + # mov al, 0xff ; mov cl, 8 ; shr al, cl ; ret + # Lifts to `al u>> (cl & 0x1f)` = `al u>> 8`, which must be 0 (the emulator must not + # re-mask the count to the operand width, which would make it a no-op). + emu = self.emulator_for(b'\xb0\xff\xb1\x08\xd2\xe8\xc3') + emu.run() + self.assertEqual(emu.get_register('rax') & 0xff, 0) + def test_rcl_rotates_through_carry(self): # stc ; mov al, 1 ; rcl al, 1 ; ret # The 9-bit value {carry=1, al=0x01} rotated left by 1 -> al = 0x03. From fd58c1793453b85da6eb520ce1fc9aad0f20c9fd Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 15 Jul 2026 19:03:55 -0400 Subject: [PATCH 29/29] [emulator] Include the vendored intx directly, drop the wrapper header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to moving intx out of binaryninjaapi.h: the intermediate wrapper header was misplaced (first under the emulator plugin, which coupled the debugger to it). Drop it and include the shared api/vendor/intx/intx.hpp directly from the emulator's own headers, the same copy and style the debugger already uses. No windows.h min/max guard is needed — the codebase already relies on NOMINMAX in the adapters that include . Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/emulator/api/emulator_intx.h | 36 ---------------------------- plugins/emulator/api/emulatorapi.h | 2 +- plugins/emulator/core/ilemulator.h | 2 +- 3 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 plugins/emulator/api/emulator_intx.h diff --git a/plugins/emulator/api/emulator_intx.h b/plugins/emulator/api/emulator_intx.h deleted file mode 100644 index d057fbc58c..0000000000 --- a/plugins/emulator/api/emulator_intx.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -Copyright 2020-2026 Vector 35 Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#pragma once - -// intx provides the wide-integer type used throughout the emulator plugin's core and -// API. It lives here (rather than in binaryninjaapi.h) so it is only pulled in by the -// emulator's own headers. If a translation unit includes before this header -// (as several debugger adapters do), its min/max macros would break intx's -// numeric_limits<>::min()/max() member definitions. Neutralize those macros just for the -// intx include, then restore them so any later code relying on windows.h min/max is -// unaffected. -#if defined(_WIN32) - #pragma push_macro("min") - #pragma push_macro("max") - #undef min - #undef max -#endif -#include "vendor/intx/intx.hpp" -#if defined(_WIN32) - #pragma pop_macro("min") - #pragma pop_macro("max") -#endif diff --git a/plugins/emulator/api/emulatorapi.h b/plugins/emulator/api/emulatorapi.h index 32a77d8e8e..c6f89b095d 100644 --- a/plugins/emulator/api/emulatorapi.h +++ b/plugins/emulator/api/emulatorapi.h @@ -17,7 +17,7 @@ limitations under the License. #pragma once #include "binaryninjaapi.h" -#include "emulator_intx.h" +#include "vendor/intx/intx.hpp" #include "ffi.h" #include #include diff --git a/plugins/emulator/core/ilemulator.h b/plugins/emulator/core/ilemulator.h index aca8d6ac47..6626cd7af3 100644 --- a/plugins/emulator/core/ilemulator.h +++ b/plugins/emulator/core/ilemulator.h @@ -1,7 +1,7 @@ #pragma once #include "binaryninjaapi.h" -#include "../api/emulator_intx.h" +#include "vendor/intx/intx.hpp" #include "ffi_global.h" #include "refcountobject.h" #include