From 52d2874fa856a5db01d097f02a88227d030b5479 Mon Sep 17 00:00:00 2001
From: MikeyLITE69 <160354396+MikeyLITE69@users.noreply.github.com>
Date: Thu, 16 Jul 2026 08:38:27 -0500
Subject: [PATCH] cpu: emulate BMI1/BMI2/ABM instructions in software when the
host lacks them (#249)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What
The native backend runs guest code directly on the host CPU. When the host doesn't
implement a BMI1/BMI2/ABM instruction that the PS5's Zen 2 cores do, it raises #UD
(STATUS_ILLEGAL_INSTRUCTION). Today the vectored handler just logs the faulting bytes
and gives up, so the title dies.
This adds a software fallback. On an illegal-instruction fault we decode the opcode
with Iced (the decoder already used elsewhere in the backend), evaluate it against the
trapped register/memory state, write the result and flags back into the CONTEXT record,
step RIP past the instruction, and resume.
Instructions covered (32- and 64-bit): ANDN, BLSI, BLSMSK, BLSR, BEXTR, BZHI, TZCNT,
LZCNT, RORX, SARX, SHLX, SHRX, PDEP, PEXT.
## Why
Users on CPUs without these extensions currently can't get past code that uses them.
This is a generic fix (no game-specific hacks) that improves compatibility on older
hosts. MULX is intentionally left out for now — its dest_hi/dest_lo operand ordering
is easy to get subtly wrong, so I'd rather add it separately with its own tests.
## How it's structured
- `BmiInstructionEmulator` holds the pure bit/flag semantics with no dependency on the
unsafe CONTEXT plumbing, so it can be unit-tested directly.
- `DirectExecutionBackend.IllegalInstruction.cs` is the thin unsafe adapter (decode →
read operands → emulate → write back → advance RIP). Anything it doesn't fully model
returns false and falls through to the existing diagnostics unchanged, so it can never
mis-handle an opcode it doesn't recognize.
- One hook in `DirectExecutionBackend.Exceptions.cs`, next to the other TryRecover* calls.
- Emits a single one-time "emulating in software" log line, not per-instruction spam.
## How I verified
- Added xUnit tests covering every instruction in both widths plus the CF/ZF/SF/OF
edge cases (src == 0, shift-count masking, index beyond operand width, etc.).
- Cross-checked all the expected values against an independent reference implementation
written from the Intel/AMD definitions; results match.
- `dotnet build` + `dotnet test` pass locally.
## Notes
New files follow .editorconfig (4-space, SPDX headers, REUSE-compliant).
---
.../Cpu/Emulation/BmiInstructionEmulator.cs | 305 +++++++++++++++
.../Cpu/Emulation/GprOperandSize.cs | 14 +
.../DirectExecutionBackend.Exceptions.cs | 5 +
...rectExecutionBackend.IllegalInstruction.cs | 362 ++++++++++++++++++
.../Cpu/BmiInstructionEmulatorTests.cs | 243 ++++++++++++
5 files changed, 929 insertions(+)
create mode 100644 src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs
create mode 100644 src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs
create mode 100644 src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs
create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs
diff --git a/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs b/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs
new file mode 100644
index 00000000..13fbd3a2
--- /dev/null
+++ b/src/SharpEmu.Core/Cpu/Emulation/BmiInstructionEmulator.cs
@@ -0,0 +1,305 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Numerics;
+
+namespace SharpEmu.Core.Cpu.Emulation;
+
+///
+/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
+/// bit-manipulation instructions.
+///
+/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
+/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
+/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
+/// register-only arithmetic so the exception handler can finish the instruction in software and
+/// resume, instead of aborting the title.
+///
+/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
+/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
+/// backend adapter. Each method returns the result already masked to the operand width and, where
+/// the instruction is defined to touch flags, updates in place. Flags
+/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
+/// deterministic across hosts.
+///
+public static class BmiInstructionEmulator
+{
+ private const uint FlagCarry = 1u << 0;
+ private const uint FlagZero = 1u << 6;
+ private const uint FlagSign = 1u << 7;
+ private const uint FlagOverflow = 1u << 11;
+
+ private static ulong WidthMask(GprOperandSize size) =>
+ size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
+
+ private static int WidthBits(GprOperandSize size) => (int)size;
+
+ private static bool SignSet(ulong value, GprOperandSize size) =>
+ size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
+
+ private static uint WithFlag(uint eflags, uint flag, bool set) =>
+ set ? eflags | flag : eflags & ~flag;
+
+ // Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
+ // the result, and the caller supplies CF because each instruction defines it differently.
+ private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
+ {
+ eflags = WithFlag(eflags, FlagCarry, carry);
+ eflags = WithFlag(eflags, FlagZero, result == 0);
+ eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
+ eflags = WithFlag(eflags, FlagOverflow, false);
+ return eflags;
+ }
+
+ /// ANDN: dest = (~src1) & src2. CF and OF are cleared.
+ public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
+ {
+ var result = (~src1 & src2) & WidthMask(size);
+ eflags = ApplyLogicFlags(eflags, result, size, carry: false);
+ return result;
+ }
+
+ /// BLSI: isolate the lowest set bit, dest = (-src) & src. CF = (src != 0).
+ public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
+ {
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var result = ((0UL - s) & s) & mask;
+ eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
+ return result;
+ }
+
+ /// BLSMSK: mask up to and including the lowest set bit, dest = (src - 1) ^ src. CF = (src == 0).
+ public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
+ {
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var result = ((s - 1) ^ s) & mask;
+ eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
+ return result;
+ }
+
+ /// BLSR: reset the lowest set bit, dest = (src - 1) & src. CF = (src == 0).
+ public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
+ {
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var result = ((s - 1) & s) & mask;
+ eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
+ return result;
+ }
+
+ ///
+ /// BEXTR: extract len bits of starting at bit start, where
+ /// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
+ ///
+ public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
+ {
+ var bits = WidthBits(size);
+ var start = (int)(control & 0xFF);
+ var length = (int)((control >> 8) & 0xFF);
+
+ ulong result;
+ if (start >= bits)
+ {
+ result = 0;
+ }
+ else
+ {
+ var shifted = (src & WidthMask(size)) >> start;
+ if (length == 0)
+ {
+ result = 0;
+ }
+ else if (length >= bits)
+ {
+ result = shifted;
+ }
+ else
+ {
+ result = shifted & ((1UL << length) - 1);
+ }
+ }
+
+ result &= WidthMask(size);
+ eflags = WithFlag(eflags, FlagZero, result == 0);
+ eflags = WithFlag(eflags, FlagCarry, false);
+ eflags = WithFlag(eflags, FlagOverflow, false);
+ return result;
+ }
+
+ ///
+ /// BZHI: zero the bits of from bit position index[7:0] upward.
+ /// CF is set when the requested position is at or beyond the operand width.
+ ///
+ public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
+ {
+ var bits = WidthBits(size);
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var n = (int)(index & 0xFF);
+
+ ulong result;
+ bool carry;
+ if (n >= bits)
+ {
+ result = s;
+ carry = true;
+ }
+ else
+ {
+ result = s & ((1UL << n) - 1);
+ carry = false;
+ }
+
+ eflags = ApplyLogicFlags(eflags, result, size, carry);
+ return result;
+ }
+
+ /// TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.
+ public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
+ {
+ var bits = WidthBits(size);
+ var s = src & WidthMask(size);
+
+ ulong result;
+ bool carry;
+ if (s == 0)
+ {
+ result = (ulong)bits;
+ carry = true;
+ }
+ else
+ {
+ result = (ulong)(size == GprOperandSize.Bits64
+ ? BitOperations.TrailingZeroCount(s)
+ : BitOperations.TrailingZeroCount((uint)s));
+ carry = false;
+ }
+
+ eflags = WithFlag(eflags, FlagCarry, carry);
+ eflags = WithFlag(eflags, FlagZero, result == 0);
+ return result;
+ }
+
+ /// LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.
+ public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
+ {
+ var bits = WidthBits(size);
+ var s = src & WidthMask(size);
+
+ ulong result;
+ bool carry;
+ if (s == 0)
+ {
+ result = (ulong)bits;
+ carry = true;
+ }
+ else
+ {
+ result = (ulong)(size == GprOperandSize.Bits64
+ ? BitOperations.LeadingZeroCount(s)
+ : BitOperations.LeadingZeroCount((uint)s));
+ carry = false;
+ }
+
+ eflags = WithFlag(eflags, FlagCarry, carry);
+ eflags = WithFlag(eflags, FlagZero, result == 0);
+ return result;
+ }
+
+ /// RORX: rotate right by (masked to the operand width). No flags.
+ public static ulong Rorx(ulong src, int count, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var rotate = count & (bits - 1);
+ if (rotate == 0)
+ {
+ return s;
+ }
+
+ return ((s >> rotate) | (s << (bits - rotate))) & mask;
+ }
+
+ /// SARX: arithmetic shift right by (masked to the operand width). No flags.
+ public static ulong Sarx(ulong src, int count, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var mask = WidthMask(size);
+ var shift = count & (bits - 1);
+ if (size == GprOperandSize.Bits64)
+ {
+ return (ulong)((long)src >> shift);
+ }
+
+ return (ulong)(uint)((int)(uint)src >> shift) & mask;
+ }
+
+ /// SHLX: logical shift left by (masked to the operand width). No flags.
+ public static ulong Shlx(ulong src, int count, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var mask = WidthMask(size);
+ var shift = count & (bits - 1);
+ return (src << shift) & mask;
+ }
+
+ /// SHRX: logical shift right by (masked to the operand width). No flags.
+ public static ulong Shrx(ulong src, int count, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var mask = WidthMask(size);
+ var s = src & mask;
+ var shift = count & (bits - 1);
+ return s >> shift;
+ }
+
+ /// PDEP: deposit contiguous low bits of into the positions selected by . No flags.
+ public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var selector = mask & WidthMask(size);
+ ulong result = 0;
+ var bit = 0;
+ for (var i = 0; i < bits; i++)
+ {
+ var position = 1UL << i;
+ if ((selector & position) != 0)
+ {
+ if (((src >> bit) & 1UL) != 0)
+ {
+ result |= position;
+ }
+
+ bit++;
+ }
+ }
+
+ return result & WidthMask(size);
+ }
+
+ /// PEXT: gather the bits of selected by into contiguous low bits. No flags.
+ public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
+ {
+ var bits = WidthBits(size);
+ var selector = mask & WidthMask(size);
+ ulong result = 0;
+ var bit = 0;
+ for (var i = 0; i < bits; i++)
+ {
+ if ((selector & (1UL << i)) != 0)
+ {
+ if (((src >> i) & 1UL) != 0)
+ {
+ result |= 1UL << bit;
+ }
+
+ bit++;
+ }
+ }
+
+ return result & WidthMask(size);
+ }
+}
diff --git a/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs b/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs
new file mode 100644
index 00000000..137ab21a
--- /dev/null
+++ b/src/SharpEmu.Core/Cpu/Emulation/GprOperandSize.cs
@@ -0,0 +1,14 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+namespace SharpEmu.Core.Cpu.Emulation;
+
+///
+/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
+/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
+///
+public enum GprOperandSize
+{
+ Bits32 = 32,
+ Bits64 = 64,
+}
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
index 9ea5199b..00dd19bd 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
@@ -128,6 +128,11 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
+ if (exceptionCode == StatusIllegalInstruction &&
+ TryRecoverIllegalInstruction(contextRecord, rip))
+ {
+ return -1;
+ }
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs
new file mode 100644
index 00000000..1ee832c6
--- /dev/null
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.IllegalInstruction.cs
@@ -0,0 +1,362 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Buffers.Binary;
+using System.Threading;
+using Iced.Intel;
+using SharpEmu.Core.Cpu.Emulation;
+
+namespace SharpEmu.Core.Cpu.Native;
+
+// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
+//
+// Guest code runs natively, so the guest and host share the same virtual address space and the
+// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
+// one of these extensions it raises #UD instead of executing the opcode; without this the title
+// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
+// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
+// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
+// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
+// does not fully model.
+public sealed partial class DirectExecutionBackend
+{
+ // Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
+ // with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
+ private const int CTX_EFLAGS = 68;
+
+ // STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
+ private const uint StatusIllegalInstruction = 0xC000001Du;
+
+ private const int MaxInstructionBytes = 15;
+
+ // Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
+ private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
+
+ private static int _bmiSoftwareFallbackAnnounced;
+ private static long _bmiInstructionsEmulated;
+
+ private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
+ {
+ if (!TryReadFaultingInstruction(rip, out var instruction))
+ {
+ return false;
+ }
+
+ if (instruction.Op0Kind != OpKind.Register ||
+ !TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
+ {
+ return false;
+ }
+
+ if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
+ {
+ return false;
+ }
+
+ WriteCtxU64(contextRecord, destOffset, result);
+ if (flagsChanged)
+ {
+ WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
+ }
+
+ WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
+
+ Interlocked.Increment(ref _bmiInstructionsEmulated);
+ if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
+ {
+ Console.Error.WriteLine(
+ "[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
+ "emulating those instructions in software.");
+ }
+
+ return true;
+ }
+
+ private unsafe bool TryEvaluate(
+ void* contextRecord,
+ in Instruction instruction,
+ GprOperandSize size,
+ out ulong result,
+ out bool flagsChanged,
+ out uint eflags)
+ {
+ result = 0;
+ flagsChanged = false;
+ eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
+
+ switch (instruction.Mnemonic)
+ {
+ case Mnemonic.Andn:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
+ !TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
+ {
+ return false;
+ }
+
+ result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
+ flagsChanged = true;
+ return true;
+
+ case Mnemonic.Blsi:
+ case Mnemonic.Blsmsk:
+ case Mnemonic.Blsr:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
+ {
+ return false;
+ }
+
+ result = instruction.Mnemonic switch
+ {
+ Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
+ Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
+ _ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
+ };
+ flagsChanged = true;
+ return true;
+
+ case Mnemonic.Bextr:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
+ !TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
+ {
+ return false;
+ }
+
+ result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
+ flagsChanged = true;
+ return true;
+
+ case Mnemonic.Bzhi:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
+ !TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
+ {
+ return false;
+ }
+
+ result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
+ flagsChanged = true;
+ return true;
+
+ case Mnemonic.Tzcnt:
+ case Mnemonic.Lzcnt:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
+ {
+ return false;
+ }
+
+ result = instruction.Mnemonic == Mnemonic.Tzcnt
+ ? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
+ : BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
+ flagsChanged = true;
+ return true;
+
+ case Mnemonic.Rorx:
+ if (instruction.Op2Kind != OpKind.Immediate8 ||
+ !TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
+ {
+ return false;
+ }
+
+ result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
+ return true;
+
+ case Mnemonic.Sarx:
+ case Mnemonic.Shlx:
+ case Mnemonic.Shrx:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
+ !TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
+ {
+ return false;
+ }
+
+ result = instruction.Mnemonic switch
+ {
+ Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
+ Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
+ _ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
+ };
+ return true;
+
+ case Mnemonic.Pdep:
+ case Mnemonic.Pext:
+ if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
+ !TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
+ {
+ return false;
+ }
+
+ result = instruction.Mnemonic == Mnemonic.Pdep
+ ? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
+ : BmiInstructionEmulator.Pext(packSrc, packMask, size);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
+ {
+ // Try the full instruction window first, then shrink so a fault near the end of a mapped
+ // page (where fewer than 15 bytes are readable) still decodes.
+ foreach (var attempt in DecodeWindowSizes)
+ {
+ var buffer = new byte[attempt];
+ if (!TryReadHostBytes(rip, buffer))
+ {
+ continue;
+ }
+
+ var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
+ decoder.IP = rip;
+ decoder.Decode(out instruction);
+ if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
+ {
+ return true;
+ }
+ }
+
+ instruction = default;
+ return false;
+ }
+
+ private unsafe bool TryReadOperand(
+ void* contextRecord,
+ in Instruction instruction,
+ int operandIndex,
+ GprOperandSize size,
+ out ulong value)
+ {
+ value = 0;
+ switch (instruction.GetOpKind(operandIndex))
+ {
+ case OpKind.Register:
+ if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
+ {
+ return false;
+ }
+
+ var raw = ReadCtxU64(contextRecord, offset);
+ value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
+ return true;
+
+ case OpKind.Memory:
+ if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
+ {
+ return false;
+ }
+
+ var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
+ var buffer = new byte[byteCount];
+ if (!TryReadHostBytes(address, buffer))
+ {
+ return false;
+ }
+
+ value = byteCount == 8
+ ? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
+ : BinaryPrimitives.ReadUInt32LittleEndian(buffer);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
+ {
+ address = 0;
+
+ // FS/GS-relative operands need the guest segment base, which is not modelled here.
+ if (instruction.SegmentPrefix != Register.None)
+ {
+ return false;
+ }
+
+ if (instruction.IsIPRelativeMemoryOperand)
+ {
+ address = instruction.IPRelativeMemoryAddress;
+ return true;
+ }
+
+ var effective = instruction.MemoryDisplacement64;
+ if (instruction.MemoryBase != Register.None)
+ {
+ if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
+ {
+ return false;
+ }
+
+ effective += ReadCtxU64(contextRecord, baseOffset);
+ }
+
+ if (instruction.MemoryIndex != Register.None)
+ {
+ if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
+ {
+ return false;
+ }
+
+ effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
+ }
+
+ address = effective;
+ return true;
+ }
+
+ // Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
+ private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
+ {
+ switch (register)
+ {
+ case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
+ case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
+ case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
+ case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
+ case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
+ case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
+ case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
+ case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
+ case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
+ case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
+ case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
+ case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
+ case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
+ case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
+ case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
+ case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
+ default:
+ if (TryGetGpr64Offset(register, out offset))
+ {
+ size = GprOperandSize.Bits64;
+ return true;
+ }
+
+ size = GprOperandSize.Bits32;
+ return false;
+ }
+ }
+
+ private static bool TryGetGpr64Offset(Register register, out int offset)
+ {
+ switch (register)
+ {
+ case Register.RAX: offset = CTX_RAX; return true;
+ case Register.RCX: offset = CTX_RCX; return true;
+ case Register.RDX: offset = CTX_RDX; return true;
+ case Register.RBX: offset = CTX_RBX; return true;
+ case Register.RSP: offset = CTX_RSP; return true;
+ case Register.RBP: offset = CTX_RBP; return true;
+ case Register.RSI: offset = CTX_RSI; return true;
+ case Register.RDI: offset = CTX_RDI; return true;
+ case Register.R8: offset = CTX_R8; return true;
+ case Register.R9: offset = CTX_R9; return true;
+ case Register.R10: offset = CTX_R10; return true;
+ case Register.R11: offset = CTX_R11; return true;
+ case Register.R12: offset = CTX_R12; return true;
+ case Register.R13: offset = CTX_R13; return true;
+ case Register.R14: offset = CTX_R14; return true;
+ case Register.R15: offset = CTX_R15; return true;
+ default: offset = 0; return false;
+ }
+ }
+}
diff --git a/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs
new file mode 100644
index 00000000..08b2c9f2
--- /dev/null
+++ b/tests/SharpEmu.Libs.Tests/Cpu/BmiInstructionEmulatorTests.cs
@@ -0,0 +1,243 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using SharpEmu.Core.Cpu.Emulation;
+using Xunit;
+
+namespace SharpEmu.Libs.Tests.Cpu;
+
+// These exercise the pure BMI1/BMI2/ABM semantics used by the illegal-instruction software
+// fallback. The expected values were computed by hand from the Intel/AMD definitions so a
+// regression in the bit math or the flag handling fails here without needing a live guest.
+public sealed class BmiInstructionEmulatorTests
+{
+ private const uint Carry = 1u << 0;
+ private const uint Zero = 1u << 6;
+ private const uint Sign = 1u << 7;
+ private const uint Overflow = 1u << 11;
+ private const uint DirectionFlag = 1u << 10; // Unrelated flag; must be preserved.
+
+ [Fact]
+ public void Andn_ClearsSourceBitsAndFlags()
+ {
+ uint flags = Overflow | Carry | DirectionFlag;
+ var result = BmiInstructionEmulator.Andn(0x0000_00F0, 0x0000_00FF, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x0000_000FUL, result);
+ Assert.Equal(0u, flags & Carry);
+ Assert.Equal(0u, flags & Overflow);
+ Assert.Equal(0u, flags & Zero);
+ Assert.Equal(0u, flags & Sign);
+ Assert.Equal(DirectionFlag, flags & DirectionFlag);
+ }
+
+ [Fact]
+ public void Andn_SetsZeroFlagWhenResultIsZero()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Andn(0xFF, 0xFF, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0UL, result);
+ Assert.Equal(Zero, flags & Zero);
+ }
+
+ [Fact]
+ public void Andn_SetsSignFlagOnHighBit32()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Andn(0, 0x8000_0000, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x8000_0000UL, result);
+ Assert.Equal(Sign, flags & Sign);
+ }
+
+ [Fact]
+ public void Andn_MasksTo64Bits()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Andn(0xFFFF_FFFF_FFFF_FF00, 0xFF, GprOperandSize.Bits64, ref flags);
+
+ Assert.Equal(0xFFUL, result);
+ }
+
+ [Theory]
+ [InlineData(0x0000_000CUL, 0x0000_0004UL, false)] // isolate lowest set bit
+ [InlineData(0x0000_0000UL, 0x0000_0000UL, true)] // src == 0 -> CF clear, ZF set
+ public void Blsi_IsolatesLowestBit(ulong src, ulong expected, bool zero)
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Blsi(src, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(expected, result);
+ Assert.Equal(src != 0 ? Carry : 0u, flags & Carry);
+ Assert.Equal(zero ? Zero : 0u, flags & Zero);
+ }
+
+ [Fact]
+ public void Blsmsk_MasksThroughLowestBit()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Blsmsk(0x0000_000C, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x0000_0007UL, result);
+ Assert.Equal(0u, flags & Carry);
+ }
+
+ [Fact]
+ public void Blsmsk_SetsCarryWhenSourceZero32()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Blsmsk(0, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0xFFFF_FFFFUL, result);
+ Assert.Equal(Carry, flags & Carry);
+ Assert.Equal(Sign, flags & Sign);
+ }
+
+ [Fact]
+ public void Blsr_ResetsLowestBit()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Blsr(0x0000_000C, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x0000_0008UL, result);
+ Assert.Equal(0u, flags & Carry);
+ }
+
+ [Fact]
+ public void Blsr_SetsCarryAndZeroWhenSourceZero()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Blsr(0, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0UL, result);
+ Assert.Equal(Carry, flags & Carry);
+ Assert.Equal(Zero, flags & Zero);
+ }
+
+ [Fact]
+ public void Bextr_ExtractsBitField()
+ {
+ uint flags = 0;
+ // start = 4, len = 8 -> bits [11:4] of 0x12345678 == 0x67
+ var control = (8UL << 8) | 4UL;
+ var result = BmiInstructionEmulator.Bextr(0x1234_5678, control, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x0000_0067UL, result);
+ Assert.Equal(0u, flags & Zero);
+ Assert.Equal(0u, flags & Carry);
+ Assert.Equal(0u, flags & Overflow);
+ }
+
+ [Fact]
+ public void Bextr_StartBeyondWidthYieldsZero()
+ {
+ uint flags = 0;
+ var control = (8UL << 8) | 40UL; // start = 40 >= 32
+ var result = BmiInstructionEmulator.Bextr(0xFFFF_FFFF, control, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0UL, result);
+ Assert.Equal(Zero, flags & Zero);
+ }
+
+ [Fact]
+ public void Bzhi_ZeroesHighBits()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 8, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0x0000_00FFUL, result);
+ Assert.Equal(0u, flags & Carry);
+ }
+
+ [Fact]
+ public void Bzhi_SetsCarryWhenIndexAtOrBeyondWidth()
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Bzhi(0xFFFF_FFFF, 32, GprOperandSize.Bits32, ref flags);
+
+ Assert.Equal(0xFFFF_FFFFUL, result);
+ Assert.Equal(Carry, flags & Carry);
+ }
+
+ [Theory]
+ [InlineData(0x0000_0008UL, GprOperandSize.Bits32, 3UL, false)]
+ [InlineData(0x0000_0001UL, GprOperandSize.Bits32, 0UL, false)]
+ [InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
+ [InlineData(0x1_0000_0000UL, GprOperandSize.Bits64, 32UL, false)]
+ public void Tzcnt_CountsTrailingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Tzcnt(src, size, ref flags);
+
+ Assert.Equal(expected, result);
+ Assert.Equal(carry ? Carry : 0u, flags & Carry);
+ }
+
+ [Theory]
+ [InlineData(0x0000_0001UL, GprOperandSize.Bits32, 31UL, false)]
+ [InlineData(0x8000_0000UL, GprOperandSize.Bits32, 0UL, false)]
+ [InlineData(0x0000_0000UL, GprOperandSize.Bits32, 32UL, true)]
+ [InlineData(0x0000_0001UL, GprOperandSize.Bits64, 63UL, false)]
+ [InlineData(0x0000_0000UL, GprOperandSize.Bits64, 64UL, true)]
+ public void Lzcnt_CountsLeadingZeros(ulong src, GprOperandSize size, ulong expected, bool carry)
+ {
+ uint flags = 0;
+ var result = BmiInstructionEmulator.Lzcnt(src, size, ref flags);
+
+ Assert.Equal(expected, result);
+ Assert.Equal(carry ? Carry : 0u, flags & Carry);
+ }
+
+ [Theory]
+ [InlineData(0x1234_5678UL, 8, GprOperandSize.Bits32, 0x7812_3456UL)]
+ [InlineData(0x1234_5678UL, 0, GprOperandSize.Bits32, 0x1234_5678UL)]
+ [InlineData(0x0123_4567_89AB_CDEFUL, 4, GprOperandSize.Bits64, 0xF012_3456_789A_BCDEUL)]
+ public void Rorx_RotatesRight(ulong src, int count, GprOperandSize size, ulong expected)
+ {
+ Assert.Equal(expected, BmiInstructionEmulator.Rorx(src, count, size));
+ }
+
+ [Theory]
+ [InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0xF800_0000UL)]
+ [InlineData(0x8000_0000UL, 36, GprOperandSize.Bits32, 0xF800_0000UL)] // count masked to 4
+ [InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0xF800_0000_0000_0000UL)]
+ public void Sarx_ArithmeticShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
+ {
+ Assert.Equal(expected, BmiInstructionEmulator.Sarx(src, count, size));
+ }
+
+ [Theory]
+ [InlineData(0x0000_0001UL, 4, GprOperandSize.Bits32, 0x0000_0010UL)]
+ [InlineData(0x0000_0001UL, 33, GprOperandSize.Bits32, 0x0000_0002UL)] // count masked to 1
+ public void Shlx_LogicalShiftLeft(ulong src, int count, GprOperandSize size, ulong expected)
+ {
+ Assert.Equal(expected, BmiInstructionEmulator.Shlx(src, count, size));
+ }
+
+ [Theory]
+ [InlineData(0x8000_0000UL, 4, GprOperandSize.Bits32, 0x0800_0000UL)]
+ [InlineData(0x8000_0000_0000_0000UL, 4, GprOperandSize.Bits64, 0x0800_0000_0000_0000UL)]
+ public void Shrx_LogicalShiftRight(ulong src, int count, GprOperandSize size, ulong expected)
+ {
+ Assert.Equal(expected, BmiInstructionEmulator.Shrx(src, count, size));
+ }
+
+ [Fact]
+ public void PdepAndPext_AreInverseForContiguousSource()
+ {
+ var deposited = BmiInstructionEmulator.Pdep(0x0000_000F, 0x0000_00AA, GprOperandSize.Bits32);
+ Assert.Equal(0x0000_00AAUL, deposited);
+
+ var extracted = BmiInstructionEmulator.Pext(0x0000_00AA, 0x0000_00AA, GprOperandSize.Bits32);
+ Assert.Equal(0x0000_000FUL, extracted);
+ }
+
+ [Fact]
+ public void Pext_GathersSelectedBits()
+ {
+ // mask selects bit positions 1,3,5,7; source has 1s only at 5 and 7 -> packed 0b1100
+ var extracted = BmiInstructionEmulator.Pext(0xF0, 0xAA, GprOperandSize.Bits32);
+ Assert.Equal(0x0000_000CUL, extracted);
+ }
+}