732 lines
36 KiB
C++
732 lines
36 KiB
C++
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#include <windows.h>
|
|
#include <Sse4aEmulation.hpp>
|
|
#include <codegen/IAmd64OnlyConverter.hpp>
|
|
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
|
|
#include <codegen/x86/Sse4aOperands.hpp>
|
|
#include <elfpatcher/windows/WindowsLoadImage.hpp>
|
|
#include <elfpatcher/windows/WindowsPeWriter.hpp>
|
|
#include <elfpatcher/windows/WindowsTrampolineBuilder.hpp>
|
|
#include <io/BufferUtils.hpp>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <span>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using namespace Elfpatcher::Windows;
|
|
using Bytes = std::vector<std::uint8_t>;
|
|
|
|
void require(bool condition, const std::string& message) {
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
void requireFailure(const std::function<void()>& operation, const char* message) {
|
|
try {
|
|
operation();
|
|
} catch (const Domain::RelinkerException&) {
|
|
return;
|
|
}
|
|
throw std::runtime_error(message);
|
|
}
|
|
|
|
template<typename TValue>
|
|
void write(Bytes& bytes, std::size_t offset, TValue value) {
|
|
if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture write is out of bounds");
|
|
std::memcpy(bytes.data() + offset, &value, sizeof(value));
|
|
}
|
|
|
|
template<typename TValue>
|
|
TValue read(const Bytes& bytes, std::size_t offset) {
|
|
TValue value;
|
|
if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture read is out of bounds");
|
|
std::memcpy(&value, bytes.data() + offset, sizeof(value));
|
|
return value;
|
|
}
|
|
|
|
struct alignas(16) State {
|
|
std::uint8_t Xmm[16][16];
|
|
std::uint64_t FlagsIn;
|
|
std::uint64_t FlagsOut;
|
|
std::uint64_t RspBefore;
|
|
std::uint64_t RspAfter;
|
|
std::uint8_t RedZone[128];
|
|
std::uint8_t Ymm[16][32];
|
|
};
|
|
|
|
constexpr std::uint64_t kCanary = 0xA5C3E17B9D24F608ull;
|
|
constexpr std::uint64_t kStatusFlags = 0x8D5;
|
|
constexpr std::uint32_t kXmmOffset = 0;
|
|
constexpr std::uint32_t kFlagsInOffset = 256;
|
|
constexpr std::uint32_t kFlagsOutOffset = 264;
|
|
constexpr std::uint32_t kRspBeforeOffset = 272;
|
|
constexpr std::uint32_t kRspAfterOffset = 280;
|
|
constexpr std::uint32_t kRedZoneOffset = 288;
|
|
|
|
static_assert(offsetof(State, FlagsIn) == kFlagsInOffset && offsetof(State, FlagsOut) == kFlagsOutOffset && offsetof(State, RspBefore) == kRspBeforeOffset && offsetof(State, RspAfter) == kRspAfterOffset && offsetof(State, RedZone) == kRedZoneOffset, "State layout drifted");
|
|
|
|
void emit(Bytes& code, std::initializer_list<std::uint8_t> bytes) {
|
|
code.insert(code.end(), bytes.begin(), bytes.end());
|
|
}
|
|
|
|
void emitU32(Bytes& code, std::uint32_t value) {
|
|
for (std::size_t index = 0; index < 4; ++index) code.push_back(static_cast<std::uint8_t>(value >> (index * 8)));
|
|
}
|
|
|
|
void movdquXmmRbx(Bytes& code, const std::uint8_t reg, const bool store) {
|
|
code.push_back(0xF3);
|
|
if (reg >= 8) code.push_back(0x44);
|
|
emit(code, {0x0F, static_cast<std::uint8_t>(store ? 0x7F : 0x6F), static_cast<std::uint8_t>(0x83 | ((reg & 7) << 3))});
|
|
emitU32(code, kXmmOffset + reg * 16);
|
|
}
|
|
|
|
void movdquXmmRsp(Bytes& code, const std::uint8_t reg, const bool store, const std::uint32_t offset) {
|
|
code.push_back(0xF3);
|
|
if (reg >= 8) code.push_back(0x44);
|
|
emit(code, {0x0F, static_cast<std::uint8_t>(store ? 0x7F : 0x6F), static_cast<std::uint8_t>(0x84 | ((reg & 7) << 3)), 0x24});
|
|
emitU32(code, offset);
|
|
}
|
|
|
|
void vmovdquYmmRbx(Bytes& code, const std::uint8_t reg, const bool store) {
|
|
emit(code, {0xC4, static_cast<std::uint8_t>(reg >= 8 ? 0x61 : 0xE1), 0x7E, static_cast<std::uint8_t>(store ? 0x7F : 0x6F), static_cast<std::uint8_t>(0x83 | ((reg & 7) << 3))});
|
|
emitU32(code, static_cast<std::uint32_t>(offsetof(State, Ymm)) + reg * 32);
|
|
}
|
|
|
|
class Harness {
|
|
public:
|
|
Harness() : _memory(VirtualAlloc(nullptr, kSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) {
|
|
if (_memory == nullptr) throw std::runtime_error("Cannot allocate executable memory");
|
|
}
|
|
|
|
~Harness() {
|
|
VirtualFree(_memory, 0, MEM_RELEASE);
|
|
}
|
|
|
|
void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state, const bool wide = false) const {
|
|
Bytes code;
|
|
emit(code, {0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xA8, 0x00, 0x00, 0x00});
|
|
for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, true, (reg - 6) * 16);
|
|
emit(code, {0x48, 0x89, 0xCB, 0xFF, 0xB3});
|
|
emitU32(code, kFlagsInOffset);
|
|
emit(code, {0x9D, 0x48, 0xB8});
|
|
for (std::size_t index = 0; index < 8; ++index) code.push_back(static_cast<std::uint8_t>(kCanary >> (index * 8)));
|
|
for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast<std::uint8_t>(-128 + slot * 8)});
|
|
emit(code, {0x48, 0x89, 0xA3});
|
|
emitU32(code, kRspBeforeOffset);
|
|
for (std::uint8_t reg = 0; reg < 16; ++reg) {
|
|
if (wide)
|
|
vmovdquYmmRbx(code, reg, false);
|
|
else
|
|
movdquXmmRbx(code, reg, false);
|
|
}
|
|
code.push_back(0xE9);
|
|
const auto jumpOffset = code.size();
|
|
emitU32(code, 0);
|
|
const auto continuation = code.size();
|
|
for (int slot = 0; slot < 16; ++slot) {
|
|
emit(code, {0x48, 0x8B, 0x44, 0x24, static_cast<std::uint8_t>(-128 + slot * 8), 0x48, 0x89, 0x83});
|
|
emitU32(code, kRedZoneOffset + slot * 8);
|
|
}
|
|
emit(code, {0x48, 0x89, 0xA3});
|
|
emitU32(code, kRspAfterOffset);
|
|
emit(code, {0x9C, 0x8F, 0x83});
|
|
emitU32(code, kFlagsOutOffset);
|
|
for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, true);
|
|
if (wide)
|
|
for (std::uint8_t reg = 0; reg < 16; ++reg) vmovdquYmmRbx(code, reg, true);
|
|
for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, false, (reg - 6) * 16);
|
|
emit(code, {0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3});
|
|
code.resize(Io::AlignUp(code.size(), std::size_t{16}), 0xCC);
|
|
const auto bodyOffset = code.size();
|
|
code.insert(code.end(), body.begin(), body.end());
|
|
write<std::int32_t>(code, jumpOffset, static_cast<std::int32_t>(bodyOffset - (jumpOffset + 4)));
|
|
write<std::int32_t>(code, bodyOffset + returnBranchOffset + 1, static_cast<std::int32_t>(continuation - (bodyOffset + returnBranchOffset + 5)));
|
|
if (code.size() > kSize) throw std::runtime_error("Harness code exceeds its page");
|
|
std::memcpy(_memory, code.data(), code.size());
|
|
FlushInstructionCache(GetCurrentProcess(), _memory, code.size());
|
|
reinterpret_cast<void (*)(State*)>(_memory)(&state);
|
|
}
|
|
|
|
private:
|
|
static constexpr std::size_t kSize = 4096;
|
|
void* _memory;
|
|
};
|
|
|
|
std::uint64_t low(const std::uint8_t (&lane)[16]) {
|
|
std::uint64_t value;
|
|
std::memcpy(&value, lane, 8);
|
|
return value;
|
|
}
|
|
|
|
std::uint64_t referenceLow(const Codegen::Sse4aOperands& operands, const State& input) {
|
|
const auto mask = operands.Length >= 64 ? ~std::uint64_t{0} : ((std::uint64_t{1} << operands.Length) - 1);
|
|
const auto destination = low(input.Xmm[operands.Destination]);
|
|
if (!operands.Insertq) return (destination >> operands.Index) & mask;
|
|
const auto hole = mask << operands.Index;
|
|
return (destination & ~hole) | ((low(input.Xmm[operands.Source]) & mask) << operands.Index);
|
|
}
|
|
|
|
std::uint64_t libcReferenceLow(const Codegen::Sse4aOperands& operands, const State& input) {
|
|
CONTEXT context{};
|
|
for (unsigned reg = 0; reg < 16; ++reg) {
|
|
std::memcpy(&context.FltSave.XmmRegisters[reg].Low, input.Xmm[reg], 8);
|
|
std::memcpy(&context.FltSave.XmmRegisters[reg].High, input.Xmm[reg] + 8, 8);
|
|
}
|
|
sse4a::Instruction instruction;
|
|
instruction.op = operands.Insertq ? sse4a::Op::Insertq : sse4a::Op::Extrq;
|
|
instruction.registerForm = operands.RegisterForm;
|
|
instruction.destination = operands.Destination;
|
|
instruction.source = operands.Source;
|
|
instruction.length = static_cast<std::uint8_t>(operands.Length == 64 ? 0 : operands.Length);
|
|
instruction.index = operands.Index;
|
|
sse4a::Execute(instruction, context);
|
|
return static_cast<std::uint64_t>(context.FltSave.XmmRegisters[operands.Destination].Low);
|
|
}
|
|
|
|
struct Case {
|
|
bool Insertq;
|
|
std::uint8_t Destination;
|
|
std::uint8_t Source;
|
|
std::uint8_t Length;
|
|
std::uint8_t Index;
|
|
bool RegisterForm = false;
|
|
};
|
|
|
|
Bytes encode(const Case& item) {
|
|
Bytes bytes = {static_cast<std::uint8_t>(item.Insertq ? 0xF2 : 0x66)};
|
|
const std::uint8_t reg = item.Insertq || item.RegisterForm ? item.Destination : 0;
|
|
const std::uint8_t rm = item.Insertq || item.RegisterForm ? item.Source : item.Destination;
|
|
const auto rex = static_cast<std::uint8_t>(0x40 | (reg >= 8 ? 4 : 0) | (rm >= 8 ? 1 : 0));
|
|
if (rex != 0x40) bytes.push_back(rex);
|
|
bytes.insert(bytes.end(), {0x0F, static_cast<std::uint8_t>(item.RegisterForm ? 0x79 : 0x78), static_cast<std::uint8_t>(0xC0 | ((reg & 7) << 3) | (rm & 7))});
|
|
if (!item.RegisterForm) bytes.insert(bytes.end(), {static_cast<std::uint8_t>(item.Length == 64 ? 0 : item.Length), item.Index});
|
|
return bytes;
|
|
}
|
|
|
|
std::string describe(const Case& item) {
|
|
const bool source = item.Insertq || item.RegisterForm;
|
|
return std::string(item.Insertq ? "insertq xmm" : "extrq xmm") + std::to_string(item.Destination) + (source ? ", xmm" + std::to_string(item.Source) : "") + (item.RegisterForm ? " (register form)" : "") + ", " + std::to_string(item.Length) + ", " + std::to_string(item.Index);
|
|
}
|
|
|
|
std::size_t g_executions = 0;
|
|
|
|
void requireEnvironment(const State& input, const State& state, const std::uint16_t written, const std::string& description) {
|
|
for (unsigned reg = 0; reg < 16; ++reg) {
|
|
if (((written >> reg) & 1) != 0) continue;
|
|
require(std::memcmp(state.Xmm[reg], input.Xmm[reg], 16) == 0, "Lowered sequence clobbered xmm" + std::to_string(reg) + ": " + description);
|
|
}
|
|
require(((state.FlagsOut ^ input.FlagsIn) & kStatusFlags) == 0, "Lowered sequence changed RFLAGS: " + description);
|
|
require(state.RspAfter == state.RspBefore, "Lowered sequence did not restore rsp: " + description);
|
|
for (int slot = 0; slot < 16; ++slot) {
|
|
std::uint64_t value;
|
|
std::memcpy(&value, state.RedZone + slot * 8, 8);
|
|
require(value == kCanary, "Lowered sequence wrote into the red zone: " + description);
|
|
}
|
|
}
|
|
|
|
void reciprocalExecution() {
|
|
const Harness harness;
|
|
const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher();
|
|
const bool wide = __builtin_cpu_supports("avx");
|
|
const float inputs[][4] = {
|
|
{1.0f, 4.0f, 0.25f, 2.0f},
|
|
{0.0f, -0.0f, INFINITY, -1.0f},
|
|
{1.00000012f, 0.99999994f, 3.0e-38f, 1.0e30f},
|
|
{NAN, -INFINITY, -4.0f, 16.0f}};
|
|
for (const bool vex : {false, true}) {
|
|
if (vex && !wide)
|
|
continue;
|
|
for (const std::uint8_t opcode : {std::uint8_t{0x52}, std::uint8_t{0x53}}) {
|
|
for (const std::uint8_t destination : {std::uint8_t{0}, std::uint8_t{2}, std::uint8_t{9}, std::uint8_t{15}}) {
|
|
for (const std::uint8_t source : {destination, std::uint8_t{0}, std::uint8_t{5}, std::uint8_t{14}}) {
|
|
Bytes site;
|
|
if (vex) {
|
|
emit(site, {0xC4, static_cast<std::uint8_t>((destination >= 8 ? 0 : 0x80) | 0x40 | (source >= 8 ? 0 : 0x20) | 1), 0x78});
|
|
} else {
|
|
const auto rex = static_cast<std::uint8_t>(0x40 | (destination >= 8 ? 4 : 0) | (source >= 8 ? 1 : 0));
|
|
if (rex != 0x40)
|
|
site.push_back(rex);
|
|
site.push_back(0x0F);
|
|
}
|
|
emit(site, {opcode, static_cast<std::uint8_t>(0xC0 | ((destination & 7) << 3) | (source & 7))});
|
|
const auto match = matcher->Match(site.data(), site.size());
|
|
require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Packed reciprocal was not lowered");
|
|
for (const auto& lanes : inputs) {
|
|
State input{};
|
|
for (std::size_t reg = 0; reg < 16; ++reg) {
|
|
for (std::size_t byte = 0; byte < 16; ++byte) {
|
|
input.Xmm[reg][byte] = static_cast<std::uint8_t>(reg * 16 + byte);
|
|
input.Ymm[reg][byte + 16] = static_cast<std::uint8_t>(reg * 16 + byte + 1);
|
|
}
|
|
}
|
|
std::memcpy(input.Xmm[source], lanes, sizeof(lanes));
|
|
for (std::size_t reg = 0; reg < 16; ++reg)
|
|
std::memcpy(input.Ymm[reg], input.Xmm[reg], 16);
|
|
input.FlagsIn = 0xAC7;
|
|
State state = input;
|
|
harness.Run(match->StubBody, match->ReturnBranchOffset, state, wide);
|
|
++g_executions;
|
|
float actual[4];
|
|
std::memcpy(actual, state.Xmm[destination], sizeof(actual));
|
|
for (std::size_t lane = 0; lane < 4; ++lane) {
|
|
const float expected = opcode == 0x52 ? 1.0f / std::sqrt(lanes[lane]) : 1.0f / lanes[lane];
|
|
require(std::isnan(expected) ? std::isnan(actual[lane]) : std::memcmp(&expected, &actual[lane], sizeof(float)) == 0, "Packed reciprocal result differs from division and square root");
|
|
}
|
|
requireEnvironment(input, state, static_cast<std::uint16_t>(1u << destination), "packed reciprocal");
|
|
if (wide) {
|
|
const std::uint8_t zero[16] = {};
|
|
for (std::size_t reg = 0; reg < 16; ++reg) {
|
|
const auto* expected = vex && reg == destination ? zero : input.Ymm[reg] + 16;
|
|
require(std::memcmp(state.Ymm[reg] + 16, expected, 16) == 0, "Packed reciprocal changed the wrong upper YMM bits");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void executeCase(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const Case& item, std::mt19937_64& random, const int images) {
|
|
const auto site = encode(item);
|
|
const auto match = matcher.Match(site.data(), site.size());
|
|
require(match.has_value() && match->Lowering != Codegen::Amd64OnlyLowering::Unsupported, "SSE4a instruction was not lowered: " + describe(item));
|
|
Bytes body;
|
|
std::size_t returnBranchOffset = 0;
|
|
if (match->Lowering == Codegen::Amd64OnlyLowering::InPlace) {
|
|
require(match->ReplacementBytes.size() == site.size(), "In-place lowering changed the length: " + describe(item));
|
|
body = match->ReplacementBytes;
|
|
returnBranchOffset = body.size();
|
|
body.insert(body.end(), {0xE9, 0, 0, 0, 0});
|
|
} else {
|
|
body = match->StubBody;
|
|
returnBranchOffset = match->ReturnBranchOffset;
|
|
require(body.size() % 16 == 0 || body.size() == returnBranchOffset + 5, "Stub body with constants is not padded to 16 bytes: " + describe(item));
|
|
}
|
|
const auto operands = Codegen::DecodeSse4a(site.data(), site.size());
|
|
auto field = operands;
|
|
if (operands.RegisterForm) {
|
|
field.Length = item.Length;
|
|
field.Index = item.Index;
|
|
}
|
|
const std::size_t controlOffset = operands.Insertq ? 8 : 0;
|
|
const std::array<std::uint64_t, 4> flags = {0x202, 0x203, 0x246, 0xAC7};
|
|
for (int image = 0; image < images; ++image) {
|
|
State input{};
|
|
for (auto& lane : input.Xmm) for (auto& byte : lane) byte = static_cast<std::uint8_t>(random());
|
|
if (operands.RegisterForm) {
|
|
auto& control = input.Xmm[operands.Source];
|
|
control[controlOffset] = static_cast<std::uint8_t>((control[controlOffset] & 0xC0) | (item.Length == 64 ? 0 : item.Length));
|
|
control[controlOffset + 1] = static_cast<std::uint8_t>((control[controlOffset + 1] & 0xC0) | item.Index);
|
|
}
|
|
input.FlagsIn = flags[static_cast<std::size_t>(image) % flags.size()];
|
|
State state = input;
|
|
harness.Run(body, returnBranchOffset, state);
|
|
++g_executions;
|
|
const auto expected = referenceLow(field, input);
|
|
require(expected == libcReferenceLow(operands, input), "Transcribed reference disagrees with the libc emulation: " + describe(item));
|
|
require(low(state.Xmm[operands.Destination]) == expected, "Lowered sequence computed the wrong field: " + describe(item));
|
|
requireEnvironment(input, state, static_cast<std::uint16_t>(1u << operands.Destination), describe(item));
|
|
}
|
|
}
|
|
|
|
struct Sha256Step {
|
|
std::uint8_t Opcode;
|
|
std::uint8_t Destination;
|
|
std::uint8_t Source;
|
|
};
|
|
|
|
Bytes encodeSha256(const Sha256Step& step) {
|
|
Bytes bytes;
|
|
const auto rex = static_cast<std::uint8_t>(0x40 | (step.Destination >= 8 ? 4 : 0) | (step.Source >= 8 ? 1 : 0));
|
|
if (rex != 0x40) bytes.push_back(rex);
|
|
bytes.insert(bytes.end(), {0x0F, 0x38, step.Opcode, static_cast<std::uint8_t>(0xC0 | ((step.Destination & 7) << 3) | (step.Source & 7))});
|
|
return bytes;
|
|
}
|
|
|
|
std::uint32_t rotr(const std::uint32_t value, const unsigned count) {
|
|
return (value >> count) | (value << (32 - count));
|
|
}
|
|
|
|
void sha256Reference(const Sha256Step& step, std::uint8_t (&xmm)[16][16]) {
|
|
std::uint32_t a[4];
|
|
std::uint32_t b[4];
|
|
std::uint32_t k[4];
|
|
std::uint32_t r[4];
|
|
std::memcpy(a, xmm[step.Destination], sizeof(a));
|
|
std::memcpy(b, xmm[step.Source], sizeof(b));
|
|
std::memcpy(k, xmm[0], sizeof(k));
|
|
const auto sigma0 = [](const std::uint32_t w) { return rotr(w, 7) ^ rotr(w, 18) ^ (w >> 3); };
|
|
const auto sigma1 = [](const std::uint32_t w) { return rotr(w, 17) ^ rotr(w, 19) ^ (w >> 10); };
|
|
if (step.Opcode == 0xCC) {
|
|
for (int lane = 0; lane < 3; ++lane) r[lane] = a[lane] + sigma0(a[lane + 1]);
|
|
r[3] = a[3] + sigma0(b[0]);
|
|
} else if (step.Opcode == 0xCD) {
|
|
r[0] = a[0] + sigma1(b[2]);
|
|
r[1] = a[1] + sigma1(b[3]);
|
|
r[2] = a[2] + sigma1(r[0]);
|
|
r[3] = a[3] + sigma1(r[1]);
|
|
} else {
|
|
std::uint32_t sa = b[3], sb = b[2], sc = a[3], sd = a[2], se = b[1], sf = b[0], sg = a[1], sh = a[0];
|
|
for (int round = 0; round < 2; ++round) {
|
|
const auto t1 = sh + (rotr(se, 6) ^ rotr(se, 11) ^ rotr(se, 25)) + ((se & sf) ^ (~se & sg)) + k[round];
|
|
const auto t2 = (rotr(sa, 2) ^ rotr(sa, 13) ^ rotr(sa, 22)) + ((sa & sb) ^ (sa & sc) ^ (sb & sc));
|
|
sh = sg; sg = sf; sf = se; se = sd + t1; sd = sc; sc = sb; sb = sa; sa = t1 + t2;
|
|
}
|
|
r[0] = sf;
|
|
r[1] = se;
|
|
r[2] = sb;
|
|
r[3] = sa;
|
|
}
|
|
std::memcpy(xmm[step.Destination], r, sizeof(r));
|
|
}
|
|
|
|
std::string describeSha256(const std::vector<Sha256Step>& steps) {
|
|
std::string text;
|
|
for (const auto& step : steps)
|
|
text += std::string(step.Opcode == 0xCB ? "sha256rnds2" : step.Opcode == 0xCC ? "sha256msg1" : "sha256msg2") + " xmm" + std::to_string(step.Destination) + ", xmm" + std::to_string(step.Source) + "; ";
|
|
return text;
|
|
}
|
|
|
|
void executeSha256(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const std::vector<Sha256Step>& steps, std::mt19937_64& random, const int images) {
|
|
std::vector<Bytes> sites;
|
|
for (const auto& step : steps) sites.push_back(encodeSha256(step));
|
|
const std::vector<std::span<const std::uint8_t>> spans(sites.begin(), sites.end());
|
|
const auto match = steps.size() == 1 ? matcher.Match(sites[0].data(), sites[0].size()) : matcher.MatchSequence(spans, {});
|
|
require(match.has_value() && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "SHA-256 was not lowered through a stub: " + describeSha256(steps));
|
|
std::uint16_t written = 0;
|
|
for (const auto& step : steps) written = static_cast<std::uint16_t>(written | (1u << step.Destination));
|
|
const std::array<std::uint64_t, 4> flags = {0x202, 0x203, 0x246, 0xAC7};
|
|
for (int image = 0; image < images; ++image) {
|
|
State input{};
|
|
for (auto& lane : input.Xmm) for (auto& byte : lane) byte = static_cast<std::uint8_t>(random());
|
|
input.FlagsIn = flags[static_cast<std::size_t>(image) % flags.size()];
|
|
State state = input;
|
|
harness.Run(match->StubBody, match->ReturnBranchOffset, state);
|
|
++g_executions;
|
|
State expected = input;
|
|
for (const auto& step : steps) sha256Reference(step, expected.Xmm);
|
|
for (unsigned reg = 0; reg < 16; ++reg)
|
|
if (((written >> reg) & 1) != 0) require(std::memcmp(state.Xmm[reg], expected.Xmm[reg], 16) == 0, "Lowered SHA-256 computed the wrong xmm" + std::to_string(reg) + ": " + describeSha256(steps));
|
|
requireEnvironment(input, state, written, describeSha256(steps));
|
|
}
|
|
}
|
|
|
|
struct Sha1Memory {
|
|
std::uint8_t ModRm;
|
|
Bytes Tail;
|
|
std::optional<std::uint8_t> Slot;
|
|
const char* Name;
|
|
};
|
|
|
|
const std::array<Sha1Memory, 4> kSha1Memory = {{
|
|
{0x03, {}, std::uint8_t{0}, "[rbx]"},
|
|
{0x43, {0x70}, std::uint8_t{7}, "[rbx+0x70]"},
|
|
{0x83, {0xF0, 0x00, 0x00, 0x00}, std::uint8_t{15}, "[rbx+0xf0]"},
|
|
{0x44, {0x24, 0xF0}, std::nullopt, "[rsp-0x10]"}}};
|
|
|
|
struct Sha1Step {
|
|
std::uint8_t Opcode;
|
|
std::uint8_t Destination;
|
|
std::uint8_t Source;
|
|
std::uint8_t Immediate = 0;
|
|
bool Memory = false;
|
|
};
|
|
|
|
Bytes encodeSha1(const Sha1Step& step) {
|
|
Bytes bytes;
|
|
const auto rex = static_cast<std::uint8_t>(0x40 | (step.Destination >= 8 ? 4 : 0) | (!step.Memory && step.Source >= 8 ? 1 : 0));
|
|
if (rex != 0x40) bytes.push_back(rex);
|
|
bytes.insert(bytes.end(), {0x0F, static_cast<std::uint8_t>(step.Opcode == 0xCC ? 0x3A : 0x38), step.Opcode});
|
|
if (step.Memory) {
|
|
const auto& memory = kSha1Memory[step.Source];
|
|
bytes.push_back(static_cast<std::uint8_t>(memory.ModRm | ((step.Destination & 7) << 3)));
|
|
bytes.insert(bytes.end(), memory.Tail.begin(), memory.Tail.end());
|
|
} else {
|
|
bytes.push_back(static_cast<std::uint8_t>(0xC0 | ((step.Destination & 7) << 3) | (step.Source & 7)));
|
|
}
|
|
if (step.Opcode == 0xCC) bytes.push_back(step.Immediate);
|
|
return bytes;
|
|
}
|
|
|
|
std::uint32_t rotl(const std::uint32_t value, const unsigned count) {
|
|
return (value << count) | (value >> (32 - count));
|
|
}
|
|
|
|
void sha1Reference(const Sha1Step& step, const State& input, std::uint8_t (&xmm)[16][16]) {
|
|
std::uint32_t x[4];
|
|
std::uint32_t y[4];
|
|
std::uint32_t r[4];
|
|
std::memcpy(x, xmm[step.Destination], sizeof(x));
|
|
if (!step.Memory) {
|
|
std::memcpy(y, xmm[step.Source], sizeof(y));
|
|
} else if (const auto slot = kSha1Memory[step.Source].Slot) {
|
|
std::memcpy(y, input.Xmm[*slot], sizeof(y));
|
|
} else {
|
|
const std::uint64_t canary[2] = {kCanary, kCanary};
|
|
std::memcpy(y, canary, sizeof(y));
|
|
}
|
|
if (step.Opcode == 0xC8) {
|
|
r[0] = y[0];
|
|
r[1] = y[1];
|
|
r[2] = y[2];
|
|
r[3] = y[3] + rotl(x[3], 30);
|
|
} else if (step.Opcode == 0xC9) {
|
|
r[0] = x[0] ^ y[2];
|
|
r[1] = x[1] ^ y[3];
|
|
r[2] = x[2] ^ x[0];
|
|
r[3] = x[3] ^ x[1];
|
|
} else if (step.Opcode == 0xCA) {
|
|
r[3] = rotl(x[3] ^ y[2], 1);
|
|
r[2] = rotl(x[2] ^ y[1], 1);
|
|
r[1] = rotl(x[1] ^ y[0], 1);
|
|
r[0] = rotl(x[0] ^ r[3], 1);
|
|
} else {
|
|
constexpr std::uint32_t keys[4] = {0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6};
|
|
const auto function = step.Immediate & 3;
|
|
std::uint32_t a = x[3], b = x[2], c = x[1], d = x[0], e = 0;
|
|
for (int round = 0; round < 4; ++round) {
|
|
std::uint32_t f = b ^ c ^ d;
|
|
if (function == 0) f = (b & c) ^ (~b & d);
|
|
if (function == 2) f = (b & c) ^ (b & d) ^ (c & d);
|
|
const auto next = f + rotl(a, 5) + y[3 - round] + e + keys[function];
|
|
e = d; d = c; c = rotl(b, 30); b = a; a = next;
|
|
}
|
|
r[0] = d;
|
|
r[1] = c;
|
|
r[2] = b;
|
|
r[3] = a;
|
|
}
|
|
std::memcpy(xmm[step.Destination], r, sizeof(r));
|
|
}
|
|
|
|
std::string describeSha1(const std::vector<Sha1Step>& steps) {
|
|
std::string text;
|
|
for (const auto& step : steps) {
|
|
text += std::string(step.Opcode == 0xCC ? "sha1rnds4" : step.Opcode == 0xC8 ? "sha1nexte" : step.Opcode == 0xC9 ? "sha1msg1" : "sha1msg2") + " xmm" + std::to_string(step.Destination) + ", ";
|
|
text += step.Memory ? std::string(kSha1Memory[step.Source].Name) : "xmm" + std::to_string(step.Source);
|
|
if (step.Opcode == 0xCC) text += ", " + std::to_string(step.Immediate);
|
|
text += "; ";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
void executeSha1(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const std::vector<Sha1Step>& steps, std::mt19937_64& random, const int images) {
|
|
std::vector<Bytes> sites;
|
|
for (const auto& step : steps) sites.push_back(encodeSha1(step));
|
|
const std::vector<std::span<const std::uint8_t>> spans(sites.begin(), sites.end());
|
|
const auto match = steps.size() == 1 ? matcher.Match(sites[0].data(), sites[0].size()) : matcher.MatchSequence(spans, {});
|
|
require(match.has_value() && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "SHA-1 was not lowered through a stub: " + describeSha1(steps));
|
|
std::uint16_t written = 0;
|
|
for (const auto& step : steps) written = static_cast<std::uint16_t>(written | (1u << step.Destination));
|
|
const std::array<std::uint64_t, 4> flags = {0x202, 0x203, 0x246, 0xAC7};
|
|
for (int image = 0; image < images; ++image) {
|
|
State input{};
|
|
for (auto& lane : input.Xmm) for (auto& byte : lane) byte = static_cast<std::uint8_t>(random());
|
|
input.FlagsIn = flags[static_cast<std::size_t>(image) % flags.size()];
|
|
State state = input;
|
|
harness.Run(match->StubBody, match->ReturnBranchOffset, state);
|
|
++g_executions;
|
|
State expected = input;
|
|
for (const auto& step : steps) sha1Reference(step, input, expected.Xmm);
|
|
for (unsigned reg = 0; reg < 16; ++reg)
|
|
if (((written >> reg) & 1) != 0) require(std::memcmp(state.Xmm[reg], expected.Xmm[reg], 16) == 0, "Lowered SHA-1 computed the wrong xmm" + std::to_string(reg) + ": " + describeSha1(steps));
|
|
requireEnvironment(input, state, written, describeSha1(steps));
|
|
}
|
|
}
|
|
|
|
void sha1Execution(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, std::mt19937_64& random) {
|
|
const std::array<std::uint8_t, 4> opcodes = {0xCC, 0xC8, 0xC9, 0xCA};
|
|
for (const auto opcode : opcodes) {
|
|
for (std::uint8_t dst = 0; dst < 16; ++dst) {
|
|
for (std::uint8_t src = 0; src < 16; ++src)
|
|
executeSha1(harness, matcher, {{opcode, dst, src, static_cast<std::uint8_t>(random())}}, random, 2);
|
|
for (std::uint8_t memory = 0; memory < kSha1Memory.size(); ++memory)
|
|
for (std::uint8_t function = 0; function < 4; ++function)
|
|
executeSha1(harness, matcher, {{opcode, dst, memory, static_cast<std::uint8_t>((random() & 0xFC) | function), true}}, random, 2);
|
|
}
|
|
}
|
|
for (int sequence = 0; sequence < 256; ++sequence) {
|
|
std::vector<Sha1Step> steps(2 + random() % 2);
|
|
for (auto& step : steps) {
|
|
const bool memory = random() % 2 == 0;
|
|
step = {opcodes[random() % opcodes.size()], static_cast<std::uint8_t>(random() % 16), static_cast<std::uint8_t>(random() % (memory ? kSha1Memory.size() : 16)), static_cast<std::uint8_t>(random()), memory};
|
|
}
|
|
executeSha1(harness, matcher, steps, random, 2);
|
|
}
|
|
}
|
|
|
|
void clzeroExecution(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, std::mt19937_64& random) {
|
|
const Bytes plain = {0x0F, 0x01, 0xFC};
|
|
const Bytes addressSize32 = {0x67, 0x0F, 0x01, 0xFC};
|
|
const std::vector<std::span<const std::uint8_t>> pair = {plain, plain};
|
|
constexpr std::size_t size = 3 * 4096;
|
|
auto* high = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
|
|
std::uint8_t* low = nullptr;
|
|
for (std::uintptr_t hint = 0x10000000; low == nullptr && hint < 0x80000000; hint += 0x10000000)
|
|
low = static_cast<std::uint8_t*>(VirtualAlloc(reinterpret_cast<void*>(hint), size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
|
|
require(high != nullptr && low != nullptr, "Cannot allocate the CLZERO buffers");
|
|
struct ClzeroCase {
|
|
std::optional<Codegen::Amd64OnlyMatch> Match;
|
|
std::uint8_t* Buffer;
|
|
std::uint64_t Junk;
|
|
std::string Name;
|
|
};
|
|
const std::vector<ClzeroCase> cases = {
|
|
{matcher.Match(plain.data(), plain.size()), high, 0, "clzero"},
|
|
{matcher.Match(addressSize32.data(), addressSize32.size()), low, 0x5A5A5A5A00000000ull, "67h clzero"},
|
|
{matcher.MatchSequence(pair, {}), high, 0, "clzero; clzero"}};
|
|
const std::array<std::uint64_t, 4> flags = {0x202, 0x203, 0x246, 0xAC7};
|
|
for (const auto& item : cases) {
|
|
require(item.Match.has_value() && item.Match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "CLZERO was not lowered through a stub: " + item.Name);
|
|
for (int image = 0; image < 64; ++image) {
|
|
const auto address = reinterpret_cast<std::uint64_t>(item.Buffer) + 64 + random() % (size - 128);
|
|
Bytes body = {0x48, 0xB8};
|
|
for (std::size_t index = 0; index < 8; ++index) body.push_back(static_cast<std::uint8_t>((address | item.Junk) >> (index * 8)));
|
|
body.insert(body.end(), {0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00});
|
|
const auto prefix = body.size();
|
|
body.insert(body.end(), item.Match->StubBody.begin(), item.Match->StubBody.end());
|
|
State input{};
|
|
for (auto& lane : input.Xmm) for (auto& byte : lane) byte = static_cast<std::uint8_t>(random());
|
|
input.FlagsIn = flags[static_cast<std::size_t>(image) % flags.size()];
|
|
State state = input;
|
|
std::memset(item.Buffer, 0xA5, size);
|
|
harness.Run(body, prefix + item.Match->ReturnBranchOffset, state);
|
|
++g_executions;
|
|
const auto line = address & ~std::uint64_t{63};
|
|
for (std::size_t index = 0; index < size; ++index) {
|
|
const auto at = reinterpret_cast<std::uint64_t>(item.Buffer + index);
|
|
require(item.Buffer[index] == (at >= line && at < line + 64 ? 0x00 : 0xA5), "Lowered CLZERO did not clear exactly the addressed line: " + item.Name);
|
|
}
|
|
requireEnvironment(input, state, 0, item.Name);
|
|
}
|
|
}
|
|
VirtualFree(high, 0, MEM_RELEASE);
|
|
VirtualFree(low, 0, MEM_RELEASE);
|
|
}
|
|
|
|
void cpuExecution() {
|
|
const Harness harness;
|
|
const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher();
|
|
std::mt19937_64 random(0x5EED);
|
|
const std::vector<Case> sites = {{false, 3, 3, 8, 40}, {true, 3, 3, 8, 8}, {true, 1, 0, 8, 0}, {true, 9, 4, 16, 16}, {true, 3, 4, 16, 16}};
|
|
for (const auto& site : sites) executeCase(harness, *matcher, site, random, 64);
|
|
const std::vector<std::pair<std::uint8_t, std::uint8_t>> pairs = {{3, 3}, {1, 0}, {9, 4}, {3, 4}, {15, 15}, {0, 15}, {8, 8}};
|
|
for (std::uint8_t length = 1; length <= 64; ++length) {
|
|
for (std::uint8_t index = 0; index + length <= 64; ++index) {
|
|
for (const auto& [dst, src] : pairs) executeCase(harness, *matcher, {true, dst, src, length, index}, random, 2);
|
|
for (const std::uint8_t dst : std::array<std::uint8_t, 4>{0, 3, 9, 15}) executeCase(harness, *matcher, {false, dst, dst, length, index}, random, 2);
|
|
const auto& [dst, src] = pairs[(length * 65u + index) % pairs.size()];
|
|
executeCase(harness, *matcher, {true, dst, src, length, index, true}, random, 2);
|
|
}
|
|
}
|
|
for (const std::uint8_t opcode : {std::uint8_t{0xCB}, std::uint8_t{0xCC}, std::uint8_t{0xCD}})
|
|
for (std::uint8_t dst = 0; dst < 16; ++dst)
|
|
for (std::uint8_t src = 0; src < 16; ++src)
|
|
executeSha256(harness, *matcher, {{opcode, dst, src}}, random, 2);
|
|
for (int sequence = 0; sequence < 256; ++sequence) {
|
|
std::vector<Sha256Step> steps(2 + random() % 2, Sha256Step{});
|
|
for (auto& step : steps) step = {static_cast<std::uint8_t>(0xCB + random() % 3), static_cast<std::uint8_t>(random() % 16), static_cast<std::uint8_t>(random() % 16)};
|
|
executeSha256(harness, *matcher, steps, random, 2);
|
|
}
|
|
clzeroExecution(harness, *matcher, random);
|
|
sha1Execution(harness, *matcher, random);
|
|
}
|
|
|
|
Bytes elfFixture(const Bytes& text) {
|
|
Bytes bytes(0x400);
|
|
bytes[0] = 0x7F;
|
|
bytes[1] = 'E';
|
|
bytes[2] = 'L';
|
|
bytes[3] = 'F';
|
|
bytes[4] = 2;
|
|
bytes[5] = 1;
|
|
bytes[6] = 1;
|
|
write<std::uint16_t>(bytes, 16, 3);
|
|
write<std::uint16_t>(bytes, 18, 62);
|
|
write<std::uint64_t>(bytes, 24, 0x1000);
|
|
write<std::uint64_t>(bytes, 32, 64);
|
|
write<std::uint16_t>(bytes, 54, 56);
|
|
write<std::uint16_t>(bytes, 56, 2);
|
|
std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC);
|
|
std::copy(text.begin(), text.end(), bytes.begin() + 0x200);
|
|
return bytes;
|
|
}
|
|
|
|
std::vector<Domain::ProgramHeader> elfHeaders() {
|
|
return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}};
|
|
}
|
|
|
|
void peBuilder() {
|
|
const Bytes site = {0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08};
|
|
Bytes text = {0xEB, 0x06};
|
|
text.insert(text.end(), site.begin(), site.end());
|
|
text.push_back(0xC3);
|
|
const auto source = elfFixture(text);
|
|
const auto headers = elfHeaders();
|
|
const auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(source, {headers[0]});
|
|
require(converted.Trampolines.size() == 1, "PE fixture conversion did not produce one stub");
|
|
const WindowsLoadImage image(converted.Bytes, headers);
|
|
auto sections = image.BuildSections();
|
|
auto nextRva = image.GetEndRva();
|
|
const auto stubSectionRva = nextRva;
|
|
WindowsTrampolineBuilder().Build(converted.Trampolines, image, sections, nextRva);
|
|
require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == stubSectionRva && (sections.back().Characteristics & SectionExecute) != 0 && nextRva == AlignRva(stubSectionRva + sections.back().Data.size()), "Stub section was not appended");
|
|
const auto& text0 = sections[0];
|
|
const auto siteRva = LoadRva + 2;
|
|
require(text0.Data[2] == 0xE9 && text0.Data[7] == 0x90 && text0.Data[8] == 0xC3, "PE site was not replaced by a jump");
|
|
const auto stubRva = static_cast<std::uint32_t>(static_cast<std::int64_t>(siteRva + 5) + read<std::int32_t>(text0.Data, 3));
|
|
require(stubRva == stubSectionRva && stubRva % 16 == 0, "PE jump does not land on the 16-aligned stub");
|
|
const auto& body = sections.back().Data;
|
|
require(body.size() == 32 && body[0] == 0x66 && body[9] == 0xE9 && body[16] == 0x00 && body[17] == 0x00 && body[18] == 0x02, "PE stub body is wrong");
|
|
require(static_cast<std::int64_t>(stubRva + 9 + 5) + read<std::int32_t>(body, 10) == siteRva + 6, "PE stub does not return past the site");
|
|
require(read<std::int32_t>(body, 5) == 7 && (stubRva + 16) % 16 == 0, "PE mask constant is not 16-aligned");
|
|
std::array<PeDirectory, 16> directories{};
|
|
const auto file = WindowsPeWriter().Write(sections, LoadRva, directories);
|
|
require(file.size() > 0x400, "PE writer rejected the stub section");
|
|
require(read<std::uint16_t>(file, 0x96) == 0x22, "PE writer marked a fixed image as relocation-stripped");
|
|
auto altered = converted.Bytes;
|
|
altered[0x205] = 0xDC;
|
|
requireFailure([&] {
|
|
const WindowsLoadImage alteredImage(altered, headers);
|
|
auto alteredSections = alteredImage.BuildSections();
|
|
auto rva = alteredImage.GetEndRva();
|
|
WindowsTrampolineBuilder().Build(converted.Trampolines, alteredImage, alteredSections, rva);
|
|
}, "Changed PE site bytes were accepted");
|
|
requireFailure([&] {
|
|
auto unmapped = converted.Trampolines;
|
|
unmapped[0].Address = 0x5000;
|
|
auto freshSections = image.BuildSections();
|
|
auto rva = image.GetEndRva();
|
|
WindowsTrampolineBuilder().Build(unmapped, image, freshSections, rva);
|
|
}, "Unmapped PE site was accepted");
|
|
auto untouched = image.BuildSections();
|
|
auto untouchedRva = image.GetEndRva();
|
|
WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva);
|
|
require(untouched.size() == 2 && untouchedRva == image.GetEndRva(), "Empty site list changed the image");
|
|
}
|
|
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
cpuExecution();
|
|
reciprocalExecution();
|
|
peBuilder();
|
|
std::cout << "AMD64-only Windows tests passed (" << g_executions << " lowered sequences executed)\n";
|
|
} catch (const std::exception& error) {
|
|
std::cerr << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|