diff --git a/docs/guide/emulator.md b/docs/guide/emulator.md new file mode 100644 index 0000000000..ed2a4964b4 --- /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: 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'') +``` + +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..cdc6dfc5ea --- /dev/null +++ b/plugins/emulator/README.md @@ -0,0 +1,91 @@ +# 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 +``` + +## 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` +- **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` +- **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` +- **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..c6f89b095d --- /dev/null +++ b/plugins/emulator/api/emulatorapi.h @@ -0,0 +1,160 @@ +/* +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 "vendor/intx/intx.hpp" +#include "ffi.h" +#include +#include +#include +#include +#include + +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 + */ + 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 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, void* 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 BinaryNinjaEmulatorAPI diff --git a/plugins/emulator/api/ffi.h b/plugins/emulator/api/ffi.h new file mode 100644 index 0000000000..23951328b0 --- /dev/null +++ b/plugins/emulator/api/ffi.h @@ -0,0 +1,198 @@ +/* +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)); + // 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, void* 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. + // 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 maxCount, 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..d52ae5394f --- /dev/null +++ b/plugins/emulator/api/ilemulator.cpp @@ -0,0 +1,555 @@ +// 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; +using namespace BinaryNinjaEmulatorAPI; + + +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 maxCount, 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); + + // 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++) + { + 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*, void* buf, size_t maxLen) +{ + LLILEmulator* self = (LLILEmulator*)ctxt; + return self->m_stdinCallback(self, static_cast(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..c83111ff61 --- /dev/null +++ b/plugins/emulator/api/python/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) + +project(emulator-python-api) + +if(BN_API_PATH) + include(${BN_API_PATH}/cmake/PythonBindings.cmake) +else() + # 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) +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..e83ba7fd0a --- /dev/null +++ b/plugins/emulator/api/python/ilemulator.py @@ -0,0 +1,637 @@ +# 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: + # 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 + 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.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, 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: + # 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: + 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 + # 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_void_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..2a6582e088 --- /dev/null +++ b/plugins/emulator/core/ilemulator.cpp @@ -0,0 +1,884 @@ +#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; + // 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; + } + } + 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; + // 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; + } + } + return totalWritten; +} + + +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; + 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)); +} + + +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; + 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)); +} + + +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*, void*, 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..6626cd7af3 --- /dev/null +++ b/plugins/emulator/core/ilemulator.h @@ -0,0 +1,161 @@ +#pragma once + +#include "binaryninjaapi.h" +#include "vendor/intx/intx.hpp" +#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; + + // True if [addr, addr + len) intersects any existing segment. + bool OverlapsExisting(uint64_t addr, size_t len) 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..3e05bbfb9c --- /dev/null +++ b/plugins/emulator/core/llilemulator.cpp @@ -0,0 +1,3883 @@ +#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) + return; + + // 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; + + auto argRegs = cc->GetIntegerArgumentRegisters(); + if (index < argRegs.size()) + { + SetRegister(argRegs[index], value); + return; + } + + // 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)); + 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); +} + + +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::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). 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, 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); +} + + +BNEndianness LLILEmulator::GetEndianness() const +{ + // A valid architecture is an invariant established at construction. + return m_arch->GetEndianness(); +} + + +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) + { + 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.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.GetConstant()); + + // --- Registers --- + case LLIL_REG: + { + 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); + } + + case LLIL_REG_SPLIT: + { + 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) + ? 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.GetSourceFlag())); + + case LLIL_FLAG_BIT: + { + uint8_t flagVal = GetFlag(expr.GetSourceFlag()); + size_t bitIndex = expr.GetBitIndex(); + return intx::uint512((flagVal >> bitIndex) & 1); + } + + // --- 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 = 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; + } + + case LLIL_SUB: + { + 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; + } + + case LLIL_AND: + { + 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; + } + + case LLIL_OR: + { + 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; + } + + case LLIL_XOR: + { + 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; + } + + // 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()); + 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; + } + + case LLIL_LSR: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + 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; + } + + case LLIL_ASR: + { + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + intx::uint512 maskedLeft = MaskToSize(left, 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; + } + + case LLIL_ROL: + { + 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); + m_lastArithmetic = {left, right, result, sz, LLIL_ROL, true}; + return result; + } + + case LLIL_ROR: + { + 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); + m_lastArithmetic = {left, right, result, sz, LLIL_ROR, true}; + return result; + } + + case LLIL_MUL: + { + 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; + } + + // --- With carry --- + case LLIL_ADC: + { + 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; + } + + case LLIL_SBB: + { + 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; + } + + 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.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); + 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.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); + 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_RRC, true}; + return result; + } + + // --- Division --- + case LLIL_DIVU: + { + 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"); + return 0; + } + return MaskToSize(left / right, sz); + } + + case LLIL_DIVS: + { + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + if (MaskToSize(right, sz) == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return SignedDivideOrModulo(left, right, sz, false); + } + + case LLIL_MODU: + { + 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"); + return 0; + } + return MaskToSize(left % right, sz); + } + + case LLIL_MODS: + { + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + if (MaskToSize(right, sz) == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return SignedDivideOrModulo(left, right, sz, true); + } + + // --- Double-precision --- + case LLIL_MULU_DP: + { + intx::uint512 left = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz / 2); + intx::uint512 right = MaskToSize(EvalExpr(expr.GetRightExpr()), sz / 2); + return MaskToSize(left * right, sz); + } + + case LLIL_MULS_DP: + { + // 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.GetLeftExpr()), sz / 2, 64); + intx::uint512 r = SignExtend(EvalExpr(expr.GetRightExpr()), sz / 2, 64); + 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 dividend = MaskToSize(EvalExpr(expr.GetLeftExpr()), sz * 2); + intx::uint512 divisor = MaskToSize(EvalExpr(expr.GetRightExpr()), sz); + if (divisor == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return MaskToSize(dividend / divisor, sz); + } + + case LLIL_DIVS_DP: + { + intx::uint512 dividend = EvalExpr(expr.GetLeftExpr()); + intx::uint512 divisor = EvalExpr(expr.GetRightExpr()); + if (MaskToSize(divisor, sz) == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "division by zero"); + return 0; + } + return SignedDivideOrModuloWide(dividend, sz * 2, divisor, sz, sz, false); + } + + case LLIL_MODU_DP: + { + 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"); + return 0; + } + return MaskToSize(dividend % divisor, sz); + } + + case LLIL_MODS_DP: + { + intx::uint512 dividend = EvalExpr(expr.GetLeftExpr()); + intx::uint512 divisor = EvalExpr(expr.GetRightExpr()); + if (MaskToSize(divisor, sz) == 0) + { + SetStopReason(ILEmulatorStopReason::Error, "modulo by zero"); + return 0; + } + return SignedDivideOrModuloWide(dividend, sz * 2, divisor, sz, sz, true); + } + + // --- Unary --- + case LLIL_NEG: + { + 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; + } + + case LLIL_NOT: + { + 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; + } + + case LLIL_SX: + { + LowLevelILInstruction src = expr.GetSourceExpr(); + intx::uint512 val = EvalExpr(src); + return MaskToSize(SignExtend(val, src.size, sz), sz); + } + + case LLIL_ZX: + { + LowLevelILInstruction src = expr.GetSourceExpr(); + return MaskToSize(EvalExpr(src), sz); + } + + case LLIL_LOW_PART: + 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.GetSourceExpr()), 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.GetSourceExpr()), 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.GetSourceExpr()), 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.GetSourceExpr()), 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.GetSourceExpr()), 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.GetSourceExpr()), 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.GetSourceExpr()), 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.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); + return leftWins ? left : right; + } + + case LLIL_MINU: + case LLIL_MAXU: + { + 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; + } + + // --- Comparisons --- + case LLIL_CMP_E: + { + 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.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.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.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.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.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.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.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.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.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.GetSourceExpr())); + return ReadMemoryValue(addr, sz); + } + + case LLIL_POP: + return Pop(sz); + + // --- Misc --- + case LLIL_BOOL_TO_INT: + return EvalExpr(expr.GetSourceExpr()) != 0 ? intx::uint512(1) : intx::uint512(0); + + case LLIL_TEST_BIT: + { + intx::uint512 left = EvalExpr(expr.GetLeftExpr()); + intx::uint512 right = EvalExpr(expr.GetRightExpr()); + return (left & right) != 0 ? intx::uint512(1) : intx::uint512(0); + } + + 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.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; + return overflow ? intx::uint512(1) : intx::uint512(0); + } + + // --- Flag conditions (Lifted IL) --- + case LLIL_FLAG_COND: + { + BNLowLevelILFlagCondition cond = expr.GetFlagCondition(); + uint32_t semClass = expr.GetSemanticFlagClass(); + return EvalFlagCondition(cond, semClass); + } + + case LLIL_FLAG_GROUP: + { + uint32_t semGroup = expr.GetSemanticFlagGroup(); + 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) + { + // 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) + // 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 + intx::uint512(ctx.carryIn)) ? 1 : 0; + if (isAdd) + { + intx::uint512 fullSum = ctx.left + ctx.right + intx::uint512(ctx.carryIn); + return ((fullSum >> (ctx.size * 8)) != 0) ? 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(ctx.carryIn)) + > intx::uint512(0xf) ? 1 : 0; + if (isSub) + return (ctx.left & intx::uint512(0xf)) < ((ctx.right & intx::uint512(0xf)) + intx::uint512(ctx.carryIn)) + ? 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.GetDestRegister(); + m_lastArithmetic.valid = false; + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); + 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.GetHighRegister(); + uint32_t lo = instr.GetLowRegister(); + m_lastArithmetic.valid = false; + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); + 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.GetDestFlag(); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); + 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.GetDestExpr())); + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + WriteMemoryValue(addr, val, instr.size); + break; + } + + case LLIL_PUSH: + { + intx::uint512 val = EvalExpr(instr.GetSourceExpr()); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + Push(val, instr.size); + break; + } + + case LLIL_GOTO: + { + size_t target = instr.GetTarget(); + m_instrIndex = target; + return; + } + + case LLIL_IF: + { + intx::uint512 cond = EvalExpr(instr.GetConditionExpr()); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + size_t target = (cond != 0) ? instr.GetTrueTarget() : instr.GetFalseTarget(); + m_instrIndex = target; + return; + } + + case LLIL_JUMP: + { + uint64_t dest = static_cast(EvalExpr(instr.GetDestExpr())); + 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.GetDestExpr())); + 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.GetDestExpr())); + 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; + + // 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 + 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.GetDestExpr())); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + if (m_builtinLibcStubs && HandleBuiltinCall(dest)) + { + 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)); + break; + } + + if (m_callHook && m_callHook(this, dest)) + { + 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)); + 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(); + 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.GetStackAdjustment(); + 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.GetDestExpr())); + if (m_stopReason != ILEmulatorStopReason::Running) + return; + + if (m_builtinLibcStubs && HandleBuiltinCall(dest)) + break; + + 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) + { + 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.GetDestExpr()); + + 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.GetVector())); + return; + + case LLIL_INTRINSIC: + { + if (m_intrinsicHook) + { + uint32_t intrinsic = instr.GetIntrinsic(); + LowLevelILInstructionList paramList = instr.GetParameterExprs(); + 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) +{ + 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) +{ + 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); +} + + +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, size_t*)) +{ + if (callback) + { + emu->object->SetIntrinsicHook( + [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[kMaxOutputs]; + uint32_t outRegs[kMaxOutputs]; + bool result = callback(ctxt, emu, intrinsic, + params.data(), params.size(), outValues, outRegs, kMaxOutputs, &outCount); + if (result) + { + for (size_t i = 0; i < outCount && i < kMaxOutputs; 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..bdc6aaad83 --- /dev/null +++ b/plugins/emulator/core/llilemulator.h @@ -0,0 +1,207 @@ +#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; + // 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 {}; + + // 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); + // 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); + // 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); + 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 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); + + // 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::Ref 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..bf200b2764 --- /dev/null +++ b/plugins/emulator/core/refcountobject.h @@ -0,0 +1,74 @@ +/* +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; + // 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); } + + 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. + // 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_CREATE(T* obj) + { + if (obj == nullptr) + return nullptr; + 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..495010defe --- /dev/null +++ b/plugins/emulator/test/emulator_test.py @@ -0,0 +1,518 @@ +#!/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, + 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) + # 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 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) + 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) + + +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_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. + # (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() 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