Add the framework for BNIL emulator and implement LLIL emulator#8314
Add the framework for BNIL emulator and implement LLIL emulator#8314xusheng6 wants to merge 2 commits into
Conversation
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size); | ||
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size); | ||
|
|
||
|
|
| std::optional<DerivedString> RecognizeConstantData( | ||
| const HighLevelILInstruction& instr) override; | ||
| }; | ||
|
|
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| namespace BinaryNinja |
There was a problem hiding this comment.
Seems odd for this to live in the BinaryNinja namespace if its a plugin.
| #include "binaryninjacore.h" | ||
| #include "exceptions.h" | ||
|
|
||
| // intx provides the wide-integer type used by the emulator plugin's API. If a |
There was a problem hiding this comment.
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?
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNegate(const BNPossibleValueSet* object, size_t size); | ||
| BINARYNINJACOREAPI BNPossibleValueSet BNPossibleValueSetNot(const BNPossibleValueSet* object, size_t size); | ||
|
|
||
|
|
| using namespace std; | ||
|
|
||
|
|
||
| map<string, string> g_pythonKeywordReplacements = { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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.
No description provided.