cpu: emulate BMI1/BMI2/ABM instructions in software when the host lacks them (#249)

## 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).
This commit is contained in:
MikeyLITE69
2026-07-16 08:38:27 -05:00
committed by GitHub
parent c5a82c1065
commit 52d2874fa8
5 changed files with 929 additions and 0 deletions
@@ -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);
}
}