Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8bc02c3
Add the BNIL (LLIL) emulator plugin
xusheng6 Jul 13, 2026
ff7af30
Add coverage for newly added IL instructions
xusheng6 Jul 13, 2026
e146066
[emulator] Remove stray blank line in binaryninjacore.h
xusheng6 Jul 15, 2026
865d5e0
[emulator] Remove stray blank line before namespace close in binaryni…
xusheng6 Jul 15, 2026
66cb585
[emulator] Move intx include out of binaryninjaapi.h into emulator he…
xusheng6 Jul 15, 2026
afa7423
[emulator] Fix signed IL ops for operands wider than 8 bytes and INT_…
xusheng6 Jul 15, 2026
0c92c7d
[emulator] Fix LLIL_ADD_OVERFLOW to compute signed overflow, not unsi…
xusheng6 Jul 15, 2026
7beb8b1
[emulator] Fix RLC/RRC rotate-through-carry semantics
xusheng6 Jul 15, 2026
cd94031
[emulator] Mask arithmetic operands consistently up front
xusheng6 Jul 15, 2026
b06ba46
[emulator] Fix carry/borrow flag for ADC/SBB at the operand-size boun…
xusheng6 Jul 15, 2026
277f674
[emulator] Fix double-precision divide/modulo operand widths and sign…
xusheng6 Jul 15, 2026
ad6ec7c
[emulator] Fix cross-segment memory read/write boundary computation
xusheng6 Jul 15, 2026
c0797a8
[emulator] Reject overlapping memory maps
xusheng6 Jul 15, 2026
a88486a
[emulator] Bound intrinsic-hook output writes to the buffer capacity
xusheng6 Jul 15, 2026
cf6b0eb
[emulator] Start EmuRefCountObject at a reference count of 1
xusheng6 Jul 15, 2026
849b897
[emulator] Don't let map allocation failures cross the C ABI boundary
xusheng6 Jul 15, 2026
7c76770
[emulator] Stop after a call stub/hook requests a stop
xusheng6 Jul 15, 2026
c2873a2
[emulator] Mask memory-read-hook value to the buffer width in Python
xusheng6 Jul 15, 2026
59868ac
[emulator] Move the public C++ API out of the BinaryNinja namespace
xusheng6 Jul 15, 2026
eacb774
[emulator] Return a reference, not a raw pointer, from LLILEmulator::…
xusheng6 Jul 15, 2026
292a1ff
[emulator] Require a valid architecture as an emulator invariant
xusheng6 Jul 15, 2026
4c3f160
[emulator] Place call arguments using the emulated function's calling…
xusheng6 Jul 15, 2026
9c79ea1
[emulator] Drop false claim that tests run in the main suite
xusheng6 Jul 15, 2026
58cda36
[emulator] Fix docs stdout example and Python-bindings cmake fallback…
xusheng6 Jul 15, 2026
414baec
[emulator] Type the stdin callback buffer as void* for a writable bin…
xusheng6 Jul 15, 2026
18c8b6a
[emulator] Use typed LLIL operand accessors instead of GetRawOperandAs*
xusheng6 Jul 15, 2026
1c4d4fb
[emulator] Add tests for flags, shifts/rotates, extension, and divisi…
xusheng6 Jul 15, 2026
c364188
[emulator] Don't re-mask shift counts; execute the IL shift literally
xusheng6 Jul 15, 2026
8775bd4
[emulator] Include the vendored intx directly, drop the wrapper header
xusheng6 Jul 15, 2026
47392a8
emulator: warn when emulating a non-default architecture
xusheng6 Jul 22, 2026
efd142b
emulator: honor non-default argument locations in SetArgument
xusheng6 Jul 22, 2026
7c64bf2
emulator: move wide-integer helpers to the ILEmulator base class
xusheng6 Jul 22, 2026
c5aff49
emulator: store flags as bool
xusheng6 Jul 22, 2026
9e81c2c
emulator: use return consistently in the LLIL call handlers
xusheng6 Jul 22, 2026
5525a11
emulator: read arguments/return values via the function calling conve…
xusheng6 Jul 22, 2026
65b488d
emulator: drop redundant string wrap around GetShortName
xusheng6 Jul 22, 2026
239a227
emulator: return std::optional from ResolveCallTargetName
xusheng6 Jul 22, 2026
dd841f5
emulator: add a per-instruction LLIL test suite
xusheng6 Jul 22, 2026
df8003c
[emulator] Fix null-architecture crash when emulating a standalone IL…
xusheng6 Jul 29, 2026
44c0a9a
[emulator] Fix MULU_DP/MULS_DP operand and product widths
xusheng6 Jul 29, 2026
6f3373c
[emulator] Fix TEST_BIT to select a bit by index, not mask
xusheng6 Jul 29, 2026
3068a19
[emulator] Resolve JUMP_TO through the instruction's own target map
xusheng6 Jul 29, 2026
89be9f4
[emulator] Fix test cases that asserted on IL the API cannot express
xusheng6 Jul 29, 2026
29b9972
[emulator] Add known-answer tests that emulate compiled base64 and MD5
xusheng6 Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions docs/guide/emulator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
# BNIL Emulator — Python API Guide

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we have reserved this kind of content for the developer guide.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but the only way to use the emulator is through its API (well except for using the BNIL debug adapter), so this should be fine


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).
32 changes: 32 additions & 0 deletions plugins/emulator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Build output
/build
/artifacts
cmake-build-*/
out/

# Compiled objects / libraries
*.o
*.obj
*.so
*.dylib
*.dll
*.a
*.lib
*.pdb
*.ilk

# The known-answer-test kernels are checked in deliberately, so the suite does not
# need a cross-compiler. Rebuild them with test/kat/build.sh.
!/test/kat/prebuilt/*.o

# Generated Python bindings
/api/python/_emulatorcore.py
/api/python/emulator_enums.py
*/__pycache__/
*.pyc

# IDE / OS
.idea/
.vscode/
.DS_Store
/.claude/
Loading
Loading