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; + } +}