[CPU] Emulate and rewrite SHA extension instructions on hosts without SHA (#909)
* [CPU] Emulate the SHA extensions when the host raises #UD The PS5 CPU supports SHA1RNDS4/SHA1NEXTE/SHA1MSG1/SHA1MSG2 and SHA256RNDS2/SHA256MSG1/SHA256MSG2, and titles use them without a CPUID check. Rosetta 2 does not translate them, so Poppy Playtime (UE4) aborted with SIGILL on sha1msg1 on Apple Silicon. Decode the faulting instruction and compute it from the saved XMM registers, like the SSE4a fallback. * [Loader] Rewrite SHA extension instructions at load time on hosts without SHA A #UD trap per SHA instruction is far too slow for titles that hash large files: Poppy Playtime took 300k traps and the stall watchdog stopped it. The red-zone patcher now replaces each SHA instruction with general-register code in its trampoline, keeping all other registers and the flags. The trap handler stays as the fallback for sites that cannot be patched.
This commit is contained in:
parent
9073974ba8
commit
ab0e77e5d6
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
|
@ -157,6 +157,11 @@ public sealed partial class DirectExecutionBackend
|
|||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == StatusIllegalInstruction &&
|
||||
TryRecoverShaInstruction(contextRecord, rip))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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<ulong> functionStarts,
|
||||
bool protectRedZone,
|
||||
bool splitVectorStores,
|
||||
bool rewriteSha,
|
||||
out PatchResult result)
|
||||
{
|
||||
var sites = new List<PatchSite>();
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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<Instruction> Expand(IList<Instruction> instructions)
|
||||
{
|
||||
var expanded = new List<Instruction>(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());
|
||||
}
|
||||
|
|
@ -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<int> 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<byte[]> 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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<Code, bool> Cases()
|
||||
{
|
||||
var data = new TheoryData<Code, bool>();
|
||||
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> { 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<byte> 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue