mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-28 11:00:53 +01:00
The constraints introduced by shared code buffers make supporting calls with the previous layout impossible. The main additional constraint imposed by call-ret that if a host location is ever pushed onto the call-ret stack, then it must forever be a valid jump target. While this is reasonable in the: unlinked, direct linked, unlinked, direct linked case; it's almost impossible to achieve in the: unlinked, indirect linked, unlinked, direct linked case while ensuring all backpatching cases are valid with the current approach. To solve this introduce an additional layer of indirection, jump thunks, these are emitted at the end of a multiblock and are used to handle the two cases of calling the initial linker, and calling an indirect linked block. Initially at the ExitFunction location a branch/call to a unique jump thunk will be emitted, which will have the code layout: 00: b 0x8 04: br TMP1 08: ldr TMP1, <Shared exit linker> 0c: blr TMP1 10: HostCode 18: GuestRIP 20: CallerOffset If a direct link can be performed, then the initial branch/call to the jump thunk can be linked/unlinked to point to the jump thunk in a single 32-bit atomic operation. For an indirect link, the HostCode member is updated with a 64 bit atomic operation, and then a 32 bit atomic operation is used to replace the branch at 00 with a load of HostCode. Indirect unlinks are done by placing back the b 0x8 at 00. Safety: (1) Sequential link (e.g. one waiting to lock, one locked and linking): Linking is idempotent, would just rewrite the same data atomically. (2) Simultaneous link or simultaneous delink: Impossible due to LookupCache locking. (3) Simultaneous link and execute: (3.1) Direct link: Either the direct link is observed at the thunk callsite, or it is not observed and the linker is entered - this is then just (1). (3.2) Indirect link: Either the branch at 00 in the thunk is observed to be replaced with an ldr, in which case the modified HostCode must be observed due to the cache flush. Alternatively the branch replacement isn't observed and it's just (1). (4) Simultaneous unlink and execute: (4.1) Direct link: Either the jump to the jump thunk is seen, which must be in its base unlinked state with the branch at 00 as that would be inserted by any previous indirect unlink. In such a case the linker would just be entered, giving (5). Alternatively the modified jump isn't seen and it calls the original host code (which is fine). (4.2) Indirect link: If an ldr is seen at 00, then the rest of that sequence will function fine as HostCode is left untouched. If a branch is seen at 00, then it will just call the linker giving (5). (5) Sequential unlink then link: Unlinking restores the callsite and jump thunk to their original contents (aside from a modified HostCode). Linking then works as usual.
381 lines
15 KiB
C++
381 lines
15 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#pragma once
|
|
|
|
#include "Common/JitSymbols.h"
|
|
#include "Interface/Core/CPUID.h"
|
|
#include "Interface/Core/X86HelperGen.h"
|
|
#include "Interface/Core/ObjectCache/ObjectCacheService.h"
|
|
#include "Interface/Core/Dispatcher/Dispatcher.h"
|
|
#include "Interface/IR/AOTIR.h"
|
|
#include <FEXCore/Config/Config.h>
|
|
#include <FEXCore/Core/Context.h>
|
|
#include <FEXCore/Core/CoreState.h>
|
|
#include <FEXCore/Core/HostFeatures.h>
|
|
#include <FEXCore/Core/SignalDelegator.h>
|
|
#include <FEXCore/Debug/InternalThreadState.h>
|
|
#include <FEXCore/IR/IR.h>
|
|
#include <FEXCore/Utils/CompilerDefs.h>
|
|
#include <FEXCore/Utils/Event.h>
|
|
#include <FEXCore/Utils/SignalScopeGuards.h>
|
|
#include <FEXCore/fextl/memory.h>
|
|
#include <FEXCore/fextl/set.h>
|
|
#include <FEXCore/fextl/string.h>
|
|
#include <FEXCore/fextl/unordered_map.h>
|
|
#include <FEXCore/fextl/vector.h>
|
|
#include <FEXHeaderUtils/Syscalls.h>
|
|
#include <stdint.h>
|
|
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
|
|
namespace FEXCore {
|
|
class CodeLoader;
|
|
class ThunkHandler;
|
|
|
|
namespace CodeSerialize {
|
|
class CodeObjectSerializeService;
|
|
}
|
|
|
|
namespace CPU {
|
|
class Arm64JITCore;
|
|
class Dispatcher;
|
|
} // namespace CPU
|
|
namespace HLE {
|
|
struct SyscallArguments;
|
|
class SyscallHandler;
|
|
class SourcecodeResolver;
|
|
struct SourcecodeMap;
|
|
} // namespace HLE
|
|
} // namespace FEXCore
|
|
|
|
namespace FEXCore::IR {
|
|
struct IRListCopy;
|
|
class IRListView;
|
|
namespace Validation {
|
|
class IRValidation;
|
|
}
|
|
} // namespace FEXCore::IR
|
|
|
|
namespace FEXCore::Context {
|
|
struct FEX_PACKED ExitFunctionLinkData {
|
|
uint64_t HostCode;
|
|
uint64_t GuestRIP;
|
|
int64_t CallerOffset;
|
|
};
|
|
|
|
struct CustomIRResult {
|
|
void* Creator;
|
|
void* Data;
|
|
|
|
CustomIRResult(void* Creator, void* Data)
|
|
: Creator(Creator)
|
|
, Data(Data) {}
|
|
};
|
|
|
|
using BlockDelinkerFunc = void (*)(FEXCore::Core::CpuStateFrame* Frame, FEXCore::Context::ExitFunctionLinkData* Record);
|
|
constexpr uint32_t TSC_SCALE_MAXIMUM = 1'000'000'000; ///< 1Ghz
|
|
|
|
class ContextImpl final : public FEXCore::Context::Context, CPU::CodeBufferManager {
|
|
public:
|
|
// Context base class implementation.
|
|
bool InitCore() override;
|
|
|
|
void ExecuteThread(FEXCore::Core::InternalThreadState* Thread) override;
|
|
|
|
void CompileRIP(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestRIP) override;
|
|
void CompileRIPCount(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestRIP, uint64_t MaxInst) override;
|
|
|
|
void HandleCallback(FEXCore::Core::InternalThreadState* Thread, uint64_t RIP) override;
|
|
|
|
bool IsAddressInCurrentBlock(FEXCore::Core::InternalThreadState* Thread, uint64_t Address, uint64_t Size) override;
|
|
bool IsCurrentBlockSingleInst(FEXCore::Core::InternalThreadState* Thread) override;
|
|
|
|
uint64_t RestoreRIPFromHostPC(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPC) override;
|
|
uint32_t ReconstructCompactedEFLAGS(FEXCore::Core::InternalThreadState* Thread, bool WasInJIT, const uint64_t* HostGPRs, uint64_t PSTATE) override;
|
|
void SetFlagsFromCompactedEFLAGS(FEXCore::Core::InternalThreadState* Thread, uint32_t EFLAGS) override;
|
|
|
|
void ReconstructXMMRegisters(const FEXCore::Core::InternalThreadState* Thread, __uint128_t* XMM_Low, __uint128_t* YMM_High) override;
|
|
void SetXMMRegistersFromState(FEXCore::Core::InternalThreadState* Thread, const __uint128_t* XMM_Low, const __uint128_t* YMM_High) override;
|
|
|
|
/**
|
|
* @brief Used to create FEX thread objects in preparation for creating a true OS thread. Does set a TID or PID.
|
|
*
|
|
* @param InitialRIP The starting RIP of this thread
|
|
* @param StackPointer The starting RSP of this thread
|
|
* @param NewThreadState The initial thread state to setup for our state, if inheriting.
|
|
*
|
|
* @return The InternalThreadState object that tracks all of the emulated thread's state
|
|
*
|
|
* Usecases:
|
|
* Parent thread Creation:
|
|
* - Thread = CreateThread(InitialRIP, InitialStack, nullptr, 0);
|
|
* - CTX->ExecuteThread(Thread);
|
|
* OS thread Creation:
|
|
* - Thread = CreateThread(0, 0, NewState, PPID);
|
|
* - Thread->ExecutionThread = FEXCore::Threads::Thread::Create(ThreadHandler, Arg);
|
|
* - ThreadHandler calls `CTX->ExecuteThread(Thread)`
|
|
* OS fork (New thread created with a clone of thread state):
|
|
* - clone{2, 3}
|
|
* - Thread = CreateThread(0, 0, CopyOfThreadState, PPID);
|
|
* - ExecuteThread(Thread); // Starts executing without creating another host thread
|
|
* Thunk callback executing guest code from native host thread
|
|
* - Thread = CreateThread(0, 0, NewState, PPID);
|
|
* - HandleCallback(Thread, RIP);
|
|
*/
|
|
|
|
FEXCore::Core::InternalThreadState* CreateThread(uint64_t InitialRIP, uint64_t StackPointer, const FEXCore::Core::CPUState* NewThreadState) override;
|
|
|
|
/**
|
|
* @brief Destroys this FEX thread object and stops tracking it internally
|
|
*
|
|
* @param Thread The internal FEX thread state object
|
|
*/
|
|
void DestroyThread(FEXCore::Core::InternalThreadState* Thread) override;
|
|
|
|
#ifndef _WIN32
|
|
void LockBeforeFork(FEXCore::Core::InternalThreadState* Thread) override;
|
|
void UnlockAfterFork(FEXCore::Core::InternalThreadState* Thread, bool Child) override;
|
|
#endif
|
|
void SetSignalDelegator(FEXCore::SignalDelegator* SignalDelegation) override;
|
|
void SetSyscallHandler(FEXCore::HLE::SyscallHandler* Handler) override;
|
|
void SetThunkHandler(FEXCore::ThunkHandler* Handler) override;
|
|
|
|
FEXCore::CPUID::FunctionResults RunCPUIDFunction(uint32_t Function, uint32_t Leaf) override;
|
|
FEXCore::CPUID::XCRResults RunXCRFunction(uint32_t Function) override;
|
|
FEXCore::CPUID::FunctionResults RunCPUIDFunctionName(uint32_t Function, uint32_t Leaf, uint32_t CPU) override;
|
|
|
|
FEXCore::IR::AOTIRCacheEntry* LoadAOTIRCacheEntry(const fextl::string& Name) override;
|
|
void UnloadAOTIRCacheEntry(FEXCore::IR::AOTIRCacheEntry* Entry) override;
|
|
|
|
void SetAOTIRLoader(AOTIRLoaderCBFn CacheReader) override {
|
|
IRCaptureCache.SetAOTIRLoader(std::move(CacheReader));
|
|
}
|
|
void SetAOTIRWriter(AOTIRWriterCBFn CacheWriter) override {
|
|
IRCaptureCache.SetAOTIRWriter(std::move(CacheWriter));
|
|
}
|
|
void SetAOTIRRenamer(AOTIRRenamerCBFn CacheRenamer) override {
|
|
IRCaptureCache.SetAOTIRRenamer(std::move(CacheRenamer));
|
|
}
|
|
|
|
void FinalizeAOTIRCache() override {
|
|
IRCaptureCache.FinalizeAOTIRCache();
|
|
}
|
|
void WriteFilesWithCode(AOTIRCodeFileWriterFn Writer) override {
|
|
IRCaptureCache.WriteFilesWithCode(Writer);
|
|
}
|
|
|
|
void OnCodeBufferAllocated(CPU::CodeBuffer&) override;
|
|
void ClearCodeCache(FEXCore::Core::InternalThreadState* Thread, bool NewCodeBuffer = true) override;
|
|
void InvalidateGuestCodeRange(FEXCore::Core::InternalThreadState* Thread, InvalidatedEntryAccumulator& Accumulator, uint64_t Start,
|
|
uint64_t Length) override;
|
|
FEXCore::ForkableSharedMutex& GetCodeInvalidationMutex() override {
|
|
return CodeInvalidationMutex;
|
|
}
|
|
|
|
void MarkMemoryShared(FEXCore::Core::InternalThreadState* Thread) override;
|
|
|
|
void ConfigureAOTGen(FEXCore::Core::InternalThreadState* Thread, fextl::set<uint64_t>* ExternalBranches, uint64_t SectionMaxAddress) override;
|
|
|
|
bool IsAddressInCodeBuffer(FEXCore::Core::InternalThreadState* Thread, uintptr_t Address) const override;
|
|
|
|
// returns false if a handler was already registered
|
|
std::optional<CustomIRResult>
|
|
AddCustomIREntrypoint(uintptr_t Entrypoint, CustomIREntrypointHandler Handler, void* Creator = nullptr, void* Data = nullptr);
|
|
|
|
void AddThunkTrampolineIRHandler(uintptr_t Entrypoint, uintptr_t GuestThunkEntrypoint) override;
|
|
|
|
void AddForceTSOInformation(const IntervalList<uint64_t>& ValidRanges, fextl::set<uint64_t>&& Instructions) override;
|
|
|
|
void RemoveForceTSOInformation(uint64_t Address, uint64_t Size) override;
|
|
|
|
public:
|
|
friend class FEXCore::HLE::SyscallHandler;
|
|
#ifdef JIT_ARM64
|
|
friend class FEXCore::CPU::Arm64JITCore;
|
|
#endif
|
|
|
|
friend class FEXCore::IR::Validation::IRValidation;
|
|
|
|
struct {
|
|
uint64_t VirtualMemSize {1ULL << 36};
|
|
uint64_t TSCScale = 0;
|
|
|
|
// Used if the JIT needs to have its interrupt fault code emitted.
|
|
bool NeedsPendingInterruptFaultCheck {false};
|
|
|
|
FEX_CONFIG_OPT(Multiblock, MULTIBLOCK);
|
|
FEX_CONFIG_OPT(SingleStepConfig, SINGLESTEP);
|
|
FEX_CONFIG_OPT(GdbServer, GDBSERVER);
|
|
FEX_CONFIG_OPT(Is64BitMode, IS64BIT_MODE);
|
|
FEX_CONFIG_OPT(TSOEnabled, TSOENABLED);
|
|
FEX_CONFIG_OPT(TSOAutoMigration, TSOAUTOMIGRATION);
|
|
FEX_CONFIG_OPT(VectorTSOEnabled, VECTORTSOENABLED);
|
|
FEX_CONFIG_OPT(MemcpySetTSOEnabled, MEMCPYSETTSOENABLED);
|
|
FEX_CONFIG_OPT(ABILocalFlags, ABILOCALFLAGS);
|
|
FEX_CONFIG_OPT(AOTIRCapture, AOTIRCAPTURE);
|
|
FEX_CONFIG_OPT(AOTIRGenerate, AOTIRGENERATE);
|
|
FEX_CONFIG_OPT(AOTIRLoad, AOTIRLOAD);
|
|
FEX_CONFIG_OPT(SMCChecks, SMCCHECKS);
|
|
FEX_CONFIG_OPT(MaxInstPerBlock, MAXINST);
|
|
FEX_CONFIG_OPT(RootFSPath, ROOTFS);
|
|
FEX_CONFIG_OPT(GlobalJITNaming, GLOBALJITNAMING);
|
|
FEX_CONFIG_OPT(LibraryJITNaming, LIBRARYJITNAMING);
|
|
FEX_CONFIG_OPT(BlockJITNaming, BLOCKJITNAMING);
|
|
FEX_CONFIG_OPT(GDBSymbols, GDBSYMBOLS);
|
|
FEX_CONFIG_OPT(ParanoidTSO, PARANOIDTSO);
|
|
FEX_CONFIG_OPT(CacheObjectCodeCompilation, CACHEOBJECTCODECOMPILATION);
|
|
FEX_CONFIG_OPT(x87ReducedPrecision, X87REDUCEDPRECISION);
|
|
FEX_CONFIG_OPT(DisableTelemetry, DISABLETELEMETRY);
|
|
FEX_CONFIG_OPT(DisableVixlIndirectCalls, DISABLE_VIXL_INDIRECT_RUNTIME_CALLS);
|
|
FEX_CONFIG_OPT(SmallTSCScale, SMALLTSCSCALE);
|
|
FEX_CONFIG_OPT(StrictInProcessSplitLocks, STRICTINPROCESSSPLITLOCKS);
|
|
} Config;
|
|
|
|
FEXCore::ForkableSharedMutex CodeInvalidationMutex;
|
|
|
|
uint32_t StrictSplitLockMutex {};
|
|
|
|
FEXCore::HostFeatures HostFeatures;
|
|
// CPUID depends on HostFeatures so needs to be initialized after that.
|
|
FEXCore::CPUIDEmu CPUID;
|
|
FEXCore::HLE::SyscallHandler* SyscallHandler {};
|
|
FEXCore::HLE::SourcecodeResolver* SourcecodeResolver {};
|
|
FEXCore::ThunkHandler* ThunkHandler {};
|
|
fextl::unique_ptr<FEXCore::CPU::Dispatcher> Dispatcher;
|
|
|
|
SignalDelegator* SignalDelegation {};
|
|
X86GeneratedCode X86CodeGen;
|
|
|
|
ContextImpl(const FEXCore::HostFeatures& Features);
|
|
~ContextImpl();
|
|
|
|
static bool ThreadRemoveCodeEntry(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestRIP);
|
|
|
|
// Wrapper which takes CpuStateFrame instead of InternalThreadState and unique_locks CodeInvalidationMutex
|
|
// Must be called from owning thread
|
|
static void ThreadRemoveCodeEntryFromJit(FEXCore::Core::CpuStateFrame* Frame, uint64_t GuestRIP) {
|
|
auto Thread = Frame->Thread;
|
|
auto lk = GuardSignalDeferringSection(static_cast<ContextImpl*>(Thread->CTX)->CodeInvalidationMutex, Thread);
|
|
|
|
// NOTE: Other threads sharing the same CodeBuffer may reference
|
|
// invalidated data ranges through their L1/L2 caches. This is
|
|
// not currently a problem since FEX does not repurpose the
|
|
// invalidated CodeBuffer memory range currently.
|
|
ThreadRemoveCodeEntry(Thread, GuestRIP);
|
|
}
|
|
|
|
void RemoveCustomIREntrypoint(uintptr_t Entrypoint);
|
|
|
|
struct GenerateIRResult {
|
|
std::optional<IR::IRListView> IRView;
|
|
uint64_t TotalInstructions;
|
|
uint64_t TotalInstructionsLength;
|
|
uint64_t StartAddr;
|
|
uint64_t Length;
|
|
bool NeedsAddGuestCodeRanges;
|
|
};
|
|
[[nodiscard]]
|
|
GenerateIRResult GenerateIR(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestRIP, bool ExtendedDebugInfo, uint64_t MaxInst);
|
|
|
|
struct CompileCodeResult {
|
|
CPU::CPUBackend::CompiledCode CompiledCode;
|
|
fextl::unique_ptr<FEXCore::Core::DebugData> DebugData;
|
|
uint64_t StartAddr;
|
|
uint64_t Length;
|
|
bool NeedsAddGuestCodeRanges;
|
|
};
|
|
[[nodiscard]]
|
|
CompileCodeResult CompileCode(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestRIP, uint64_t MaxInst = 0);
|
|
uintptr_t CompileBlock(FEXCore::Core::CpuStateFrame* Frame, uint64_t GuestRIP, uint64_t MaxInst = 0);
|
|
uintptr_t CompileSingleStep(FEXCore::Core::CpuStateFrame* Frame, uint64_t GuestRIP);
|
|
|
|
IR::OpSize GetGPROpSize() const {
|
|
return Config.Is64BitMode ? IR::OpSize::i64Bit : IR::OpSize::i32Bit;
|
|
}
|
|
|
|
FEXCore::JITSymbols Symbols;
|
|
|
|
FEXCore::Utils::PooledAllocatorVirtual OpDispatcherAllocator;
|
|
FEXCore::Utils::PooledAllocatorVirtual FrontendAllocator;
|
|
FEXCore::Utils::PooledAllocatorVirtual CPUBackendAllocator;
|
|
|
|
// If Atomic-based TSO emulation is enabled or not.
|
|
bool IsAtomicTSOEnabled() const {
|
|
return AtomicTSOEmulationEnabled;
|
|
}
|
|
|
|
// If atomic-based TSO emulation is enabled for vector operations.
|
|
bool IsVectorAtomicTSOEnabled() const {
|
|
return VectorAtomicTSOEmulationEnabled;
|
|
}
|
|
|
|
// If atomic-based TSO emulation is enabled for memcpy operations.
|
|
bool IsMemcpyAtomicTSOEnabled() const {
|
|
return MemcpyAtomicTSOEmulationEnabled;
|
|
}
|
|
|
|
void SetHardwareTSOSupport(bool HardwareTSOSupported) override {
|
|
SupportsHardwareTSO = HardwareTSOSupported;
|
|
UpdateAtomicTSOEmulationConfig();
|
|
}
|
|
|
|
void EnableExitOnHLT() override {
|
|
ExitOnHLT = true;
|
|
}
|
|
|
|
bool ExitOnHLTEnabled() const {
|
|
return ExitOnHLT;
|
|
}
|
|
|
|
protected:
|
|
void UpdateAtomicTSOEmulationConfig() {
|
|
if (SupportsHardwareTSO) {
|
|
// If the hardware supports TSO then we don't need to emulate it through atomics.
|
|
AtomicTSOEmulationEnabled = false;
|
|
VectorAtomicTSOEmulationEnabled = false;
|
|
MemcpyAtomicTSOEmulationEnabled = false;
|
|
} else if (Config.ParanoidTSO) {
|
|
AtomicTSOEmulationEnabled = true;
|
|
VectorAtomicTSOEmulationEnabled = true;
|
|
MemcpyAtomicTSOEmulationEnabled = true;
|
|
} else {
|
|
// Atomic TSO emulation only enabled if the config option is enabled.
|
|
AtomicTSOEmulationEnabled = (IsMemoryShared || !Config.TSOAutoMigration) && Config.TSOEnabled;
|
|
// Atomic vector TSO emulation only enabled if TSO emulation is enabled and also vector TSO is enabled.
|
|
VectorAtomicTSOEmulationEnabled = (IsMemoryShared || !Config.TSOAutoMigration) && Config.TSOEnabled && Config.VectorTSOEnabled;
|
|
// Atomic memcpy TSO emulation only enabled if TSO emulation is enabled and also memcpy TSO is enabled.
|
|
MemcpyAtomicTSOEmulationEnabled = (IsMemoryShared || !Config.TSOAutoMigration) && Config.TSOEnabled && Config.MemcpySetTSOEnabled;
|
|
}
|
|
}
|
|
|
|
private:
|
|
/**
|
|
* @brief Initializes the JIT compilers for the thread
|
|
*
|
|
* @param State The internal FEX thread state object
|
|
*
|
|
* InitializeCompiler is called inside of CreateThread, so you likely don't need this
|
|
*/
|
|
void InitializeCompiler(FEXCore::Core::InternalThreadState* Thread);
|
|
|
|
IR::AOTIRCaptureCache IRCaptureCache;
|
|
fextl::unique_ptr<FEXCore::CodeSerialize::CodeObjectSerializeService> CodeObjectCacheService;
|
|
|
|
bool IsMemoryShared = false;
|
|
bool SupportsHardwareTSO = false;
|
|
bool AtomicTSOEmulationEnabled = true;
|
|
bool VectorAtomicTSOEmulationEnabled = false;
|
|
bool MemcpyAtomicTSOEmulationEnabled = false;
|
|
|
|
bool ExitOnHLT = false;
|
|
FEX_CONFIG_OPT(AppFilename, APP_FILENAME);
|
|
|
|
std::shared_mutex CustomIRMutex;
|
|
std::atomic<bool> HasCustomIRHandlers {};
|
|
fextl::unordered_map<uint64_t, std::tuple<CustomIREntrypointHandler, void*, void*>> CustomIRHandlers;
|
|
IntervalList<uint64_t> ForceTSOValidRanges; // The ranges for which ForceTSOInstructions has populated data
|
|
fextl::set<uint64_t> ForceTSOInstructions;
|
|
};
|
|
} // namespace FEXCore::Context
|