diff --git a/src/SharpEmu.Core/Cpu/Emulation/ShaInstructionEmulator.cs b/src/SharpEmu.Core/Cpu/Emulation/ShaInstructionEmulator.cs
new file mode 100644
index 0000000..1c6108e
--- /dev/null
+++ b/src/SharpEmu.Core/Cpu/Emulation/ShaInstructionEmulator.cs
@@ -0,0 +1,149 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Numerics;
+
+namespace SharpEmu.Core.Cpu.Emulation;
+
+///
+/// Calculates the results of the SHA extension instructions for instruction recovery.
+/// Dword 0 of a value is bits 31:0 and dword 3 is bits 127:96, as in the Intel SDM.
+///
+public static class ShaInstructionEmulator
+{
+ public static UInt128 Sha1Rnds4(UInt128 source1, UInt128 source2, int function)
+ {
+ var a = Dword(source1, 3);
+ var b = Dword(source1, 2);
+ var c = Dword(source1, 1);
+ var d = Dword(source1, 0);
+ var e = 0u;
+ var k = (function & 3) switch
+ {
+ 0 => 0x5A827999u,
+ 1 => 0x6ED9EBA1u,
+ 2 => 0x8F1BBCDCu,
+ _ => 0xCA62C1D6u,
+ };
+
+ // The first message dword already carries E (SHA1NEXTE adds it).
+ for (var round = 0; round < 4; round++)
+ {
+ var next = Sha1Function(function, b, c, d) + BitOperations.RotateLeft(a, 5) + Dword(source2, 3 - round) + e + k;
+ e = d;
+ d = c;
+ c = BitOperations.RotateLeft(b, 30);
+ b = a;
+ a = next;
+ }
+
+ return FromDwords(d, c, b, a);
+ }
+
+ public static UInt128 Sha1Nexte(UInt128 source1, UInt128 source2)
+ {
+ var e = BitOperations.RotateLeft(Dword(source1, 3), 30);
+ return FromDwords(Dword(source2, 0), Dword(source2, 1), Dword(source2, 2), Dword(source2, 3) + e);
+ }
+
+ public static UInt128 Sha1Msg1(UInt128 source1, UInt128 source2)
+ {
+ var w0 = Dword(source1, 3);
+ var w1 = Dword(source1, 2);
+ var w2 = Dword(source1, 1);
+ var w3 = Dword(source1, 0);
+ var w4 = Dword(source2, 3);
+ var w5 = Dword(source2, 2);
+ return FromDwords(w5 ^ w3, w4 ^ w2, w3 ^ w1, w2 ^ w0);
+ }
+
+ public static UInt128 Sha1Msg2(UInt128 source1, UInt128 source2)
+ {
+ var w13 = Dword(source2, 2);
+ var w14 = Dword(source2, 1);
+ var w15 = Dword(source2, 0);
+ var w16 = BitOperations.RotateLeft(Dword(source1, 3) ^ w13, 1);
+ var w17 = BitOperations.RotateLeft(Dword(source1, 2) ^ w14, 1);
+ var w18 = BitOperations.RotateLeft(Dword(source1, 1) ^ w15, 1);
+ var w19 = BitOperations.RotateLeft(Dword(source1, 0) ^ w16, 1);
+ return FromDwords(w19, w18, w17, w16);
+ }
+
+ // SHA256RNDS2 takes its two round-constant-plus-message dwords from XMM0[63:0].
+ public static UInt128 Sha256Rnds2(UInt128 source1, UInt128 source2, UInt128 xmm0)
+ {
+ var a = Dword(source2, 3);
+ var b = Dword(source2, 2);
+ var c = Dword(source1, 3);
+ var d = Dword(source1, 2);
+ var e = Dword(source2, 1);
+ var f = Dword(source2, 0);
+ var g = Dword(source1, 1);
+ var h = Dword(source1, 0);
+
+ for (var round = 0; round < 2; round++)
+ {
+ var t1 = Ch(e, f, g) + BigSigma1(e) + Dword(xmm0, round) + h;
+ var t2 = Maj(a, b, c) + BigSigma0(a);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2;
+ }
+
+ return FromDwords(f, e, b, a);
+ }
+
+ public static UInt128 Sha256Msg1(UInt128 source1, UInt128 source2)
+ {
+ var w4 = Dword(source2, 0);
+ var w3 = Dword(source1, 3);
+ var w2 = Dword(source1, 2);
+ var w1 = Dword(source1, 1);
+ var w0 = Dword(source1, 0);
+ return FromDwords(w0 + SmallSigma0(w1), w1 + SmallSigma0(w2), w2 + SmallSigma0(w3), w3 + SmallSigma0(w4));
+ }
+
+ public static UInt128 Sha256Msg2(UInt128 source1, UInt128 source2)
+ {
+ var w14 = Dword(source2, 2);
+ var w15 = Dword(source2, 3);
+ var w16 = Dword(source1, 0) + SmallSigma1(w14);
+ var w17 = Dword(source1, 1) + SmallSigma1(w15);
+ var w18 = Dword(source1, 2) + SmallSigma1(w16);
+ var w19 = Dword(source1, 3) + SmallSigma1(w17);
+ return FromDwords(w16, w17, w18, w19);
+ }
+
+ public static uint Dword(UInt128 value, int index) => (uint)(value >> (32 * index));
+
+ public static UInt128 FromDwords(uint dword0, uint dword1, uint dword2, uint dword3) =>
+ ((UInt128)dword3 << 96) | ((UInt128)dword2 << 64) | ((UInt128)dword1 << 32) | dword0;
+
+ private static uint Sha1Function(int function, uint b, uint c, uint d) => (function & 3) switch
+ {
+ 0 => (b & c) ^ (~b & d),
+ 2 => (b & c) ^ (b & d) ^ (c & d),
+ _ => b ^ c ^ d,
+ };
+
+ private static uint Ch(uint e, uint f, uint g) => (e & f) ^ (~e & g);
+
+ private static uint Maj(uint a, uint b, uint c) => (a & b) ^ (a & c) ^ (b & c);
+
+ private static uint BigSigma0(uint x) =>
+ BitOperations.RotateRight(x, 2) ^ BitOperations.RotateRight(x, 13) ^ BitOperations.RotateRight(x, 22);
+
+ private static uint BigSigma1(uint x) =>
+ BitOperations.RotateRight(x, 6) ^ BitOperations.RotateRight(x, 11) ^ BitOperations.RotateRight(x, 25);
+
+ private static uint SmallSigma0(uint x) =>
+ BitOperations.RotateRight(x, 7) ^ BitOperations.RotateRight(x, 18) ^ (x >> 3);
+
+ private static uint SmallSigma1(uint x) =>
+ BitOperations.RotateRight(x, 17) ^ BitOperations.RotateRight(x, 19) ^ (x >> 10);
+}
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
index d4a1372..17ebcfd 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs
@@ -157,6 +157,11 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
+ if (exceptionCode == StatusIllegalInstruction &&
+ TryRecoverShaInstruction(contextRecord, rip))
+ {
+ return -1;
+ }
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.ShaCompat.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.ShaCompat.cs
new file mode 100644
index 0000000..dd8e8ee
--- /dev/null
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.ShaCompat.cs
@@ -0,0 +1,107 @@
+// 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;
+
+// The PS5 CPU has the SHA extensions and titles use them without a CPUID check.
+// Rosetta 2 does not translate them, so they raise #UD on Apple Silicon hosts.
+public sealed partial class DirectExecutionBackend
+{
+ private static int _shaSoftwareFallbackAnnounced;
+ private static long _shaInstructionsEmulated;
+
+ private unsafe bool TryRecoverShaInstruction(void* contextRecord, ulong rip)
+ {
+ if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
+ !TryReadFaultingInstruction(rip, out var instruction))
+ {
+ return false;
+ }
+
+ switch (instruction.Mnemonic)
+ {
+ case Mnemonic.Sha1rnds4:
+ case Mnemonic.Sha1nexte:
+ case Mnemonic.Sha1msg1:
+ case Mnemonic.Sha1msg2:
+ case Mnemonic.Sha256rnds2:
+ case Mnemonic.Sha256msg1:
+ case Mnemonic.Sha256msg2:
+ break;
+ default:
+ return false;
+ }
+
+ if (instruction.GetOpKind(0) != OpKind.Register ||
+ !TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset) ||
+ !TryReadXmmOperand(contextRecord, in instruction, 1, out var source2))
+ {
+ return false;
+ }
+
+ var source1 = ReadCtxXmm(contextRecord, destOffset);
+ var result = instruction.Mnemonic switch
+ {
+ Mnemonic.Sha1rnds4 => ShaInstructionEmulator.Sha1Rnds4(source1, source2, instruction.Immediate8),
+ Mnemonic.Sha1nexte => ShaInstructionEmulator.Sha1Nexte(source1, source2),
+ Mnemonic.Sha1msg1 => ShaInstructionEmulator.Sha1Msg1(source1, source2),
+ Mnemonic.Sha1msg2 => ShaInstructionEmulator.Sha1Msg2(source1, source2),
+ Mnemonic.Sha256rnds2 => ShaInstructionEmulator.Sha256Rnds2(
+ source1, source2, ReadCtxXmm(contextRecord, Win64ContextXmm0Offset)),
+ Mnemonic.Sha256msg1 => ShaInstructionEmulator.Sha256Msg1(source1, source2),
+ _ => ShaInstructionEmulator.Sha256Msg2(source1, source2),
+ };
+
+ WriteCtxU64(contextRecord, destOffset, (ulong)result);
+ WriteCtxU64(contextRecord, destOffset + 8, (ulong)(result >> 64));
+ WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
+
+ Interlocked.Increment(ref _shaInstructionsEmulated);
+ if (Interlocked.Exchange(ref _shaSoftwareFallbackAnnounced, 1) == 0)
+ {
+ Console.Error.WriteLine(
+ "[LOADER][INFO] Host lacks the SHA extensions used by the guest; " +
+ "emulating those instructions in software.");
+ }
+
+ return true;
+ }
+
+ private unsafe bool TryReadXmmOperand(void* contextRecord, in Instruction instruction, int operandIndex, out UInt128 value)
+ {
+ value = 0;
+ switch (instruction.GetOpKind(operandIndex))
+ {
+ case OpKind.Register:
+ if (!TryGetXmmOffset(instruction.GetOpRegister(operandIndex), out var offset))
+ {
+ return false;
+ }
+
+ value = ReadCtxXmm(contextRecord, offset);
+ return true;
+
+ case OpKind.Memory:
+ var buffer = new byte[16];
+ if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address) ||
+ !TryReadHostBytes(address, buffer))
+ {
+ return false;
+ }
+
+ value = BinaryPrimitives.ReadUInt128LittleEndian(buffer);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private static unsafe UInt128 ReadCtxXmm(void* contextRecord, int offset) =>
+ new(ReadCtxU64(contextRecord, offset + 8), ReadCtxU64(contextRecord, offset));
+}
diff --git a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
index 2110b09..e48c32e 100644
--- a/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
+++ b/src/SharpEmu.Core/Loader/GuestRedZonePatcher.cs
@@ -39,7 +39,8 @@ internal static class GuestRedZonePatcher
var protectRedZone = !string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RED_ZONE_PATCH"), "1", StringComparison.Ordinal);
var splitVectorStores = RosettaVectorStorePatch.IsRequired;
- if (!protectRedZone && !splitVectorStores)
+ var rewriteSha = ShaInstructionRewrite.IsRequired;
+ if (!protectRedZone && !splitVectorStores && !rewriteSha)
{
return default;
}
@@ -52,7 +53,8 @@ internal static class GuestRedZonePatcher
return default;
}
- var sites = CollectPatchSites(memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, out var scan);
+ var sites = CollectPatchSites(
+ memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
if (sites.Count == 0)
{
Console.Error.WriteLine(
@@ -61,7 +63,8 @@ internal static class GuestRedZonePatcher
}
var requiredBytes = AlignUp(
- checked((ulong)sites.Count * (splitVectorStores ? 80UL : EstimatedTrampolineBytesPerSite) + PageSize),
+ checked((ulong)sites.Count * (splitVectorStores ? 80UL : EstimatedTrampolineBytesPerSite) +
+ (ulong)scan.ShaInstructionCount * ShaInstructionRewrite.MaximumExpansionBytes + PageSize),
PageSize);
if (!TryAllocateTrampolines(
physicalMemory,
@@ -82,7 +85,7 @@ internal static class GuestRedZonePatcher
var failed = 0;
foreach (var site in sites)
{
- if (!TryPatchSite(memory, site, ref trampolineCursor, trampolineEnd, splitVectorStores))
+ if (!TryPatchSite(memory, site, ref trampolineCursor, trampolineEnd, splitVectorStores, rewriteSha))
{
failed++;
continue;
@@ -101,7 +104,7 @@ internal static class GuestRedZonePatcher
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone patch: functions={result.Functions} red_zone={result.RedZoneFunctions} " +
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
- $"rosetta_vector_stores={result.VectorStoreCount} " +
+ $"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
return result;
}
@@ -113,6 +116,7 @@ internal static class GuestRedZonePatcher
IReadOnlyList functionStarts,
bool protectRedZone,
bool splitVectorStores,
+ bool rewriteSha,
out PatchResult result)
{
var sites = new List();
@@ -120,6 +124,7 @@ internal static class GuestRedZonePatcher
var redZoneFunctionCount = 0;
var instructionCount = 0;
var vectorStoreCount = 0;
+ var shaInstructionCount = 0;
foreach (var header in programHeaders)
{
@@ -169,7 +174,7 @@ internal static class GuestRedZonePatcher
functionCount++;
instructionCount += decoded.Count;
var usesRedZone = protectRedZone && decoded.Any(static entry => UsesRedZone(entry.Instruction));
- if (!usesRedZone && !splitVectorStores)
+ if (!usesRedZone && !splitVectorStores && !rewriteSha)
{
continue;
}
@@ -182,8 +187,10 @@ internal static class GuestRedZonePatcher
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
{
var instruction = decoded[instructionIndex].Instruction;
- if ((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
- !IsFaultableGuestMemoryInstruction(instruction) ||
+ var isShaSite = rewriteSha && ShaInstructionRewrite.CanRewrite(instruction);
+ if ((!isShaSite &&
+ ((!usesRedZone && !(splitVectorStores && RosettaVectorStorePatch.RequiresStoreSplit(instruction))) ||
+ !IsFaultableGuestMemoryInstruction(instruction))) ||
!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span))
{
continue;
@@ -194,6 +201,10 @@ internal static class GuestRedZonePatcher
{
vectorStoreCount += span.Instructions.Count(static instruction => RosettaVectorStorePatch.RequiresStoreSplit(instruction));
}
+ if (rewriteSha)
+ {
+ shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
+ }
instructionIndex += span.Instructions.Count - 1;
}
}
@@ -206,6 +217,7 @@ internal static class GuestRedZonePatcher
Instructions = instructionCount,
CandidateSites = sites.Count,
VectorStoreCount = vectorStoreCount,
+ ShaInstructionCount = shaInstructionCount,
};
return sites;
}
@@ -378,11 +390,16 @@ internal static class GuestRedZonePatcher
PatchSite site,
ref ulong trampolineCursor,
ulong trampolineEnd,
- bool splitVectorStores)
+ bool splitVectorStores,
+ bool rewriteSha)
{
var writer = new ListCodeWriter();
var relocatedAddress = trampolineCursor + 5;
var instructions = splitVectorStores ? RosettaVectorStorePatch.SplitVectorStores(site.Instructions) : site.Instructions;
+ if (rewriteSha)
+ {
+ instructions = ShaInstructionRewrite.Expand(instructions);
+ }
var block = new InstructionBlock(writer, instructions, relocatedAddress);
if (!BlockEncoder.TryEncode(64, block, out _, out _, BlockEncoderOptions.None))
{
@@ -557,6 +574,8 @@ internal static class GuestRedZonePatcher
public int VectorStoreCount { get; init; }
+ public int ShaInstructionCount { get; init; }
+
public int PatchedSites { get; init; }
public int FailedSites { get; init; }
diff --git a/src/SharpEmu.Core/Loader/ShaInstructionRewrite.cs b/src/SharpEmu.Core/Loader/ShaInstructionRewrite.cs
new file mode 100644
index 0000000..6c82226
--- /dev/null
+++ b/src/SharpEmu.Core/Loader/ShaInstructionRewrite.cs
@@ -0,0 +1,405 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Runtime.Intrinsics.X86;
+using Iced.Intel;
+using static Iced.Intel.AssemblerRegisters;
+
+namespace SharpEmu.Core.Loader;
+
+// Rewrite SHA extension instructions into general-register code when the host CPU lacks them.
+// Rosetta 2 and some x86 hosts raise #UD for them, and a signal per instruction is far too slow
+// for titles that hash whole files. The expansion keeps every register except the destination
+// XMM register and keeps the flags. It runs inside the trampoline, after the red zone is skipped.
+internal static class ShaInstructionRewrite
+{
+ // pushfq plus the saved general registers.
+ private const int SavedBytes = 8 * 13;
+
+ // Scratch layout: source 1 (and result), source 2, XMM0 (SHA256RNDS2 round inputs).
+ private const int Source1 = 0;
+ private const int Source2 = 16;
+ private const int Xmm0Copy = 32;
+ private const int ScratchBytes = 48;
+
+ private static readonly Register[] SavedRegisters =
+ {
+ Register.RAX, Register.RCX, Register.RDX, Register.RBX, Register.RSI, Register.RDI,
+ Register.R8, Register.R9, Register.R10, Register.R11, Register.R12, Register.R13,
+ };
+
+ internal static bool IsRequired { get; } = HostLacksSha() &&
+ !string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_SHA_REWRITE"), "1", StringComparison.Ordinal);
+
+ // Bytes the expansion of one instruction needs at most in the trampoline.
+ internal const int MaximumExpansionBytes = 1024;
+
+ private static bool HostLacksSha()
+ {
+ if (!X86Base.IsSupported)
+ {
+ return false;
+ }
+
+ var (_, ebx, _, _) = X86Base.CpuId(7, 0);
+ return (ebx & (1 << 29)) == 0;
+ }
+
+ internal static bool IsShaInstruction(in Instruction instruction) => instruction.Mnemonic is
+ Mnemonic.Sha1rnds4 or Mnemonic.Sha1nexte or Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
+ Mnemonic.Sha256rnds2 or Mnemonic.Sha256msg1 or Mnemonic.Sha256msg2;
+
+ // A memory source with a segment prefix or an RIP-relative address cannot be rebuilt here.
+ internal static bool CanRewrite(in Instruction instruction)
+ {
+ if (!IsShaInstruction(instruction) || instruction.Op0Kind != OpKind.Register)
+ {
+ return false;
+ }
+
+ return instruction.Op1Kind == OpKind.Register ||
+ (instruction.Op1Kind == OpKind.Memory &&
+ instruction.SegmentPrefix == Register.None &&
+ !instruction.IsIPRelativeMemoryOperand);
+ }
+
+ internal static IList Expand(IList instructions)
+ {
+ var expanded = new List(instructions.Count);
+ foreach (var instruction in instructions)
+ {
+ if (!CanRewrite(instruction))
+ {
+ expanded.Add(instruction);
+ continue;
+ }
+
+ var assembler = new Assembler(64);
+ EmitExpansion(assembler, instruction);
+ var first = true;
+ ulong generatedIp = 0xFFFF_0000_0000_0000UL + ((instruction.IP & 0xFFFF_FFFF) << 12);
+ foreach (var generated in assembler.Instructions)
+ {
+ var copy = generated;
+ // Keep branch targets at the first generated instruction.
+ copy.IP = first ? instruction.IP : generatedIp++;
+ first = false;
+ expanded.Add(copy);
+ }
+ }
+
+ return expanded;
+ }
+
+ internal static void EmitExpansion(Assembler a, in Instruction instruction)
+ {
+ var destination = ToXmm(instruction.Op0Register);
+ a.pushfq();
+ foreach (var register in SavedRegisters)
+ {
+ a.push(ToGpr64(register));
+ }
+
+ a.sub(rsp, ScratchBytes);
+ if (instruction.Op1Kind == OpKind.Memory)
+ {
+ // LEA reads the guest registers before any of them change.
+ a.lea(rax, RebaseMemory(instruction));
+ a.mov(rcx, __qword_ptr[rax]);
+ a.mov(__qword_ptr[rsp + Source2], rcx);
+ a.mov(rcx, __qword_ptr[rax + 8]);
+ a.mov(__qword_ptr[rsp + Source2 + 8], rcx);
+ }
+ else
+ {
+ a.movdqu(__xmmword_ptr[rsp + Source2], ToXmm(instruction.Op1Register));
+ }
+
+ a.movdqu(__xmmword_ptr[rsp + Source1], destination);
+ if (instruction.Mnemonic == Mnemonic.Sha256rnds2)
+ {
+ a.movdqu(__xmmword_ptr[rsp + Xmm0Copy], xmm0);
+ }
+
+ switch (instruction.Mnemonic)
+ {
+ case Mnemonic.Sha1rnds4: EmitSha1Rnds4(a, instruction.Immediate8 & 3); break;
+ case Mnemonic.Sha1nexte: EmitSha1Nexte(a); break;
+ case Mnemonic.Sha1msg1: EmitSha1Msg1(a); break;
+ case Mnemonic.Sha1msg2: EmitSha1Msg2(a); break;
+ case Mnemonic.Sha256rnds2: EmitSha256Rnds2(a); break;
+ case Mnemonic.Sha256msg1: EmitSha256Msg1(a); break;
+ default: EmitSha256Msg2(a); break;
+ }
+
+ a.movdqu(destination, __xmmword_ptr[rsp + Source1]);
+ a.add(rsp, ScratchBytes);
+ for (var index = SavedRegisters.Length - 1; index >= 0; index--)
+ {
+ a.pop(ToGpr64(SavedRegisters[index]));
+ }
+
+ a.popfq();
+ }
+
+ // The guest stack pointer is above the scratch area, the saved registers and the red zone.
+ private static AssemblerMemoryOperand RebaseMemory(in Instruction instruction)
+ {
+ var displacement = unchecked((long)instruction.MemoryDisplacement64);
+ if (instruction.MemoryBase == Register.RSP)
+ {
+ displacement += ScratchBytes + SavedBytes + 128;
+ }
+
+ var baseRegister = instruction.MemoryBase == Register.None ? default : ToGpr64(instruction.MemoryBase);
+ var memory = instruction.MemoryBase == Register.None ? __[displacement] : __[baseRegister + displacement];
+ if (instruction.MemoryIndex != Register.None)
+ {
+ memory = instruction.MemoryBase == Register.None
+ ? __[ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement]
+ : __[baseRegister + ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement];
+ }
+
+ return memory;
+ }
+
+ private static AssemblerMemoryOperand Dword(int slot, int index) => __dword_ptr[rsp + slot + 4 * index];
+
+ private static void EmitSha1Rnds4(Assembler a, int function)
+ {
+ var k = function switch
+ {
+ 0 => 0x5A827999,
+ 1 => 0x6ED9EBA1,
+ 2 => unchecked((int)0x8F1BBCDC),
+ _ => unchecked((int)0xCA62C1D6),
+ };
+
+ a.mov(eax, Dword(Source1, 3));
+ a.mov(ebx, Dword(Source1, 2));
+ a.mov(ecx, Dword(Source1, 1));
+ a.mov(edx, Dword(Source1, 0));
+ a.xor(esi, esi);
+ for (var round = 0; round < 4; round++)
+ {
+ switch (function)
+ {
+ case 0:
+ // (b & c) ^ (~b & d) == d ^ (b & (c ^ d))
+ a.mov(edi, ecx);
+ a.xor(edi, edx);
+ a.and(edi, ebx);
+ a.xor(edi, edx);
+ break;
+ case 2:
+ // (b & c) | (d & (b | c)) is the majority of b, c and d.
+ a.mov(edi, ebx);
+ a.or(edi, ecx);
+ a.and(edi, edx);
+ a.mov(r8d, ebx);
+ a.and(r8d, ecx);
+ a.or(edi, r8d);
+ break;
+ default:
+ a.mov(edi, ebx);
+ a.xor(edi, ecx);
+ a.xor(edi, edx);
+ break;
+ }
+
+ a.add(edi, Dword(Source2, 3 - round));
+ a.add(edi, esi);
+ a.add(edi, k);
+ a.mov(r8d, eax);
+ a.rol(r8d, (byte)5);
+ a.add(edi, r8d);
+ a.mov(esi, edx);
+ a.mov(edx, ecx);
+ a.mov(ecx, ebx);
+ a.rol(ecx, (byte)30);
+ a.mov(ebx, eax);
+ a.mov(eax, edi);
+ }
+
+ a.mov(Dword(Source1, 3), eax);
+ a.mov(Dword(Source1, 2), ebx);
+ a.mov(Dword(Source1, 1), ecx);
+ a.mov(Dword(Source1, 0), edx);
+ }
+
+ private static void EmitSha1Nexte(Assembler a)
+ {
+ a.mov(eax, Dword(Source1, 3));
+ a.rol(eax, (byte)30);
+ a.add(eax, Dword(Source2, 3));
+ a.mov(Dword(Source1, 3), eax);
+ for (var index = 0; index < 3; index++)
+ {
+ a.mov(ecx, Dword(Source2, index));
+ a.mov(Dword(Source1, index), ecx);
+ }
+ }
+
+ private static void EmitSha1Msg1(Assembler a)
+ {
+ // w0..w3 from source 1 (w0 in dword 3), w4 and w5 from source 2.
+ a.mov(eax, Dword(Source1, 3));
+ a.mov(ebx, Dword(Source1, 2));
+ a.mov(ecx, Dword(Source1, 1));
+ a.mov(edx, Dword(Source1, 0));
+ a.mov(esi, Dword(Source2, 3));
+ a.mov(edi, Dword(Source2, 2));
+ a.xor(eax, ecx);
+ a.xor(ebx, edx);
+ a.xor(ecx, esi);
+ a.xor(edx, edi);
+ a.mov(Dword(Source1, 3), eax);
+ a.mov(Dword(Source1, 2), ebx);
+ a.mov(Dword(Source1, 1), ecx);
+ a.mov(Dword(Source1, 0), edx);
+ }
+
+ private static void EmitSha1Msg2(Assembler a)
+ {
+ a.mov(eax, Dword(Source1, 3));
+ a.xor(eax, Dword(Source2, 2));
+ a.rol(eax, (byte)1);
+ a.mov(ebx, Dword(Source1, 2));
+ a.xor(ebx, Dword(Source2, 1));
+ a.rol(ebx, (byte)1);
+ a.mov(ecx, Dword(Source1, 1));
+ a.xor(ecx, Dword(Source2, 0));
+ a.rol(ecx, (byte)1);
+ a.mov(edx, Dword(Source1, 0));
+ a.xor(edx, eax);
+ a.rol(edx, (byte)1);
+ a.mov(Dword(Source1, 3), eax);
+ a.mov(Dword(Source1, 2), ebx);
+ a.mov(Dword(Source1, 1), ecx);
+ a.mov(Dword(Source1, 0), edx);
+ }
+
+ private static void EmitSha256Rnds2(Assembler a)
+ {
+ // a..h in eax, ebx, ecx, edx, esi, edi, r8d, r9d.
+ a.mov(eax, Dword(Source2, 3));
+ a.mov(ebx, Dword(Source2, 2));
+ a.mov(ecx, Dword(Source1, 3));
+ a.mov(edx, Dword(Source1, 2));
+ a.mov(esi, Dword(Source2, 1));
+ a.mov(edi, Dword(Source2, 0));
+ a.mov(r8d, Dword(Source1, 1));
+ a.mov(r9d, Dword(Source1, 0));
+ for (var round = 0; round < 2; round++)
+ {
+ // r10d = h + Sigma1(e) + Ch(e, f, g) + wk
+ EmitRotateXor(a, r10d, esi, r11d, 6, 11, 25);
+ a.add(r10d, r9d);
+ a.add(r10d, Dword(Xmm0Copy, round));
+ a.mov(r11d, edi);
+ a.xor(r11d, r8d);
+ a.and(r11d, esi);
+ a.xor(r11d, r8d);
+ a.add(r10d, r11d);
+
+ // r11d = Sigma0(a) + Maj(a, b, c)
+ EmitRotateXor(a, r11d, eax, r12d, 2, 13, 22);
+ a.mov(r12d, eax);
+ a.or(r12d, ebx);
+ a.and(r12d, ecx);
+ a.mov(r13d, eax);
+ a.and(r13d, ebx);
+ a.or(r12d, r13d);
+ a.add(r11d, r12d);
+
+ a.mov(r9d, r8d);
+ a.mov(r8d, edi);
+ a.mov(edi, esi);
+ a.mov(esi, edx);
+ a.add(esi, r10d);
+ a.mov(edx, ecx);
+ a.mov(ecx, ebx);
+ a.mov(ebx, eax);
+ a.mov(eax, r10d);
+ a.add(eax, r11d);
+ }
+
+ a.mov(Dword(Source1, 3), eax);
+ a.mov(Dword(Source1, 2), ebx);
+ a.mov(Dword(Source1, 1), esi);
+ a.mov(Dword(Source1, 0), edi);
+ }
+
+ private static void EmitSha256Msg1(Assembler a)
+ {
+ // w0..w3 in edx, ecx, ebx, eax; w4 in esi. Results go to r9d..r12d.
+ a.mov(eax, Dword(Source1, 3));
+ a.mov(ebx, Dword(Source1, 2));
+ a.mov(ecx, Dword(Source1, 1));
+ a.mov(edx, Dword(Source1, 0));
+ a.mov(esi, Dword(Source2, 0));
+ EmitSmallSigma(a, r9d, esi, r8d, 7, 18, 3);
+ a.add(r9d, eax);
+ EmitSmallSigma(a, r10d, eax, r8d, 7, 18, 3);
+ a.add(r10d, ebx);
+ EmitSmallSigma(a, r11d, ebx, r8d, 7, 18, 3);
+ a.add(r11d, ecx);
+ EmitSmallSigma(a, r12d, ecx, r8d, 7, 18, 3);
+ a.add(r12d, edx);
+ a.mov(Dword(Source1, 3), r9d);
+ a.mov(Dword(Source1, 2), r10d);
+ a.mov(Dword(Source1, 1), r11d);
+ a.mov(Dword(Source1, 0), r12d);
+ }
+
+ private static void EmitSha256Msg2(Assembler a)
+ {
+ // w16 = s1[0] + sigma1(w14); w17 = s1[1] + sigma1(w15); w18 and w19 use w16 and w17.
+ a.mov(esi, Dword(Source2, 2));
+ EmitSmallSigma(a, eax, esi, r8d, 17, 19, 10);
+ a.add(eax, Dword(Source1, 0));
+ a.mov(esi, Dword(Source2, 3));
+ EmitSmallSigma(a, ebx, esi, r8d, 17, 19, 10);
+ a.add(ebx, Dword(Source1, 1));
+ EmitSmallSigma(a, ecx, eax, r8d, 17, 19, 10);
+ a.add(ecx, Dword(Source1, 2));
+ EmitSmallSigma(a, edx, ebx, r8d, 17, 19, 10);
+ a.add(edx, Dword(Source1, 3));
+ a.mov(Dword(Source1, 0), eax);
+ a.mov(Dword(Source1, 1), ebx);
+ a.mov(Dword(Source1, 2), ecx);
+ a.mov(Dword(Source1, 3), edx);
+ }
+
+ // result = ror(x, r1) ^ ror(x, r2) ^ ror(x, r3)
+ private static void EmitRotateXor(
+ Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int r3)
+ {
+ a.mov(result, x);
+ a.ror(result, (byte)r1);
+ a.mov(temp, x);
+ a.ror(temp, (byte)r2);
+ a.xor(result, temp);
+ a.mov(temp, x);
+ a.ror(temp, (byte)r3);
+ a.xor(result, temp);
+ }
+
+ // result = ror(x, r1) ^ ror(x, r2) ^ (x >> shift)
+ private static void EmitSmallSigma(
+ Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int shift)
+ {
+ a.mov(result, x);
+ a.ror(result, (byte)r1);
+ a.mov(temp, x);
+ a.ror(temp, (byte)r2);
+ a.xor(result, temp);
+ a.mov(temp, x);
+ a.shr(temp, (byte)shift);
+ a.xor(result, temp);
+ }
+
+ private static AssemblerRegisterXMM ToXmm(Register register) => new(register);
+
+ private static AssemblerRegister64 ToGpr64(Register register) => new(register.GetFullRegister());
+}
diff --git a/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionEmulatorTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionEmulatorTests.cs
new file mode 100644
index 0000000..4c29617
--- /dev/null
+++ b/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionEmulatorTests.cs
@@ -0,0 +1,188 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Buffers.Binary;
+using System.Security.Cryptography;
+using SharpEmu.Core.Cpu.Emulation;
+using Xunit;
+using static SharpEmu.Core.Cpu.Emulation.ShaInstructionEmulator;
+
+namespace SharpEmu.Libs.Tests.Cpu;
+
+// The hosts that need this fallback cannot run SHA-NI, so each test builds a complete hash
+// from the emulated instructions in the order SHA-NI code uses them, and compares the digest
+// with the .NET implementation. A wrong lane order or round step changes the digest.
+public sealed class ShaInstructionEmulatorTests
+{
+ public static TheoryData MessageLengths => new() { 0, 3, 55, 56, 64, 119, 1000 };
+
+ [Theory]
+ [MemberData(nameof(MessageLengths))]
+ public void Sha1Instructions_ProduceTheSha1Digest(int length)
+ {
+ var message = CreateMessage(length);
+ var state = new uint[] { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 };
+ foreach (var block in PadMessage(message))
+ {
+ Sha1Block(state, block);
+ }
+
+ var digest = new byte[20];
+ for (var i = 0; i < state.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(digest.AsSpan(4 * i), state[i]);
+ }
+
+ Assert.Equal(SHA1.HashData(message), digest);
+ }
+
+ [Theory]
+ [MemberData(nameof(MessageLengths))]
+ public void Sha256Instructions_ProduceTheSha256Digest(int length)
+ {
+ var message = CreateMessage(length);
+ var state = new uint[]
+ {
+ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
+ };
+ foreach (var block in PadMessage(message))
+ {
+ Sha256Block(state, block);
+ }
+
+ var digest = new byte[32];
+ for (var i = 0; i < state.Length; i++)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(digest.AsSpan(4 * i), state[i]);
+ }
+
+ Assert.Equal(SHA256.HashData(message), digest);
+ }
+
+ [Fact]
+ public void Sha1Nexte_AddsRotatedEToTheTopDwordOnly()
+ {
+ var result = Sha1Nexte(FromDwords(0, 0, 0, 0x8000_0001), FromDwords(1, 2, 3, 4));
+
+ Assert.Equal(FromDwords(1, 2, 3, 4 + 0x6000_0000), result);
+ }
+
+ // SHA1RNDS4: ABCD in source1 (A in dword 3), W0+E..W3 in source2 (W0+E in dword 3).
+ private static void Sha1Block(uint[] state, byte[] block)
+ {
+ var messages = new UInt128[20];
+ for (var i = 0; i < 4; i++)
+ {
+ messages[i] = FromDwords(
+ ReadWord(block, 4 * i + 3), ReadWord(block, 4 * i + 2), ReadWord(block, 4 * i + 1), ReadWord(block, 4 * i));
+ }
+
+ for (var i = 4; i < 20; i++)
+ {
+ messages[i] = Sha1Msg2(Sha1Msg1(messages[i - 4], messages[i - 3]) ^ messages[i - 2], messages[i - 1]);
+ }
+
+ var abcd = FromDwords(state[3], state[2], state[1], state[0]);
+ var abcdSave = abcd;
+ var eSave = FromDwords(0, 0, 0, state[4]);
+ var e = Add(eSave, messages[0]);
+ for (var group = 0; group < 20; group++)
+ {
+ var previous = abcd;
+ abcd = Sha1Rnds4(abcd, e, group / 5);
+ e = Sha1Nexte(previous, group < 19 ? messages[group + 1] : eSave);
+ }
+
+ abcd = Add(abcd, abcdSave);
+ state[0] = Dword(abcd, 3);
+ state[1] = Dword(abcd, 2);
+ state[2] = Dword(abcd, 1);
+ state[3] = Dword(abcd, 0);
+ state[4] = Dword(e, 3);
+ }
+
+ // SHA256RNDS2 keeps the state as ABEF and CDGH with the first letter in dword 3.
+ private static void Sha256Block(uint[] state, byte[] block)
+ {
+ var messages = new UInt128[16];
+ for (var i = 0; i < 4; i++)
+ {
+ messages[i] = FromDwords(
+ ReadWord(block, 4 * i), ReadWord(block, 4 * i + 1), ReadWord(block, 4 * i + 2), ReadWord(block, 4 * i + 3));
+ }
+
+ for (var i = 4; i < 16; i++)
+ {
+ var w7 = FromDwords(
+ Dword(messages[i - 2], 1), Dword(messages[i - 2], 2), Dword(messages[i - 2], 3), Dword(messages[i - 1], 0));
+ messages[i] = Sha256Msg2(Add(Sha256Msg1(messages[i - 4], messages[i - 3]), w7), messages[i - 1]);
+ }
+
+ var abef = FromDwords(state[5], state[4], state[1], state[0]);
+ var cdgh = FromDwords(state[7], state[6], state[3], state[2]);
+ var abefSave = abef;
+ var cdghSave = cdgh;
+ for (var group = 0; group < 16; group++)
+ {
+ var wk = Add(messages[group], FromDwords(
+ RoundConstants[4 * group], RoundConstants[4 * group + 1],
+ RoundConstants[4 * group + 2], RoundConstants[4 * group + 3]));
+ cdgh = Sha256Rnds2(cdgh, abef, wk);
+ abef = Sha256Rnds2(abef, cdgh, wk >> 64);
+ }
+
+ abef = Add(abef, abefSave);
+ cdgh = Add(cdgh, cdghSave);
+ state[0] = Dword(abef, 3);
+ state[1] = Dword(abef, 2);
+ state[4] = Dword(abef, 1);
+ state[5] = Dword(abef, 0);
+ state[2] = Dword(cdgh, 3);
+ state[3] = Dword(cdgh, 2);
+ state[6] = Dword(cdgh, 1);
+ state[7] = Dword(cdgh, 0);
+ }
+
+ private static UInt128 Add(UInt128 left, UInt128 right) => FromDwords(
+ Dword(left, 0) + Dword(right, 0), Dword(left, 1) + Dword(right, 1),
+ Dword(left, 2) + Dword(right, 2), Dword(left, 3) + Dword(right, 3));
+
+ private static uint ReadWord(byte[] block, int index) =>
+ BinaryPrimitives.ReadUInt32BigEndian(block.AsSpan(4 * index));
+
+ private static byte[] CreateMessage(int length)
+ {
+ var message = new byte[length];
+ for (var i = 0; i < length; i++)
+ {
+ message[i] = (byte)(i * 31 + 7);
+ }
+
+ return message;
+ }
+
+ private static IEnumerable PadMessage(byte[] message)
+ {
+ var paddedLength = (message.Length + 8) / 64 * 64 + 64;
+ var padded = new byte[paddedLength];
+ message.CopyTo(padded, 0);
+ padded[message.Length] = 0x80;
+ BinaryPrimitives.WriteUInt64BigEndian(padded.AsSpan(paddedLength - 8), (ulong)message.Length * 8);
+ for (var offset = 0; offset < paddedLength; offset += 64)
+ {
+ yield return padded[offset..(offset + 64)];
+ }
+ }
+
+ private static readonly uint[] RoundConstants =
+ {
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
+ };
+}
diff --git a/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionRewriteTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionRewriteTests.cs
new file mode 100644
index 0000000..9980226
--- /dev/null
+++ b/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionRewriteTests.cs
@@ -0,0 +1,289 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using System.Buffers.Binary;
+using Iced.Intel;
+using SharpEmu.Core.Cpu.Emulation;
+using SharpEmu.Core.Loader;
+using Xunit;
+
+namespace SharpEmu.Libs.Tests.Cpu;
+
+// The rewritten code must run on hosts without SHA, so these tests execute it with a small
+// interpreter for the few instructions the expansion uses, and compare the result with
+// ShaInstructionEmulator. They also check that nothing else in the guest state changes.
+public sealed class ShaInstructionRewriteTests
+{
+ private const ulong StackTop = 0x10000;
+ private const ulong DataAddress = 0x20000;
+
+ public static TheoryData Cases()
+ {
+ var data = new TheoryData();
+ foreach (var code in new[]
+ {
+ Code.Sha1rnds4_xmm_xmmm128_imm8, Code.Sha1nexte_xmm_xmmm128, Code.Sha1msg1_xmm_xmmm128,
+ Code.Sha1msg2_xmm_xmmm128, Code.Sha256rnds2_xmm_xmmm128, Code.Sha256msg1_xmm_xmmm128,
+ Code.Sha256msg2_xmm_xmmm128,
+ })
+ {
+ data.Add(code, false);
+ data.Add(code, true);
+ }
+
+ return data;
+ }
+
+ [Theory]
+ [MemberData(nameof(Cases))]
+ public void Expansion_MatchesTheInstructionAndKeepsOtherState(Code code, bool memorySource)
+ {
+ var random = new System.Random((int)code * 2 + (memorySource ? 1 : 0));
+ for (var iteration = 0; iteration < 16; iteration++)
+ {
+ var machine = new Machine(random);
+ var function = iteration & 3;
+ var instruction = CreateInstruction(code, memorySource, function, machine);
+ var expected = Evaluate(code, function, machine.Xmm[3], memorySource ? machine.ReadMemory128(DataAddress + 0x40) : machine.Xmm[7], machine.Xmm[0]);
+ var before = machine.Clone();
+
+ var program = new List { Instruction.Create(Code.Lea_r64_m, Register.RSP, new MemoryOperand(Register.RSP, -128)) };
+ program.AddRange(ShaInstructionRewrite.Expand(new[] { instruction }));
+ program.Add(Instruction.Create(Code.Lea_r64_m, Register.RSP, new MemoryOperand(Register.RSP, 128)));
+ foreach (var step in program)
+ {
+ machine.Execute(step);
+ }
+
+ Assert.Equal(expected, machine.Xmm[3]);
+ machine.Xmm[3] = before.Xmm[3];
+ Assert.Equal(before.Gpr, machine.Gpr);
+ Assert.Equal(before.Xmm, machine.Xmm);
+ Assert.Equal(before.Flags, machine.Flags);
+ // The 128-byte red zone below the guest stack pointer keeps its bytes.
+ Assert.Equal(before.Stack.AsSpan((int)(StackTop - 0x1000 - 128), 128).ToArray(),
+ machine.Stack.AsSpan((int)(StackTop - 0x1000 - 128), 128).ToArray());
+ }
+ }
+
+ [Fact]
+ public void Expand_LeavesOtherInstructionsUnchanged()
+ {
+ var other = Instruction.Create(Code.Add_r32_rm32, Register.EAX, Register.ECX);
+ var expanded = ShaInstructionRewrite.Expand(new[] { other });
+
+ Assert.Equal(new[] { other }, expanded);
+ }
+
+ private static Instruction CreateInstruction(Code code, bool memorySource, int function, Machine machine)
+ {
+ Instruction instruction;
+ if (memorySource)
+ {
+ // Address the source through a base and a scaled index register.
+ machine.Gpr[(int)(Register.RSI - Register.RAX)] = DataAddress;
+ machine.Gpr[(int)(Register.RDI - Register.RAX)] = 4;
+ var memory = new MemoryOperand(Register.RSI, Register.RDI, 8, 0x20, 1);
+ instruction = code == Code.Sha1rnds4_xmm_xmmm128_imm8
+ ? Instruction.Create(code, Register.XMM3, memory, function)
+ : Instruction.Create(code, Register.XMM3, memory);
+ }
+ else
+ {
+ instruction = code == Code.Sha1rnds4_xmm_xmmm128_imm8
+ ? Instruction.Create(code, Register.XMM3, Register.XMM7, function)
+ : Instruction.Create(code, Register.XMM3, Register.XMM7);
+ }
+
+ instruction.IP = 0x1000;
+ return instruction;
+ }
+
+ private static UInt128 Evaluate(Code code, int function, UInt128 source1, UInt128 source2, UInt128 xmm0) => code switch
+ {
+ Code.Sha1rnds4_xmm_xmmm128_imm8 => ShaInstructionEmulator.Sha1Rnds4(source1, source2, function),
+ Code.Sha1nexte_xmm_xmmm128 => ShaInstructionEmulator.Sha1Nexte(source1, source2),
+ Code.Sha1msg1_xmm_xmmm128 => ShaInstructionEmulator.Sha1Msg1(source1, source2),
+ Code.Sha1msg2_xmm_xmmm128 => ShaInstructionEmulator.Sha1Msg2(source1, source2),
+ Code.Sha256rnds2_xmm_xmmm128 => ShaInstructionEmulator.Sha256Rnds2(source1, source2, xmm0),
+ Code.Sha256msg1_xmm_xmmm128 => ShaInstructionEmulator.Sha256Msg1(source1, source2),
+ _ => ShaInstructionEmulator.Sha256Msg2(source1, source2),
+ };
+
+ // Interprets the subset of x86-64 the expansion emits. Flags are modelled only as the value
+ // PUSHFQ and POPFQ save and restore; the expansion must not leak any other change.
+ private sealed class Machine
+ {
+ public ulong[] Gpr = new ulong[16];
+ public UInt128[] Xmm = new UInt128[16];
+ public ulong Flags;
+ public byte[] Stack = new byte[StackTop];
+ public byte[] Data = new byte[0x100];
+
+ public Machine(System.Random random)
+ {
+ for (var index = 0; index < 16; index++)
+ {
+ Gpr[index] = (ulong)random.NextInt64();
+ Xmm[index] = new UInt128((ulong)random.NextInt64(), (ulong)random.NextInt64());
+ }
+
+ Gpr[(int)(Register.RSP - Register.RAX)] = StackTop - 0x1000;
+ Flags = 0x246;
+ random.NextBytes(Stack);
+ random.NextBytes(Data);
+ }
+
+ private Machine()
+ {
+ }
+
+ public Machine Clone() => new()
+ {
+ Gpr = (ulong[])Gpr.Clone(),
+ Xmm = (UInt128[])Xmm.Clone(),
+ Flags = Flags,
+ Stack = (byte[])Stack.Clone(),
+ Data = (byte[])Data.Clone(),
+ };
+
+ public UInt128 ReadMemory128(ulong address) => BinaryPrimitives.ReadUInt128LittleEndian(Span(address, 16));
+
+ public void Execute(in Instruction instruction)
+ {
+ switch (instruction.Mnemonic)
+ {
+ case Mnemonic.Pushfq:
+ Push(Flags);
+ break;
+ case Mnemonic.Popfq:
+ Flags = Pop();
+ break;
+ case Mnemonic.Push:
+ Push(Gpr[GprIndex(instruction.Op0Register)]);
+ break;
+ case Mnemonic.Pop:
+ Gpr[GprIndex(instruction.Op0Register)] = Pop();
+ break;
+ case Mnemonic.Lea:
+ Gpr[GprIndex(instruction.Op0Register)] = Address(instruction);
+ break;
+ case Mnemonic.Movdqu:
+ if (instruction.Op0Kind == OpKind.Memory)
+ {
+ BinaryPrimitives.WriteUInt128LittleEndian(Span(Address(instruction), 16), Xmm[instruction.Op1Register - Register.XMM0]);
+ }
+ else
+ {
+ Xmm[instruction.Op0Register - Register.XMM0] = ReadMemory128(Address(instruction));
+ }
+
+ break;
+ case Mnemonic.Mov:
+ Write(instruction, 0, Read(instruction, 1));
+ break;
+ case Mnemonic.Add:
+ case Mnemonic.Sub:
+ case Mnemonic.Xor:
+ case Mnemonic.And:
+ case Mnemonic.Or:
+ case Mnemonic.Rol:
+ case Mnemonic.Ror:
+ case Mnemonic.Shr:
+ Write(instruction, 0, Alu(instruction, Read(instruction, 0), Read(instruction, 1)));
+ break;
+ default:
+ throw new InvalidOperationException($"Unexpected instruction {instruction}");
+ }
+ }
+
+ private static ulong Alu(in Instruction instruction, ulong left, ulong right)
+ {
+ var is32 = instruction.Op0Kind == OpKind.Register && instruction.Op0Register.IsGPR32();
+ ulong result = instruction.Mnemonic switch
+ {
+ Mnemonic.Add => left + right,
+ Mnemonic.Sub => left - right,
+ Mnemonic.Xor => left ^ right,
+ Mnemonic.And => left & right,
+ Mnemonic.Or => left | right,
+ Mnemonic.Rol => System.Numerics.BitOperations.RotateLeft((uint)left, (int)right),
+ Mnemonic.Ror => System.Numerics.BitOperations.RotateRight((uint)left, (int)right),
+ _ => (uint)left >> (int)right,
+ };
+ return is32 ? (uint)result : result;
+ }
+
+ private ulong Read(in Instruction instruction, int operand)
+ {
+ switch (instruction.GetOpKind(operand))
+ {
+ case OpKind.Register:
+ var register = instruction.GetOpRegister(operand);
+ var value = Gpr[GprIndex(register)];
+ return register.IsGPR32() ? (uint)value : value;
+ case OpKind.Memory:
+ return instruction.MemorySize == MemorySize.UInt32 || instruction.MemorySize == MemorySize.Int32
+ ? BinaryPrimitives.ReadUInt32LittleEndian(Span(Address(instruction), 4))
+ : BinaryPrimitives.ReadUInt64LittleEndian(Span(Address(instruction), 8));
+ default:
+ return instruction.GetImmediate(operand);
+ }
+ }
+
+ private void Write(in Instruction instruction, int operand, ulong value)
+ {
+ if (instruction.GetOpKind(operand) == OpKind.Register)
+ {
+ var register = instruction.GetOpRegister(operand);
+ // A 32-bit write clears the upper half, as on x86-64.
+ Gpr[GprIndex(register)] = register.IsGPR32() ? (uint)value : value;
+ return;
+ }
+
+ if (instruction.MemorySize == MemorySize.UInt32 || instruction.MemorySize == MemorySize.Int32)
+ {
+ BinaryPrimitives.WriteUInt32LittleEndian(Span(Address(instruction), 4), (uint)value);
+ }
+ else
+ {
+ BinaryPrimitives.WriteUInt64LittleEndian(Span(Address(instruction), 8), value);
+ }
+ }
+
+ private ulong Address(in Instruction instruction)
+ {
+ var address = instruction.MemoryDisplacement64;
+ if (instruction.MemoryBase != Register.None)
+ {
+ address += Gpr[GprIndex(instruction.MemoryBase)];
+ }
+
+ if (instruction.MemoryIndex != Register.None)
+ {
+ address += Gpr[GprIndex(instruction.MemoryIndex)] * (ulong)instruction.MemoryIndexScale;
+ }
+
+ return address;
+ }
+
+ private Span Span(ulong address, int length) => address >= DataAddress
+ ? Data.AsSpan((int)(address - DataAddress), length)
+ : Stack.AsSpan((int)address, length);
+
+ private void Push(ulong value)
+ {
+ Gpr[4] -= 8;
+ BinaryPrimitives.WriteUInt64LittleEndian(Span(Gpr[4], 8), value);
+ }
+
+ private ulong Pop()
+ {
+ var value = BinaryPrimitives.ReadUInt64LittleEndian(Span(Gpr[4], 8));
+ Gpr[4] += 8;
+ return value;
+ }
+
+ private static int GprIndex(Register register) => register.GetFullRegister() - Register.RAX;
+ }
+}