Skip to content

Add the framework for BNIL emulator and implement LLIL emulator#8314

Open
xusheng6 wants to merge 2 commits into
devfrom
test_emulator
Open

Add the framework for BNIL emulator and implement LLIL emulator#8314
xusheng6 wants to merge 2 commits into
devfrom
test_emulator

Conversation

@xusheng6

Copy link
Copy Markdown
Member

No description provided.

xusheng6 and others added 2 commits July 13, 2026 12:11
Add an experimental plugin under plugins/emulator that emulates Binary Ninja's
Low Level IL with full register, flag, and memory state, structured like the
debugger: a core engine (emulatorcore) exposing a C ABI, plus C++ (emulatorapi)
and Python bindings. Supports cross-function emulation, breakpoints, arguments,
built-in libc stubs, user hooks (call/syscall/memory/pre-instruction/intrinsic/
stdio), and JSON state serialization.

Includes a self-contained Python unittest suite (plugins/emulator/test), a user
guide (docs/guide/emulator.md) wired into the mkdocs nav, and the intx vendor
library for wide register values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
if (!platform)
return;

Ref<CallingConvention> cc = platform->GetDefaultCallingConvention();

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.

This does not seem correct, and also seems to be in conflict with what the docs say

Arguments are placed using the function's default calling convention:

Yet this is using the default platform and default calling convention, not the function arg list or cc.

}

// Stack argument
size_t addrSize = m_arch->GetAddressSize();

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.

We need to be careful using address size for stack slot size given those are two completely different things, but incidentally this will probably be fine assuming we never touch the platform GetAddressSize which on something like the x32 abi for linux x86-64 will report as 4 bytes instead of the 8 byte stack slot size.


BNEndianness LLILEmulator::GetEndianness() const
{
if (m_arch)

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.

How is the emulator suppose to function without an architecture? I feel like we should hold that as an invariant instead of trying to half-way support having no architecture in some random member functions.

// State serialization
std::string SaveState() const;
bool LoadState(const std::string& json, BinaryNinja::BinaryView* view);
BinaryNinja::BinaryView* GetView() const { return m_view; }

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.

It seems ill advised to pass back a raw pointer to the view in a public member function especially because the view member itself is ref counted.

bool HandleUnknownCall(uint64_t dest);
void EnsureHeapMapped();

// Stub implementations

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.

IMO we should not be writing stub functions in the body of the emulator like this, I would personally try and move these out into platform, dll, specific files

Comment thread binaryninjacore.h
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size);
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size);


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.

erroneous formatting?

Comment thread binaryninjaapi.h
std::optional<DerivedString> RecognizeConstantData(
const HighLevelILInstruction& instr) override;
};

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.

erroneous formatting?

#include <utility>
#include <vector>

namespace BinaryNinja

@plafosse plafosse Jul 13, 2026

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.

Seems odd for this to live in the BinaryNinja namespace if its a plugin.

Comment thread binaryninjaapi.h
#include "binaryninjacore.h"
#include "exceptions.h"

// intx provides the wide-integer type used by the emulator plugin's API. If a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm confused why this is necessary in binaryninjaapi.h when this header does not provide the emulator's API. Why isn't this in emulatorapi.h instead?

Comment thread binaryninjacore.h
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size);
BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size);


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems unnecessary.

using namespace std;


map<string, string> g_pythonKeywordReplacements = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I know we've done this elsewhere, but do we really need a fifth copy of this in the API repository? Every existing copy of the file is already different, and I have no idea if the differences are significant.

Can we not use the executable produced by python/generator.cpp as part of building the Python bindings?


case LLIL_REG_SPLIT:
{
uint32_t hi = expr.GetRawOperandAsRegister(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason this code is using GetRawOperandAs… everywhere rather than using the typed accessors? We've had bugs in the past from code like this where operand indices were mixed up.

case LLIL_DIVS:
{
// For signed division, work with 64-bit values since intx doesn't provide signed types
int64_t left = static_cast<int64_t>(static_cast<uint64_t>(SignExtend(EvalExpr(expr.GetRawOperandAsExpr(0)), sz, 8)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is surprising that a bunch of operations on signed integers are implemented such that they silently do the wrong thing for sizes > 8.

{
// Evaluate the return address expression for side effects
// (pops RA from stack on x86, reads LR on ARM — no-op)
EvalExpr(instr.GetRawOperandAsExpr(0));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this check m_stopReason before continuing on?

The pattern of having to manually check m_stopReason seems rather error-prone.

intx::uint512 left = EvalExpr(expr.GetRawOperandAsExpr(0));
intx::uint512 right = EvalExpr(expr.GetRawOperandAsExpr(1));
intx::uint512 result = MaskToSize(left + right, sz);
m_lastArithmetic = {MaskToSize(left, sz), MaskToSize(right, sz), result, sz, LLIL_ADD, true};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by the MaskToSize calls here. The add is evaluated here without masking the operands, but is recorded in m_lastArithmetic with them masked to sz.

Other instructions seem to mask one or more operands before they're used. Sometimes things are masked to sz, other times to sz / 2. It's hard to tell when reading this code why different instructions do things differently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants