Skip to content

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

Open
xusheng6 wants to merge 29 commits into
devfrom
test_emulator
Open

Add the framework for BNIL emulator and implement LLIL emulator#8314
xusheng6 wants to merge 29 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>
Comment thread plugins/emulator/core/llilemulator.cpp Outdated
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.

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.

Fixed in 8a42cba — SetArgument now derives the calling convention from the function being emulated (via its LLIL function) and only falls back to the platform default when the function has none.

}

// 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.

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.

Yeah I think it is fine to leave to as is, though I still added a comment for it in 8a42cba

Comment thread plugins/emulator/core/llilemulator.cpp Outdated

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.

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 think the intention is the emulator does not work without an arch. This is just AI being too tempting to add a nullptr check on everything.

Fixed in 53fab4b — a valid architecture is now a hard invariant: BNCreateLLILEmulator* reject creation (with a logged error) when the view/IL has no architecture, and the scattered null-arch guards are gone.

Comment thread plugins/emulator/core/llilemulator.h Outdated
// 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.

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.

Fixed in 02fd0ac — GetView() now returns Ref instead of a raw pointer.

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

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.

Agree in principle; leaving this for a follow-up since relocating the ~40 libc/Win32 stubs into platform/DLL-specific files is a larger restructure than the rest of this pass.

This is also somewhat outside of the scope of the BNIL emulator for now -- I mainly want the emulator to be able to emulate the BNIL, and these are just added so that it can run some small programs as a test

Comment thread binaryninjacore.h Outdated
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?

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.

Fixed in 0d1183d — removed the stray blank line before the namespace close.

Comment thread binaryninjaapi.h Outdated
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?

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.

Fixed by removing it

Comment thread plugins/emulator/api/emulatorapi.h Outdated
#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.

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.

Fixed in 067252a — moved the public C++ API to a BinaryNinjaEmulatorAPI namespace (mirroring BinaryNinjaDebuggerAPI); the debugger adapter follows the rename in 6bd1699.

Comment thread binaryninjaapi.h Outdated
#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?

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.

Before this PR, the debugger already uses intx and has a copy of it. Now, since the emulator also uses it, I moved the intx headers to the vendor folder of binaryninja-api, and have the emulator and the debugger both use it. However, this leaves to a build failure related to the definition of MIN and MAX. Previously, the debugger has a #define NOMINMAX and that is why the debugger can compile just fine. I was unaware of this and made the chagnes in binaryninjaapi.h. I am now fixing the build in a different way in fd58c17 and Vector35/debugger@cd65c42

Comment thread binaryninjacore.h Outdated
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.

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.

Fixed in 9b0180c — removed the stray extra blank line.

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?

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.

Not changed yet. The four plugin generators (warp/sharedcache/kernelcache/emulator) each keep their own copy, and the emulator's differs substantively from the core python/generator.cpp (~389 lines), so reusing the core executable would need a cross-plugin generator refactor rather than a local change. Happy to take that on separately if we want to unify all of them.

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.

How does the emulator's copy of this differ and why?

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 had a closer look and it is more nuanced than what I believed. To start with, the debugger version of the generator.cpp contains strings like "BNDebuggerFreeString", "BNDebugger", etc (see https://github.com/Vector35/debugger/blob/f52fbef2ac734a23c673e98a1e42e9fffa6e8ba0/api/python/generator.cpp#L445), so I assumed each of them would have a few different ones and it might be challenging to merge them. However, I noticed that the ones added after the debugger (i.e., warp, sharedcache, kernel cache), as well as the emulator one we are looking at, also contains the debugger related string. These are definitely not required for them, yet it appears to be working, so there should definitely an effort to clean them up, and then hopefully merge all of the copies

That said, I do not think it is a good idea to deal with it in this PR, so I created https://github.com/Vector35/binaryninja/issues/1673 to track it seperately

Comment thread plugins/emulator/core/llilemulator.cpp Outdated

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.

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.

Fixed in 75fc503 — converted ~130 sites in EvalExpr/ExecuteCurrentInstruction to the typed accessors (GetLeftExpr/GetRightExpr/GetCarryExpr, GetDestRegister, GetHighRegister/GetLowRegister, etc.), each verified to read the same operand index. Only LLIL_EXTERN_PTR is left raw (no equivalent typed constant+offset pair). Full test suite passes.

Comment thread plugins/emulator/core/llilemulator.cpp Outdated
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.

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.

Fixed in 8d069d9 — added wide-signed helpers so DIVS/MODS/MULS_DP/CMP_S*/MINS/MAXS operate on the full 512-bit value instead of truncating through int64_t; this also fixes the INT_MIN / -1 case (it wraps instead of trapping). The double-precision divides were fixed in 7864ee3.

Comment thread plugins/emulator/core/llilemulator.cpp Outdated
{
// 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.

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.

Fixed in dbbd4be — the CALL/CALL_STACK_ADJUST/TAILCALL cases now check m_stopReason after the builtin-stub/hook handling and bail if a stop was requested, instead of falling through into the call.

Comment thread plugins/emulator/core/llilemulator.cpp Outdated
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.

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.

Fixed in 868ce1b — the basic arithmetic ops now mask both operands to sz once up front and use those masked values consistently (computation + flag context), with a comment explaining why the double-precision ops mask to sz/2.

I think we have single and double precision multiplications, and for a double precision one, the result of the calculation is sz wide, so we need to mask the inputs to sz/2. I am not entirely sure this is correct, my memory about these double precision things are a bit vague. @plafosse do you think this is the right way to do it?

@xusheng6

Copy link
Copy Markdown
Member Author

We also want to add at least one test for each LLIL instruction

xusheng6 and others added 20 commits July 15, 2026 13:05
Addresses review feedback (erroneous formatting) on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…njaapi.h

Addresses review feedback (erroneous formatting) on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aders

binaryninjaapi.h does not provide the emulator API, so the wide-integer
type it pulls in for the plugin does not belong there. Move the (windows.h
min/max-guarded) intx include into a shared emulator_intx.h included by the
emulator's own core and API roots instead.

Addresses review feedback on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…MIN/-1

DIVS/MODS/MULS_DP/CMP_S*/MINS/MAXS funneled operands through int64_t via
SignExtend(v, sz, 8), which silently truncated any operand wider than 8 bytes
to its low 64 bits, and native signed division of INT_MIN by -1 is undefined
behavior (SIGFPE on x86).

Add wide-signed helpers (IsNegative/SignedLess/SignedDivideOrModulo) that
operate directly on the 512-bit value: comparisons use sign-aware unsigned
compares, and division works on magnitudes then reapplies the sign, so
INT_MIN / -1 wraps to INT_MIN (and % to 0) instead of trapping.

Addresses bdash review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gned carry

ADD_OVERFLOW returned (result < left), i.e. the unsigned carry-out, but the
operation denotes the signed overflow flag. Compute it as: both operands share
a sign and the result's sign differs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rotate-through-carry implementations mixed a pre-shifted value with an
un-rotated operand (RLC) and dropped the low bits instead of wrapping them
(RRC), producing wrong results (e.g. 1-byte RLC(0x80, carry=0, count=1) gave
0x01 instead of 0x00). Implement both as a true rotate of the (bits+1)-bit
{carry:value} quantity, and record m_lastArithmetic like ROL/ROR so a later
flag read is not stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The basic two-operand arithmetic cases masked operands only when recording the
flag context, while evaluating the operation on the raw operands, and the
double-precision cases mask to sz/2 for unrelated reasons. This was noted as
confusing in review. Mask both operands to sz once at the top of each case and
use those masked values for both the computation and m_lastArithmetic, and add
a comment explaining why the double-precision ops mask differently. No behavior
change.

Addresses bdash review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dary

ADC/SBB folded the carry-in into the stored right operand as
MaskToSize(right + carry, sz), which wraps to 0 when right is all-ones and
carry is 1, so the carry/borrow flag was then computed against the wrong value
(e.g. SBB(0, 0xFFFFFFFF, 1) reported no borrow though one occurs). Track the
carry-in explicitly in the flag context and compute carry-out/borrow-out from
the full-width sum/difference so it no longer wraps. Half-carry now also
accounts for the carry-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…edness

For DIVU_DP/DIVS_DP/MODU_DP/MODS_DP the instruction size is the result (quotient)
size N: the dividend is a double-width 2N value and the divisor is N. The code
instead masked the dividend to N, the divisor to N/2 and the result to N/2,
truncating the high half of any true double-width dividend, and the signed
variants additionally funneled operands through int64_t. Mask the dividend to
2*sz and the divisor/result to sz, and compute the signed variants via a
width-aware helper (also fixing INT_MIN/-1). The *_DP multiplies are unchanged;
a comment now explains why their width convention differs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unmapped-region skip logic compared the next segment's start against
addr + len (the original request end, which can overflow uint64_t for high
addresses) instead of the running cursor. Compute the gap relative to cur so a
mapped segment following an unmapped hole is not skipped past, and high
addresses do not misbehave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EmulatorMemory::Map appended a new segment without checking for overlap with
existing ones. Since FindSegment returns the first containing segment, an
overlapping map left part of the address range shadowed, so reads returned
stale/zero bytes and writes could miss a region. Refuse an overlapping map with
a logged warning, preserving the no-overlap invariant the read/write paths rely
on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The intrinsic-hook FFI passed output buffers (outValues/outRegs) with no
capacity parameter, and both the C++ and Python bridges wrote one entry per
pair the user hook returned. A hook returning more pairs than the fixed 64-entry
buffers the trampoline allocates overran them (stack corruption). Add a maxCount
parameter to the callback contract and clamp writes to it on both the C++ and
Python sides; the Python side also masks values to their field widths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reference-counted base started at 0 and relied on the create helper's
AddAPIRef to reach 1, leaving the object destructible by any transient
AddRef/Release during construction. Start at 1 (the birth reference handed to
the creator) like core's RefCountObject, and have the create helper hand off
that reference without an extra AddRef.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MapMemory allocates a buffer of a caller-controlled length; a std::bad_alloc /
length_error from a huge or garbage length would propagate out of the extern "C"
map functions, which is undefined behavior. Catch allocation failures in
EmulatorMemory::Map (the single point the four map FFI entry points funnel
through) and turn them into a logged no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the CALL/CALL_STACK_ADJUST/TAILCALL cases, a builtin stub or the call hook
can request a stop (e.g. on an error) while returning false, but the code only
checked the boolean return and fell through into the call/tailcall path,
executing further despite the pending stop. Check m_stopReason after the stub
and hook handling and return if a stop was requested.

Addresses bdash review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Python memory-read hook did result.to_bytes(buf_len, ...), which raises
OverflowError if the hook returns a value that does not fit in buf_len bytes;
the bare except then swallowed it and silently returned False, dropping a valid
hook result. Mask the value to the buffer width first (matching the C++ bridge's
truncating behavior) so it is stored instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plugin's LLILEmulator wrapper lived in namespace BinaryNinja, which is
reserved for the core API. Move it to its own BinaryNinjaEmulatorAPI namespace,
mirroring the debugger's BinaryNinjaDebuggerAPI.

Addresses plafosse review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…GetView

The view member is reference counted; returning a raw BinaryView* from a public
getter invites use-after-free if the emulator outlives the caller's assumptions.
Return Ref<BinaryView> so callers hold their own reference.

Addresses emesare review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The emulator half-supported a null architecture with scattered guards that
silently no-op'd or fell back to little-endian. Instead reject creation of an
emulator whose view/IL has no architecture (BNCreateLLILEmulator* returns null
with a logged error) and drop the now-unnecessary null-arch guards, so m_arch is
a hard invariant everywhere it is used.

Addresses emesare review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… convention

SetArgument always used the platform's default calling convention, contradicting
the documented behavior of using the function's own convention. Derive the
calling convention from the function being emulated (via its LLIL function),
falling back to the platform default only when the function has none. Also note
the address-size-as-stack-slot-size assumption for exotic ABIs.

Addresses emesare review comments on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xusheng6 and others added 7 commits July 15, 2026 13:53
The README stated tests/python/test_emulator.py loads this module so the tests
run as part of the main Binary Ninja test suite, but that file does not exist and
nothing under tests/ references the emulator. Remove the inaccurate paragraph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… path

- docs/guide/emulator.md: the stdout callback example used sys.stdout without
  importing sys; use print(..., end='') so the snippet runs as shown.
- api/python/CMakeLists.txt: the non-BN_API_PATH include fell back to
  ${CMAKE_SOURCE_DIR}/api/cmake/..., which does not exist for an out-of-tree
  build; point it at the api root via a correct relative path (matching warp).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ding

The stdin callback's output buffer was a char* in the FFI, so the generated
Python binding exposed it as c_char_p and ctypes handed the callback an
immutable bytes copy, making the memmove into it write to the wrong memory.
Type the buffer void* across the FFI/C++ bridge so the generated binding uses a
writable c_void_p pointer; the Python callback now receives the real buffer
address to write into.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace ~130 raw operand accesses in EvalExpr/ExecuteCurrentInstruction with the
typed LowLevelILInstruction accessors (GetLeftExpr/GetRightExpr/GetCarryExpr,
GetSourceExpr, GetDestRegister, GetHighRegister/GetLowRegister, GetConstant,
GetDestExpr, GetParameterExprs, GetTarget/GetTrueTarget/GetFalseTarget, etc.),
which are self-documenting and avoid the operand-index mixups raw indexing
invites. Each mapping was verified against the LowLevelILInstructionAccessor
specializations to read the same operand index; LLIL_EXTERN_PTR is intentionally
left raw (its constant+offset has no equivalent typed pair). No behavior change;
the full emulator test suite passes.

Addresses bdash review comment on PR #8314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on/memory edges

Cover the previously-untested behavior that recent fixes touch: flag computation
(overflow/sign/carry/zero via setcc, signed compare via setl), sign/zero
extension (movsx/movzx), shifts and rotate-through-carry (shl/shr/sar/rcl),
signed division of INT_MIN by -1 (must wrap, not trap), division by zero (stops
with Error), cross-segment memory reads, and rejection of overlapping maps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LSL/LSR/ASR masked the shift count with & (sz*8-1), imposing x86-style masking
on every architecture. But the architecture's own count masking is already
encoded in the lifted IL (x86 emits e.g. `al u>> (cl & 0x1f)` and pre-masks
immediates; aarch64 emits a raw `w0 u>> w1`), so re-masking was wrong: it made
8/16-bit x86 shifts by >= the operand width a no-op instead of 0, and imposed
x86 semantics on other arches. Perform the literal shift instead — a count >=
the operand width yields 0 for the logical shifts and a full sign-fill for the
arithmetic shift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to moving intx out of binaryninjaapi.h: the intermediate wrapper header
was misplaced (first under the emulator plugin, which coupled the debugger to it).
Drop it and include the shared api/vendor/intx/intx.hpp directly from the
emulator's own headers, the same copy and style the debugger already uses. No
windows.h min/max guard is needed — the codebase already relies on NOMINMAX in
the adapters that include <windows.h>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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