Files
FEX-Emu--FEX/FEXCore/Source/Interface/Core/OpcodeDispatcher.cpp
T
Ryan Houdek 63d52c2a1a OpcodeDispatcher: Fixes memory leak in IREmitter pool allocator
While not a leak in the traditional sense, we were causing pool
allocations to never become free until the thread was closed.

This meant in the case of a game running with >200 threads or so, these
would add up very quickly. So some minor reworking so the IREmitter
doesn't allocate a buffer until first JIT, and making sure to actually
disown the buffer on dispatch error resolved the problems.

Fixes an edge case where Ender Lilies was consuming 409MB with THP
enabled on my desktop, and now it is something like 6MB once idling for
a bit to have the pool allocations do its magic.
2026-03-10 20:55:09 -07:00

5021 lines
177 KiB
C++

// SPDX-License-Identifier: MIT
/*
$info$
tags: frontend|x86-to-ir, opcodes|dispatcher-implementations
desc: Handles x86/64 ops to IR, no-pf opt, local-flags opt
$end_info$
*/
#include "FEXCore/Core/HostFeatures.h"
#include "FEXCore/Utils/Telemetry.h"
#include "Interface/Context/Context.h"
#include "Interface/Core/OpcodeDispatcher.h"
#include "Interface/Core/X86Tables/X86Tables.h"
#include "Interface/IR/IR.h"
#include "Interface/IR/IREmitter.h"
#include <FEXCore/Config/Config.h>
#include <FEXCore/Core/Context.h>
#include <FEXCore/Core/CoreState.h>
#include <FEXCore/Core/X86Enums.h>
#include <FEXCore/HLE/SyscallHandler.h>
#include <FEXCore/IR/IR.h>
#include <FEXCore/Utils/EnumUtils.h>
#include <FEXCore/Utils/LogManager.h>
#include <FEXHeaderUtils/BitUtils.h>
#include <algorithm>
#include <array>
#include <cstdint>
namespace FEXCore::IR {
using X86Tables::OpToIndex;
#define OpcodeArgs [[maybe_unused]] FEXCore::X86Tables::DecodedOp Op
void OpDispatchBuilder::SyscallOp(OpcodeArgs, bool IsSyscallInst) {
constexpr size_t SyscallArgs = 7;
using SyscallArray = std::array<uint64_t, SyscallArgs>;
size_t NumArguments {};
const SyscallArray* GPRIndexes {};
static constexpr SyscallArray GPRIndexes_64 = {
FEXCore::X86State::REG_RAX, FEXCore::X86State::REG_RDI, FEXCore::X86State::REG_RSI, FEXCore::X86State::REG_RDX,
FEXCore::X86State::REG_R10, FEXCore::X86State::REG_R8, FEXCore::X86State::REG_R9,
};
static constexpr SyscallArray GPRIndexes_32 = {
FEXCore::X86State::REG_RAX, FEXCore::X86State::REG_RBX, FEXCore::X86State::REG_RCX, FEXCore::X86State::REG_RDX,
FEXCore::X86State::REG_RSI, FEXCore::X86State::REG_RDI, FEXCore::X86State::REG_RBP,
};
const auto OSABI = CTX->SyscallHandler->GetOSABI();
if (OSABI == FEXCore::HLE::SyscallOSABI::OS_LINUX64) {
NumArguments = GPRIndexes_64.size();
GPRIndexes = &GPRIndexes_64;
} else if (OSABI == FEXCore::HLE::SyscallOSABI::OS_LINUX32) {
NumArguments = GPRIndexes_32.size();
GPRIndexes = &GPRIndexes_32;
} else if (OSABI == FEXCore::HLE::SyscallOSABI::OS_GENERIC) {
// All registers will be spilled before the syscall and filled afterwards so no JIT-side argument handling is necessary.
NumArguments = 0;
GPRIndexes = nullptr;
} else {
ERROR_AND_DIE_FMT("Unhandled OSABI syscall");
}
// Calculate flags early.
CalculateDeferredFlags();
const auto GPRSize = GetGPROpSize();
auto NewRIP = GetRelocatedPC(Op, -Op->InstSize);
_StoreContextGPR(GPRSize, NewRIP, offsetof(FEXCore::Core::CPUState, rip));
Ref Arguments[SyscallArgs] {
InvalidNode, InvalidNode, InvalidNode, InvalidNode, InvalidNode, InvalidNode, InvalidNode,
};
for (size_t i = 0; i < NumArguments; ++i) {
Arguments[i] = LoadGPRRegister(GPRIndexes->at(i));
}
if (IsSyscallInst) {
// If this is the `Syscall` instruction rather than `int 0x80` then we need to do some additional work.
// RCX = RIP after this instruction
// R11 = EFlags
// Calculate flags.
CalculateDeferredFlags();
auto RFLAG = GetPackedRFLAG();
StoreGPRRegister(X86State::REG_R11, RFLAG, OpSize::i64Bit);
auto RIPAfterInst = GetRelocatedPC(Op);
StoreGPRRegister(X86State::REG_RCX, RIPAfterInst, OpSize::i64Bit);
}
FlushRegisterCache();
auto SyscallOp = _Syscall(Arguments[0], Arguments[1], Arguments[2], Arguments[3], Arguments[4], Arguments[5], Arguments[6]);
// Generic ABI doesn't store result in RAX.
if (OSABI != FEXCore::HLE::SyscallOSABI::OS_GENERIC) {
StoreGPRRegister(X86State::REG_RAX, SyscallOp);
}
if (Op->TableInfo->Flags & X86Tables::InstFlags::FLAGS_BLOCK_END) {
// RIP could have been updated after coming back from the Syscall.
NewRIP = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, rip));
ExitFunction(NewRIP);
}
}
void OpDispatchBuilder::ThunkOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
uint8_t* sha256 = (uint8_t*)(Op->PC + 2);
if (Is64BitMode) {
// x86-64 ABI puts the function argument in RDI
Thunk(LoadGPRRegister(X86State::REG_RDI), *reinterpret_cast<SHA256Sum*>(sha256));
} else {
// x86 fastcall ABI puts the function argument in ECX
Thunk(LoadGPRRegister(X86State::REG_RCX), *reinterpret_cast<SHA256Sum*>(sha256));
}
auto NewRIP = Pop(GPRSize);
// Store the new RIP
ExitFunction(NewRIP, BranchHint::Return);
BlockSetRIP = true;
}
void OpDispatchBuilder::LEAOp(OpcodeArgs) {
// LEA specifically ignores segment prefixes
const auto SrcSize = OpSizeFromSrc(Op);
const auto OpAddr = X86Tables::DecodeFlags::GetOpAddr(Op->Flags, 0);
OpSize DstSize {};
if (Is64BitMode) {
DstSize = OpAddr == X86Tables::DecodeFlags::FLAG_OPERAND_SIZE_LAST ? OpSize::i16Bit :
OpAddr == X86Tables::DecodeFlags::FLAG_WIDENING_SIZE_LAST ? OpSize::i64Bit :
OpSize::i32Bit;
} else {
DstSize = OpAddr == X86Tables::DecodeFlags::FLAG_OPERAND_SIZE_LAST ? OpSize::i16Bit : OpSize::i32Bit;
}
auto Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], SrcSize, Op->Flags, {.LoadData = false, .AllowUpperGarbage = SrcSize > DstSize});
StoreResultGPR_WithOpSize(Op, Op->Dest, Src, DstSize);
}
void OpDispatchBuilder::NOPOp(OpcodeArgs) {}
void OpDispatchBuilder::RETOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
Ref SP = _RMWHandle(LoadGPRRegister(X86State::REG_RSP));
Ref NewRIP = Pop(GPRSize, SP);
if (Op->OP == 0xC2) {
auto Offset = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
SP = Add(GPRSize, SP, Offset);
}
// Store the new stack pointer
StoreGPRRegister(X86State::REG_RSP, SP);
// Store the new RIP
ExitFunction(NewRIP, BranchHint::Return);
BlockSetRIP = true;
}
/*
stack contains:
Size of each member is 64-bit, 32-bit, or 16-bit depending on operating size
RIP
CS
EFLAGS
RSP
SS
*/
void OpDispatchBuilder::IRETOp(OpcodeArgs) {
// Operand Size override unsupported!
if ((Op->Flags & X86Tables::DecodeFlags::FLAG_OPERAND_SIZE) != 0) {
LogMan::Msg::EFmt("IRET only implemented for 64bit and 32bit sizes");
DecodeFailure = true;
return;
}
const auto GPRSize = GetGPROpSize();
Ref SP = _RMWHandle(LoadGPRRegister(X86State::REG_RSP));
// RIP (64/32/16 bits)
auto NewRIP = Pop(GPRSize, SP);
// CS (lower 16 used)
auto NewSegmentCS = Pop(GPRSize, SP);
_StoreContextGPR(OpSize::i16Bit, NewSegmentCS, offsetof(FEXCore::Core::CPUState, cs_idx));
UpdatePrefixFromSegment(NewSegmentCS, FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX);
// eflags (lower 16 used)
SetPackedRFLAG(false, Pop(GPRSize, SP));
if (Is64BitMode) {
// RSP and SS only happen in 64-bit mode or if this is a CPL mode jump!
// FEX doesn't support a CPL mode switch, so don't need to worry about this on 32-bit
StoreGPRRegister(X86State::REG_RSP, Pop(GPRSize, SP));
// ss
auto NewSegmentSS = Pop(GPRSize, SP);
_StoreContextGPR(OpSize::i16Bit, NewSegmentSS, offsetof(FEXCore::Core::CPUState, ss_idx));
UpdatePrefixFromSegment(NewSegmentSS, FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX);
} else {
// Store the stack in 32-bit mode
StoreGPRRegister(X86State::REG_RSP, SP);
}
ExitFunction(NewRIP);
BlockSetRIP = true;
}
void OpDispatchBuilder::CallbackReturnOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
// Store the new RIP
_CallbackReturn();
auto NewRIP = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, rip));
// This ExitFunction won't actually get hit but needs to exist
ExitFunction(NewRIP);
BlockSetRIP = true;
}
void OpDispatchBuilder::SecondaryALUOp(OpcodeArgs) {
FEXCore::IR::IROps IROp, AtomicIROp;
#define OPD(group, prefix, Reg) (((group - FEXCore::X86Tables::TYPE_GROUP_1) << 6) | (prefix) << 3 | (Reg))
switch (Op->OP) {
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x80), 0):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x81), 0):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x83), 0):
IROp = FEXCore::IR::IROps::OP_ADD;
AtomicIROp = FEXCore::IR::IROps::OP_ATOMICFETCHADD;
break;
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x80), 1):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x81), 1):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x83), 1):
IROp = FEXCore::IR::IROps::OP_OR;
AtomicIROp = FEXCore::IR::IROps::OP_ATOMICFETCHOR;
break;
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x80), 4):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x81), 4):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x83), 4):
IROp = FEXCore::IR::IROps::OP_ANDWITHFLAGS;
AtomicIROp = FEXCore::IR::IROps::OP_ATOMICFETCHAND;
break;
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x80), 5):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x81), 5):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x83), 5):
IROp = FEXCore::IR::IROps::OP_SUB;
AtomicIROp = FEXCore::IR::IROps::OP_ATOMICFETCHSUB;
break;
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x80), 6):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x81), 6):
case OPD(FEXCore::X86Tables::TYPE_GROUP_1, OpToIndex(0x83), 6):
IROp = FEXCore::IR::IROps::OP_XOR;
AtomicIROp = FEXCore::IR::IROps::OP_ATOMICFETCHXOR;
break;
default:
IROp = FEXCore::IR::IROps::OP_LAST;
AtomicIROp = FEXCore::IR::IROps::OP_LAST;
LogMan::Msg::EFmt("Unknown ALU Op: 0x{:x}", Op->OP);
DecodeFailure = true;
return;
};
#undef OPD
ALUOp(Op, IROp, AtomicIROp, 1);
}
void OpDispatchBuilder::ADCOp(OpcodeArgs, uint32_t SrcIndex) {
// Calculate flags early.
CalculateDeferredFlags();
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromDst(Op);
const auto OpSize = std::max(OpSize::i32Bit, Size);
Ref Before {};
if (DestIsLockedMem(Op)) {
auto ALUOp = IncrementByCarry(OpSize, Src);
HandledLock = true;
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
Before = _AtomicFetchAdd(Size, ALUOp, DestMem);
} else {
Before = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
}
Ref Result;
if (!DestIsLockedMem(Op) && Op->Src[SrcIndex].IsLiteral() && Op->Src[SrcIndex].Literal() == 0 && Size >= OpSize::i32Bit) {
HandleNZCV_RMW();
RectifyCarryInvert(true);
Result = _AdcZeroWithFlags(OpSize, Before);
SetRFLAG<FEXCore::X86State::RFLAG_AF_RAW_LOC>(Before);
CalculatePF(Result);
CFInverted = false;
} else {
Result = CalculateFlags_ADC(Size, Before, Src);
}
if (!DestIsLockedMem(Op)) {
StoreResultGPR(Op, Result);
}
}
void OpDispatchBuilder::SBBOp(OpcodeArgs, uint32_t SrcIndex) {
// Calculate flags early.
CalculateDeferredFlags();
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromDst(Op);
const auto OpSize = std::max(OpSize::i32Bit, Size);
Ref Result {};
Ref Before {};
if (DestIsLockedMem(Op)) {
HandledLock = true;
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
auto SrcPlusCF = IncrementByCarry(OpSize, Src);
Before = _AtomicFetchSub(Size, SrcPlusCF, DestMem);
} else {
Before = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
}
Result = CalculateFlags_SBB(Size, Before, Src);
if (!DestIsLockedMem(Op)) {
StoreResultGPR(Op, Result);
}
}
void OpDispatchBuilder::SALCOp(OpcodeArgs) {
CalculateDeferredFlags();
auto Result = NZCVSelect(OpSize::i32Bit, CondClass::UGE /* CF = 1 */, _InlineConstant(0xffffffff), _InlineConstant(0));
StoreResultGPR(Op, Result);
}
void OpDispatchBuilder::PUSHOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
Push(Size, LoadSourceGPR(Op, Op->Src[0], Op->Flags));
}
void OpDispatchBuilder::PUSHREGOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
Push(Size, LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true}));
}
void OpDispatchBuilder::PUSHAOp(OpcodeArgs) {
// 32bit only
const auto Size = OpSizeFromSrc(Op);
Ref OldSP = _Copy(LoadGPRRegister(X86State::REG_RSP));
Push(Size, LoadGPRRegister(X86State::REG_RAX));
Push(Size, LoadGPRRegister(X86State::REG_RCX));
Push(Size, LoadGPRRegister(X86State::REG_RDX));
Push(Size, LoadGPRRegister(X86State::REG_RBX));
Push(Size, OldSP);
Push(Size, LoadGPRRegister(X86State::REG_RBP));
Push(Size, LoadGPRRegister(X86State::REG_RSI));
Push(Size, LoadGPRRegister(X86State::REG_RDI));
}
void OpDispatchBuilder::PUSHSegmentOp(OpcodeArgs, uint32_t SegmentReg) {
const auto SrcSize = OpSizeFromSrc(Op);
const auto DstSize = OpSizeFromDst(Op);
Ref Src {};
if (!Is64BitMode) {
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, es_idx));
break;
}
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, cs_idx));
break;
}
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, ss_idx));
break;
}
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, ds_idx));
break;
}
case FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, fs_idx));
break;
}
case FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX: {
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, gs_idx));
break;
}
default: FEX_UNREACHABLE;
}
} else {
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, es_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, cs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, ss_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, ds_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, fs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX:
Src = _LoadContextGPR(SrcSize, offsetof(FEXCore::Core::CPUState, gs_cached));
break;
default: FEX_UNREACHABLE;
}
}
// Store our value to the new stack location
// AMD hardware zexts segment selector to 32bit
// Intel hardware inserts segment selector
Push(DstSize, Src);
}
void OpDispatchBuilder::POPOp(OpcodeArgs) {
Ref Value = Pop(OpSizeFromSrc(Op));
StoreResultGPR(Op, Value);
}
void OpDispatchBuilder::POPAOp(OpcodeArgs) {
// 32bit only
const auto Size = OpSizeFromSrc(Op);
Ref SP = _RMWHandle(LoadGPRRegister(X86State::REG_RSP));
StoreGPRRegister(X86State::REG_RDI, Pop(Size, SP), Size);
StoreGPRRegister(X86State::REG_RSI, Pop(Size, SP), Size);
StoreGPRRegister(X86State::REG_RBP, Pop(Size, SP), Size);
// Skip loading RSP because it'll be correct at the end
SP = _RMWHandle(Add(OpSize::i64Bit, SP, IR::OpSizeToSize(Size)));
StoreGPRRegister(X86State::REG_RBX, Pop(Size, SP), Size);
StoreGPRRegister(X86State::REG_RDX, Pop(Size, SP), Size);
StoreGPRRegister(X86State::REG_RCX, Pop(Size, SP), Size);
StoreGPRRegister(X86State::REG_RAX, Pop(Size, SP), Size);
// Store the new stack pointer
StoreGPRRegister(X86State::REG_RSP, SP);
}
void OpDispatchBuilder::POPSegmentOp(OpcodeArgs, uint32_t SegmentReg) {
const auto SrcSize = OpSizeFromSrc(Op);
const auto DstSize = OpSizeFromDst(Op);
auto NewSegment = Pop(SrcSize);
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, es_idx));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, cs_idx));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
// Unset the 'active' bit in the packed TF, skipping the single step exception after this instruction
SetRFLAG<FEXCore::X86State::RFLAG_TF_RAW_LOC>(_And(OpSize::i32Bit, GetRFLAG(FEXCore::X86State::RFLAG_TF_RAW_LOC), Constant(1)));
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, ss_idx));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, ds_idx));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX:
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, fs_idx));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX:
_StoreContextGPR(DstSize, NewSegment, offsetof(FEXCore::Core::CPUState, gs_idx));
break;
default: break; // Do nothing
}
UpdatePrefixFromSegment(NewSegment, SegmentReg);
}
void OpDispatchBuilder::LEAVEOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto OperandSize = (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_OPERAND_SIZE) ? OpSize::i16Bit : GPRSize;
// First we move RBP in to RSP and then behave effectively like a pop
auto SP = _RMWHandle(LoadGPRRegister(X86State::REG_RBP));
auto NewGPR = Pop(OperandSize, SP);
// Store the new stack pointer
StoreGPRRegister(X86State::REG_RSP, SP, OperandSize);
// Store what we loaded to RBP
StoreGPRRegister(X86State::REG_RBP, NewGPR, OperandSize);
}
void OpDispatchBuilder::CALLOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
BlockSetRIP = true;
// Call instruction only uses up to 32-bit signed displacement
const int64_t TargetOffset = Op->Src[0].Literal();
const auto ConstantPC = GetRelocatedPC(Op);
// Push the return address.
Push(GPRSize, ConstantPC);
if (TargetOffset != 0) {
// Store the RIP
const uint64_t NextRIP = Op->PC + Op->InstSize;
ExitRelocatedPC(Op, TargetOffset, BranchHint::Call, ConstantPC, [&]() {
auto CallReturnJumpTarget = JumpTargets.find(NextRIP);
if (CallReturnJumpTarget != JumpTargets.end() && CallReturnJumpTarget->second.IsEntryPoint) {
return CallReturnJumpTarget->second.BlockEntry;
}
return InvalidNode;
}());
} else {
NeedsBlockEnd = true;
}
}
void OpDispatchBuilder::CALLAbsoluteOp(OpcodeArgs) {
BlockSetRIP = true;
const auto Size = OpSizeFromSrc(Op);
Ref JMPPCOffset = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
// Push the return address.
auto ConstantPC = GetRelocatedPC(Op);
Push(Size, ConstantPC);
// Store the RIP
const uint64_t NextRIP = Op->PC + Op->InstSize;
ExitFunction(JMPPCOffset, BranchHint::Call, ConstantPC, [&]() {
auto CallReturnJumpTarget = JumpTargets.find(NextRIP);
if (CallReturnJumpTarget != JumpTargets.end() && CallReturnJumpTarget->second.IsEntryPoint) {
return CallReturnJumpTarget->second.BlockEntry;
}
return InvalidNode;
}());
}
std::optional<CondClass> OpDispatchBuilder::DecodeNZCVCondition(uint8_t OP) {
switch (OP) {
case 0x0: { // JO - Jump if OF == 1
return CondClass::FU;
}
case 0x1: { // JNO - Jump if OF == 0
return CondClass::FNU;
}
case 0x2: { // JC - Jump if CF == 1
return CFInverted ? CondClass::ULT : CondClass::UGE;
}
case 0x3: { // JNC - Jump if CF == 0
return CFInverted ? CondClass::UGE : CondClass::ULT;
}
case 0x4: { // JE - Jump if ZF == 1
return CondClass::EQ;
}
case 0x5: { // JNE - Jump if ZF == 0
return CondClass::NEQ;
}
case 0x6: { // JNA - Jump if CF == 1 || ZF == 1
// With CF, we want (C == 0 || Z == 1). By De Morgan's, that's
// equivalent to !(C == 1 && Z == 0). That's .ls
RectifyCarryInvert(true);
return CondClass::ULE;
}
case 0x7: { // JA - Jump if CF == 0 && ZF == 0
// With CF inverted, we want (C == 1 && Z == 0). That's .hi
RectifyCarryInvert(true);
return CondClass::UGT;
}
case 0x8: { // JS - Jump if SF == 1
return CondClass::MI;
}
case 0x9: { // JNS - Jump if SF == 0
return CondClass::PL;
}
case 0xC: { // SF <> OF
return CondClass::SLT;
}
case 0xD: { // SF = OF
return CondClass::SGE;
}
case 0xE: { // ZF = 1 || SF <> OF
return CondClass::SLE;
}
case 0xF: { // ZF = 0 && SF = OF
return CondClass::SGT;
}
default:
// Other conditions do not map directly, caller gets to deal with it.
return std::nullopt;
}
}
static bool ParityJumpIsJP(uint8_t OP) {
LOGMAN_THROW_A_FMT(OP == 0xA || OP == 0xB, "JP or JNP");
return OP == 0xA;
}
Ref OpDispatchBuilder::SelectCC0All1(uint8_t OP) {
if (auto Cond = DecodeNZCVCondition(OP); Cond) {
// Use raw select since DecodeNZCVCondition handles the carry invert
return _NZCVSelect(OpSize::i64Bit, *Cond, _InlineConstant(~0ULL), _InlineConstant(0));
} else {
// Raw value contains inverted PF in bottom bit
return _Sbfe(OpSize::i64Bit, 1, 0, LoadPFRaw(false, ParityJumpIsJP(OP)));
}
}
void OpDispatchBuilder::SETccOp(OpcodeArgs) {
CalculateDeferredFlags();
Ref SrcCond;
if (auto Cond = DecodeNZCVCondition(Op->OP & 0xf); Cond) {
// Use raw select since DecodeNZCVCondition handles the carry invert
SrcCond = _NZCVSelect01(*Cond);
} else {
SrcCond = LoadPFRaw(true, ParityJumpIsJP(Op->OP & 0xf));
}
StoreResultGPR(Op, SrcCond);
}
void OpDispatchBuilder::CMOVOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto OP = Op->OP & 0xF;
const auto ResultSize = std::max(OpSize::i32Bit, OpSizeFromSrc(Op));
CalculateDeferredFlags();
// Destination is always a GPR.
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GPRSize, Op->Flags);
Ref Src {}, SrcCond {};
if (Op->Src[0].IsGPR()) {
Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], GPRSize, Op->Flags);
} else {
Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
}
if (auto Cond = DecodeNZCVCondition(OP); Cond) {
// Use raw select since DecodeNZCVCondition handles the carry invert
SrcCond = _NZCVSelect(ResultSize, *Cond, Src, Dest);
} else {
// Raw value contains inverted PF in bottom bit
Ref Cmp = LoadPFRaw(false, ParityJumpIsJP(OP));
SaveNZCV();
// Because we're only clobbering NZCV internally, we ignore all carry flag
// shenanigans and just use the raw test and raw select.
_TestNZ(OpSize::i32Bit, Cmp, _InlineConstant(1));
SrcCond = _NZCVSelect(ResultSize, CondClass::NEQ, Src, Dest);
}
StoreResultGPR(Op, SrcCond);
}
void OpDispatchBuilder::CondJUMPOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
// Jump instruction only uses up to 32-bit signed displacement
int64_t TargetOffset = Op->Src[0].Literal();
uint64_t InstRIP = Op->PC + Op->InstSize;
uint64_t Target = InstRIP + TargetOffset;
if (GetGPROpSize() == OpSize::i32Bit) {
// If the GPRSize is 4 then we need to be careful about PC wrapping
if (TargetOffset < 0 && -TargetOffset > InstRIP) {
// Invert the signed value if we are underflowing
TargetOffset = 0x1'0000'0000ULL + TargetOffset;
} else if (TargetOffset >= 0 && Target >= 0x1'0000'0000ULL) {
// We are overflowing, wrap around
TargetOffset = TargetOffset - 0x1'0000'0000ULL;
}
Target &= 0xFFFFFFFFU;
}
FlushRegisterCache();
auto TrueBlock = JumpTargets.find(Target);
auto FalseBlock = JumpTargets.find(Op->PC + Op->InstSize);
auto CurrentBlock = GetCurrentBlock();
{
IRPair<IR::IROp_CondJump> CondJump_;
auto OP = Op->OP & 0xF;
auto Cond = DecodeNZCVCondition(OP);
if (Cond) {
CondJump_ = CondJumpNZCV(*Cond);
} else {
LOGMAN_THROW_A_FMT(OP == 0xA || OP == 0xB, "only PF left");
CondJump_ = CondJumpBit(LoadPFRaw(false, false), 0, OP == 0xB);
}
// Taking branch block
if (TrueBlock != JumpTargets.end()) {
SetTrueJumpTarget(CondJump_, TrueBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
auto JumpTarget = CreateNewCodeBlockAtEnd();
SetTrueJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Store the new RIP
ExitRelocatedPC(Op, TargetOffset);
}
// Failure to take branch
if (FalseBlock != JumpTargets.end()) {
SetFalseJumpTarget(CondJump_, FalseBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
// Place it after this block for fallthrough optimization
auto JumpTarget = CreateNewCodeBlockAfter(CurrentBlock);
SetFalseJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Leave block & store the new RIP
ExitRelocatedPC(Op);
}
}
}
void OpDispatchBuilder::CondJUMPRCXOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
auto JcxGPRSize = GetGPROpSize();
JcxGPRSize = (Op->Flags & X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) ? (JcxGPRSize >> 1) : JcxGPRSize;
uint64_t Target = Op->PC + Op->InstSize + Op->Src[0].Literal();
Ref CondReg = LoadGPRRegister(X86State::REG_RCX, JcxGPRSize);
auto TrueBlock = JumpTargets.find(Target);
auto FalseBlock = JumpTargets.find(Op->PC + Op->InstSize);
auto CurrentBlock = GetCurrentBlock();
{
auto CondJump_ = CondJump(CondReg, CondClass::EQ);
// Taking branch block
if (TrueBlock != JumpTargets.end()) {
SetTrueJumpTarget(CondJump_, TrueBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
auto JumpTarget = CreateNewCodeBlockAtEnd();
SetTrueJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Store the new RIP
ExitRelocatedPC(Op, Op->Src[0].Literal());
}
// Failure to take branch
if (FalseBlock != JumpTargets.end()) {
SetFalseJumpTarget(CondJump_, FalseBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
// Place it after the current block for fallthrough behavior
auto JumpTarget = CreateNewCodeBlockAfter(CurrentBlock);
SetFalseJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Leave block & store the new RIP
ExitRelocatedPC(Op);
}
}
}
void OpDispatchBuilder::LoopOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
bool CheckZF = Op->OP != 0xE2;
bool ZFTrue = Op->OP == 0xE1;
BlockSetRIP = true;
auto SrcSize = (Op->Flags & X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) ? OpSize::i32Bit : OpSize::i64Bit;
auto OpSize = SrcSize == OpSize::i64Bit ? OpSize::i64Bit : OpSize::i32Bit;
if (!Is64BitMode) {
// RCX size is 32-bit or 16-bit when executing in 32-bit mode.
SrcSize = IR::SizeToOpSize(IR::OpSizeToSize(SrcSize) >> 1);
OpSize = OpSize::i32Bit;
}
uint64_t Target = Op->PC + Op->InstSize + Op->Src[1].Literal();
Ref CondReg = LoadSourceGPR_WithOpSize(Op, Op->Src[0], SrcSize, Op->Flags);
CondReg = Sub(OpSize, CondReg, 1);
StoreResultGPR(Op, Op->Src[0], CondReg);
// If LOOPE then jumps to target if RCX != 0 && ZF == 1
// If LOOPNE then jumps to target if RCX != 0 && ZF == 0
//
// To handle efficiently, smash RCX to zero if ZF is wrong (1 csel).
if (CheckZF) {
const auto cond = ZFTrue ? CondClass::EQ : CondClass::NEQ;
CondReg = NZCVSelect(OpSize, cond, CondReg, _InlineConstant(0));
}
CalculateDeferredFlags();
auto TrueBlock = JumpTargets.find(Target);
auto FalseBlock = JumpTargets.find(Op->PC + Op->InstSize);
{
auto CondJump_ = CondJump(CondReg);
// Taking branch block
if (TrueBlock != JumpTargets.end()) {
SetTrueJumpTarget(CondJump_, TrueBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
auto JumpTarget = CreateNewCodeBlockAtEnd();
SetTrueJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Store the new RIP
ExitRelocatedPC(Op, Op->Src[1].Literal());
}
// Failure to take branch
if (FalseBlock != JumpTargets.end()) {
SetFalseJumpTarget(CondJump_, FalseBlock->second.BlockEntry);
} else {
// Make sure to start a new block after ending this one
// Place after this block for fallthrough behavior
auto JumpTarget = CreateNewCodeBlockAfter(GetCurrentBlock());
SetFalseJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
// Leave block & store the new RIP
ExitRelocatedPC(Op);
}
}
}
void OpDispatchBuilder::JUMPOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
// Jump instruction only uses up to 32-bit signed displacement
int64_t TargetOffset = Op->Src[0].Literal();
uint64_t InstRIP = Op->PC + Op->InstSize;
uint64_t TargetRIP = InstRIP + TargetOffset;
if (GetGPROpSize() == OpSize::i32Bit) {
// If the GPRSize is 4 then we need to be careful about PC wrapping
if (TargetOffset < 0 && -TargetOffset > InstRIP) {
// Invert the signed value if we are underflowing
TargetOffset = 0x1'0000'0000ULL + TargetOffset;
} else if (TargetOffset >= 0 && TargetRIP >= 0x1'0000'0000ULL) {
// We are overflowing, wrap around
TargetOffset = TargetOffset - 0x1'0000'0000ULL;
}
TargetRIP &= 0xFFFFFFFFU;
}
CalculateDeferredFlags();
// This is just an unconditional relative literal jump
if (Multiblock) {
auto JumpBlock = JumpTargets.find(TargetRIP);
if (JumpBlock != JumpTargets.end()) {
Jump(GetNewJumpBlock(TargetRIP));
} else {
// If the block isn't a jump target then we need to create an exit block
auto Jump_ = Jump();
// Place after this block for fallthrough behavior
auto JumpTarget = CreateNewCodeBlockAfter(GetCurrentBlock());
SetJumpTarget(Jump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
ExitRelocatedPC(Op, TargetOffset);
}
} else {
ExitRelocatedPC(Op, TargetOffset);
}
}
void OpDispatchBuilder::JUMPAbsoluteOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
// This is just an unconditional jump
// This uses ModRM to determine its location
// No way to use this effectively in multiblock
auto RIPOffset = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
// Store the new RIP
ExitFunction(RIPOffset);
}
void OpDispatchBuilder::JUMPFARIndirectOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
// This is just an unconditional jump
// This uses ModRM to determine its location
// No way to use this effectively in multiblock
Ref Src = MakeSegmentAddress(Op, Op->Dest);
AddressMode SrcCS = {.Base = Src, .Offset = 4, .AddrSize = OpSize::i64Bit};
auto RIPOffset = _LoadMemGPRAutoTSO(OpSize::i32Bit, Src, OpSize::i8Bit);
auto NewSegmentCS = _LoadMemGPRAutoTSO(OpSize::i16Bit, SrcCS, OpSize::i8Bit);
// Set up the new CSSegment.
_StoreContextGPR(OpSize::i16Bit, NewSegmentCS, offsetof(FEXCore::Core::CPUState, cs_idx));
UpdatePrefixFromSegment(NewSegmentCS, FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX);
// Store the new RIP
ExitFunction(RIPOffset);
}
void OpDispatchBuilder::CALLFARIndirectOp(OpcodeArgs) {
const auto SrcSize = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REX_WIDENING ? OpSize::i64Bit : OpSize::i32Bit;
// Calculate flags early.
CalculateDeferredFlags();
BlockSetRIP = true;
Ref Src = MakeSegmentAddress(Op, Op->Dest);
AddressMode SrcCS = {.Base = Src, .Offset = 4, .AddrSize = OpSize::i64Bit};
auto RIPOffset = _LoadMemGPRAutoTSO(OpSize::i32Bit, Src, OpSize::i8Bit);
auto NewSegmentCS = _LoadMemGPRAutoTSO(OpSize::i16Bit, SrcCS, OpSize::i8Bit);
auto CurrentCS = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, cs_idx));
auto NewRIP = GetRelocatedPC(Op);
// Push the current CS
Push(SrcSize, CurrentCS);
// Push the return address.
Push(SrcSize, NewRIP);
// Set up the new CSSegment.
_StoreContextGPR(OpSize::i16Bit, NewSegmentCS, offsetof(FEXCore::Core::CPUState, cs_idx));
UpdatePrefixFromSegment(NewSegmentCS, FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX);
// Store the new RIP
ExitFunction(RIPOffset);
}
void OpDispatchBuilder::RETFARIndirectOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto SrcSize = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REX_WIDENING ? OpSize::i64Bit : OpSize::i32Bit;
Ref SP = _RMWHandle(LoadGPRRegister(X86State::REG_RSP));
Ref NewRIP = Pop(SrcSize, SP);
Ref NewSegmentCS = Pop(SrcSize, SP);
// Optional SP offset.
if (Op->Src[0].IsLiteral()) {
SP = Add(GPRSize, SP, Op->Src[0].Literal());
}
// Store the new stack pointer
StoreGPRRegister(X86State::REG_RSP, SP);
_StoreContextGPR(OpSize::i16Bit, NewSegmentCS, offsetof(FEXCore::Core::CPUState, cs_idx));
UpdatePrefixFromSegment(NewSegmentCS, FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX);
// Store the new RIP
ExitFunction(NewRIP);
BlockSetRIP = true;
}
void OpDispatchBuilder::TESTOp(OpcodeArgs, uint32_t SrcIndex) {
// TEST is an instruction that does an AND between the sources
// Result isn't stored in result, only writes to flags
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.AllowUpperGarbage = true});
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromDst(Op);
LOGMAN_THROW_A_FMT(Size >= IR::OpSize::i8Bit && Size <= IR::OpSize::i64Bit, "Invalid size");
uint64_t Const;
bool AlwaysNonnegative = false;
if (IsValueConstant(WrapNode(Src), &Const)) {
// Optimize out masking constants
if (Const == (Size == OpSize::i64Bit ? ~0ULL : ((1ull << IR::OpSizeAsBits(Size)) - 1))) {
Src = Dest;
}
// Optimize test with non-sign bits
AlwaysNonnegative = (Const & (1ull << (IR::OpSizeAsBits(Size) - 1))) == 0;
}
if (Dest == Src) {
// Optimize out the AND.
SetNZP_ZeroCV(Size, Src);
} else if (Size < OpSize::i32Bit && AlwaysNonnegative) {
// If we know the result is always nonnegative, we can use a 32-bit test.
auto Res = _And(OpSize::i32Bit, Dest, Src);
CalculatePF(Res);
SetNZ_ZeroCV(OpSize::i32Bit, Res);
} else {
HandleNZ00Write();
CalculatePF(_AndWithFlags(Size, Dest, Src));
}
InvalidateAF();
}
void OpDispatchBuilder::ARPLOp(OpcodeArgs) {
// ARPL r/m16, r16
// If the RPL field in the destination selector is less privileged than the
// RPL field in the source selector, then adjust destination RPL to match
// source RPL and set ZF=1. Otherwise ZF=0 and destination is unchanged.
//
// Only ZF is modified by ARPL.
constexpr auto Size = OpSize::i16Bit;
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, Size, Op->Flags, {.AllowUpperGarbage = true});
Ref Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], Size, Op->Flags, {.AllowUpperGarbage = true});
// RPL is the low two bits of the selector.
Ref DestRPL = _Bfe(OpSize::i32Bit, 2, 0, Dest);
Ref SrcRPL = _Bfe(OpSize::i32Bit, 2, 0, Src);
// NeedUpdate is 1 when DestRPL < SrcRPL, else 0.
Ref NeedUpdate = _Select(OpSize::i32Bit, OpSize::i32Bit, CondClass::ULT, DestRPL, SrcRPL, Constant(1), Constant(0));
SetRFLAG<FEXCore::X86State::RFLAG_ZF_RAW_LOC>(NeedUpdate);
// Compute adjusted destination selector: (Dest & ~3) | SrcRPL.
auto NewDest = _Bfxil(OpSize::i32Bit, 2, 0, Dest, SrcRPL);
// Conditionally select updated selector based on NeedUpdate.
Ref FinalDest = _Select(OpSize::i32Bit, OpSize::i32Bit, CondClass::NEQ, NeedUpdate, Constant(0), NewDest, Dest);
StoreResultGPR_WithOpSize(Op, Op->Dest, FinalDest, Size);
}
void OpDispatchBuilder::MOVSXDOp(OpcodeArgs) {
// This instruction is a bit special
// if SrcSize == 2
// Then lower 16 bits of destination is written without changing the upper 48 bits
// else /* Size == 4 */
// if REX_WIDENING:
// Sext(32, Src)
// else
// Zext(32, Src)
//
auto Size = std::min<IR::OpSize>(OpSize::i32Bit, OpSizeFromSrc(Op));
bool Sext = (Size != OpSize::i16Bit) && Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REX_WIDENING;
Ref Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], Size, Op->Flags, {.AllowUpperGarbage = Sext});
if (Size == OpSize::i16Bit) {
// This'll make sure to insert in to the lower 16bits without modifying upper bits
StoreResultGPR_WithOpSize(Op, Op->Dest, Src, Size);
} else if (Sext) {
// With REX.W then Sext
Src = _Sbfe(OpSize::i64Bit, IR::OpSizeAsBits(Size), 0, Src);
StoreResultGPR(Op, Src);
} else {
// Without REX.W then Zext (store result implicitly zero extends)
StoreResultGPR(Op, Src);
}
}
void OpDispatchBuilder::MOVSXOp(OpcodeArgs) {
// Load garbage in upper bits, since we're sign extending anyway
const auto Size = OpSizeFromSrc(Op);
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
// Sign-extend to DstSize and zero-extend to the register size, using a fast
// path for 32-bit dests where the native 32-bit Sbfe zero extends the top.
const auto DstSize = OpSizeFromDst(Op);
Src = _Sbfe(DstSize == OpSize::i64Bit ? OpSize::i64Bit : OpSize::i32Bit, IR::OpSizeAsBits(Size), 0, Src);
StoreResultGPR(Op, Op->Dest, Src);
}
void OpDispatchBuilder::MOVZXOp(OpcodeArgs) {
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
// Store result implicitly zero extends
StoreResultGPR(Op, Src);
}
void OpDispatchBuilder::CMPOp(OpcodeArgs, uint32_t SrcIndex) {
// CMP is an instruction that does a SUB between the sources
// Result isn't stored in result, only writes to flags
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.AllowUpperGarbage = true});
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
CalculateFlags_SUB(OpSizeFromSrc(Op), Dest, Src);
}
void OpDispatchBuilder::CQOOp(OpcodeArgs) {
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Size = OpSizeFromSrc(Op);
Ref Upper = _Sbfe(std::max(OpSize::i32Bit, Size), 1, GetSrcBitSize(Op) - 1, Src);
StoreResultGPR(Op, Upper);
}
void OpDispatchBuilder::XCHGOp(OpcodeArgs) {
// Load both the source and the destination
if (Op->OP == 0x90 && Op->Src[0].IsGPR() && Op->Src[0].Data.GPR.GPR == FEXCore::X86State::REG_RAX && Op->Dest.IsGPR() &&
Op->Dest.Data.GPR.GPR == FEXCore::X86State::REG_RAX) {
// This is one heck of a sucky special case
// If we are the 0x90 XCHG opcode (Meaning source is GPR RAX)
// and destination register is ALSO RAX
// and in this very specific case we are 32bit or above
// Then this is a no-op
// This is because 0x90 without a prefix is technically `xchg eax, eax`
// But this would result in a zext on 64bit, which would ruin the no-op nature of the instruction
// So x86-64 spec mandates this special case that even though it is a 32bit instruction and
// is supposed to zext the result, it is a true no-op
//
// x86 spec text here:
//
// XCHG (E)AX, (E)AX (encoded instruction byte is 90H) is an alias for
// NOP regardless of data size prefixes, including REX.W.
//
// Note that also includes 16-bit so we don't gate this on size. The
// sequence (66 90) is a valid two-byte nop that we also ignore.
if (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX) {
// If this instruction has a REP prefix then this is architecturally
// defined to be a `PAUSE` instruction. On older processors this ends up
// being a true `REP NOP` which is why they stuck this here.
_Yield();
}
return;
}
// AllowUpperGarbage: OK to allow as it will be overwritten by StoreResult.
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
if (DestIsMem(Op)) {
HandledLock = (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_LOCK) != 0;
Ref Dest = MakeSegmentAddress(Op, Op->Dest);
if (IsMonoBackpatcherBlock) {
_MonoBackpatcherWrite(OpSizeFromSrc(Op), Src, Dest);
} else {
auto Result = _AtomicSwap(OpSizeFromSrc(Op), Src, Dest);
StoreResultGPR(Op, Op->Src[0], Result);
}
} else {
// AllowUpperGarbage: OK to allow as it will be overwritten by StoreResult.
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
// Swap the contents
// Order matters here since we don't want to swap context contents for one that effects the other
StoreResultGPR(Op, Op->Dest, Src);
StoreResultGPR(Op, Op->Src[0], Dest);
}
}
void OpDispatchBuilder::CDQOp(OpcodeArgs) {
const auto DstSize = OpSizeFromDst(Op);
const auto SrcSize = DstSize / 2;
Ref Src = LoadGPRRegister(X86State::REG_RAX, SrcSize, 0, true);
Src = _Sbfe(DstSize <= OpSize::i32Bit ? OpSize::i32Bit : OpSize::i64Bit, IR::OpSizeAsBits(SrcSize), 0, Src);
StoreResultGPR_WithOpSize(Op, Op->Dest, Src, DstSize);
}
void OpDispatchBuilder::SAHFOp(OpcodeArgs) {
// Extract AH
Ref Src = LoadGPRRegister(X86State::REG_RAX, OpSize::i8Bit, 8);
// Clear bits that aren't supposed to be set
Src = _Andn(OpSize::i64Bit, Src, Constant(0b101000));
// Set the bit that is always set here
Src = _Or(OpSize::i64Bit, Src, _InlineConstant(0b10));
// Store the lower 8 bits in to RFLAGS
SetPackedRFLAG(true, Src);
}
void OpDispatchBuilder::LAHFOp(OpcodeArgs) {
// Load the lower 8 bits of the Rflags register
auto RFLAG = GetPackedRFLAG(0xFF);
// Store the lower 8 bits of the rflags register in to AH
StoreGPRRegister(X86State::REG_RAX, RFLAG, OpSize::i8Bit, 8);
}
void OpDispatchBuilder::FLAGControlOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
switch (Op->OP) {
case 0xF5: // CMC
CarryInvert();
break;
case 0xF8: // CLC
SetCFInverted(Constant(1));
break;
case 0xF9: // STC
SetCFInverted(Constant(0));
break;
case 0xFC: // CLD
// Transformed
StoreDF(Constant(1));
break;
case 0xFD: // STD
StoreDF(Constant(-1));
break;
}
}
void OpDispatchBuilder::MOVSegOp(OpcodeArgs, bool ToSeg) {
// In x86-64 mode the accesses to the segment registers end up being constant zero moves
// Aside from FS/GS
// In x86-64 mode the accesses to segment registers can actually still touch the segments
// These write to the selector portion of the register
//
// FS and GS are specially handled here though
// AMD documentation is /wrong/ in this regard
// AMD documentation claims that the MOV to SReg and POP SReg registers will load a 32bit
// value in to the HIDDEN portions of the FS and GS registers /OR/ ignored if a null selector is
// selected for the registers
// This statement is actually untrue, the instructions will /actually/ load 16bits in to the selector portion of the register!
// Tested on a Zen+ CPU, the selector is the portion that is modified!
// We don't currently support FS/GS selector modifying, so this needs to be asserted out
// The loads here also load the selector, NOT the base
if (ToSeg) {
Ref Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], OpSize::i16Bit, Op->Flags);
switch (Op->Dest.Data.GPR.GPR) {
case FEXCore::X86State::REG_RAX: // ES
case FEXCore::X86State::REG_R8: // ES
_StoreContextGPR(OpSize::i16Bit, Src, offsetof(FEXCore::Core::CPUState, es_idx));
UpdatePrefixFromSegment(Src, FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX);
break;
case FEXCore::X86State::REG_RBX: // DS
case FEXCore::X86State::REG_R11: // DS
_StoreContextGPR(OpSize::i16Bit, Src, offsetof(FEXCore::Core::CPUState, ds_idx));
UpdatePrefixFromSegment(Src, FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX);
break;
case FEXCore::X86State::REG_RCX: // CS
case FEXCore::X86State::REG_R9: // CS
// CPL3 can't write to this
Break(FEXCore::IR::BreakDefinition {
.ErrorRegister = 0,
.Signal = SIGILL,
.TrapNumber = 0,
.si_code = 0,
});
break;
case FEXCore::X86State::REG_RDX: // SS
case FEXCore::X86State::REG_R10: // SS
_StoreContextGPR(OpSize::i16Bit, Src, offsetof(FEXCore::Core::CPUState, ss_idx));
UpdatePrefixFromSegment(Src, FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX);
break;
case FEXCore::X86State::REG_RBP: // GS
case FEXCore::X86State::REG_R13: // GS
if (!Is64BitMode) {
_StoreContextGPR(OpSize::i16Bit, Src, offsetof(FEXCore::Core::CPUState, gs_idx));
UpdatePrefixFromSegment(Src, FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX);
} else {
LogMan::Msg::EFmt("We don't support modifying GS selector in 64bit mode!");
DecodeFailure = true;
}
break;
case FEXCore::X86State::REG_RSP: // FS
case FEXCore::X86State::REG_R12: // FS
if (!Is64BitMode) {
_StoreContextGPR(OpSize::i16Bit, Src, offsetof(FEXCore::Core::CPUState, fs_idx));
UpdatePrefixFromSegment(Src, FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX);
} else {
LogMan::Msg::EFmt("We don't support modifying FS selector in 64bit mode!");
DecodeFailure = true;
}
break;
default: UnimplementedOp(Op); return;
}
} else {
Ref Segment {};
switch (Op->Src[0].Data.GPR.GPR) {
case FEXCore::X86State::REG_RAX: // ES
case FEXCore::X86State::REG_R8: // ES
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, es_idx));
break;
case FEXCore::X86State::REG_RBX: // DS
case FEXCore::X86State::REG_R11: // DS
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, ds_idx));
break;
case FEXCore::X86State::REG_RCX: // CS
case FEXCore::X86State::REG_R9: // CS
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, cs_idx));
break;
case FEXCore::X86State::REG_RDX: // SS
case FEXCore::X86State::REG_R10: // SS
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, ss_idx));
break;
case FEXCore::X86State::REG_RBP: // GS
case FEXCore::X86State::REG_R13: // GS
if (Is64BitMode) {
Segment = Constant(0);
} else {
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, gs_idx));
}
break;
case FEXCore::X86State::REG_RSP: // FS
case FEXCore::X86State::REG_R12: // FS
if (Is64BitMode) {
Segment = Constant(0);
} else {
Segment = _LoadContextGPR(OpSize::i16Bit, offsetof(FEXCore::Core::CPUState, fs_idx));
}
break;
default: UnimplementedOp(Op); return;
}
if (DestIsMem(Op)) {
// If the destination is memory then we always store 16-bits only
StoreResultGPR_WithOpSize(Op, Op->Dest, Segment, OpSize::i16Bit);
} else {
// If the destination is a GPR then we follow register storing rules
StoreResultGPR(Op, Segment);
}
}
}
void OpDispatchBuilder::MOVOffsetOp(OpcodeArgs) {
switch (Op->OP) {
case 0xA0:
case 0xA1: {
// Source is memory(literal)
// Dest is GPR
auto Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.ForceLoad = true});
StoreResultGPR(Op, Op->Dest, Src);
break;
}
case 0xA2:
case 0xA3: {
// Source is GPR
// Dest is memory(literal)
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
// This one is a bit special since the destination is a literal
// So the destination gets stored in Src[1]
StoreResultGPR(Op, Op->Src[1], Src);
break;
}
}
}
void OpDispatchBuilder::CPUIDOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
Ref Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], GPRSize, Op->Flags);
Ref Leaf = LoadGPRRegister(X86State::REG_RCX);
Ref RAX = _AllocateGPR(false);
Ref RBX = _AllocateGPR(false);
Ref RCX = _AllocateGPR(false);
Ref RDX = _AllocateGPR(false);
_CPUID(Src, Leaf, RAX, RBX, RCX, RDX);
StoreGPRRegister(X86State::REG_RAX, RAX);
StoreGPRRegister(X86State::REG_RBX, RBX);
StoreGPRRegister(X86State::REG_RCX, RCX);
StoreGPRRegister(X86State::REG_RDX, RDX);
}
uint32_t OpDispatchBuilder::GetConstantShift(X86Tables::DecodedOp Op, bool Is1Bit) {
if (Is1Bit) {
return 1;
} else {
// x86 masks the shift by 0x3F or 0x1F depending on size of op
const auto Size = OpSizeFromSrc(Op);
uint64_t Mask = Size == OpSize::i64Bit ? 0x3F : 0x1F;
return Op->Src[1].Literal() & Mask;
}
}
void OpDispatchBuilder::XGetBVOp(OpcodeArgs) {
Ref Function = LoadGPRRegister(X86State::REG_RCX);
auto RAX = _AllocateGPR(false);
auto RDX = _AllocateGPR(false);
_XGetBV(Function, RAX, RDX);
StoreGPRRegister(X86State::REG_RAX, RAX);
StoreGPRRegister(X86State::REG_RDX, RDX);
}
void OpDispatchBuilder::SHLOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
auto Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
auto Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
Ref Result = _Lshl(Size == OpSize::i64Bit ? OpSize::i64Bit : OpSize::i32Bit, Dest, Src);
HandleShift(Op, Result, Dest, ShiftType::LSL, Src);
}
void OpDispatchBuilder::SHLImmediateOp(OpcodeArgs, bool SHL1Bit) {
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
uint64_t Shift = GetConstantShift(Op, SHL1Bit);
const auto Size = GetSrcBitSize(Op);
Ref Result = _Lshl(Size == 64 ? OpSize::i64Bit : OpSize::i32Bit, Dest, Constant(Shift));
CalculateFlags_ShiftLeftImmediate(OpSizeFromSrc(Op), Result, Dest, Shift);
CalculateDeferredFlags();
StoreResultGPR(Op, Result);
}
void OpDispatchBuilder::SHROp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
auto Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= OpSize::i32Bit});
auto Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
auto ALUOp = _Lshr(std::max(OpSize::i32Bit, Size), Dest, Src);
HandleShift(Op, ALUOp, Dest, ShiftType::LSR, Src);
}
void OpDispatchBuilder::SHRImmediateOp(OpcodeArgs, bool SHR1Bit) {
const auto Size = GetSrcBitSize(Op);
auto Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= 32});
uint64_t Shift = GetConstantShift(Op, SHR1Bit);
auto ALUOp = _Lshr(Size == 64 ? OpSize::i64Bit : OpSize::i32Bit, Dest, Constant(Shift));
CalculateFlags_ShiftRightImmediate(OpSizeFromSrc(Op), ALUOp, Dest, Shift);
CalculateDeferredFlags();
StoreResultGPR(Op, ALUOp);
}
void OpDispatchBuilder::SHLDOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
const auto Size = GetSrcBitSize(Op);
// Allow garbage on the Src if it will be ignored by the Lshr below
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = Size >= 32});
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
// Allow garbage on the shift, we're masking it anyway.
Ref Shift = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
// x86 masks the shift by 0x3F or 0x1F depending on size of op.
if (Size == 64) {
Shift = _And(OpSize::i64Bit, Shift, _InlineConstant(0x3F));
} else {
Shift = _And(OpSize::i64Bit, Shift, _InlineConstant(0x1F));
}
// a64 masks the bottom bits, so if we're using a native 32/64-bit shift, we
// can negate to do the subtract (it's congruent), which saves a constant.
auto ShiftRight = Size >= 32 ? _Neg(OpSize::i64Bit, Shift) : Sub(OpSize::i64Bit, Constant(Size), Shift);
auto Tmp1 = _Lshl(OpSize::i64Bit, Dest, Shift);
auto Tmp2 = _Lshr(Size == 64 ? OpSize::i64Bit : OpSize::i32Bit, Src, ShiftRight);
Ref Res = _Or(OpSize::i64Bit, Tmp1, Tmp2);
// If shift count was zero then output doesn't change
// Needs to be checked for the 32bit operand case
// where shift = 0 and the source register still gets Zext
//
// TODO: With a backwards pass ahead-of-time, we could stick this in the
// if(shift) used for flags.
//
// TODO: This whole function wants to be wrapped in the if. Maybe b/w pass is
// a good idea after all.
Res = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::EQ, Shift, Constant(0), Dest, Res);
HandleShift(Op, Res, Dest, ShiftType::LSL, Shift);
}
void OpDispatchBuilder::SHLDImmediateOp(OpcodeArgs) {
uint64_t Shift = GetConstantShift(Op, false);
const auto Size = GetSrcBitSize(Op);
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = Size >= 32});
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= 32});
if (Shift != 0) {
Ref Res {};
if (Size < 32) {
Ref ShiftLeft = Constant(Shift);
auto ShiftRight = Size - Shift;
auto Tmp1 = _Lshl(OpSize::i64Bit, Dest, ShiftLeft);
Ref Tmp2 = ShiftRight ? _Lshr(OpSize::i32Bit, Src, Constant(ShiftRight)) : Src;
Res = _Or(OpSize::i64Bit, Tmp1, Tmp2);
} else {
// 32-bit and 64-bit SHLD behaves like an EXTR where the lower bits are filled from the source.
Res = _Extr(OpSizeFromSrc(Op), Dest, Src, Size - Shift);
}
CalculateFlags_ShiftLeftImmediate(OpSizeFromSrc(Op), Res, Dest, Shift);
CalculateDeferredFlags();
StoreResultGPR(Op, Res);
} else if (Shift == 0 && Size == 32) {
// Ensure Zext still occurs
StoreResultGPR(Op, Dest);
}
}
void OpDispatchBuilder::SHRDOp(OpcodeArgs) {
// Calculate flags early.
// This instruction conditionally generates flags so we need to insure sane state going in.
CalculateDeferredFlags();
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
Ref Shift = LoadGPRRegister(X86State::REG_RCX);
const auto Size = GetDstBitSize(Op);
// x86 masks the shift by 0x3F or 0x1F depending on size of op
if (Size == 64) {
Shift = _And(OpSize::i64Bit, Shift, _InlineConstant(0x3F));
} else {
Shift = _And(OpSize::i64Bit, Shift, _InlineConstant(0x1F));
}
auto ShiftLeft = Sub(OpSize::i64Bit, Constant(Size), Shift);
auto Tmp1 = _Lshr(Size == 64 ? OpSize::i64Bit : OpSize::i32Bit, Dest, Shift);
auto Tmp2 = _Lshl(OpSize::i64Bit, Src, ShiftLeft);
Ref Res = _Or(OpSize::i64Bit, Tmp1, Tmp2);
// If shift count was zero then output doesn't change
// Needs to be checked for the 32bit operand case
// where shift = 0 and the source register still gets Zext
Res = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::EQ, Shift, Constant(0), Dest, Res);
HandleShift(Op, Res, Dest, ShiftType::LSR, Shift);
}
void OpDispatchBuilder::SHRDImmediateOp(OpcodeArgs) {
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
uint64_t Shift = GetConstantShift(Op, false);
const auto Size = GetSrcBitSize(Op);
if (Shift != 0) {
Ref Res {};
if (Size < 32) {
Ref ShiftRight = Constant(Shift);
auto ShiftLeft = Constant(Size - Shift);
auto Tmp1 = _Lshr(OpSize::i32Bit, Dest, ShiftRight);
auto Tmp2 = _Lshl(OpSize::i64Bit, Src, ShiftLeft);
Res = _Or(OpSize::i64Bit, Tmp1, Tmp2);
} else {
// 32-bit and 64-bit SHRD behaves like an EXTR where the upper bits are filled from the source.
Res = _Extr(OpSizeFromSrc(Op), Src, Dest, Shift);
}
StoreResultGPR(Op, Res);
CalculateFlags_ShiftRightDoubleImmediate(OpSizeFromSrc(Op), Res, Dest, Shift);
} else if (Shift == 0 && Size == 32) {
// Ensure Zext still occurs
StoreResultGPR(Op, Dest);
}
}
void OpDispatchBuilder::ASHROp(OpcodeArgs, bool Immediate, bool SHR1Bit) {
const auto Size = OpSizeFromSrc(Op);
const auto OpSize = std::max(OpSize::i32Bit, OpSizeFromDst(Op));
// If Size < 4, then we Sbfe the Dest so we can have garbage.
// Otherwise, if Size = Opsize, then both are 4 or 8 and match the a64
// semantics directly, so again we can have garbage. The only case where we
// need zero-extension here is when the sizes mismatch.
auto Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = (OpSize == Size) || (Size < OpSize::i32Bit)});
if (Size < OpSize::i32Bit) {
Dest = _Sbfe(OpSize::i64Bit, IR::OpSizeAsBits(Size), 0, Dest);
}
if (Immediate) {
uint64_t Shift = GetConstantShift(Op, SHR1Bit);
Ref Result = _Ashr(OpSize, Dest, Constant(Shift));
CalculateFlags_SignShiftRightImmediate(OpSizeFromSrc(Op), Result, Dest, Shift);
CalculateDeferredFlags();
StoreResultGPR(Op, Result);
} else {
auto Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
Ref Result = _Ashr(OpSize, Dest, Src);
HandleShift(Op, Result, Dest, ShiftType::ASR, Src);
}
}
void OpDispatchBuilder::RotateOp(OpcodeArgs, bool Left, bool IsImmediate, bool Is1Bit) {
CalculateDeferredFlags();
const uint32_t Size = GetSrcBitSize(Op);
const auto OpSize = Size == 64 ? OpSize::i64Bit : OpSize::i32Bit;
uint64_t UnmaskedConst {};
// x86 masks the shift by 0x3F or 0x1F depending on size of op. But it's
// equivalent to mask to the actual size of the op, that way we can bound
// things tighter for 8-bit later in the function.
uint64_t Mask = Size == 8 ? 7 : (Size == 64 ? 0x3F : 0x1F);
ArithRef UnmaskedSrc;
if (Is1Bit || IsImmediate) {
UnmaskedConst = GetConstantShift(Op, Is1Bit);
UnmaskedSrc = ARef(UnmaskedConst);
} else {
UnmaskedSrc = ARef(LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true}));
}
auto Src = UnmaskedSrc.And(Mask);
// We fill the upper bits so we allow garbage on load.
auto Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
if (Size < 32) {
// ARM doesn't support 8/16bit rotates. Emulate with an insert
// StoreResult truncates back to a 8/16 bit value
Dest = _Bfi(OpSize, Size, Left ? (32 - Size) : Size, Dest, Dest);
}
// To rotate 64-bits left, right-rotate by (64 - Shift) = -Shift mod 64.
auto Res = _Ror(OpSize, Dest, (Left ? Src.Neg() : Src).Ref());
StoreResultGPR(Op, Res);
if (Is1Bit || IsImmediate) {
if (UnmaskedSrc.C) {
// Extract the last bit shifted in to CF
SetCFDirect(Res, Left ? 0 : Size - 1, true);
// For ROR, OF is the XOR of the new CF bit and the most significant bit of the result.
// For ROL, OF is the LSB and MSB XOR'd together.
// OF is architecturally only defined for 1-bit rotate.
if (UnmaskedSrc.C == 1) {
auto NewOF = _XorShift(OpSize, Res, Res, ShiftType::LSR, Left ? Size - 1 : 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(NewOF, Left ? 0 : Size - 2, true);
}
}
} else {
HandleNZCVWrite();
RectifyCarryInvert(true);
// We deferred the masking for 8-bit to the flag section, do it here.
if (Size == 8) {
Src = UnmaskedSrc.And(0x1F);
}
_RotateFlags(OpSizeFromSrc(Op), Res, Src.Ref(), Left);
}
}
void OpDispatchBuilder::ANDNBMIOp(OpcodeArgs) {
auto* Src1 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Src2 = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
auto Dest = _Andn(OpSizeFromSrc(Op), Src2, Src1);
StoreResultGPR(Op, Dest);
CalculateFlags_Logical(OpSizeFromSrc(Op), Dest);
}
void OpDispatchBuilder::BEXTRBMIOp(OpcodeArgs) {
// Essentially (Src1 >> Start) & ((1 << Length) - 1)
// along with some edge-case handling and flag setting.
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
auto* Src1 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Src2 = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromSrc(Op);
const auto SrcSize = IR::OpSizeAsBits(Size);
const auto MaxSrcBit = SrcSize - 1;
auto MaxSrcBitOp = Constant(MaxSrcBit);
// Shift the operand down to the starting bit
auto Start = _Bfe(OpSizeFromSrc(Op), 8, 0, Src2);
auto Shifted = _Lshr(Size, Src1, Start);
// Shifts larger than operand size need to be set to zero.
auto SanitizedShifted = _Select(Size, Size, CondClass::ULE, Start, MaxSrcBitOp, Shifted, Constant(0));
// Now handle the length specifier.
auto Length = _Bfe(Size, 8, 8, Src2);
// Now build up the mask
// (1 << Length) - 1 = ~(~0 << Length)
auto AllOnes = Constant(~0ull);
auto InvertedMask = _Lshl(Size, AllOnes, Length);
// Now put it all together and make the result.
auto Masked = _Andn(Size, SanitizedShifted, InvertedMask);
// Sanitize the length. If it is above the max, we don't do the masking.
auto Dest = _Select(Size, Size, CondClass::ULE, Length, MaxSrcBitOp, Masked, SanitizedShifted);
// Finally store the result.
StoreResultGPR(Op, Dest);
// ZF is set properly. CF and OF are defined as being set to zero. SF, PF, and
// AF are undefined.
SetNZ_ZeroCV(GetOpSize(Dest), Dest);
InvalidatePF_AF();
}
void OpDispatchBuilder::BLSIBMIOp(OpcodeArgs) {
// Equivalent to performing: SRC & -SRC
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
const auto Size = OpSizeFromSrc(Op);
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto NegatedSrc = _Neg(Size, Src);
auto Result = _And(Size, Src, NegatedSrc);
StoreResultGPR(Op, Result);
// CF is cleared if Src is zero, otherwise it's set. However, Src is zero iff
// Result is zero, so we can test the result instead. So, CF is just the
// inverted ZF.
//
// ZF/SF/OF set as usual.
SetNZ_ZeroCV(Size, Result);
InvalidatePF_AF();
SetCFInverted(GetRFLAG(X86State::RFLAG_ZF_RAW_LOC));
}
void OpDispatchBuilder::BLSMSKBMIOp(OpcodeArgs) {
// Equivalent to: (Src - 1) ^ Src
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
const auto Size = OpSizeFromSrc(Op);
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Result = _Xor(Size, Sub(Size, Src, 1), Src);
StoreResultGPR(Op, Result);
InvalidatePF_AF();
// CF set according to the Src
auto CFInv = To01(OpSize::i64Bit, Src);
// The output of BLSMSK is always nonzero, so TST will clear Z (along with C
// and O) while setting S.
SetNZ_ZeroCV(Size, Result);
SetCFInverted(CFInv);
}
void OpDispatchBuilder::BLSRBMIOp(OpcodeArgs) {
// Equivalent to: (Src - 1) & Src
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
const auto Size = OpSizeFromSrc(Op);
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Result = _And(Size, Sub(Size, Src, 1), Src);
StoreResultGPR(Op, Result);
auto CFInv = To01(OpSize::i64Bit, Src);
SetNZ_ZeroCV(Size, Result);
SetCFInverted(CFInv);
InvalidatePF_AF();
}
// Handles SARX, SHLX, and SHRX
void OpDispatchBuilder::BMI2Shift(OpcodeArgs) {
// In the event the source is a memory operand, use the
// exact width instead of the GPR size.
const auto GPRSize = GetGPROpSize();
const auto Size = OpSizeFromSrc(Op);
const auto SrcSize = Op->Src[0].IsGPR() ? GPRSize : Size;
auto* Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], SrcSize, Op->Flags);
auto* Shift = LoadSourceGPR_WithOpSize(Op, Op->Src[1], GPRSize, Op->Flags, {.AllowUpperGarbage = true});
Ref Result;
if (Op->OP == 0x6F7) {
// SARX
Result = _Ashr(Size, Src, Shift);
} else if (Op->OP == 0x5F7) {
// SHLX
Result = _Lshl(Size, Src, Shift);
} else {
// SHRX
Result = _Lshr(Size, Src, Shift);
}
StoreResultGPR(Op, Result);
}
void OpDispatchBuilder::BZHI(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
const auto OperandSize = IR::OpSizeAsBits(Size);
// In 32-bit mode we only look at bottom 32-bit, no 8 or 16-bit BZHI so no
// need to zero-extend sources
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Index = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
// Clear the high bits specified by the index. A64 only considers bottom bits
// of the shift, so we don't need to mask bottom 8-bits ourselves.
// Out-of-bounds results ignored after.
auto Mask = _Lshl(Size, Constant(-1), Index);
auto MaskResult = _Andn(Size, Src, Mask);
// If the index is above OperandSize, we don't clear anything. BZHI only
// considers the bottom 8-bits, so we really want to know if the bottom 8-bits
// have their top bits set. Test exactly that.
//
// Because we're clobbering flags internally we ignore all carry invert
// shenanigans and use the raw versions here.
_TestNZ(OpSize::i64Bit, Index, Constant(0xFF & ~(OperandSize - 1)));
auto Result = _NZCVSelect(Size, CondClass::NEQ, Src, MaskResult);
StoreResultGPR(Op, Result);
auto CFInv = _NZCVSelect01(CondClass::EQ);
InvalidatePF_AF();
SetNZ_ZeroCV(Size, Result);
SetCFInverted(CFInv);
}
void OpDispatchBuilder::RORX(OpcodeArgs) {
const auto SrcSize = OpSizeFromSrc(Op);
const auto SrcSizeBits = IR::OpSizeAsBits(SrcSize);
const auto Amount = Op->Src[1].Literal() & (SrcSizeBits - 1);
const auto GPRSize = GetGPROpSize();
const auto DoRotation = Amount != 0 && Amount < SrcSizeBits;
const auto IsSameGPR = Op->Src[0].IsGPR() && Op->Dest.IsGPR() && Op->Src[0].Data.GPR.GPR == Op->Dest.Data.GPR.GPR;
const auto SrcSizeIsGPRSize = SrcSize == GPRSize;
// If we don't need to rotate and our source is the same as the destination
// then we don't need to do anything at all. We still need to be careful,
// since 32-bit operations on 64-bit mode still need to zero-extend the
// destination register. So also compare source size and GPR size.
//
// Very unlikely, but hey, we can do nothing faster.
if (!DoRotation && IsSameGPR && SrcSizeIsGPRSize) [[unlikely]] {
return;
}
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Result = Src;
if (DoRotation) [[likely]] {
Result = _Ror(OpSizeFromSrc(Op), Src, _InlineConstant(Amount));
}
StoreResultGPR(Op, Result);
}
void OpDispatchBuilder::MULX(OpcodeArgs) {
// RDX is the implied source operand in the instruction
const auto OpSize = OpSizeFromSrc(Op);
// Src1 can be a memory operand, so ensure we constrain to the
// absolute width of the access in that scenario.
const auto GPRSize = GetGPROpSize();
const auto Src1Size = Op->Src[1].IsGPR() ? GPRSize : OpSize;
Ref Src1 = LoadSourceGPR_WithOpSize(Op, Op->Src[1], Src1Size, Op->Flags);
Ref Src2 = LoadGPRRegister(X86State::REG_RDX, GPRSize);
// As per the Intel Software Development Manual, if the destination and
// first operand correspond to the same register, then the result
// will be the high half of the multiplication result.
if (Op->Dest.Data.GPR.GPR == Op->Src[0].Data.GPR.GPR) {
Ref ResultHi = _UMulH(OpSize, Src1, Src2);
StoreResultGPR(Op, Op->Dest, ResultHi);
} else {
Ref ResultLo = _UMul(OpSize, Src1, Src2);
Ref ResultHi = _UMulH(OpSize, Src1, Src2);
StoreResultGPR(Op, Op->Src[0], ResultLo);
StoreResultGPR(Op, Op->Dest, ResultHi);
}
}
void OpDispatchBuilder::PDEP(OpcodeArgs) {
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
auto* Input = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Mask = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
auto Result = _PDep(OpSizeFromSrc(Op), Input, Mask);
StoreResultGPR(Op, Op->Dest, Result);
}
void OpDispatchBuilder::PEXT(OpcodeArgs) {
LOGMAN_THROW_A_FMT(Op->InstSize >= 4, "No masking needed");
auto* Input = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Mask = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
auto Result = _PExt(OpSizeFromSrc(Op), Input, Mask);
StoreResultGPR(Op, Op->Dest, Result);
}
void OpDispatchBuilder::ADXOp(OpcodeArgs) {
const auto OpSize = OpSizeFromSrc(Op);
// Only 32/64-bit anyway so allow garbage, we use 32-bit ops.
auto* Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto* Before = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
// Handles ADCX and ADOX
const bool IsADCX = Op->OP == 0x1F6;
auto Zero = Constant(0);
// Before we go trashing NZCV, save the current NZCV state.
Ref OldNZCV = GetNZCV();
// We want to use arm64 adc. For ADOX, copy the overflow flag into CF. For
// ADCX, we just rectify the carry.
if (IsADCX) {
RectifyCarryInvert(false);
} else {
// If overflow, 0 - 0 sets carry. Else, forces carry to 0.
_CondSubNZCV(OpSize::i32Bit, Zero, Zero, CondClass::FU, 0x0 /* nzcv */);
}
// Do the actual add.
HandleNZCV_RMW();
auto Result = _AdcWithFlags(OpSize, Src, Before);
StoreResultGPR(Op, Result);
// Now restore all flags except the one we're updating.
if (CTX->HostFeatures.SupportsFlagM) {
// For ADOX, we need to copy the new carry into the overflow flag. If carry is clear (ULT with uninverted
// carry), 0 - 0 clears overflow. Else, force overflow on.
if (!IsADCX) {
_CondSubNZCV(OpSize::i32Bit, Zero, Zero, CondClass::ULT, 0x1 /* nzcV */);
}
_RmifNZCV(OldNZCV, 28, IsADCX ? 0xd /* NzcV */ : 0xe /* NZCv */);
} else {
// For either operation, insert the new flag into the old NZCV.
bool SavedCFInvert = CFInverted;
CFInverted = false;
Ref OutputCF = GetRFLAG(X86State::RFLAG_CF_RAW_LOC, IsADCX);
CFInverted = IsADCX ? true : SavedCFInvert;
Ref NewNZCV = _Bfi(OpSize::i32Bit, 1, IsADCX ? 29 : 28, OldNZCV, OutputCF);
SetNZCV(NewNZCV);
}
}
void OpDispatchBuilder::RCROp1Bit(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
// We expliclty mask for <32-bit so allow garbage
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
const auto Size = GetSrcBitSize(Op);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
Ref Res;
// Our new CF will be bit 0 of the source. Set upfront to avoid a move.
SetCFDirect(Dest, 0, true);
uint32_t Shift = 1;
if (Size == 32 || Size == 64) {
// Rotate and insert CF in the upper bit
Res = _Extr(OpSizeFromSrc(Op), CF, Dest, Shift);
} else {
// Res = Src >> Shift
Res = _Bfe(OpSize::i32Bit, Size - Shift, Shift, Dest);
// inject the CF
Res = _Orlshl(OpSize::i32Bit, Res, CF, Size - Shift);
}
StoreResultGPR(Op, Res);
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(_XorShift(OpSize::i64Bit, Res, Res, ShiftType::LSR, 1), Size - 2, true);
}
void OpDispatchBuilder::RCROp8x1Bit(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
const auto SizeBit = GetSrcBitSize(Op);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Our new CF will be bit (Shift - 1) of the source
SetCFDirect(Dest, 0, true);
// Rotate and insert CF in the upper bit
Ref Res = _Bfe(OpSize::i32Bit, 7, 1, Dest);
Res = _Bfi(OpSize::i32Bit, 1, 7, Res, CF);
StoreResultGPR(Op, Res);
// OF is the top two MSBs XOR'd together
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(_XorShift(OpSize::i32Bit, Res, Res, ShiftType::LSR, 1), SizeBit - 2, true);
}
void OpDispatchBuilder::RCROp(OpcodeArgs) {
const auto Size = GetSrcBitSize(Op);
if (Size == 8 || Size == 16) {
RCRSmallerOp(Op);
return;
}
const auto Mask = (Size == 64) ? 0x3F : 0x1F;
// Calculate flags early.
CalculateDeferredFlags();
const auto OpSize = OpSizeFromSrc(Op);
Ref Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
uint64_t Const;
if (IsValueConstant(WrapNode(Src), &Const)) {
Const &= Mask;
if (!Const) {
ZeroShiftResult(Op);
return;
}
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
// Res = Src >> Shift
Ref Res = _Lshr(OpSize, Dest, Src);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Constant folded version of the above, with fused shifts.
if (Const > 1) {
Res = _Orlshl(OpSize, Res, Dest, Size + 1 - Const);
}
// Our new CF will be bit (Shift - 1) of the source.
SetCFDirect(Dest, Const - 1, true);
// Since shift != 0 we can inject the CF
Res = _Orlshl(OpSize, Res, CF, Size - Const);
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
if (Const == 1) {
auto Xor = _XorShift(OpSize, Res, Res, ShiftType::LSR, 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(Xor, Size - 2, true);
}
StoreResultGPR(Op, Res);
return;
}
Ref SrcMasked = _And(OpSize, Src, _InlineConstant(Mask));
Calculate_ShiftVariable(
Op, SrcMasked,
[this, Op, Size, OpSize]() {
// Rematerialize loads to avoid crossblock liveness
Ref Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
// Res = Src >> Shift
Ref Res = _Lshr(OpSize, Dest, Src);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Res |= (Dest << (Size - Shift + 1));
// Expressed as Res | ((Src << (Size - Shift)) << 1) to get correct
// behaviour for Shift without clobbering NZCV. Then observe that modulo
// Size, Size - Shift = -Shift so we can use a simple Neg.
//
// The masking of Lshl means we don't need mask the source, since:
//
// -(x & Mask) & Mask = (-x) & Mask
Ref NegSrc = _Neg(OpSize, Src);
Res = _Orlshl(OpSize, Res, _Lshl(OpSize, Dest, NegSrc), 1);
// Our new CF will be bit (Shift - 1) of the source. this is hoisted up to
// avoid the need to copy the source. Again, the Lshr absorbs the masking.
auto NewCF = _Lshr(OpSize, Dest, Sub(OpSize, Src, 1));
SetCFDirect(NewCF, 0, true);
// Since shift != 0 we can inject the CF
Res = _Or(OpSize, Res, _Lshl(OpSize, CF, NegSrc));
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
auto Xor = _XorShift(OpSize, Res, Res, ShiftType::LSR, 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(Xor, Size - 2, true);
StoreResultGPR(Op, Res);
},
OpSizeFromSrc(Op) == OpSize::i32Bit ? std::make_optional(&OpDispatchBuilder::ZeroShiftResult) : std::nullopt);
}
void OpDispatchBuilder::RCRSmallerOp(OpcodeArgs) {
CalculateDeferredFlags();
const auto Size = GetSrcBitSize(Op);
// x86 masks the shift by 0x3F or 0x1F depending on size of op
auto Src = ARef(LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true}));
Src = Src.And(0x1F);
// CF only changes if we actually shifted. OF undefined if we didn't shift.
// The result is unchanged if we didn't shift. So branch over the whole thing.
Calculate_ShiftVariable(Op, Src.Ref(), [this, Op, Size]() {
// Rematerialized to avoid crossblock liveness
auto Src = ARef(LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true}));
Src = Src.And(0x1F);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
Ref Tmp {};
// Insert the incoming value across the temporary 64bit source
// Make sure to insert at <BitSize> + 1 offsets
// We need to cover 32bits plus the amount that could rotate in
if (Size == 8) {
// 8-bit optimal cascade
// Cascade: 0
// Data: -> [7:0]
// CF: -> [8:8]
// Cascade: 1
// Data: -> [16:9]
// CF: -> [17:17]
// Cascade: 2
// Data: -> [25:18]
// CF: -> [26:26]
// Cascade: 3
// Data: -> [34:27]
// CF: -> [35:35]
// Cascade: 4
// Data: -> [43:36]
// CF: -> [44:44]
// Insert CF, Destination already at [7:0]
Tmp = _Bfi(OpSize::i64Bit, 1, 8, Dest, CF);
// First Cascade, copies 9 bits from itself.
Tmp = _Bfi(OpSize::i64Bit, 9, 9, Tmp, Tmp);
// Second cascade, copies 18 bits from itself.
Tmp = _Bfi(OpSize::i64Bit, 18, 18, Tmp, Tmp);
// Final cascade, copies 9 bits again from itself.
Tmp = _Bfi(OpSize::i64Bit, 9, 36, Tmp, Tmp);
} else {
// 16-bit optimal cascade
// Cascade: 0
// Data: -> [15:0]
// CF: -> [16:16]
// Cascade: 1
// Data: -> [32:17]
// CF: -> [33:33]
// Cascade: 2
// Data: -> [49:34]
// CF: -> [50:50]
// Insert CF, Destination already at [15:0]
Tmp = _Bfi(OpSize::i64Bit, 1, 16, Dest, CF);
// First Cascade, copies 17 bits from itself.
Tmp = _Bfi(OpSize::i64Bit, 17, 17, Tmp, Tmp);
// Final Cascade, copies 17 bits from itself again.
Tmp = _Bfi(OpSize::i64Bit, 17, 34, Tmp, Tmp);
}
// Entire bitfield has been setup. Just extract the 8 or 16bits we need.
// 64-bit shift used because we want to rotate in our cascaded upper bits
// rather than zeroes.
Ref Res = _Lshr(OpSize::i64Bit, Tmp, Src.Ref());
StoreResultGPR(Op, Res);
// Our new CF will be bit (Shift - 1) of the source. 32-bit Lshr masks the
// same as x86, but if we constant fold we must mask ourselves.
if (Src.IsConstant) {
SetCFDirect(Tmp, (Src.C & 0x1f) - 1, true);
} else {
auto NewCF = _Lshr(OpSize::i32Bit, Tmp, Sub(OpSize::i32Bit, Src.Ref(), 1));
SetCFDirect(NewCF, 0, true);
}
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
if (!Src.IsConstant || Src.C == 1) {
auto NewOF = _XorShift(OpSize::i32Bit, Res, Res, ShiftType::LSR, 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(NewOF, Size - 2, true);
}
});
}
void OpDispatchBuilder::RCLOp1Bit(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
const auto Size = GetSrcBitSize(Op);
const auto OpSize = Size == 64 ? OpSize::i64Bit : OpSize::i32Bit;
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Rotate left and insert CF in to lowest bit
// TODO: Use `adc Res, xzr, Dest, lsl 1` to save an instruction
Ref Res = _Orlshl(OpSize, CF, Dest, 1);
// Our new CF will be the top bit of the source
SetCFDirect(Dest, Size - 1, true);
// OF is the top two MSBs XOR'd together
// Top two MSBs is CF and top bit of result
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(_Xor(OpSize, Res, Dest), Size - 1, true);
StoreResultGPR(Op, Res);
}
void OpDispatchBuilder::RCLOp(OpcodeArgs) {
const auto Size = GetSrcBitSize(Op);
if (Size == 8 || Size == 16) {
RCLSmallerOp(Op);
return;
}
const auto Mask = (Size == 64) ? 0x3F : 0x1F;
// Calculate flags early.
CalculateDeferredFlags();
Ref Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
const auto OpSize = OpSizeFromSrc(Op);
uint64_t Const;
if (IsValueConstant(WrapNode(Src), &Const)) {
Const &= Mask;
if (!Const) {
ZeroShiftResult(Op);
return;
}
// Res = Src << Shift
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Res = _Lshl(OpSize, Dest, Src);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Res |= (Src << (Size - Shift + 1));
if (Const > 1) {
Res = _Orlshr(OpSize, Res, Dest, Size + 1 - Const);
}
// Our new CF will be bit (Shift - 1) of the source
SetCFDirect(Dest, Size - Const, true);
// Since Shift != 0 we can inject the CF
Res = _Orlshl(OpSize, Res, CF, Const - 1);
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
if (Const == 1) {
auto NewOF = _Xor(OpSize, Res, Dest);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(NewOF, Size - 1, true);
}
StoreResultGPR(Op, Res);
return;
}
Ref SrcMasked = _And(OpSize, Src, _InlineConstant(Mask));
Calculate_ShiftVariable(
Op, SrcMasked,
[this, Op, Size, OpSize]() {
// Rematerialized to avoid crossblock liveness
Ref Src = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
// Res = Src << Shift
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Res = _Lshl(OpSize, Dest, Src);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
// Res |= (Dest >> (Size - Shift + 1)), expressed as
// Res | ((Dest >> (-Shift)) >> 1), since Size - Shift = -Shift mod
// Size. The shift aborbs the masking.
auto NegSrc = _Neg(OpSize, Src);
Res = _Orlshr(OpSize, Res, _Lshr(OpSize, Dest, NegSrc), 1);
// Our new CF will be bit (Shift - 1) of the source
auto NewCF = _Lshr(OpSize, Dest, NegSrc);
SetCFDirect(NewCF, 0, true);
// Since Shift != 0 we can inject the CF. Shift absorbs the masking.
Ref CFShl = Sub(OpSize, Src, 1);
auto TmpCF = _Lshl(OpSize, CF, CFShl);
Res = _Or(OpSize, Res, TmpCF);
// OF is the top two MSBs XOR'd together
// Only when Shift == 1, it is undefined otherwise
//
// Note that NewCF has garbage in the upper bits, but we ignore them here
// and mask as part of the set after.
auto NewOF = _XorShift(OpSize, Res, NewCF, ShiftType::LSL, Size - 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(NewOF, Size - 1, true);
StoreResultGPR(Op, Res);
},
OpSizeFromSrc(Op) == OpSize::i32Bit ? std::make_optional(&OpDispatchBuilder::ZeroShiftResult) : std::nullopt);
}
void OpDispatchBuilder::RCLSmallerOp(OpcodeArgs) {
CalculateDeferredFlags();
const auto Size = GetSrcBitSize(Op);
// x86 masks the shift by 0x3F or 0x1F depending on size of op
auto Src = ARef(LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true}));
Src = Src.And(0x1F);
// CF only changes if we actually shifted. OF undefined if we didn't shift.
// The result is unchanged if we didn't shift. So branch over the whole thing.
Calculate_ShiftVariable(Op, Src.Ref(), [this, Op, Size]() {
// Rematerialized to avoid crossblock liveness
auto Src = ARef(LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true}));
Src = Src.And(0x1F);
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
Ref Tmp = Constant(0);
for (size_t i = 0; i < (32 + Size + 1); i += (Size + 1)) {
// Insert incoming value
Tmp = _Bfi(OpSize::i64Bit, Size, 63 - i - Size, Tmp, Dest);
// Insert CF
Tmp = _Bfi(OpSize::i64Bit, 1, 63 - i, Tmp, CF);
}
// Insert incoming value
Tmp = _Bfi(OpSize::i64Bit, Size, 0, Tmp, Dest);
// The data is now set up like this
// [Data][CF]:[Data][CF]:[Data][CF]:[Data][CF]
// Shift 1 more bit that expected to get our result
// Shifting to the right will now behave like a rotate to the left
// Which we emulate with a _Ror
Ref Res = _Ror(OpSize::i64Bit, Tmp, Src.Neg().Ref());
StoreResultGPR(Op, Res);
// Our new CF is now at the bit position that we are shifting
// Either 0 if CF hasn't changed (CF is living in bit 0)
// or higher
auto NewCF = _Ror(OpSize::i64Bit, Tmp, Src.Presub(63).Ref());
SetCFDirect(NewCF, 0, true);
// OF is the XOR of the NewCF and the MSB of the result
// Only defined for 1-bit rotates.
if (!Src.IsConstant || Src.C == 1) {
auto NewOF = _XorShift(OpSize::i64Bit, NewCF, Res, ShiftType::LSR, Size - 1);
SetRFLAG<FEXCore::X86State::RFLAG_OF_RAW_LOC>(NewOF, 0, true);
}
});
}
void OpDispatchBuilder::BTOp(OpcodeArgs, uint32_t SrcIndex, BTAction Action) {
Ref Value;
ArithRef Src;
bool IsNonconstant = Op->Src[SrcIndex].IsGPR();
const uint32_t Size = GetDstBitSize(Op);
const uint32_t Mask = Size - 1;
if (IsNonconstant) {
// Because we mask explicitly with And/Bfe/Sbfe after, we can allow garbage here.
Src = ARef(LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.AllowUpperGarbage = true}));
} else {
// Can only be an immediate
// Masked by operand size
Src = ARef(Op->Src[SrcIndex].Literal() & Mask);
}
if (Op->Dest.IsGPR()) {
// When the destination is a GPR, we don't care about garbage in the upper bits.
// Load the full register.
auto Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GetGPROpSize(), Op->Flags);
Value = Dest;
// Get the bit selection from the src. We need to mask for 8/16-bit, but
// rely on the implicit masking of Lshr for native sizes.
unsigned LshrSize = std::max<uint8_t>(IR::OpSizeToSize(OpSize::i32Bit), Size / 8);
auto BitSelect = (Size == (LshrSize * 8)) ? Src : Src.And(Mask);
auto LshrOpSize = IR::SizeToOpSize(LshrSize);
// OF/SF/AF/PF undefined. ZF must be preserved. We choose to preserve OF/SF
// too since we just use an rmif to insert into CF directly. We could
// optimize perhaps.
//
// Set CF before the action to save a move, except for complements where we
// can reuse the invert.
if (Action != BTAction::BTComplement) {
if (IsNonconstant) {
Value = _Lshr(IR::SizeToOpSize(LshrSize), Value, BitSelect.Ref());
}
SetRFLAG(Value, X86State::RFLAG_CF_RAW_LOC, Src.IsConstant ? Src.C : 0, true);
CFInverted = false;
}
switch (Action) {
case BTAction::BTNone: {
/* Nothing to do */
break;
}
case BTAction::BTClear: {
Dest = _Andn(LshrOpSize, Dest, BitSelect.MaskBit(LshrOpSize).Ref());
StoreResultGPR(Op, Dest);
break;
}
case BTAction::BTSet: {
Dest = _Or(LshrOpSize, Dest, BitSelect.MaskBit(LshrOpSize).Ref());
StoreResultGPR(Op, Dest);
break;
}
case BTAction::BTComplement: {
Dest = _Xor(LshrOpSize, Dest, BitSelect.MaskBit(LshrOpSize).Ref());
if (IsNonconstant) {
Value = _Lshr(LshrOpSize, Dest, BitSelect.Ref());
} else {
Value = Dest;
}
SetRFLAG(Value, X86State::RFLAG_CF_RAW_LOC, Src.IsConstant ? Src.C : 0, true);
CFInverted = true;
StoreResultGPR(Op, Dest);
break;
}
}
} else {
// Load the address to the memory location
Ref Dest = MakeSegmentAddress(Op, Op->Dest);
// Get the bit selection from the src
auto BitSelect = Src.Bfe(0, 3);
// Address is provided as bits we want BYTE offsets
// Extract Signed offset
Src = Src.Sbfe(3, Size - 3);
// Get the address offset by shifting out the size of the op (To shift out the bit selection)
// Then use that to index in to the memory location by size of op
AddressMode Address = {.Base = Dest, .Index = Src.Ref(), .AddrSize = OpSize::i64Bit};
switch (Action) {
case BTAction::BTNone: {
Value = _LoadMemGPRAutoTSO(OpSize::i8Bit, Address, OpSize::i8Bit);
break;
}
case BTAction::BTClear: {
Ref BitMask = BitSelect.MaskBit(OpSize::i64Bit).Ref();
if (DestIsLockedMem(Op)) {
HandledLock = true;
Value = _AtomicFetchCLR(OpSize::i8Bit, BitMask, LoadEffectiveAddress(this, Address, GetGPROpSize(), true));
} else {
Value = _LoadMemGPRAutoTSO(OpSize::i8Bit, Address, OpSize::i8Bit);
auto Modified = _Andn(OpSize::i64Bit, Value, BitMask);
_StoreMemGPRAutoTSO(OpSize::i8Bit, Address, Modified, OpSize::i8Bit);
}
break;
}
case BTAction::BTSet: {
Ref BitMask = BitSelect.MaskBit(OpSize::i64Bit).Ref();
if (DestIsLockedMem(Op)) {
HandledLock = true;
Value = _AtomicFetchOr(OpSize::i8Bit, BitMask, LoadEffectiveAddress(this, Address, GetGPROpSize(), true));
} else {
Value = _LoadMemGPRAutoTSO(OpSize::i8Bit, Address, OpSize::i8Bit);
auto Modified = _Or(OpSize::i64Bit, Value, BitMask);
_StoreMemGPRAutoTSO(OpSize::i8Bit, Address, Modified, OpSize::i8Bit);
}
break;
}
case BTAction::BTComplement: {
Ref BitMask = BitSelect.MaskBit(OpSize::i64Bit).Ref();
if (DestIsLockedMem(Op)) {
HandledLock = true;
Value = _AtomicFetchXor(OpSize::i8Bit, BitMask, LoadEffectiveAddress(this, Address, GetGPROpSize(), true));
} else {
Value = _LoadMemGPRAutoTSO(OpSize::i8Bit, Address, OpSize::i8Bit);
auto Modified = _Xor(OpSize::i64Bit, Value, BitMask);
_StoreMemGPRAutoTSO(OpSize::i8Bit, Address, Modified, OpSize::i8Bit);
}
break;
}
}
// Now shift in to the correct bit location
if (!BitSelect.IsDefinitelyZero()) {
Value = _Lshr(std::max(OpSize::i32Bit, GetOpSize(Value)), Value, BitSelect.Ref());
}
// OF/SF/ZF/AF/PF undefined.
SetCFDirect(Value, 0, true);
}
}
void OpDispatchBuilder::IMUL1SrcOp(OpcodeArgs) {
/* We're just going to sign-extend the non-garbage anyway.. */
Ref Src1 = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Src2 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromSrc(Op);
const auto SizeBits = IR::OpSizeAsBits(Size);
Ref Dest {};
Ref ResultHigh {};
switch (Size) {
case OpSize::i8Bit:
case OpSize::i16Bit: {
Src1 = _Sbfe(OpSize::i64Bit, SizeBits, 0, Src1);
Src2 = _Sbfe(OpSize::i64Bit, SizeBits, 0, Src2);
Dest = _Mul(OpSize::i64Bit, Src1, Src2);
ResultHigh = _Sbfe(OpSize::i64Bit, SizeBits, SizeBits, Dest);
break;
}
case OpSize::i32Bit: {
ResultHigh = _SMull(Src1, Src2);
ResultHigh = _Sbfe(OpSize::i64Bit, SizeBits, SizeBits, ResultHigh);
// Flipped order to save a move
Dest = _Mul(OpSize::i32Bit, Src1, Src2);
break;
}
case OpSize::i64Bit: {
ResultHigh = _MulH(OpSize::i64Bit, Src1, Src2);
// Flipped order to save a move
Dest = _Mul(OpSize::i64Bit, Src1, Src2);
break;
}
default: FEX_UNREACHABLE;
}
StoreResultGPR(Op, Dest);
CalculateFlags_MUL(Size, Dest, ResultHigh);
}
void OpDispatchBuilder::IMUL2SrcOp(OpcodeArgs) {
Ref Src1 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
Ref Src2 = LoadSourceGPR(Op, Op->Src[1], Op->Flags, {.AllowUpperGarbage = true});
const auto Size = OpSizeFromSrc(Op);
const auto SizeBits = IR::OpSizeAsBits(Size);
Ref Dest {};
Ref ResultHigh {};
switch (Size) {
case OpSize::i8Bit:
case OpSize::i16Bit: {
Src1 = _Sbfe(OpSize::i64Bit, SizeBits, 0, Src1);
Src2 = ARef(Src2).Sbfe(0, SizeBits).Ref();
Dest = _Mul(OpSize::i64Bit, Src1, Src2);
ResultHigh = _Sbfe(OpSize::i64Bit, SizeBits, SizeBits, Dest);
break;
}
case OpSize::i32Bit: {
ResultHigh = _SMull(Src1, Src2);
ResultHigh = _Sbfe(OpSize::i64Bit, SizeBits, SizeBits, ResultHigh);
// Flipped order to save a move
Dest = _Mul(OpSize::i32Bit, Src1, Src2);
break;
}
case OpSize::i64Bit: {
ResultHigh = _MulH(OpSize::i64Bit, Src1, Src2);
// Flipped order to save a move
Dest = _Mul(OpSize::i64Bit, Src1, Src2);
break;
}
default: FEX_UNREACHABLE;
}
StoreResultGPR(Op, Dest);
CalculateFlags_MUL(Size, Dest, ResultHigh);
}
void OpDispatchBuilder::IMULOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
const auto SizeBits = IR::OpSizeAsBits(Size);
Ref Src1 = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Src2 = LoadGPRRegister(X86State::REG_RAX);
if (Size != OpSize::i64Bit) {
Src1 = _Sbfe(OpSize::i64Bit, SizeBits, 0, Src1);
Src2 = _Sbfe(OpSize::i64Bit, SizeBits, 0, Src2);
}
// 64-bit special cased to save a move
Ref Result = Size < OpSize::i64Bit ? _Mul(OpSize::i64Bit, Src1, Src2) : nullptr;
Ref ResultHigh {};
if (Size == OpSize::i8Bit) {
// Result is stored in AX
StoreGPRRegister(X86State::REG_RAX, Result, OpSize::i16Bit);
ResultHigh = _Sbfe(OpSize::i64Bit, 8, 8, Result);
} else if (Size == OpSize::i16Bit) {
// 16bits stored in AX
// 16bits stored in DX
StoreGPRRegister(X86State::REG_RAX, Result, Size);
ResultHigh = _Sbfe(OpSize::i64Bit, 16, 16, Result);
StoreGPRRegister(X86State::REG_RDX, ResultHigh, Size);
} else if (Size == OpSize::i32Bit) {
// 32bits stored in EAX
// 32bits stored in EDX
// Make sure they get Zext correctly
auto LocalResult = _Bfe(OpSize::i64Bit, 32, 0, Result);
auto LocalResultHigh = _Bfe(OpSize::i64Bit, 32, 32, Result);
ResultHigh = _Sbfe(OpSize::i64Bit, 32, 32, Result);
Result = _Sbfe(OpSize::i64Bit, 32, 0, Result);
StoreGPRRegister(X86State::REG_RAX, LocalResult);
StoreGPRRegister(X86State::REG_RDX, LocalResultHigh);
} else if (Size == OpSize::i64Bit) {
if (!Is64BitMode) {
LogMan::Msg::EFmt("Doesn't exist in 32bit mode");
DecodeFailure = true;
return;
}
// 64bits stored in RAX
// 64bits stored in RDX
ResultHigh = _MulH(OpSize::i64Bit, Src1, Src2);
Result = _Mul(OpSize::i64Bit, Src1, Src2);
StoreGPRRegister(X86State::REG_RAX, Result);
StoreGPRRegister(X86State::REG_RDX, ResultHigh);
}
CalculateFlags_MUL(Size, Result, ResultHigh);
}
void OpDispatchBuilder::MULOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
const auto SizeBits = IR::OpSizeAsBits(Size);
Ref Src1 = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Src2 = LoadGPRRegister(X86State::REG_RAX);
Ref Result {};
if (Size != OpSize::i64Bit) {
Src1 = _Bfe(OpSize::i64Bit, SizeBits, 0, Src1);
Src2 = _Bfe(OpSize::i64Bit, SizeBits, 0, Src2);
Result = _UMul(OpSize::i64Bit, Src1, Src2);
}
Ref ResultHigh {};
if (Size == OpSize::i8Bit) {
// Result is stored in AX
StoreGPRRegister(X86State::REG_RAX, Result, OpSize::i16Bit);
ResultHigh = _Bfe(OpSize::i64Bit, 8, 8, Result);
} else if (Size == OpSize::i16Bit) {
// 16bits stored in AX
// 16bits stored in DX
StoreGPRRegister(X86State::REG_RAX, Result, Size);
ResultHigh = _Bfe(OpSize::i64Bit, 16, 16, Result);
StoreGPRRegister(X86State::REG_RDX, ResultHigh, Size);
} else if (Size == OpSize::i32Bit) {
// 32bits stored in EAX
// 32bits stored in EDX
Ref ResultLow = _Bfe(OpSize::i64Bit, 32, 0, Result);
ResultHigh = _Bfe(OpSize::i64Bit, 32, 32, Result);
StoreGPRRegister(X86State::REG_RAX, ResultLow);
StoreGPRRegister(X86State::REG_RDX, ResultHigh);
} else if (Size == OpSize::i64Bit) {
if (!Is64BitMode) {
LogMan::Msg::EFmt("Doesn't exist in 32bit mode");
DecodeFailure = true;
return;
}
// 64bits stored in RAX
// 64bits stored in RDX
//
// Calculate high first to allow better RA.
ResultHigh = _UMulH(OpSize::i64Bit, Src1, Src2);
Result = _UMul(OpSize::i64Bit, Src1, Src2);
StoreGPRRegister(X86State::REG_RAX, Result);
StoreGPRRegister(X86State::REG_RDX, ResultHigh);
}
CalculateFlags_UMUL(ResultHigh);
}
void OpDispatchBuilder::NOTOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
const auto SizeBits = IR::OpSizeAsBits(Size);
LOGMAN_THROW_A_FMT(Size >= IR::OpSize::i8Bit && Size <= IR::OpSize::i64Bit, "Invalid size");
Ref MaskConst {};
if (Size == OpSize::i64Bit) {
MaskConst = Constant(~0ULL);
} else {
MaskConst = Constant((1ULL << SizeBits) - 1);
}
if (DestIsLockedMem(Op)) {
HandledLock = true;
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
// Result unused
_AtomicFetchXor(Size, MaskConst, DestMem);
} else if (!Op->Dest.IsGPR()) {
// GPR version plays fast and loose with sizes, be safe for memory tho.
Ref Src = LoadSourceGPR(Op, Op->Dest, Op->Flags);
Src = _Xor(OpSize::i64Bit, Src, MaskConst);
StoreResultGPR(Op, Src);
} else {
// Specially handle high bits so we can invert in place with the correct
// mask and a larger type.
auto Dest = Op->Dest;
if (Dest.Data.GPR.HighBits) {
LOGMAN_THROW_A_FMT(Size == OpSize::i8Bit, "Only 8-bit GPRs get high bits");
MaskConst = Constant(0xFF00);
Dest.Data.GPR.HighBits = false;
}
// Always load full size, we explicitly want the upper bits to get the
// insert behaviour for free/implicitly.
const auto GPRSize = GetGPROpSize();
Ref Src = LoadSourceGPR_WithOpSize(Op, Dest, GPRSize, Op->Flags);
// For 8/16-bit, use 64-bit invert so we invert in place, while getting
// insert behaviour. For 32-bit, use 32-bit invert to zero the upper bits.
const auto EffectiveSize = Size == OpSize::i32Bit ? OpSize::i32Bit : GPRSize;
// If we're inverting the whole thing, use Not instead of Xor to save a constant.
if (Size >= OpSize::i32Bit) {
Src = _Not(EffectiveSize, Src);
} else {
Src = _Xor(EffectiveSize, Src, MaskConst);
}
// Always store 64-bit, the Not/Xor correctly handle the upper bits and this
// way we can delete the store.
StoreResultGPR_WithOpSize(Op, Dest, Src, GPRSize);
}
}
void OpDispatchBuilder::XADDOp(OpcodeArgs) {
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.LoadData = false});
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
Ref Result;
if (Op->Dest.IsGPR()) {
// If this is a GPR then we can just do an Add
Result = CalculateFlags_ADD(OpSizeFromSrc(Op), Dest, Src);
// Previous value in dest gets stored in src
StoreResultGPR(Op, Op->Src[0], Dest);
// Calculated value gets stored in dst (order is important if dst is same as src)
StoreResultGPR(Op, Result);
} else {
HandledLock = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_LOCK;
Dest = AppendSegmentOffset(Dest, Op->Flags);
auto Before = _AtomicFetchAdd(OpSizeFromSrc(Op), Src, Dest);
CalculateFlags_ADD(OpSizeFromSrc(Op), Before, Src);
StoreResultGPR(Op, Op->Src[0], Before);
}
}
void OpDispatchBuilder::PopcountOp(OpcodeArgs) {
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = CTX->HostFeatures.SupportsCSSC || GetSrcSize(Op) >= 4});
Src = _Popcount(OpSizeFromSrc(Op), Src);
StoreResultGPR(Op, Src);
// We need to set ZF while clearing the rest of NZCV. The result of a popcount
// is in the range [0, 63]. In particular, it is always positive. So a
// combined NZ test will correctly zero SF/CF/OF while setting ZF.
SetNZ_ZeroCV(OpSize::i32Bit, Src);
ZeroPF_AF();
}
Ref OpDispatchBuilder::CalculateAFForDecimal(Ref A) {
auto Nibble = _And(OpSize::i64Bit, A, Constant(0xF));
auto Greater = Select01(OpSize::i64Bit, CondClass::UGT, Nibble, Constant(9));
return _Or(OpSize::i64Bit, LoadAF(), Greater);
}
void OpDispatchBuilder::DAAOp(OpcodeArgs) {
CalculateDeferredFlags();
auto AL = LoadGPRRegister(X86State::REG_RAX, OpSize::i8Bit);
auto CFInv = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC, true);
auto AF = CalculateAFForDecimal(AL);
// CF |= (AL > 0x99);
CFInv = _And(OpSize::i64Bit, CFInv, Select01(OpSize::i64Bit, CondClass::ULE, AL, Constant(0x99)));
// AL = AF ? (AL + 0x6) : AL;
AL = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::NEQ, AF, Constant(0), Add(OpSize::i64Bit, AL, 0x6), AL);
// AL = CF ? (AL + 0x60) : AL;
AL = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::EQ, CFInv, Constant(0), Add(OpSize::i64Bit, AL, 0x60), AL);
// SF, ZF, PF set according to result. CF set per above. OF undefined.
StoreGPRRegister(X86State::REG_RAX, AL, OpSize::i8Bit);
SetNZ_ZeroCV(OpSize::i8Bit, AL);
SetCFInverted(CFInv);
CalculatePF(AL);
SetAFAndFixup(AF);
}
void OpDispatchBuilder::DASOp(OpcodeArgs) {
CalculateDeferredFlags();
auto AL = LoadGPRRegister(X86State::REG_RAX, OpSize::i8Bit);
auto CF = GetRFLAG(FEXCore::X86State::RFLAG_CF_RAW_LOC);
auto AF = CalculateAFForDecimal(AL);
// CF |= (AL > 0x99);
CF = _Or(OpSize::i64Bit, CF, Select01(OpSize::i64Bit, CondClass::UGT, AL, Constant(0x99)));
// NewCF = CF | (AF && (Borrow from AL - 6))
auto NewCF = _Or(OpSize::i32Bit, CF, _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::ULT, AL, Constant(6), AF, CF));
// AL = AF ? (AL - 0x6) : AL;
AL = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::NEQ, AF, Constant(0), Sub(OpSize::i64Bit, AL, 0x6), AL);
// AL = CF ? (AL - 0x60) : AL;
AL = _Select(OpSize::i64Bit, OpSize::i64Bit, CondClass::NEQ, CF, Constant(0), Sub(OpSize::i64Bit, AL, 0x60), AL);
// SF, ZF, PF set according to result. CF set per above. OF undefined.
StoreGPRRegister(X86State::REG_RAX, AL, OpSize::i8Bit);
SetNZ_ZeroCV(OpSize::i8Bit, AL);
SetCFDirect(NewCF);
CalculatePF(AL);
SetAFAndFixup(AF);
}
void OpDispatchBuilder::AAAOp(OpcodeArgs) {
auto A = LoadGPRRegister(X86State::REG_RAX);
auto AF = CalculateAFForDecimal(A);
// CF = AF, OF/SF/ZF/PF undefined
SetCFDirect_InvalidateNZV(AF);
SetAFAndFixup(AF);
CalculateDeferredFlags();
// AX = CF ? (AX + 0x106) : 0
A = NZCVSelect(OpSize::i32Bit, CondClass::UGE /* CF = 1 */, Add(OpSize::i32Bit, A, 0x106), A);
// AL = AL & 0x0F
A = _And(OpSize::i32Bit, A, Constant(0xFF0F));
StoreGPRRegister(X86State::REG_RAX, A, OpSize::i16Bit);
}
void OpDispatchBuilder::AASOp(OpcodeArgs) {
auto A = LoadGPRRegister(X86State::REG_RAX);
auto AF = CalculateAFForDecimal(A);
// CF = AF, OF/SF/ZF/PF undefined
SetCFDirect_InvalidateNZV(AF);
SetAFAndFixup(AF);
CalculateDeferredFlags();
// AX = CF ? (AX - 0x106) : 0
A = NZCVSelect(OpSize::i32Bit, CondClass::UGE /* CF = 1 */, Sub(OpSize::i32Bit, A, 0x106), A);
// AL = AL & 0x0F
A = _And(OpSize::i32Bit, A, Constant(0xFF0F));
StoreGPRRegister(X86State::REG_RAX, A, OpSize::i16Bit);
}
void OpDispatchBuilder::AAMOp(OpcodeArgs) {
auto AL = LoadGPRRegister(X86State::REG_RAX, OpSize::i8Bit);
auto Imm8 = Constant(Op->Src[0].Literal() & 0xFF);
Ref Quotient = _AllocateGPR(true);
Ref Remainder = _AllocateGPR(true);
_UDiv(OpSize::i64Bit, AL, Invalid(), Imm8, Quotient, Remainder);
auto Res = _AddShift(OpSize::i64Bit, Remainder, Quotient, ShiftType::LSL, 8);
StoreGPRRegister(X86State::REG_RAX, Res, OpSize::i16Bit);
SetNZ_ZeroCV(OpSize::i8Bit, Res);
CalculatePF(Res);
InvalidateAF();
}
void OpDispatchBuilder::AADOp(OpcodeArgs) {
auto A = LoadGPRRegister(X86State::REG_RAX);
auto AH = _Lshr(OpSize::i32Bit, A, Constant(8));
auto Imm8 = Constant(Op->Src[0].Literal() & 0xFF);
auto NewAL = Add(OpSize::i64Bit, A, _Mul(OpSize::i64Bit, AH, Imm8));
auto Result = _And(OpSize::i64Bit, NewAL, Constant(0xFF));
StoreGPRRegister(X86State::REG_RAX, Result, OpSize::i16Bit);
SetNZ_ZeroCV(OpSize::i8Bit, Result);
CalculatePF(Result);
InvalidateAF();
}
void OpDispatchBuilder::XLATOp(OpcodeArgs) {
Ref Src = MakeSegmentAddress(X86State::REG_RBX, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
Ref Offset = LoadGPRRegister(X86State::REG_RAX, OpSize::i8Bit);
AddressMode A = {.Base = Src, .Index = Offset, .AddrSize = OpSize::i64Bit};
auto Res = _LoadMemGPRAutoTSO(OpSize::i8Bit, A, OpSize::i8Bit);
StoreGPRRegister(X86State::REG_RAX, Res, OpSize::i8Bit);
}
void OpDispatchBuilder::ReadSegmentReg(OpcodeArgs, OpDispatchBuilder::Segment Seg) {
// 64-bit only
// Doesn't hit the segment register optimization
const auto Size = OpSizeFromSrc(Op);
Ref Src {};
if (Seg == Segment::FS) {
Src = _LoadContextGPR(Size, offsetof(FEXCore::Core::CPUState, fs_cached));
} else {
Src = _LoadContextGPR(Size, offsetof(FEXCore::Core::CPUState, gs_cached));
}
StoreResultGPR(Op, Src);
}
void OpDispatchBuilder::WriteSegmentReg(OpcodeArgs, OpDispatchBuilder::Segment Seg) {
// Documentation claims that the 32-bit version of this instruction inserts in to the lower 32-bits of the segment
// This is incorrect and it instead zero extends the 32-bit value to 64-bit
const auto Size = OpSizeFromDst(Op);
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
if (Seg == Segment::FS) {
_StoreContextGPR(Size, Src, offsetof(FEXCore::Core::CPUState, fs_cached));
} else {
_StoreContextGPR(Size, Src, offsetof(FEXCore::Core::CPUState, gs_cached));
}
}
void OpDispatchBuilder::EnterOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto OperandSize = (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_OPERAND_SIZE) ? OpSize::i16Bit : GPRSize;
const uint64_t Value = Op->Src[0].Literal();
const uint16_t AllocSpace = Value & 0xFFFF;
const uint8_t Level = (Value >> 16) & 0x1F;
const auto PushValue = [&](IR::OpSize Size, Ref Src) -> Ref {
auto OldSP = LoadGPRRegister(X86State::REG_RSP);
auto NewSP = _Push(GPRSize, Size, Src, OldSP);
// Store the new stack pointer
StoreGPRRegister(X86State::REG_RSP, NewSP);
return NewSP;
};
auto OldBP = LoadGPRRegister(X86State::REG_RBP);
auto NewSP = PushValue(OperandSize, OldBP);
auto temp_RBP = NewSP;
if (Level > 0) {
for (uint8_t i = 1; i < Level; ++i) {
auto MemLoc = Sub(GPRSize, OldBP, i * IR::OpSizeToSize(OperandSize));
auto Mem = _LoadMemGPR(OperandSize, MemLoc, OperandSize);
NewSP = PushValue(OperandSize, Mem);
}
NewSP = PushValue(OperandSize, temp_RBP);
}
NewSP = Sub(GPRSize, NewSP, AllocSpace);
StoreGPRRegister(X86State::REG_RSP, NewSP);
StoreGPRRegister(X86State::REG_RBP, temp_RBP);
}
void OpDispatchBuilder::SGDTOp(OpcodeArgs) {
auto DestAddress = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.LoadData = false});
// Store an emulated value in the format of:
// uint16_t Limit;
// {uint32_t,uint64_t} Base;
//
// Limit is always 0
// Base is always in kernel space at: 0xFFFFFFFFFFFE0000ULL
//
// Operand size prefix is ignored on this instruction, size purely depends on operating mode.
uint64_t GDTAddress = 0xFFFFFFFFFFFE0000ULL;
auto GDTStoreSize = OpSize::i64Bit;
if (!Is64BitMode) {
// Mask off upper bits if 32-bit result.
GDTAddress &= ~0U;
GDTStoreSize = OpSize::i32Bit;
}
_StoreMemGPRAutoTSO(OpSize::i16Bit, DestAddress, Constant(0));
_StoreMemGPRAutoTSO(GDTStoreSize, AddressMode {.Base = DestAddress, .Offset = 2, .AddrSize = OpSize::i64Bit}, Constant(GDTAddress));
}
void OpDispatchBuilder::SIDTOp(OpcodeArgs) {
auto DestAddress = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.LoadData = false});
// See SGDTOp, matches Linux in reported values
uint64_t IDTAddress = 0xFFFFFE0000000000ULL;
auto IDTStoreSize = OpSize::i64Bit;
if (!Is64BitMode) {
// Mask off upper bits if 32-bit result.
IDTAddress &= ~0U;
IDTStoreSize = OpSize::i32Bit;
}
_StoreMemGPRAutoTSO(OpSize::i16Bit, DestAddress, Constant(0xfff));
_StoreMemGPRAutoTSO(IDTStoreSize, AddressMode {.Base = DestAddress, .Offset = 2, .AddrSize = OpSize::i64Bit}, Constant(IDTAddress));
}
void OpDispatchBuilder::SMSWOp(OpcodeArgs) {
const bool IsMemDst = DestIsMem(Op);
IR::OpSize DstSize {OpSize::iInvalid};
Ref Const = Constant((1U << 31) | ///< PG - Paging
(0U << 30) | ///< CD - Cache Disable
(0U << 29) | ///< NW - Not Writethrough (Legacy, now ignored)
///< [28:19] - Reserved
(1U << 18) | ///< AM - Alignment Mask
///< 17 - Reserved
(1U << 16) | ///< WP - Write Protect
///< [15:6] - Reserved
(1U << 5) | ///< NE - Numeric Error
(1U << 4) | ///< ET - Extension Type (Legacy, now reserved and 1)
(0U << 3) | ///< TS - Task Switched
(0U << 2) | ///< EM - Emulation
(1U << 1) | ///< MP - Monitor Coprocessor
(1U << 0)); ///< PE - Protection Enabled
const auto OpAddr = X86Tables::DecodeFlags::GetOpAddr(Op->Flags, 0);
if (Is64BitMode) {
DstSize = OpAddr == X86Tables::DecodeFlags::FLAG_OPERAND_SIZE_LAST ? OpSize::i16Bit :
OpAddr == X86Tables::DecodeFlags::FLAG_WIDENING_SIZE_LAST ? OpSize::i64Bit :
OpSize::i32Bit;
if (!IsMemDst && DstSize == OpSize::i32Bit) {
// Special-case version of `smsw ebx`. This instruction does an insert in to the lower 32-bits on 64-bit hosts.
// Override and insert.
auto Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GetGPROpSize(), Op->Flags);
Const = _Bfi(OpSize::i64Bit, 32, 0, Dest, Const);
DstSize = OpSize::i64Bit;
}
} else {
DstSize = OpAddr == X86Tables::DecodeFlags::FLAG_OPERAND_SIZE_LAST ? OpSize::i16Bit : OpSize::i32Bit;
}
if (IsMemDst) {
// Memory destinatino always writes only 16-bits.
DstSize = OpSize::i16Bit;
}
StoreResultGPR_WithOpSize(Op, Op->Dest, Const, DstSize);
}
OpDispatchBuilder::CycleCounterPair OpDispatchBuilder::CycleCounter(bool SelfSynchronizingLoads) {
Ref CounterLow {};
Ref CounterHigh {};
auto Counter = _CycleCounter(SelfSynchronizingLoads);
if (CTX->Config.TSCScale) {
CounterLow = _Lshl(OpSize::i32Bit, Counter, Constant(CTX->Config.TSCScale));
CounterHigh = _Lshr(OpSize::i64Bit, Counter, Constant(32 - CTX->Config.TSCScale));
} else {
CounterLow = _Bfe(OpSize::i64Bit, 32, 0, Counter);
CounterHigh = _Bfe(OpSize::i64Bit, 32, 32, Counter);
}
return {
.CounterLow = CounterLow,
.CounterHigh = CounterHigh,
};
}
void OpDispatchBuilder::RDTSCOp(OpcodeArgs) {
auto Counter = CycleCounter(false);
StoreGPRRegister(X86State::REG_RAX, Counter.CounterLow);
StoreGPRRegister(X86State::REG_RDX, Counter.CounterHigh);
}
void OpDispatchBuilder::INCOp(OpcodeArgs) {
Ref Dest;
Ref Result;
const auto Size = GetSrcBitSize(Op);
const bool IsLocked = DestIsLockedMem(Op);
if (IsLocked) {
HandledLock = true;
Ref DestAddress = MakeSegmentAddress(Op, Op->Dest);
Dest = _AtomicFetchAdd(OpSizeFromSrc(Op), Constant(1), DestAddress);
} else {
Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= 32});
}
CalculateDeferredFlags();
if (Size < 32 && CTX->HostFeatures.SupportsFlagM) {
// Addition producing upper garbage
Result = Add(OpSize::i32Bit, Dest, 1);
CalculatePF(Result);
CalculateAF(Dest, Constant(1));
// Correctly set NZ flags, preserving C
HandleNZCV_RMW();
_SetSmallNZV(OpSizeFromSrc(Op), Result);
// Fix up V flag. INC overflows only when incrementing a positive and
// getting a negative. So compare the sign bits to calculate V.
_RmifNZCV(_Andn(OpSize::i32Bit, Result, Dest), Size - 1, 1);
} else {
Result = CalculateFlags_ADD(OpSizeFromSrc(Op), Dest, Constant(1), false);
}
if (!IsLocked) {
StoreResultGPR(Op, Result);
}
}
void OpDispatchBuilder::DECOp(OpcodeArgs) {
Ref Dest;
Ref Result;
const auto Size = GetSrcBitSize(Op);
const bool IsLocked = DestIsLockedMem(Op);
if (IsLocked) {
HandledLock = true;
Ref DestAddress = MakeSegmentAddress(Op, Op->Dest);
// Use Add instead of Sub to avoid a NEG
Dest = _AtomicFetchAdd(OpSizeFromSrc(Op), Constant(Size == 64 ? -1 : ((1ULL << Size) - 1)), DestAddress);
} else {
Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= 32});
}
CalculateDeferredFlags();
if (Size < 32 && CTX->HostFeatures.SupportsFlagM) {
// Subtraction producing upper garbage
Result = Sub(OpSize::i32Bit, Dest, 1);
CalculatePF(Result);
CalculateAF(Dest, Constant(1));
// Correctly set NZ flags, preserving C
HandleNZCV_RMW();
_SetSmallNZV(OpSizeFromSrc(Op), Result);
// Fix up V flag. DEC overflows only when decrementing a negative and
// getting a positive. So compare the sign bits to calculate V.
_RmifNZCV(_Andn(OpSize::i32Bit, Dest, Result), Size - 1, 1);
} else {
Result = CalculateFlags_SUB(OpSizeFromSrc(Op), Dest, Constant(1), false);
}
if (!IsLocked) {
StoreResultGPR(Op, Result);
}
}
void OpDispatchBuilder::STOSOp(OpcodeArgs) {
if (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) {
LogMan::Msg::EFmt("STOSOp: Can't handle address size override (OP: 0x{:04X}, Flags: 0x{:08X})", Op->OP, Op->Flags);
DecodeFailure = true;
return;
}
const auto Size = OpSizeFromSrc(Op);
const bool Repeat = (Op->Flags & (FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX | FEXCore::X86Tables::DecodeFlags::FLAG_REPNE_PREFIX)) != 0;
if (!Repeat) {
// Src is used only for a store of the same size so allow garbage
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
// Only ES prefix
Ref Dest = MakeSegmentAddress(X86State::REG_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
// Store to memory where RDI points
if (CTX->IsMemcpyAtomicTSOEnabled()) {
_StoreMemGPRAutoTSO(Size, Dest, Src, Size);
} else {
_StoreMem(RegClass::GPR, Size, Src, Dest, Invalid(), OpSize::i8Bit, MemOffsetType::SXTX, 1);
}
// Offset the pointer
Ref TailDest = LoadGPRRegister(X86State::REG_RDI);
StoreGPRRegister(X86State::REG_RDI, OffsetByDir(TailDest, IR::OpSizeToSize(Size)));
} else {
// FEX doesn't support partial faulting REP instructions.
// Converting this to a `MemSet` IR op optimizes this quite significantly in our codegen.
// If FEX is to gain support for faulting REP instructions, then this implementation needs to change significantly.
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
Ref Dest = LoadGPRRegister(X86State::REG_RDI);
// Only ES prefix
auto Segment = GetSegment(0, FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
Ref Counter = LoadGPRRegister(X86State::REG_RCX);
auto Result = _MemSet(CTX->IsAtomicTSOEnabled(), Size, Segment ?: InvalidNode, Dest, Src, Counter, LoadDir(1));
StoreGPRRegister(X86State::REG_RCX, Constant(0));
StoreGPRRegister(X86State::REG_RDI, Result);
}
}
void OpDispatchBuilder::MOVSOp(OpcodeArgs) {
if (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) {
LogMan::Msg::EFmt("MOVSOp: Can't handle address size override (OP: 0x{:04X}, Flags: 0x{:08X})", Op->OP, Op->Flags);
DecodeFailure = true;
return;
}
// RA now can handle these to be here, to avoid DF accesses
const auto Size = OpSizeFromSrc(Op);
if (Op->Flags & (FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX | FEXCore::X86Tables::DecodeFlags::FLAG_REPNE_PREFIX)) {
auto SrcAddr = LoadGPRRegister(X86State::REG_RSI);
auto DstAddr = LoadGPRRegister(X86State::REG_RDI);
auto Counter = LoadGPRRegister(X86State::REG_RCX);
auto DstSegment = GetSegment(0, FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
auto SrcSegment = GetSegment(Op->Flags, FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX);
if (DstSegment) {
DstAddr = Add(OpSize::i64Bit, DstAddr, DstSegment);
}
if (SrcSegment) {
SrcAddr = Add(OpSize::i64Bit, SrcAddr, SrcSegment);
}
Ref Result_Src = _AllocateGPR(false);
Ref Result_Dst = _AllocateGPR(false);
_MemCpy(CTX->IsAtomicTSOEnabled(), Size, DstAddr, SrcAddr, Counter, LoadDir(1), Result_Dst, Result_Src);
if (DstSegment) {
Result_Dst = Sub(OpSize::i64Bit, Result_Dst, DstSegment);
}
if (SrcSegment) {
Result_Src = Sub(OpSize::i64Bit, Result_Src, SrcSegment);
}
StoreGPRRegister(X86State::REG_RCX, Constant(0));
StoreGPRRegister(X86State::REG_RDI, Result_Dst);
StoreGPRRegister(X86State::REG_RSI, Result_Src);
} else {
Ref RSI = MakeSegmentAddress(X86State::REG_RSI, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
Ref RDI = MakeSegmentAddress(X86State::REG_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
if (CTX->IsMemcpyAtomicTSOEnabled()) {
auto Src = _LoadMemGPRAutoTSO(Size, RSI, Size);
// Store to memory where RDI points
_StoreMemGPRAutoTSO(Size, RDI, Src, Size);
} else {
auto Src = _LoadMem(RegClass::GPR, Size, RSI, Invalid(), OpSize::i8Bit, MemOffsetType::SXTX, 1);
_StoreMem(RegClass::GPR, Size, Src, RDI, Invalid(), OpSize::i8Bit, MemOffsetType::SXTX, 1);
}
RSI = OffsetByDir(RSI, IR::OpSizeToSize(Size));
RDI = OffsetByDir(RDI, IR::OpSizeToSize(Size));
StoreGPRRegister(X86State::REG_RSI, RSI);
StoreGPRRegister(X86State::REG_RDI, RDI);
}
}
IR::OpSize OpDispatchBuilder::GetStringOpSize(X86Tables::DecodedOp Op) const {
LOGMAN_THROW_A_FMT(Is64BitMode || !(Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE), "Invalid modifier on 32bit address");
return !Is64BitMode || (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) ? OpSize::i32Bit : OpSize::i64Bit;
}
void OpDispatchBuilder::CMPSOp(OpcodeArgs) {
if (!Is64BitMode && (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE)) {
LogMan::Msg::EFmt("CMPSOp: Address size override (0x67) not supported in 32-bit mode (OP: 0x{:04X}).", Op->OP);
DecodeFailure = true;
return;
}
const auto Size = OpSizeFromSrc(Op);
OpSize AddrSize = GetStringOpSize(Op);
bool Repeat = Op->Flags & (FEXCore::X86Tables::DecodeFlags::FLAG_REPNE_PREFIX | FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX);
if (!Repeat) {
Ref Src_RSI = LoadGPRRegister(X86State::REG_RSI, AddrSize);
Ref Src_RDI = LoadGPRRegister(X86State::REG_RDI, AddrSize);
Ref Dest_RSI = AppendSegmentOffset(Src_RSI, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
Ref Dest_RDI = AppendSegmentOffset(Src_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
auto Src1 = _LoadMemGPRAutoTSO(Size, Dest_RDI, Size);
auto Src2 = _LoadMemGPRAutoTSO(Size, Dest_RSI, Size);
CalculateFlags_SUB(OpSizeFromSrc(Op), Src2, Src1);
Dest_RDI = OffsetByDir(Src_RDI, IR::OpSizeToSize(Size));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
Dest_RDI = _Bfe(OpSize::i64Bit, 32, 0, Dest_RDI);
StoreGPRRegister(X86State::REG_RDI, Dest_RDI);
} else {
StoreGPRRegister(X86State::REG_RDI, Dest_RDI, AddrSize);
}
Dest_RSI = OffsetByDir(Src_RSI, IR::OpSizeToSize(Size));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
Dest_RSI = _Bfe(OpSize::i64Bit, 32, 0, Dest_RSI);
StoreGPRRegister(X86State::REG_RSI, Dest_RSI);
} else {
StoreGPRRegister(X86State::REG_RSI, Dest_RSI, AddrSize);
}
} else {
// Calculate flags early.
CalculateDeferredFlags();
bool REPE = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX;
// If rcx = 0, skip the whole loop.
Ref Counter = LoadGPRRegister(X86State::REG_RCX);
auto OuterJump = CondJump(Counter, CondClass::EQ);
auto BeforeLoop = CreateNewCodeBlockAfter(GetCurrentBlock());
SetFalseJumpTarget(OuterJump, BeforeLoop);
SetCurrentCodeBlock(BeforeLoop);
StartNewBlock();
ForeachDirection([this, Op, Size, AddrSize, REPE](int32_t PtrDir) {
IRPair<IROp_CondJump> InnerJump;
auto JumpIntoLoop = Jump();
// Setup for the loop
auto LoopHeader = CreateNewCodeBlockAfter(GetCurrentBlock());
SetCurrentCodeBlock(LoopHeader);
StartNewBlock();
SetJumpTarget(JumpIntoLoop, LoopHeader);
// Working loop
{
Ref Src_RSI = LoadGPRRegister(X86State::REG_RSI, AddrSize);
Ref Src_RDI = LoadGPRRegister(X86State::REG_RDI, AddrSize);
Ref Dest_RSI = AppendSegmentOffset(Src_RSI, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
Ref Dest_RDI = AppendSegmentOffset(Src_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
auto Src1 = _LoadMemGPRAutoTSO(Size, Dest_RDI, Size);
auto Src2 = _LoadMemGPR(Size, Dest_RSI, Size);
// We'll calculate PF/AF after the loop, so use them as temporaries here.
StoreRegister(Core::CPUState::PF_AS_GREG, false, Src1);
StoreRegister(Core::CPUState::AF_AS_GREG, false, Src2);
Ref TailCounter = LoadGPRRegister(X86State::REG_RCX);
// Decrement counter
TailCounter = SubWithFlags(OpSize::i64Bit, TailCounter, 1);
// Store the counter since we don't have phis
StoreGPRRegister(X86State::REG_RCX, TailCounter);
Dest_RDI = Add(AddrSize, Src_RDI, PtrDir * static_cast<int32_t>(IR::OpSizeToSize(Size)));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
Dest_RDI = _Bfe(OpSize::i64Bit, 32, 0, Dest_RDI);
StoreGPRRegister(X86State::REG_RDI, Dest_RDI);
} else {
StoreGPRRegister(X86State::REG_RDI, Dest_RDI, AddrSize);
}
Dest_RSI = Add(AddrSize, Src_RSI, PtrDir * static_cast<int32_t>(IR::OpSizeToSize(Size)));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
Dest_RSI = _Bfe(OpSize::i64Bit, 32, 0, Dest_RSI);
StoreGPRRegister(X86State::REG_RSI, Dest_RSI);
} else {
StoreGPRRegister(X86State::REG_RSI, Dest_RSI, AddrSize);
}
// If TailCounter != 0, compare sources.
// If TailCounter == 0, set ZF iff that would break.
_CondSubNZCV(OpSize::i64Bit, Src2, Src1, CondClass::NEQ, REPE ? 0 : (1 << 2) /* Z */);
CachedNZCV = nullptr;
NZCVDirty = false;
InnerJump = CondJumpNZCV(REPE ? CondClass::EQ : CondClass::NEQ);
// Jump back to the start if we have more work to do
SetTrueJumpTarget(InnerJump, LoopHeader);
}
// Make sure to start a new block after ending this one
auto LoopEnd = CreateNewCodeBlockAfter(GetCurrentBlock());
SetFalseJumpTarget(InnerJump, LoopEnd);
SetCurrentCodeBlock(LoopEnd);
StartNewBlock();
});
// Make sure to start a new block after ending this one
{
// Grab the sources from the last iteration so we can set flags.
auto Src1 = LoadGPR(Core::CPUState::PF_AS_GREG);
auto Src2 = LoadGPR(Core::CPUState::AF_AS_GREG);
CalculateFlags_SUB(OpSizeFromSrc(Op), Src2, Src1);
}
auto Jump_ = Jump();
auto Exit = CreateNewCodeBlockAfter(GetCurrentBlock());
SetJumpTarget(Jump_, Exit);
SetTrueJumpTarget(OuterJump, Exit);
SetCurrentCodeBlock(Exit);
StartNewBlock();
}
}
void OpDispatchBuilder::LODSOp(OpcodeArgs) {
if (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) {
LogMan::Msg::EFmt("LODSOp: Can't handle address size override (OP: 0x{:04X}, Flags: 0x{:08X})", Op->OP, Op->Flags);
DecodeFailure = true;
return;
}
const auto Size = OpSizeFromSrc(Op);
const bool Repeat = (Op->Flags & (FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX | FEXCore::X86Tables::DecodeFlags::FLAG_REPNE_PREFIX)) != 0;
if (!Repeat) {
Ref Dest_RSI = MakeSegmentAddress(X86State::REG_RSI, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
auto Src = _LoadMemGPRAutoTSO(Size, Dest_RSI, Size);
StoreResultGPR(Op, Src);
// Offset the pointer
Ref TailDest_RSI = LoadGPRRegister(X86State::REG_RSI);
StoreGPRRegister(X86State::REG_RSI, OffsetByDir(TailDest_RSI, IR::OpSizeToSize(Size)));
} else {
// Calculate flags early. because end of block
CalculateDeferredFlags();
ForeachDirection([this, Op, Size](int32_t PtrDir) {
// XXX: Theoretically LODS could be optimized to
// RSI += {-}(RCX * Size)
// RAX = [RSI - Size]
// But this might violate the case of an application scanning pages for read permission and catching the fault
// May or may not matter
auto JumpStart = Jump();
// Make sure to start a new block after ending this one
auto LoopStart = CreateNewCodeBlockAfter(GetCurrentBlock());
SetJumpTarget(JumpStart, LoopStart);
SetCurrentCodeBlock(LoopStart);
StartNewBlock();
Ref Counter = LoadGPRRegister(X86State::REG_RCX);
// Can we end the block?
// We leave if RCX = 0
auto CondJump_ = CondJump(Counter, CondClass::EQ);
auto LoopTail = CreateNewCodeBlockAfter(LoopStart);
SetFalseJumpTarget(CondJump_, LoopTail);
SetCurrentCodeBlock(LoopTail);
StartNewBlock();
// Working loop
{
Ref Dest_RSI = MakeSegmentAddress(X86State::REG_RSI, Op->Flags, X86Tables::DecodeFlags::FLAG_DS_PREFIX);
auto Src = _LoadMemGPRAutoTSO(Size, Dest_RSI, Size);
StoreResultGPR(Op, Src);
Ref TailCounter = LoadGPRRegister(X86State::REG_RCX);
Ref TailDest_RSI = LoadGPRRegister(X86State::REG_RSI);
// Decrement counter
TailCounter = Sub(OpSize::i64Bit, TailCounter, 1);
// Store the counter since we don't have phis
StoreGPRRegister(X86State::REG_RCX, TailCounter);
// Offset the pointer
TailDest_RSI = Add(OpSize::i64Bit, TailDest_RSI, PtrDir * static_cast<int32_t>(IR::OpSizeToSize(Size)));
StoreGPRRegister(X86State::REG_RSI, TailDest_RSI);
// Jump back to the start, we have more work to do
Jump(LoopStart);
}
// Make sure to start a new block after ending this one
auto LoopEnd = CreateNewCodeBlockAfter(LoopTail);
SetTrueJumpTarget(CondJump_, LoopEnd);
SetCurrentCodeBlock(LoopEnd);
StartNewBlock();
});
}
}
void OpDispatchBuilder::SCASOp(OpcodeArgs) {
if (!Is64BitMode && (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE)) {
LogMan::Msg::EFmt("SCASOp: Address size override (0x67) not supported in 32-bit mode (OP: 0x{:04X}).", Op->OP);
DecodeFailure = true;
return;
}
const auto Size = OpSizeFromSrc(Op);
OpSize AddrSize = GetStringOpSize(Op);
const bool Repeat = (Op->Flags & (FEXCore::X86Tables::DecodeFlags::FLAG_REPNE_PREFIX | FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX)) != 0;
if (!Repeat) {
Ref Src_RDI = LoadGPRRegister(X86State::REG_RDI, AddrSize);
Ref Dest_RDI = AppendSegmentOffset(Src_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
auto Src1 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Src2 = _LoadMemGPRAutoTSO(Size, Dest_RDI, Size);
CalculateFlags_SUB(OpSizeFromSrc(Op), Src1, Src2);
Ref TailDest_RDI = OffsetByDir(Src_RDI, IR::OpSizeToSize(Size));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
TailDest_RDI = _Bfe(OpSize::i64Bit, 32, 0, TailDest_RDI);
StoreGPRRegister(X86State::REG_RDI, TailDest_RDI);
} else {
StoreGPRRegister(X86State::REG_RDI, TailDest_RDI, AddrSize);
}
} else {
// Calculate flags early. because end of block
CalculateDeferredFlags();
ForeachDirection([this, Op, Size, AddrSize](int32_t Dir) {
bool REPE = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REP_PREFIX;
auto JumpStart = Jump();
// Make sure to start a new block after ending this one
auto LoopStart = CreateNewCodeBlockAfter(GetCurrentBlock());
SetJumpTarget(JumpStart, LoopStart);
SetCurrentCodeBlock(LoopStart);
StartNewBlock();
Ref Counter = LoadGPRRegister(X86State::REG_RCX);
// Can we end the block?
// We leave if RCX = 0
auto CondJump_ = CondJump(Counter, CondClass::EQ);
IRPair<IROp_CondJump> InternalCondJump;
auto LoopTail = CreateNewCodeBlockAfter(LoopStart);
SetFalseJumpTarget(CondJump_, LoopTail);
SetCurrentCodeBlock(LoopTail);
StartNewBlock();
// Working loop
{
Ref Src_RDI = LoadGPRRegister(X86State::REG_RDI, AddrSize);
Ref Dest_RDI = AppendSegmentOffset(Src_RDI, 0, X86Tables::DecodeFlags::FLAG_ES_PREFIX, true);
auto Src1 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Src2 = _LoadMemGPRAutoTSO(Size, Dest_RDI, Size);
CalculateFlags_SUB(OpSizeFromSrc(Op), Src1, Src2);
// Calculate flags early.
CalculateDeferredFlags();
Ref TailCounter = LoadGPRRegister(X86State::REG_RCX);
Ref Src_RDI_Tail = LoadGPRRegister(X86State::REG_RDI, AddrSize);
// Decrement counter
TailCounter = Sub(OpSize::i64Bit, TailCounter, 1);
// Store the counter since we don't have phis
StoreGPRRegister(X86State::REG_RCX, TailCounter);
Ref TailDest_RDI = Add(AddrSize, Src_RDI_Tail, Dir * static_cast<int32_t>(IR::OpSizeToSize(Size)));
if (Is64BitMode && AddrSize == OpSize::i32Bit) {
TailDest_RDI = _Bfe(OpSize::i64Bit, 32, 0, TailDest_RDI);
StoreGPRRegister(X86State::REG_RDI, TailDest_RDI);
} else {
StoreGPRRegister(X86State::REG_RDI, TailDest_RDI, AddrSize);
}
CalculateDeferredFlags();
InternalCondJump = CondJumpNZCV(REPE ? CondClass::EQ : CondClass::NEQ);
// Jump back to the start if we have more work to do
SetTrueJumpTarget(InternalCondJump, LoopStart);
}
// Make sure to start a new block after ending this one
auto LoopEnd = CreateNewCodeBlockAfter(LoopTail);
SetTrueJumpTarget(CondJump_, LoopEnd);
SetFalseJumpTarget(InternalCondJump, LoopEnd);
SetCurrentCodeBlock(LoopEnd);
StartNewBlock();
});
}
}
void OpDispatchBuilder::BSWAPOp(OpcodeArgs) {
Ref Dest;
const auto Size = OpSizeFromSrc(Op);
if (Size == OpSize::i16Bit) {
// BSWAP of 16bit is undef. ZEN+ causes the lower 16bits to get zero'd
Dest = Constant(0);
} else {
Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GetGPROpSize(), Op->Flags);
Dest = _Rev(Size, Dest);
}
StoreResultGPR(Op, Dest);
}
void OpDispatchBuilder::PUSHFOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
Push(Size, GetPackedRFLAG());
}
void OpDispatchBuilder::POPFOp(OpcodeArgs) {
const auto Size = OpSizeFromSrc(Op);
Ref Src = Pop(Size);
// Add back our flag constants
// Bit 1 is always 1
// Bit 9 is always 1 because we always have interrupts enabled
Src = _Or(OpSize::i64Bit, Src, Constant(0x202));
SetPackedRFLAG(false, Src);
auto NewRIP = GetRelocatedPC(Op);
ExitFunction(NewRIP, BranchHint::CheckTF);
BlockSetRIP = true;
}
void OpDispatchBuilder::NEGOp(OpcodeArgs) {
HandledLock = (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_LOCK) != 0;
const auto Size = OpSizeFromSrc(Op);
auto ZeroConst = Constant(0);
if (DestIsLockedMem(Op)) {
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
Ref Dest = _AtomicFetchNeg(Size, DestMem);
CalculateFlags_SUB(Size, ZeroConst, Dest);
} else {
Ref Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
Ref Result = CalculateFlags_SUB(Size, ZeroConst, Dest);
StoreResultGPR(Op, Result);
}
}
void OpDispatchBuilder::DIVOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
auto Size = OpSizeFromSrc(Op);
// This loads the divisor. 32-bit/64-bit paths mask inside the JIT, 8/16 do not.
Ref Divisor = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = Size >= OpSize::i32Bit});
if (Size == OpSize::i64Bit && !Is64BitMode) {
LogMan::Msg::EFmt("Doesn't exist in 32bit mode");
DecodeFailure = true;
return;
}
Ref Quotient = _AllocateGPR(true);
Ref Remainder = _AllocateGPR(true);
if (Size == OpSize::i8Bit) {
Ref Src1 = LoadGPRRegister(X86State::REG_RAX, OpSize::i16Bit);
_UDiv(OpSize::i16Bit, Src1, Invalid(), Divisor, Quotient, Remainder);
// AX[15:0] = concat<URem[7:0]:UDiv[7:0]>
auto ResultAX = _Bfi(GPRSize, 8, 8, Quotient, Remainder);
StoreGPRRegister(X86State::REG_RAX, ResultAX, OpSize::i16Bit);
} else {
Ref Src1 = LoadGPRRegister(X86State::REG_RAX);
Ref Src2 = LoadGPRRegister(X86State::REG_RDX);
_UDiv(Size, Src1, Src2, Divisor, Quotient, Remainder);
if (Size == OpSize::i32Bit) {
Quotient = _Bfe(OpSize::i32Bit, IR::OpSizeAsBits(Size), 0, Quotient);
Remainder = _Bfe(OpSize::i32Bit, IR::OpSizeAsBits(Size), 0, Remainder);
Size = OpSize::iInvalid;
}
StoreGPRRegister(X86State::REG_RAX, Quotient, Size);
StoreGPRRegister(X86State::REG_RDX, Remainder, Size);
}
}
void OpDispatchBuilder::IDIVOp(OpcodeArgs) {
// This loads the divisor
Ref Divisor = LoadSourceGPR(Op, Op->Dest, Op->Flags);
const auto GPRSize = GetGPROpSize();
auto Size = OpSizeFromSrc(Op);
if (Size == OpSize::i64Bit && !Is64BitMode) {
LogMan::Msg::EFmt("Doesn't exist in 32bit mode");
DecodeFailure = true;
return;
}
Ref Quotient = _AllocateGPR(true);
Ref Remainder = _AllocateGPR(true);
if (Size == OpSize::i8Bit) {
Ref Src1 = LoadGPRRegister(X86State::REG_RAX);
Src1 = _Sbfe(OpSize::i64Bit, 16, 0, Src1);
Divisor = _Sbfe(OpSize::i64Bit, 8, 0, Divisor);
_Div(OpSize::i64Bit, Src1, Invalid(), Divisor, Quotient, Remainder);
// AX[15:0] = concat<URem[7:0]:UDiv[7:0]>
auto ResultAX = _Bfi(GPRSize, 8, 8, Quotient, Remainder);
StoreGPRRegister(X86State::REG_RAX, ResultAX, OpSize::i16Bit);
} else {
Ref Src1 = LoadGPRRegister(X86State::REG_RAX);
Ref Src2 = LoadGPRRegister(X86State::REG_RDX);
_Div(Size, Src1, Src2, Divisor, Quotient, Remainder);
if (Size == OpSize::i32Bit) {
Quotient = _Bfe(OpSize::i32Bit, IR::OpSizeAsBits(Size), 0, Quotient);
Remainder = _Bfe(OpSize::i32Bit, IR::OpSizeAsBits(Size), 0, Remainder);
Size = OpSize::iInvalid;
}
StoreGPRRegister(X86State::REG_RAX, Quotient, Size);
StoreGPRRegister(X86State::REG_RDX, Remainder, Size);
}
}
void OpDispatchBuilder::BSFOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto DstSize = OpSizeFromDst(Op) == OpSize::i16Bit ? OpSize::i16Bit : GPRSize;
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, DstSize, Op->Flags, {.AllowUpperGarbage = true});
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
// Find the LSB of this source
auto Result = _FindLSB(OpSizeFromSrc(Op), Src);
// OF, SF, AF, PF, CF all undefined
// ZF is set to 1 if the source was zero
SetZ_InvalidateNCV(OpSizeFromSrc(Op), Src);
// If Src was zero then the destination doesn't get modified.
//
// Although Intel does not guarantee that semantic, AMD does and Intel
// hardware satisfies it. We provide the stronger AMD behaviour as
// applications might rely on that in the wild.
auto SelectOp = NZCVSelect(GPRSize, CondClass::EQ, Dest, Result);
StoreResultGPR_WithOpSize(Op, Op->Dest, SelectOp, DstSize);
}
void OpDispatchBuilder::BSROp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto DstSize = OpSizeFromDst(Op) == OpSize::i16Bit ? OpSize::i16Bit : GPRSize;
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, DstSize, Op->Flags, {.AllowUpperGarbage = true});
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
// Find the MSB of this source
auto Result = _FindMSB(OpSizeFromSrc(Op), Src);
// OF, SF, AF, PF, CF all undefined
// ZF is set to 1 if the source was zero
SetZ_InvalidateNCV(OpSizeFromSrc(Op), Src);
// If Src was zero then the destination doesn't get modified
auto SelectOp = NZCVSelect(GPRSize, CondClass::EQ, Dest, Result);
StoreResultGPR_WithOpSize(Op, Op->Dest, SelectOp, DstSize);
}
void OpDispatchBuilder::CMPXCHGOp(OpcodeArgs) {
// CMPXCHG ModRM, reg, {RAX}
// MemData = *ModRM.dest
// if (RAX == MemData)
// modRM.dest = reg;
// ZF = 1
// else
// ZF = 0
// RAX = MemData
//
// CASL Xs, Xt, Xn
// MemData = *Xn
// if (MemData == Xs)
// *Xn = Xt
// Xs = MemData
const auto GPRSize = GetGPROpSize();
auto Size = OpSizeFromSrc(Op);
if (Op->Dest.IsGPR()) {
// This is our source register
Ref Src2 = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
Ref Src3 = LoadGPRRegister(X86State::REG_RAX);
// If the destination is also the accumulator, we get some algebraic
// simplifications. Not sure if this is actually hit but it's in
// InstCountCI.
bool Trivial = Op->Dest.Data.GPR.GPR == X86State::REG_RAX && !Op->Dest.IsGPRDirect() && !Op->Dest.Data.GPR.HighBits;
Ref Src1 {};
Ref Src1Lower {};
if (GPRSize == OpSize::i64Bit && Size == OpSize::i32Bit) {
Src1 = LoadSourceGPR_WithOpSize(Op, Op->Dest, GPRSize, Op->Flags, {.AllowUpperGarbage = true});
Src1Lower = Trivial ? Src1 : _Bfe(GPRSize, IR::OpSizeAsBits(Size), 0, Src1);
} else {
Src1 = LoadSourceGPR_WithOpSize(Op, Op->Dest, Size, Op->Flags, {.AllowUpperGarbage = true});
Src1Lower = Src1;
}
// Compare RAX with the destination, setting flags accordingly.
CalculateFlags_SUB(OpSizeFromSrc(Op), Src3, Src1Lower);
CalculateDeferredFlags();
if (!Trivial) {
if (GPRSize == OpSize::i64Bit && Size == OpSize::i32Bit) {
// This allows us to only hit the ZEXT case on failure
Ref RAXResult = NZCVSelect(OpSize::i64Bit, CondClass::EQ, Src3, Src1Lower);
// When the size is 4 we need to make sure not zext the GPR when the comparison fails
StoreGPRRegister(X86State::REG_RAX, RAXResult);
} else {
StoreGPRRegister(X86State::REG_RAX, Src1Lower, Size);
}
}
// Op1 = RAX == Op1 ? Op2 : Op1
// If they match then set the rm operand to the input
// else don't set the rm operand
Ref Src2Lower = Src2;
if (GPRSize == OpSize::i64Bit && Size == OpSize::i32Bit) {
Src2Lower = _Bfe(GPRSize, IR::OpSizeAsBits(Size), 0, Src2);
}
Ref DestResult = Trivial ? Src2 : NZCVSelect(OpSize::i64Bit, CondClass::EQ, Src2Lower, Src1);
// Store in to GPR Dest
if (GPRSize == OpSize::i64Bit && Size == OpSize::i32Bit) {
StoreResultGPR_WithOpSize(Op, Op->Dest, DestResult, GPRSize);
} else {
StoreResultGPR(Op, DestResult);
}
} else {
Ref Src2 = LoadSourceGPR(Op, Op->Src[0], Op->Flags);
HandledLock = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_LOCK;
auto Src3 = LoadGPRRegister(X86State::REG_RAX);
auto Src3Lower = _Bfe(OpSize::i64Bit, OpSizeAsBits(Size), 0, Src3);
// If this is a memory location then we want the pointer to it
Ref Src1 = MakeSegmentAddress(Op, Op->Dest);
// DataSrc = *Src1
// if (DataSrc == Src3) { *Src1 == Src2; } Src2 = DataSrc
// This will write to memory! Careful!
// Third operand must be a calculated guest memory address
Ref CASResult = _CAS(Size, Src3, Src2, Src1);
Ref RAXResult = CASResult;
CalculateFlags_SUB(OpSizeFromSrc(Op), Src3Lower, CASResult);
CalculateDeferredFlags();
if (GPRSize == OpSize::i64Bit && Size == OpSize::i32Bit) {
// This allows us to only hit the ZEXT case on failure
RAXResult = _NZCVSelect(OpSize::i64Bit, CondClass::EQ, Src3, CASResult);
Size = OpSize::i64Bit;
}
// RAX gets the result of the CAS op
StoreGPRRegister(X86State::REG_RAX, RAXResult, Size);
}
}
void OpDispatchBuilder::CMPXCHGPairOp(OpcodeArgs) {
// Calculate flags early.
CalculateDeferredFlags();
// REX.W used to determine if it is 16byte or 8byte
// Unlike CMPXCHG, the destination can only be a memory location
const auto Size = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REX_WIDENING ? OpSize::i64Bit : OpSize::i32Bit;
HandledLock = (Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_LOCK) != 0;
// If this is a memory location then we want the pointer to it
Ref Src1 = MakeSegmentAddress(Op, Op->Dest);
// Load the full 64-bit registers, all the users ignore the upper 32-bits for
// 32-bit only cmpxchg. This saves some zero extension.
Ref Expected_Lower = LoadGPRRegister(X86State::REG_RAX);
Ref Expected_Upper = LoadGPRRegister(X86State::REG_RDX);
Ref Desired_Lower = LoadGPRRegister(X86State::REG_RBX);
Ref Desired_Upper = LoadGPRRegister(X86State::REG_RCX);
// ssa0 = Expected
// ssa1 = Desired
// ssa2 = MemoryLocation
// DataSrc = *MemSrc
// if (DataSrc == Expected) { *MemSrc == Desired; } Expected = DataSrc
// This will write to memory! Careful!
// Third operand must be a calculated guest memory address
Ref Result_Lower = _AllocateGPR(true);
Ref Result_Upper = _AllocateGPRAfter(Result_Lower);
_CASPair(Size, Expected_Lower, Expected_Upper, Desired_Lower, Desired_Upper, Src1, Result_Lower, Result_Upper);
HandleNZCV_RMW();
_CmpPairZ(Size, Result_Lower, Result_Upper, Expected_Lower, Expected_Upper);
CalculateDeferredFlags();
auto UpdateIfNotZF = [this](auto Reg, auto Value) {
// Always use 64-bit csel to preserve existing upper bits. If we have a
// 32-bit cmpxchg in a 64-bit context, Value will be zeroed in upper bits.
StoreGPRRegister(Reg, NZCVSelect(OpSize::i64Bit, CondClass::NEQ, Value, LoadGPRRegister(Reg)));
};
UpdateIfNotZF(X86State::REG_RAX, Result_Lower);
UpdateIfNotZF(X86State::REG_RDX, Result_Upper);
}
void OpDispatchBuilder::CreateJumpBlocks(const fextl::vector<FEXCore::Frontend::Decoder::DecodedBlocks>* Blocks) {
Ref PrevCodeBlock {};
for (auto& Target : *Blocks) {
auto CodeNode = CreateCodeNode(Target.IsEntryPoint, Target.Entry - Entry);
JumpTargets.try_emplace(Target.Entry, JumpTargetInfo {CodeNode, false, Target.IsEntryPoint});
if (PrevCodeBlock) {
LinkCodeBlocks(PrevCodeBlock, CodeNode);
}
PrevCodeBlock = CodeNode;
}
}
void OpDispatchBuilder::BeginFunction(uint64_t RIP, const fextl::vector<FEXCore::Frontend::Decoder::DecodedBlocks>* Blocks,
uint32_t NumInstructions, bool _Is64BitMode, bool MonoBackpatcherBlock) {
Entry = RIP;
Is64BitMode = _Is64BitMode;
LOGMAN_THROW_A_FMT(Is64BitMode == CTX->Config.Is64BitMode, "Expected operating mode to not change at runtime!");
IsMonoBackpatcherBlock = MonoBackpatcherBlock;
auto IRHeader = _IRHeader(InvalidNode, RIP, 0, NumInstructions, 0, 0);
CreateJumpBlocks(Blocks);
auto Block = GetNewJumpBlock(RIP);
SetCurrentCodeBlock(Block);
IRHeader.first->Blocks = Block->Wrapped(DualListData.ListBegin());
CurrentHeader = IRHeader.first;
}
void OpDispatchBuilder::Finalize() {
// This usually doesn't emit any IR but in the case of hitting the block instruction limit it will
FlushRegisterCache();
const auto GPRSize = GetGPROpSize();
// Node 0 is invalid node
Ref RealNode = reinterpret_cast<Ref>(GetNode(1));
const FEXCore::IR::IROp_Header* IROp = RealNode->Op(DualListData.DataBegin());
LOGMAN_THROW_A_FMT(IROp->Op == OP_IRHEADER, "First op in function must be our header");
// Let's walk the jump blocks and see if we have handled every block target
for (auto& Handler : JumpTargets) {
if (Handler.second.HaveEmitted) {
continue;
}
// We haven't emitted. Dump out to the dispatcher
SetCurrentCodeBlock(Handler.second.BlockEntry);
ExitFunction(_InlineEntrypointOffset(GPRSize, Handler.first - Entry));
}
}
uint8_t OpDispatchBuilder::GetDstSize(X86Tables::DecodedOp Op) const {
const uint32_t DstSizeFlag = X86Tables::DecodeFlags::GetSizeDstFlags(Op->Flags);
LOGMAN_THROW_A_FMT(DstSizeFlag != 0 && DstSizeFlag != X86Tables::DecodeFlags::SIZE_MASK, "Invalid destination size for op");
return 1u << (DstSizeFlag - 1);
}
uint8_t OpDispatchBuilder::GetSrcSize(X86Tables::DecodedOp Op) const {
const uint32_t SrcSizeFlag = X86Tables::DecodeFlags::GetSizeSrcFlags(Op->Flags);
LOGMAN_THROW_A_FMT(SrcSizeFlag != 0 && SrcSizeFlag != X86Tables::DecodeFlags::SIZE_MASK, "Invalid destination size for op");
return 1u << (SrcSizeFlag - 1);
}
uint32_t OpDispatchBuilder::GetSrcBitSize(X86Tables::DecodedOp Op) const {
return GetSrcSize(Op) * 8;
}
uint32_t OpDispatchBuilder::GetDstBitSize(X86Tables::DecodedOp Op) const {
return GetDstSize(Op) * 8;
}
Ref OpDispatchBuilder::GetSegment(uint32_t Flags, uint32_t DefaultPrefix, bool Override) {
const auto GPRSize = GetGPROpSize();
uint32_t Prefix = Flags & FEXCore::X86Tables::DecodeFlags::FLAG_SEGMENTS;
if (Is64BitMode) {
if (Prefix == FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX) {
return _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, fs_cached));
} else if (Prefix == FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX) {
return _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, gs_cached));
}
// If there was any other segment in 64bit then it is ignored
} else {
if (Prefix == FEXCore::X86Tables::DecodeFlags::FLAG_NO_PREFIX || Override) {
// If there was no prefix then use the default one if available
// Or the argument only uses a specific prefix (with override set)
Prefix = DefaultPrefix;
}
// With the segment register optimization we store the GDT bases directly in the segment register to remove indexed loads
Ref SegmentResult {};
switch (Prefix) {
[[likely]] case FEXCore::X86Tables::DecodeFlags::FLAG_NO_PREFIX:
return nullptr;
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, es_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, cs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, ss_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, ds_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, fs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX:
SegmentResult = _LoadContextGPR(GPRSize, offsetof(FEXCore::Core::CPUState, gs_cached));
break;
default: FEX_UNREACHABLE;
}
CheckLegacySegmentRead(SegmentResult, Prefix);
return SegmentResult;
}
return nullptr;
}
Ref OpDispatchBuilder::AppendSegmentOffset(Ref Value, uint32_t Flags, uint32_t DefaultPrefix, bool Override) {
auto Segment = GetSegment(Flags, DefaultPrefix, Override);
if (Segment) {
Value = Add(std::max(OpSize::i32Bit, std::max(GetOpSize(Value), GetOpSize(Segment))), Value, Segment);
}
return Value;
}
void OpDispatchBuilder::CheckLegacySegmentRead(Ref NewNode, uint32_t SegmentReg) {
#ifndef FEX_DISABLE_TELEMETRY
if (SegmentReg == FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX || SegmentReg == FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX) {
// FS and GS segments aren't considered legacy.
return;
}
if (!(SegmentsNeedReadCheck & SegmentReg)) {
// If the block has done multiple reads of a segment register then skip redundant read checks.
// Segment write will cause another read check.
return;
}
if (CTX->Config.DisableTelemetry()) {
// Telemetry disabled at runtime.
return;
}
FEXCore::Telemetry::TelemetryType TelemIndex {};
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_USES_32BIT_SEGMENT_ES;
SegmentsNeedReadCheck &= ~FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_USES_32BIT_SEGMENT_CS;
SegmentsNeedReadCheck &= ~FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_USES_32BIT_SEGMENT_SS;
SegmentsNeedReadCheck &= ~FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_USES_32BIT_SEGMENT_DS;
SegmentsNeedReadCheck &= ~FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX;
break;
default: FEX_UNREACHABLE;
}
// Will set the telemetry value if NewNode is != 0
_TelemetrySetValue(NewNode, TelemIndex);
// Telemetry will dirty flags, and user code does not expect LoadSource to clobber flags, fix that up here as this is an edge case.
CalculateDeferredFlags();
#endif
}
void OpDispatchBuilder::CheckLegacySegmentWrite(Ref NewNode, uint32_t SegmentReg) {
#ifndef FEX_DISABLE_TELEMETRY
if (SegmentReg == FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX || SegmentReg == FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX) {
// FS and GS segments aren't considered legacy.
return;
}
if (CTX->Config.DisableTelemetry()) {
// Telemetry disabled at runtime.
return;
}
FEXCore::Telemetry::TelemetryType TelemIndex {};
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_WRITES_32BIT_SEGMENT_ES;
SegmentsNeedReadCheck |= FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_WRITES_32BIT_SEGMENT_CS;
SegmentsNeedReadCheck |= FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_WRITES_32BIT_SEGMENT_SS;
SegmentsNeedReadCheck |= FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX;
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
TelemIndex = FEXCore::Telemetry::TelemetryType::TYPE_WRITES_32BIT_SEGMENT_DS;
SegmentsNeedReadCheck |= FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX;
break;
default: FEX_UNREACHABLE;
}
// Will set the telemetry value if NewNode is != 0
_TelemetrySetValue(NewNode, TelemIndex);
// Telemetry will dirty flags, and user code does not expect LoadSource to clobber flags, fix that up here as this is an edge case.
CalculateDeferredFlags();
#endif
}
void OpDispatchBuilder::UpdatePrefixFromSegment(Ref Segment, uint32_t SegmentReg) {
// Use BFE to extract the selector index in bits [15,3] of the segment register.
// In some cases the upper 16-bits of the 32-bit GPR contain garbage to ignore.
auto GDT = _Bfe(OpSize::i32Bit, 1, 2, Segment);
// Fun quirk, if we mask the selector then it is premultiplied by 8 which we need to do for accessing anyway.
auto SegmentOffset = _And(OpSize::i32Bit, Segment, _Constant(0xfff8));
Ref SegmentBase = _LoadContextGPRIndexed(GDT, OpSize::i64Bit, offsetof(FEXCore::Core::CPUState, segment_arrays[0]), 8);
Ref NewSegment = _LoadMemGPR(OpSize::i64Bit, SegmentBase, SegmentOffset, OpSize::i8Bit, MemOffsetType::UXTW, 1);
CheckLegacySegmentWrite(NewSegment, SegmentReg);
// Extract the 32-bit base from the GDT segment.
auto Upper32 = _Lshr(OpSize::i64Bit, NewSegment, _Constant(32));
auto Masked = _And(OpSize::i32Bit, Upper32, _Constant(0xFF00'0000));
Ref Merged = _Orlshr(OpSize::i32Bit, Masked, NewSegment, 16);
NewSegment = _Bfi(OpSize::i32Bit, 8, 16, Merged, Upper32);
switch (SegmentReg) {
case FEXCore::X86Tables::DecodeFlags::FLAG_ES_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, es_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_CS_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, cs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_SS_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, ss_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_DS_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, ds_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_FS_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, fs_cached));
break;
case FEXCore::X86Tables::DecodeFlags::FLAG_GS_PREFIX:
_StoreContextGPR(OpSize::i32Bit, NewSegment, offsetof(FEXCore::Core::CPUState, gs_cached));
break;
default: break; // Do nothing
}
}
AddressMode OpDispatchBuilder::DecodeAddress(const X86Tables::DecodedOp& Op, const X86Tables::DecodedOperand& Operand,
MemoryAccessType AccessType, bool IsLoad) {
const auto GPRSize = GetGPROpSize();
AddressMode A {};
A.Segment = GetSegment(Op->Flags);
A.AddrSize = (Op->Flags & X86Tables::DecodeFlags::FLAG_ADDRESS_SIZE) != 0 ? (GPRSize >> 1) : GPRSize;
A.NonTSO = AccessType == MemoryAccessType::NONTSO || AccessType == MemoryAccessType::STREAM;
if (Operand.IsLiteral()) {
A.Offset = Operand.Literal();
if (Operand.Data.Literal.Size != 8 && IsLoad) {
// zero extend
uint64_t width = Operand.Data.Literal.Size * 8;
A.Offset &= ((1ULL << width) - 1);
}
} else if (Operand.IsGPR()) {
// Not an address, let the caller deal with it
A.AddrSize = GPRSize;
} else if (Operand.IsGPRDirect()) {
A.Base = LoadGPRRegister(Operand.Data.GPR.GPR, GPRSize);
A.NonTSO |= IsNonTSOReg(AccessType, Operand.Data.GPR.GPR);
} else if (Operand.IsGPRIndirect() || Operand.IsGPRIndirectRelocation()) {
A.Base = LoadGPRRegister(Operand.Data.GPRIndirect.GPR, GPRSize);
if (Operand.IsGPRIndirectRelocation()) {
A.Base = Add(GPRSize, _EntrypointOffset(GPRSize, Operand.Data.GPRIndirect.Displacement), A.Base);
} else {
A.Offset = static_cast<int32_t>(Operand.Data.GPRIndirect.Displacement);
}
A.NonTSO |= IsNonTSOReg(AccessType, Operand.Data.GPRIndirect.GPR);
} else if (Operand.IsRIPRelative() || Operand.IsRIPRelativeRelocation()) {
if (Is64BitMode) {
A.Base = GetRelocatedPC(Op, static_cast<int32_t>(Operand.Data.RIPLiteral.Value));
} else {
// 32bit this isn't RIP relative but instead absolute
if (Operand.IsRIPRelativeRelocation()) {
A.Base = _EntrypointOffset(GPRSize, Operand.Data.RIPLiteral.Value);
} else {
A.Offset = Operand.Data.RIPLiteral.Value;
}
}
} else if (Operand.IsSIB() || Operand.IsSIBRelocation()) {
const bool IsVSIB = IsLoad && ((Op->Flags & X86Tables::DecodeFlags::FLAG_VSIB_BYTE) != 0);
if (Operand.Data.SIB.Base != FEXCore::X86State::REG_INVALID) {
A.Base = LoadGPRRegister(Operand.Data.SIB.Base, GPRSize);
}
// NOTE: VSIB cannot have the index * scale portion calculated ahead of time,
// since the index in this case is a vector. So, we can't just apply the scale
// to it, since this needs to be applied to each element in the index register
// after said element has been sign extended. So, we pass this through for the
// instruction implementation to handle.
//
// What we do handle though, is the applying the displacement value to
// the base register (if a base register is provided), since this is a
// part of the address calculation that can be done ahead of time.
if (!IsVSIB && Operand.Data.SIB.Index != FEXCore::X86State::REG_INVALID) {
A.Index = LoadGPRRegister(Operand.Data.SIB.Index, GPRSize);
A.IndexScale = Operand.Data.SIB.Scale;
}
if (Operand.IsSIBRelocation()) {
auto EPOffset = _EntrypointOffset(GPRSize, Operand.Data.SIB.Offset);
if (A.Base) {
A.Base = Add(GPRSize, EPOffset, A.Base);
} else {
A.Base = EPOffset;
}
} else {
A.Offset = static_cast<int32_t>(Operand.Data.SIB.Offset);
}
A.NonTSO |= IsNonTSOReg(AccessType, Operand.Data.SIB.Base) || IsNonTSOReg(AccessType, Operand.Data.SIB.Index);
} else if (Operand.IsLiteralRelocation()) {
A.Base = _EntrypointOffset(GPRSize, Operand.Data.LiteralRelocation.EntrypointOffset);
} else {
LOGMAN_MSG_A_FMT("Unknown Src Type: {}\n", Operand.Type);
}
return A;
}
Ref OpDispatchBuilder::LoadSource_WithOpSize(RegClass Class, const X86Tables::DecodedOp& Op, const X86Tables::DecodedOperand& Operand,
IR::OpSize OpSize, uint32_t Flags, const LoadSourceOptions& Options) {
auto [Align, LoadData, ForceLoad, AccessType, AllowUpperGarbage] = Options;
AddressMode A = DecodeAddress(Op, Operand, AccessType, true /* IsLoad */);
if (Operand.IsGPR()) {
const auto gpr = Operand.Data.GPR.GPR;
const auto highIndex = Operand.Data.GPR.HighBits ? 1 : 0;
if (gpr >= FEXCore::X86State::REG_MM_0) {
LOGMAN_THROW_A_FMT(OpSize == OpSize::i64Bit, "full");
if (MMXState != MMXState_MMX) {
ChgStateX87_MMX();
}
A.Base = LoadContext(OpSize::i64Bit, MM0Index + gpr - FEXCore::X86State::REG_MM_0);
} else if (gpr >= FEXCore::X86State::REG_XMM_0) {
const auto gprIndex = gpr - X86State::REG_XMM_0;
// Load the full register size if it is a XMM register source.
A.Base = LoadXMMRegister(gprIndex);
// Now extract the subregister if it was a partial load /smaller/ than SSE size
// TODO: Instead of doing the VMov implicitly on load, hunt down all use cases that require partial loads and do it after load.
// We don't have information here to know if the operation needs zero upper bits or can contain data.
if (!AllowUpperGarbage && OpSize < OpSize::i128Bit) {
A.Base = _VMov(OpSize, A.Base);
}
} else {
A.Base = LoadGPRRegister(gpr, OpSize, highIndex ? 8 : 0, AllowUpperGarbage);
}
}
if ((IsOperandMem(Operand, true) && LoadData) || ForceLoad) {
if (OpSize == OpSize::f80Bit) {
Ref MemSrc = LoadEffectiveAddress(this, A, GetGPROpSize(), true);
if (CTX->HostFeatures.SupportsSVE128 || CTX->HostFeatures.SupportsSVE256) {
return _LoadMemX87SVEOptPredicate(OpSize::i128Bit, OpSize::i16Bit, MemSrc);
} else {
// For X87 extended doubles, Split the load.
auto Res = _LoadMem(Class, OpSize::i64Bit, MemSrc, Align == OpSize::iInvalid ? OpSize : Align);
return _VLoadVectorElement(OpSize::i128Bit, OpSize::i16Bit, Res, 4, Add(OpSize::i64Bit, MemSrc, 8));
}
}
return _LoadMemAutoTSO(Class, OpSize, A, Align == OpSize::iInvalid ? OpSize : Align);
} else {
return LoadEffectiveAddress(this, A, GetGPROpSize(), false, AllowUpperGarbage);
}
}
Ref OpDispatchBuilder::LoadGPRRegister(uint32_t GPR, IR::OpSize Size, uint8_t Offset, bool AllowUpperGarbage) {
const auto GPRSize = GetGPROpSize();
if (Size == OpSize::iInvalid) {
Size = GPRSize;
}
Ref Reg = LoadGPR(GPR);
if ((!AllowUpperGarbage && (Size != GPRSize)) || Offset != 0) {
// Extract the subregister if requested.
const auto OpSize = std::max(OpSize::i32Bit, Size);
if (AllowUpperGarbage) {
Reg = _Lshr(OpSize, Reg, Constant(Offset));
} else {
Reg = _Bfe(OpSize, IR::OpSizeAsBits(Size), Offset, Reg);
}
}
return Reg;
}
void OpDispatchBuilder::StoreGPRRegister(uint32_t GPR, const Ref Src, IR::OpSize Size, uint8_t Offset) {
const auto GPRSize = GetGPROpSize();
if (Size == OpSize::iInvalid) {
Size = GPRSize;
}
Ref Reg = Src;
if (Size != GPRSize || Offset != 0) {
// Need to do an insert if not automatic size or zero offset.
Reg = ARef(Reg).BfiInto(LoadGPRRegister(GPR), Offset, IR::OpSizeAsBits(Size));
}
StoreRegister(GPR, false, Reg);
}
void OpDispatchBuilder::StoreXMMRegister(uint32_t XMM, const Ref Src) {
StoreRegister(XMM, true, Src);
}
Ref OpDispatchBuilder::LoadSource(RegClass Class, const X86Tables::DecodedOp& Op, const X86Tables::DecodedOperand& Operand, uint32_t Flags,
const LoadSourceOptions& Options) {
const auto OpSize = OpSizeFromSrc(Op);
return LoadSource_WithOpSize(Class, Op, Operand, OpSize, Flags, Options);
}
void OpDispatchBuilder::StoreResult_WithOpSize(RegClass Class, FEXCore::X86Tables::DecodedOp Op, const X86Tables::DecodedOperand& Operand,
Ref Src, IR::OpSize OpSize, IR::OpSize Align, MemoryAccessType AccessType) {
if (Operand.IsGPR()) {
// 8Bit and 16bit destination types store their result without effecting the upper bits
// 32bit ops ZEXT the result to 64bit
const auto GPRSize = GetGPROpSize();
const auto gpr = Operand.Data.GPR.GPR;
if (gpr >= FEXCore::X86State::REG_MM_0) {
LOGMAN_THROW_A_FMT(OpSize == OpSize::i64Bit, "full");
LOGMAN_THROW_A_FMT(Class == RegClass::FPR, "MMX is floaty");
if (MMXState != MMXState_MMX) {
ChgStateX87_MMX();
}
uint8_t Index = MM0Index + gpr - FEXCore::X86State::REG_MM_0;
StoreContext(Index, Src);
RegCache.Partial |= (1ull << (uint64_t)Index);
} else if (gpr >= FEXCore::X86State::REG_XMM_0) {
const auto gprIndex = gpr - X86State::REG_XMM_0;
const auto VectorSize = GetGuestVectorLength();
auto Result = Src;
if (OpSize != VectorSize) {
// Partial writes can come from FPRs.
// TODO: Fix the instructions doing partial writes rather than dealing with it here.
LOGMAN_THROW_A_FMT(Class != RegClass::GPR, "Partial writes from GPR not allowed. Instruction: {}", Op->TableInfo->Name);
// XMM-size is handled in implementations.
if (VectorSize != OpSize::i256Bit || OpSize != OpSize::i128Bit) {
auto SrcVector = LoadXMMRegister(gprIndex);
Result = _VInsElement(VectorSize, OpSize, 0, 0, SrcVector, Src);
}
}
StoreXMMRegister(gprIndex, Result);
} else {
if (GPRSize == OpSize::i64Bit && OpSize == OpSize::i32Bit) {
// If the Source IR op is 64 bits, we need to zext the upper bits
// For all other sizes, the upper bits are guaranteed to already be zero
Ref Value = GetOpSize(Src) == OpSize::i64Bit ? ARef(Src).Bfe(0, 32).Ref() : Src;
StoreGPRRegister(gpr, Value, GPRSize);
LOGMAN_THROW_A_FMT(!Operand.Data.GPR.HighBits, "Can't handle 32bit store to high 8bit register");
} else {
LOGMAN_THROW_A_FMT(!(GPRSize == OpSize::i32Bit && OpSize > OpSize::i32Bit), "Oops had a {} GPR load", OpSize);
if (GPRSize != OpSize) {
// if the GPR isn't the full size then we need to insert.
// eg:
// mov al, 2 ; Move in to lower 8-bits.
// mov ah, 2 ; Move in to upper 8-bits of 16-bit reg.
// mov ax, 2 ; Move in to lower 16-bits of reg.
StoreGPRRegister(gpr, Src, OpSize, Operand.Data.GPR.HighBits * 8);
} else {
StoreGPRRegister(gpr, Src, std::min(GPRSize, OpSize));
}
}
}
return;
}
AddressMode A = DecodeAddress(Op, Operand, AccessType, false /* IsLoad */);
if (OpSize == OpSize::f80Bit) {
Ref MemStoreDst = LoadEffectiveAddress(this, A, GetGPROpSize(), true);
if (CTX->HostFeatures.SupportsSVE128 || CTX->HostFeatures.SupportsSVE256) {
_StoreMemX87SVEOptPredicate(OpSize::i128Bit, OpSize::i16Bit, Src, MemStoreDst);
} else {
// For X87 extended doubles, split before storing
_StoreMemFPR(OpSize::i64Bit, MemStoreDst, Src, Align);
auto Upper = _VExtractToGPR(OpSize::i128Bit, OpSize::i64Bit, Src, 1);
_StoreMemGPR(OpSize::i16Bit, Upper, MemStoreDst, Constant(8), std::min(Align, OpSize::i64Bit), MemOffsetType::SXTX, 1);
}
} else {
_StoreMemAutoTSO(Class, OpSize, A, Src, Align == OpSize::iInvalid ? OpSize : Align);
}
}
void OpDispatchBuilder::StoreResult(RegClass Class, X86Tables::DecodedOp Op, const X86Tables::DecodedOperand& Operand, Ref Src,
IR::OpSize Align, MemoryAccessType AccessType) {
StoreResult_WithOpSize(Class, Op, Operand, Src, OpSizeFromDst(Op), Align, AccessType);
}
void OpDispatchBuilder::StoreResult(RegClass Class, X86Tables::DecodedOp Op, Ref Src, IR::OpSize Align, MemoryAccessType AccessType) {
StoreResult(Class, Op, Op->Dest, Src, Align, AccessType);
}
OpDispatchBuilder::OpDispatchBuilder(FEXCore::Context::ContextImpl* ctx)
: IREmitter {ctx->OpDispatcherAllocator, ctx->HostFeatures.SupportsTSOImm9}
, CTX {ctx} {
if (CTX->HostFeatures.SupportsAVX && CTX->HostFeatures.SupportsSVE256) {
SaveAVXStateFunc = &OpDispatchBuilder::SaveAVXState;
RestoreAVXStateFunc = &OpDispatchBuilder::RestoreAVXState;
DefaultAVXStateFunc = &OpDispatchBuilder::DefaultAVXState;
} else if (CTX->HostFeatures.SupportsAVX) {
SaveAVXStateFunc = &OpDispatchBuilder::AVX128_SaveAVXState;
RestoreAVXStateFunc = &OpDispatchBuilder::AVX128_RestoreAVXState;
DefaultAVXStateFunc = &OpDispatchBuilder::AVX128_DefaultAVXState;
}
}
void OpDispatchBuilder::ResetWorkingList() {
IREmitter::ReownOrClaimBuffer();
JumpTargets.clear();
BlockSetRIP = false;
DecodeFailure = false;
ShouldDump = false;
CurrentCodeBlock = nullptr;
RegCache.Written = 0;
RegCache.Cached = 0;
}
void OpDispatchBuilder::UnhandledOp(OpcodeArgs) {
DecodeFailure = true;
}
void OpDispatchBuilder::MOVGPROp(OpcodeArgs, uint32_t SrcIndex) {
// StoreResult will store with the same size as the input, so we allow upper
// garbage on the input. The zero extension would be pointless.
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIndex], Op->Flags, {.Align = OpSize::i8Bit, .AllowUpperGarbage = true});
StoreResultGPR(Op, Src, OpSize::i8Bit);
}
void OpDispatchBuilder::MOVGPRNTOp(OpcodeArgs) {
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.Align = OpSize::i8Bit});
StoreResultGPR(Op, Src, OpSize::i8Bit, MemoryAccessType::STREAM);
}
void OpDispatchBuilder::ALUOp(OpcodeArgs, FEXCore::IR::IROps ALUIROp, FEXCore::IR::IROps AtomicFetchOp, unsigned SrcIdx) {
// On x86, the canonical way to zero a register is XOR with itself. Detect and
// emit optimal arm64 assembly.
if (!DestIsLockedMem(Op) && ALUIROp == FEXCore::IR::IROps::OP_XOR && Op->Dest.IsGPR() && Op->Src[SrcIdx].IsGPR() &&
Op->Dest.Data.GPR == Op->Src[SrcIdx].Data.GPR) {
// Set flags for zero result with inverted carry. We subtract an arbitrary
// register from itself to get the zero, since `subs wzr, #0` is not
// encodable. This is optimal and works regardless of the opsize.
auto Zero = LoadGPR(Op->Dest.Data.GPR.GPR);
HandleNZ00Write();
InvalidateAF();
CalculatePF(SubWithFlags(OpSize::i32Bit, Zero, Zero));
CFInverted = true;
FlushRegisterCache();
// Move 0 into the register
StoreResultGPR(Op, Constant(0));
return;
}
auto Size = OpSizeFromDst(Op);
auto ResultSize = Size;
auto RoundedSize = Size;
if (ALUIROp != FEXCore::IR::IROps::OP_ANDWITHFLAGS) {
RoundedSize = std::max(OpSize::i32Bit, RoundedSize);
}
// X86 basic ALU ops just do the operation between the destination and a single source
Ref Src = LoadSourceGPR(Op, Op->Src[SrcIdx], Op->Flags, {.AllowUpperGarbage = true});
// Try to eliminate the masking after 8/16-bit operations with constants, by
// promoting to a full size operation that preserves the upper bits.
uint64_t Const;
bool IsConst = IsValueConstant(WrapNode(Src), &Const);
if (Size < OpSize::i32Bit && !DestIsLockedMem(Op) && Op->Dest.IsGPR() && !Op->Dest.Data.GPR.HighBits && IsConst &&
(ALUIROp == IR::IROps::OP_XOR || ALUIROp == IR::IROps::OP_OR || ALUIROp == IR::IROps::OP_ANDWITHFLAGS)) {
RoundedSize = ResultSize = GetGPROpSize();
LOGMAN_THROW_A_FMT(Const < (1ull << IR::OpSizeAsBits(Size)), "does not clobber");
// For AND, we can play the same trick but we instead need the upper bits of
// the constant to be all-1s instead of all-0s to preserve. We also can't
// use andwithflags in this case, since we've promoted to 64-bit so the
// negate flag would be wrong, but using the regular logical operation path
// instead still ends up a net win for uops.
//
// In the common case where the constant is of the form (1 << x) - 1, the
// adjusted constant here will inline into the arm64 and instruction, so if
// flags are not needed, we save an instruction overall.
if (ALUIROp == IR::IROps::OP_ANDWITHFLAGS) {
Src = Constant(Const | ~((1ull << IR::OpSizeAsBits(Size)) - 1));
ALUIROp = IR::IROps::OP_AND;
}
}
Ref Result {};
Ref Dest {};
if (DestIsLockedMem(Op)) {
HandledLock = true;
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
DeriveOp(FetchOp, AtomicFetchOp, _AtomicFetchAdd(Size, Src, DestMem));
Dest = FetchOp;
} else {
Dest = LoadSourceGPR(Op, Op->Dest, Op->Flags, {.AllowUpperGarbage = true});
}
const auto OpSize = RoundedSize;
uint64_t Mask = Size == OpSize::i64Bit ? ~0ull : ((1ull << IR::OpSizeAsBits(Size)) - 1);
if (IsConst && Const == Mask && !DestIsLockedMem(Op) && ALUIROp == IR::IROps::OP_XOR && Size >= OpSize::i32Bit) {
Result = _Not(OpSize, Dest);
} else if (IsConst && Const == Mask && !DestIsLockedMem(Op) && ALUIROp == IR::IROps::OP_AND) {
Result = Dest;
} else {
DeriveOp(ALUOp, ALUIROp, _AndWithFlags(OpSize, Dest, Src));
Result = ALUOp;
}
// Flags set
switch (ALUIROp) {
case FEXCore::IR::IROps::OP_ADD: Result = CalculateFlags_ADD(Size, Dest, Src); break;
case FEXCore::IR::IROps::OP_SUB: Result = CalculateFlags_SUB(Size, Dest, Src); break;
case FEXCore::IR::IROps::OP_XOR:
case FEXCore::IR::IROps::OP_AND:
case FEXCore::IR::IROps::OP_OR: {
CalculateFlags_Logical(Size, Result);
break;
}
case FEXCore::IR::IROps::OP_ANDWITHFLAGS: {
HandleNZ00Write();
CalculatePF(Result);
InvalidateAF();
break;
}
default: break;
}
if (!DestIsLockedMem(Op)) {
StoreResultGPR_WithOpSize(Op, Op->Dest, Result, ResultSize, OpSize::iInvalid, MemoryAccessType::DEFAULT);
}
}
void OpDispatchBuilder::LSLOp(OpcodeArgs) {
// Emulate by always returning failure, this deviates from both Linux and Windows but
// shouldn't be depended on by anything.
SetRFLAG<FEXCore::X86State::RFLAG_ZF_RAW_LOC>(Constant(0));
}
void OpDispatchBuilder::INTOp(OpcodeArgs) {
IR::BreakDefinition Reason;
bool SetRIPToNext = false;
switch (Op->OP) {
case 0xCD: { // INT imm8
uint8_t Literal = Op->Src[0].Literal();
#ifndef _WIN32
constexpr uint8_t SYSCALL_LITERAL = 0x80;
if (Literal == SYSCALL_LITERAL) {
if (Is64BitMode) [[unlikely]] {
LogMan::Msg::EFmt("[Unsupported] Trying to execute 32-bit syscall from a 64-bit process.");
UnhandledOp(Op);
return;
}
// Syscall on linux
SyscallOp(Op, false);
return;
}
#else
constexpr uint8_t SYSCALL_LITERAL = 0x2E;
if (Literal == SYSCALL_LITERAL) {
// Can be used for both 64-bit and 32-bit syscalls on windows
SyscallOp(Op, false);
return;
}
#endif
#ifdef ARCHITECTURE_arm64ec
// This is used when QueryPerformanceCounter is called on recent Windows versions, it causes CNTVCT to be written into RAX.
constexpr uint8_t GET_CNTVCT_LITERAL = 0x81;
if (Literal == GET_CNTVCT_LITERAL) {
StoreGPRRegister(X86State::REG_RAX, _CycleCounter(false));
return;
}
#endif
Reason.ErrorRegister = Literal << 3 | (0b010);
Reason.Signal = Core::FAULT_SIGSEGV;
// GP is raised when task-gate isn't setup to be valid
Reason.TrapNumber = X86State::X86_TRAPNO_GP;
Reason.si_code = 0x80;
break;
}
case 0xCE: // INTO
Reason.ErrorRegister = 0;
Reason.Signal = Core::FAULT_SIGSEGV;
Reason.TrapNumber = X86State::X86_TRAPNO_OF;
Reason.si_code = 0x80;
break;
case 0xF1: // INT1
Reason.ErrorRegister = 0;
Reason.Signal = Core::FAULT_SIGTRAP;
Reason.TrapNumber = X86State::X86_TRAPNO_DB;
Reason.si_code = 1;
SetRIPToNext = true;
break;
case 0xF4: { // HLT
Reason.ErrorRegister = 0;
Reason.Signal = Core::FAULT_SIGSEGV;
Reason.TrapNumber = X86State::X86_TRAPNO_GP;
Reason.si_code = 0x80;
break;
}
case 0x0B: // UD2
Reason.ErrorRegister = 0;
Reason.Signal = Core::FAULT_SIGILL;
Reason.TrapNumber = X86State::X86_TRAPNO_UD;
Reason.si_code = 2;
break;
case 0xCC: // INT3
Reason.ErrorRegister = 0;
Reason.Signal = Core::FAULT_SIGTRAP;
Reason.TrapNumber = X86State::X86_TRAPNO_BP;
Reason.si_code = 0x80;
SetRIPToNext = true;
break;
default: FEX_UNREACHABLE;
}
// Calculate flags early.
FlushRegisterCache();
const auto GPRSize = GetGPROpSize();
if (SetRIPToNext) {
BlockSetRIP = SetRIPToNext;
// We want to set RIP to the next instruction after INT3/INT1
auto NewRIP = GetRelocatedPC(Op);
_StoreContextGPR(GPRSize, NewRIP, offsetof(FEXCore::Core::CPUState, rip));
} else if (Op->OP != 0xCE) {
auto NewRIP = GetRelocatedPC(Op, -Op->InstSize);
_StoreContextGPR(GPRSize, NewRIP, offsetof(FEXCore::Core::CPUState, rip));
}
if (Op->OP == 0xCE) { // Conditional to only break if Overflow == 1
CalculateDeferredFlags();
// If condition doesn't hold then keep going
// CondClass::FNU means OF == 0
auto CondJump_ = CondJumpNZCV(CondClass::FNU);
auto FalseBlock = CreateNewCodeBlockAfter(GetCurrentBlock());
SetFalseJumpTarget(CondJump_, FalseBlock);
SetCurrentCodeBlock(FalseBlock);
StartNewBlock();
auto NewRIP = GetRelocatedPC(Op);
_StoreContextGPR(GPRSize, NewRIP, offsetof(FEXCore::Core::CPUState, rip));
Break(Reason);
// Make sure to start a new block after ending this one
auto JumpTarget = CreateNewCodeBlockAfter(FalseBlock);
SetTrueJumpTarget(CondJump_, JumpTarget);
SetCurrentCodeBlock(JumpTarget);
StartNewBlock();
} else {
BlockSetRIP = true;
Break(Reason);
}
}
void OpDispatchBuilder::TZCNT(OpcodeArgs) {
// _FindTrailingZeroes ignores upper garbage so we don't need to mask
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
Src = _FindTrailingZeroes(OpSizeFromSrc(Op), Src);
StoreResultGPR(Op, Src);
CalculateFlags_ZCNT(OpSizeFromSrc(Op), Src);
}
void OpDispatchBuilder::LZCNT(OpcodeArgs) {
// _CountLeadingZeroes clears upper garbage so we don't need to mask
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.AllowUpperGarbage = true});
auto Res = _CountLeadingZeroes(OpSizeFromSrc(Op), Src);
StoreResultGPR(Op, Res);
CalculateFlags_ZCNT(OpSizeFromSrc(Op), Res);
}
void OpDispatchBuilder::MOVBEOp(OpcodeArgs) {
const auto GPRSize = GetGPROpSize();
const auto SrcSize = OpSizeFromSrc(Op);
Ref Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.Align = OpSize::i8Bit});
if (DestIsMem(Op) || SrcSize != OpSize::i16Bit) {
Src = _Rev(SrcSize, Src);
StoreResultGPR(Op, Op->Dest, Src);
} else {
Src = _Rev(std::max(OpSize::i32Bit, SrcSize), Src);
// 16-bit does an insert.
// Rev of 16-bit value as 32-bit replaces the result in the upper 16-bits of the result.
// bfxil the 16-bit result in to the GPR.
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GPRSize, Op->Flags);
auto Result = _Bfxil(GPRSize, 16, 16, Dest, Src);
StoreResultGPR_WithOpSize(Op, Op->Dest, Result, GPRSize);
}
}
void OpDispatchBuilder::CLWBOrTPause(OpcodeArgs) {
if (DestIsMem(Op)) {
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
_CacheLineClean(DestMem);
} else {
if (!CTX->HostFeatures.SupportsWFXT) {
UnimplementedOp(Op);
} else {
auto RAX = LoadGPRRegister(X86State::REG_RAX);
auto RDX = LoadGPRRegister(X86State::REG_RDX);
// Incoming source register is unused.
_WFET(RDX, RAX);
// OF, SF, ZF, AF, PF, CF all zero.
// CF is used if the OS deadline is set, which we don't do anything with.
ZeroPF_AF();
ZeroNZCV();
}
}
}
void OpDispatchBuilder::CLFLUSHOPT(OpcodeArgs) {
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
_CacheLineClear(DestMem, false);
}
void OpDispatchBuilder::LoadFenceOrXRSTOR(OpcodeArgs) {
// 0xE8 signifies LFENCE
if (Op->ModRM == 0xE8) {
_Fence(FenceType::Load);
} else {
XRstorOpImpl(Op);
}
}
void OpDispatchBuilder::MemFenceOrXSAVEOPT(OpcodeArgs) {
if (Op->ModRM == 0xF0) {
// 0xF0 is MFENCE
_Fence(FenceType::LoadStore);
} else {
XSaveOp(Op);
}
}
void OpDispatchBuilder::StoreFenceOrCLFlush(OpcodeArgs) {
if (Op->ModRM == 0xF8) {
// 0xF8 is SFENCE
_Fence(FenceType::Store);
} else {
// This is a CLFlush
Ref DestMem = MakeSegmentAddress(Op, Op->Dest);
_CacheLineClear(DestMem, true);
}
}
void OpDispatchBuilder::UMonitorOrCLRSSBSY(OpcodeArgs) {
if (DestIsMem(Op) || !CTX->HostFeatures.SupportsWFXT) {
// CLRSSBSY
UnimplementedOp(Op);
} else {
// Explicit NOP implementation of umonitor.
}
}
void OpDispatchBuilder::UMWaitOp(OpcodeArgs) {
if (DestIsMem(Op) || !CTX->HostFeatures.SupportsWFXT) {
UnimplementedOp(Op);
} else {
// Explicit NOP implementation of umwait.
// Still zero flags.
//
// OF, SF, ZF, AF, PF, CF all zero.
ZeroPF_AF();
ZeroNZCV();
}
}
void OpDispatchBuilder::CLZeroOp(OpcodeArgs) {
if (!CTX->HostFeatures.SupportsCLZERO) {
UnimplementedOp(Op);
return;
}
Ref DestMem = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.LoadData = false});
_CacheLineZero(DestMem);
}
void OpDispatchBuilder::Prefetch(OpcodeArgs, bool ForStore, bool Stream, uint8_t Level) {
Ref DestMem = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.LoadData = false});
_Prefetch(ForStore, Stream, Level, DestMem, Invalid(), MemOffsetType::SXTX, 1);
}
void OpDispatchBuilder::RDTSCPOp(OpcodeArgs) {
// RDTSCP is slightly different than RDTSC
// IA32_TSC_AUX is returned in RCX
// All previous loads are globally visible
// - Explicitly does not wait for stores to be globally visible
// - Explicitly use an MFENCE before this instruction if you want this behaviour
// This instruction is not an execution fence, so subsequent instructions can execute after this
// - Explicitly use an LFENCE after RDTSCP if you want to block this behaviour
auto Counter = CycleCounter(true);
auto ID = _ProcessorID();
StoreGPRRegister(X86State::REG_RAX, Counter.CounterLow);
StoreGPRRegister(X86State::REG_RCX, ID);
StoreGPRRegister(X86State::REG_RDX, Counter.CounterHigh);
}
void OpDispatchBuilder::RDPIDOp(OpcodeArgs) {
StoreResultGPR(Op, _ProcessorID());
}
void OpDispatchBuilder::CRC32(OpcodeArgs) {
if (!CTX->HostFeatures.SupportsCRC) {
UnimplementedOp(Op);
return;
}
const auto GPRSize = GetGPROpSize();
// Destination GPR size is always 4 or 8 bytes depending on widening
const auto DstSize = Op->Flags & FEXCore::X86Tables::DecodeFlags::FLAG_REX_WIDENING ? OpSize::i64Bit : OpSize::i32Bit;
Ref Dest = LoadSourceGPR_WithOpSize(Op, Op->Dest, GPRSize, Op->Flags);
// Incoming memory is 8, 16, 32, or 64
Ref Src {};
if (Op->Src[0].IsGPR()) {
Src = LoadSourceGPR_WithOpSize(Op, Op->Src[0], GPRSize, Op->Flags);
} else {
Src = LoadSourceGPR(Op, Op->Src[0], Op->Flags, {.Align = OpSize::i8Bit});
}
auto Result = _CRC32(Dest, Src, OpSizeFromSrc(Op));
StoreResultGPR_WithOpSize(Op, Op->Dest, Result, DstSize);
}
template<bool Reseed>
void OpDispatchBuilder::RDRANDOp(OpcodeArgs) {
if (!CTX->HostFeatures.SupportsRAND) {
UnimplementedOp(Op);
return;
}
StoreResultGPR(Op, _RDRAND(Reseed));
// If the rng number is valid then NZCV is 0b0000, otherwise NZCV is 0b0100
auto CF_inv = GetRFLAG(X86State::RFLAG_ZF_RAW_LOC);
// OF, SF, ZF, AF, PF all zero. CF indicates if valid.
ZeroPF_AF();
if (!CTX->HostFeatures.SupportsFlagM) {
ZeroNZCV();
SetCFInverted(CF_inv);
} else {
// Accelerated path. Invalid is 0 or 1, so set NZCV with a single rmif.
HandleNZCVWrite();
_RmifNZCV(CF_inv, (64 - 1) /* rotate bit 0 into bit 1 = C */, 0xf);
CFInverted = true;
}
}
template void OpDispatchBuilder::RDRANDOp<true>(OpcodeArgs);
template void OpDispatchBuilder::RDRANDOp<false>(OpcodeArgs);
void OpDispatchBuilder::BreakOp(OpcodeArgs, FEXCore::IR::BreakDefinition BreakDefinition) {
const auto GPRSize = GetGPROpSize();
// We don't actually support this instruction
// Multiblock may hit it though
_StoreContextGPR(GPRSize, GetRelocatedPC(Op, -Op->InstSize), offsetof(FEXCore::Core::CPUState, rip));
Break(BreakDefinition);
if (Multiblock) {
auto NextBlock = CreateNewCodeBlockAfter(GetCurrentBlock());
SetCurrentCodeBlock(NextBlock);
StartNewBlock();
} else {
BlockSetRIP = true;
}
}
void OpDispatchBuilder::UnimplementedOp(OpcodeArgs) {
BreakOp(Op, FEXCore::IR::BreakDefinition {
.ErrorRegister = 0,
.Signal = SIGILL,
.TrapNumber = X86State::X86_TRAPNO_UD,
.si_code = 2, ///< ILL_ILLOPN
});
}
void OpDispatchBuilder::PermissionRestrictedOp(OpcodeArgs) {
BreakOp(Op, FEXCore::IR::BreakDefinition {
.ErrorRegister = 0,
.Signal = SIGSEGV,
.TrapNumber = X86State::X86_TRAPNO_GP,
.si_code = 0x80,
});
}
void OpDispatchBuilder::InvalidOp(OpcodeArgs) {
BreakOp(Op, FEXCore::IR::BreakDefinition {
.ErrorRegister = 0,
.Signal = SIGILL,
.TrapNumber = 0,
.si_code = 0,
});
}
void OpDispatchBuilder::NoExecOp(OpcodeArgs) {
BreakOp(Op, FEXCore::IR::BreakDefinition {
.ErrorRegister = X86State::X86_PF_PROT | X86State::X86_PF_USER | X86State::X86_PF_INSTR,
.Signal = Core::FAULT_SIGSEGV,
.TrapNumber = X86State::X86_TRAPNO_PF,
.si_code = 2, // SEGV_ACCERR
});
}
#undef OpcodeArgs
} // namespace FEXCore::IR