sharpemu/tests/SharpEmu.Libs.Tests/Cpu/ShaInstructionRewriteTests.cs

290 lines
12 KiB
C#

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