mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-06 09:00:17 +02:00
This doesn't fully fix #5912 but gets a step closer. Instead of just hashing the filename, hash in the build-id as well when it exists. This isn't all encompassing because the build-id may not exist in all cases. Crypt Of the Necrodancer for example doesn't ship with the build-id on their 64-bit build. Although their legacy 32-bit build had it. The build-id is always a hash, depending on tool it is either 64-bit or 160-bit in all the executables I found. Although it can be anything so make sure to be flexible enough to support everything.
1005 lines
42 KiB
C++
1005 lines
42 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#include "Utils/crc32.h"
|
|
#include "FEXCore/Utils/LogManager.h"
|
|
#include "FEXCore/Utils/MathUtils.h"
|
|
#include "FEXCore/Utils/TypeDefines.h"
|
|
#include "FEXCore/fextl/memory.h"
|
|
#include <FEXCore/Utils/Profiler.h>
|
|
#include <FEXCore/Utils/SpinWaitLock.h>
|
|
|
|
#include <Interface/Context/Context.h>
|
|
#include <Interface/Core/ArchHelpers/Arm64Emitter.h>
|
|
#include <Interface/Core/Dispatcher/Dispatcher.h>
|
|
#include <Interface/Core/JIT/DebugData.h>
|
|
#include <Interface/Core/JIT/Relocations.h>
|
|
#include <Interface/Core/LookupCache.h>
|
|
#include <Interface/Core/OpcodeDispatcher.h>
|
|
#include <Interface/IR/PassManager.h>
|
|
|
|
#include <FEXCore/Core/Thunks.h>
|
|
#include <FEXCore/HLE/SourcecodeResolver.h>
|
|
#include <FEXCore/HLE/SyscallHandler.h>
|
|
|
|
#include <FEXHeaderUtils/Filesystem.h>
|
|
|
|
#include <algorithm>
|
|
#include <git_version.h>
|
|
|
|
#include <span>
|
|
#include <xxhash.h>
|
|
|
|
#include <FEXCore/Utils/AllocatorHooks.h>
|
|
|
|
#include <fstream>
|
|
|
|
namespace FEXCore {
|
|
|
|
#if __clang_major__ < 16
|
|
ExecutableFileInfo::ExecutableFileInfo(fextl::unique_ptr<HLE::SourcecodeMap> Map, uint64_t FileId, fextl::string Filename)
|
|
: SourcecodeMap(std::move(Map))
|
|
, FileId(FileId)
|
|
, Filename(Filename) {}
|
|
#endif
|
|
ExecutableFileInfo::~ExecutableFileInfo() = default;
|
|
|
|
MappedCodeCacheFile::~MappedCodeCacheFile() {
|
|
if (CacheManager) {
|
|
CacheManager->UnregisterMappedCodeBuffer(*this);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
if (!CodeBuffer.empty()) {
|
|
FEXCore::Allocator::munmap(CodeBuffer.data(), CodeBuffer.size_bytes());
|
|
}
|
|
#elif defined(_M_ARM64EC)
|
|
if (!CodeBuffer.empty()) {
|
|
FEXCore::Allocator::VirtualFree(CodeBuffer.data(), CodeBuffer.size_bytes());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void AbstractCodeCache::RegisterMappedCodeBuffer(MappedCodeCacheFile& Code) {
|
|
MappedCodeBuffers.push_back(Code.CodeBuffer);
|
|
// Unregister on destruction of Code
|
|
Code.CacheManager = this;
|
|
}
|
|
|
|
void AbstractCodeCache::UnregisterMappedCodeBuffer(MappedCodeCacheFile& Code) {
|
|
std::erase_if(MappedCodeBuffers, [&](const auto& Elem) { return Elem.data() == Code.CodeBuffer.data(); });
|
|
}
|
|
|
|
bool AbstractCodeCache::IsAddressInMappedCodeBuffer(uintptr_t Address) const {
|
|
for (const auto& Range : MappedCodeBuffers) {
|
|
auto Start = reinterpret_cast<uintptr_t>(Range.data());
|
|
if (Address >= Start && Address < Start + Range.size_bytes()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fextl::string CodeMap::GetBaseFilename(const ExecutableFileInfo& MainExecutable, bool AddNombSuffix) {
|
|
auto FileId = MainExecutable.FileId;
|
|
|
|
std::string_view base_filename = FHU::Filesystem::GetFilename(std::string_view {MainExecutable.Filename});
|
|
if (FileId != 0xffff'ffff'ffff'ffff) {
|
|
return fextl::fmt::format("{}-{:016x}{}", base_filename, MainExecutable.FileId, AddNombSuffix ? "-nomb" : "");
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
fextl::map<CodeMapFileId, CodeMap::ParsedContents> CodeMap::ParseCodeMap(std::ifstream& File) {
|
|
fextl::map<CodeMapFileId, CodeMap::ParsedContents> Ret;
|
|
while (true) {
|
|
Entry Entry;
|
|
File.read(reinterpret_cast<char*>(&Entry), sizeof(Entry));
|
|
if (!File) {
|
|
break;
|
|
}
|
|
|
|
if (Entry.FileId == LoadExternalLibrary.FileId && Entry.BlockOffset == LoadExternalLibrary.BlockOffset) {
|
|
ExternalLibraryInfo Info;
|
|
File.read(reinterpret_cast<char*>(&Info), sizeof(Info));
|
|
|
|
fextl::string Filename;
|
|
std::getline(File, Filename, '\0');
|
|
|
|
// Align to 4-byte boundary
|
|
char Null[4];
|
|
File.read(Null, AlignUp(Filename.size() + 1, 4) - Filename.size() - 1);
|
|
if (!File) {
|
|
break;
|
|
}
|
|
Ret[Info.ExternalFileId].Filename = std::move(Filename);
|
|
} else if ((Entry.FileId == SetExecutableFileId::Marker32.FileId && Entry.BlockOffset == SetExecutableFileId::Marker32.BlockOffset) ||
|
|
(Entry.FileId == SetExecutableFileId::Marker64.FileId && Entry.BlockOffset == SetExecutableFileId::Marker64.BlockOffset)) {
|
|
CodeMapFileId ExecutableFileId;
|
|
File.read(reinterpret_cast<char*>(&ExecutableFileId), sizeof(ExecutableFileId));
|
|
if (!File) {
|
|
break;
|
|
}
|
|
Ret[ExecutableFileId].ExecutableBitness =
|
|
(Entry.FileId == SetExecutableFileId::Marker32.FileId && Entry.BlockOffset == SetExecutableFileId::Marker32.BlockOffset) ? 32 : 64;
|
|
} else {
|
|
if (!Ret.contains(Entry.FileId)) {
|
|
if (Entry.FileId == 0xffff'ffff'ffff'ffff) {
|
|
ERROR_AND_DIE_FMT("Malformed code map");
|
|
} else {
|
|
LogMan::Msg::EFmt("Code map referenced unknown file id {:016x}", Entry.FileId);
|
|
}
|
|
} else {
|
|
Ret[Entry.FileId].Blocks.insert(Entry.BlockOffset);
|
|
}
|
|
}
|
|
|
|
if (!File) {
|
|
break;
|
|
}
|
|
}
|
|
return Ret;
|
|
}
|
|
|
|
CodeMapWriter::CodeMapWriter(CodeMapOpener& Opener, bool OpenEagerly)
|
|
: Buffer(4096)
|
|
, FileOpener(Opener) {
|
|
if (OpenEagerly) {
|
|
CodeMapFD = FileOpener.OpenCodeMapFile();
|
|
}
|
|
}
|
|
|
|
CodeMapWriter::~CodeMapWriter() {
|
|
if (CodeMapFD.value_or(-1) != -1) {
|
|
Flush(BufferOffset);
|
|
close(*CodeMapFD);
|
|
}
|
|
}
|
|
|
|
bool CodeMapWriter::IsWriteEnabled(const ExecutableFileSectionInfo& Section) {
|
|
if (CodeMapFD == -1) {
|
|
return false;
|
|
}
|
|
|
|
// PV libraries can't yet be read by FEXServer, so skip dumping them
|
|
if (Section.FileInfo.Filename.starts_with("/run/pressure-vessel")) {
|
|
return false;
|
|
}
|
|
|
|
if (CodeMapFD) {
|
|
return true;
|
|
}
|
|
|
|
// Acquire mutex and re-check CodeMapFD to avoid race conditions
|
|
auto lk = std::unique_lock {Mutex};
|
|
if (!CodeMapFD) {
|
|
CodeMapFD = FileOpener.OpenCodeMapFile();
|
|
}
|
|
|
|
return CodeMapFD != -1;
|
|
}
|
|
|
|
void CodeMapWriter::Flush(size_t Offset) {
|
|
// Acquire exclusive lock and flush circular buffer
|
|
std::unique_lock Lock {Mutex};
|
|
Flush(Offset, Lock);
|
|
}
|
|
|
|
void CodeMapWriter::Flush(size_t Offset, std::unique_lock<std::shared_mutex>&) {
|
|
write(*CodeMapFD, Buffer.data(), Offset);
|
|
BufferOffset = 0;
|
|
}
|
|
|
|
void CodeMapWriter::AppendBlock(const FEXCore::ExecutableFileSectionInfo& SectionInfo, uint64_t BlockEntry) {
|
|
if (!IsWriteEnabled(SectionInfo)) {
|
|
return;
|
|
}
|
|
|
|
BlockEntry -= SectionInfo.FileStartVA;
|
|
if (BlockEntry > std::numeric_limits<uint32_t>::max()) {
|
|
ERROR_AND_DIE_FMT("Cannot write code map");
|
|
}
|
|
|
|
// Register new library if not already known
|
|
bool NewLibraryLoad = false;
|
|
{
|
|
// Check prior registration with shared lock
|
|
std::shared_lock Lock {Mutex};
|
|
NewLibraryLoad = !KnownFileIds.contains(SectionInfo.FileInfo.FileId);
|
|
}
|
|
if (NewLibraryLoad) {
|
|
// Register to map with exclusive lock
|
|
std::unique_lock Lock {Mutex};
|
|
NewLibraryLoad &= KnownFileIds.insert(SectionInfo.FileInfo.FileId).second;
|
|
}
|
|
if (NewLibraryLoad) {
|
|
// Add entry to code map
|
|
AppendLibraryLoad(SectionInfo.FileInfo);
|
|
}
|
|
|
|
// Register the actual code block
|
|
CodeMap::Entry DataEntry {SectionInfo.FileInfo.FileId, static_cast<uint32_t>(BlockEntry)};
|
|
AppendData(std::as_bytes(std::span {&DataEntry, 1}));
|
|
}
|
|
|
|
void CodeMapWriter::AppendLibraryLoad(const FEXCore::ExecutableFileInfo& FileInfo) {
|
|
// See CodeMap::ExternalLibraryInfo
|
|
auto ExternalFileId = FileInfo.FileId;
|
|
auto TotalSize = AlignUp(sizeof(CodeMap::LoadExternalLibrary) + sizeof(ExternalFileId) + FileInfo.Filename.size() + 1, 4);
|
|
const auto Data = reinterpret_cast<char*>(alloca(TotalSize));
|
|
auto WritePtr = std::copy_n(reinterpret_cast<const char*>(&CodeMap::LoadExternalLibrary), sizeof(CodeMap::LoadExternalLibrary), Data);
|
|
WritePtr = std::copy_n(reinterpret_cast<const char*>(&ExternalFileId), sizeof(ExternalFileId), WritePtr);
|
|
WritePtr = std::copy(FileInfo.Filename.begin(), FileInfo.Filename.end(), WritePtr);
|
|
std::fill(WritePtr, Data + TotalSize, 0);
|
|
AppendData(std::as_bytes(std::span {Data, TotalSize}));
|
|
}
|
|
|
|
void CodeMapWriter::AppendSetMainExecutable(const FEXCore::ExecutableFileInfo& FileInfo, bool Is64Bit) {
|
|
CodeMap::SetExecutableFileId Data {Is64Bit ? CodeMap::SetExecutableFileId::Marker64 : CodeMap::SetExecutableFileId::Marker32, FileInfo.FileId};
|
|
AppendData(std::span {reinterpret_cast<const std::byte*>(&Data), sizeof(Data)});
|
|
}
|
|
|
|
void CodeMapWriter::AppendData(std::span<const std::byte> Data) {
|
|
std::shared_lock Lock {Mutex};
|
|
auto Offset = BufferOffset.fetch_add(Data.size_bytes());
|
|
if (Offset + Data.size_bytes() > Buffer.size()) {
|
|
// Acquire exclusive lock and flush the buffer.
|
|
// Under heavy pressure, multiple threads may observe an exhausted buffer simultaneously.
|
|
// The thread with the last in-bounds Offset is responsible for flushing the buffer.
|
|
Lock.unlock();
|
|
bool IsResponsibleForFlush = false;
|
|
{
|
|
std::unique_lock ExclusiveLock {Mutex};
|
|
IsResponsibleForFlush = (Offset <= Buffer.size());
|
|
if (IsResponsibleForFlush) {
|
|
Flush(Offset, ExclusiveLock);
|
|
}
|
|
}
|
|
if (!IsResponsibleForFlush) {
|
|
// Wait for the buffer to be flushed on the responsible thread
|
|
Utils::SpinWaitLock::WaitPred<std::less_equal<>, size_t>(reinterpret_cast<size_t*>(&BufferOffset), Buffer.size());
|
|
}
|
|
AppendData(Data);
|
|
return;
|
|
}
|
|
|
|
memcpy(&Buffer.at(Offset), Data.data(), Data.size_bytes());
|
|
}
|
|
|
|
} // namespace FEXCore
|
|
|
|
namespace FEXCore::Context {
|
|
|
|
CodeCache::CodeCache(ContextImpl& CTX_)
|
|
: CTX(CTX_) {}
|
|
CodeCache::~CodeCache() = default;
|
|
|
|
struct CodeCacheHeader {
|
|
std::array<char, 4> Magic = ExpectedMagic;
|
|
// Version history:
|
|
// 1: Initial version
|
|
// 2: Padding code buffer data to enable direct mapping
|
|
uint32_t FormatVersion = 2;
|
|
uint8_t FEXVersion[20] = {};
|
|
uint32_t NumBlocks;
|
|
uint32_t NumCodePages;
|
|
uint32_t CodeBufferSize;
|
|
uint32_t NumRelocations;
|
|
uint32_t padding;
|
|
uint64_t SerializedBaseAddress;
|
|
// TODO: Consider including information from LookupCache.BlockLinks
|
|
|
|
static constexpr std::array<char, 4> ExpectedMagic = {'F', 'X', 'C', 'C'};
|
|
};
|
|
|
|
template<typename T>
|
|
concept OrderedContainer = requires { typename T::key_compare; };
|
|
|
|
bool CodeCache::SaveData(Core::InternalThreadState& Thread, int fd, const ExecutableFileSectionInfo& SourceBinary, uint64_t SerializedBaseAddress) {
|
|
auto CodeBuffer = CTX.GetLatest();
|
|
auto& LookupCache = *Thread.LookupCache->Shared;
|
|
auto Relocations = Thread.CPUBackend->TakeRelocations(SourceBinary.FileStartVA);
|
|
|
|
// Write file header
|
|
CodeCacheHeader header {};
|
|
static_assert(GIT_HASH.size() == sizeof(header.FEXVersion));
|
|
std::ranges::copy(GIT_HASH, header.FEXVersion);
|
|
header.NumBlocks = LookupCache.BlockList.size();
|
|
header.NumCodePages = LookupCache.CodePages.size();
|
|
header.CodeBufferSize = FEXCore::AlignUp(CodeBuffer->AllocatedSpaceUsed(), Utils::FEX_PAGE_SIZE);
|
|
header.NumRelocations = Relocations.size();
|
|
header.SerializedBaseAddress = SerializedBaseAddress;
|
|
::write(fd, &header, sizeof(header));
|
|
|
|
// Dump guest<->host block mappings
|
|
{
|
|
// Cache contents must be deterministic, so copy the unordered block list and then sort by key
|
|
static_assert(!OrderedContainer<decltype(LookupCache.BlockList)>, "Already deterministic; drop temporary container");
|
|
fextl::vector<std::pair<uint64_t, const GuestToHostMap::BlockEntry*>> BlockList;
|
|
BlockList.reserve(LookupCache.BlockList.size());
|
|
for (auto& [Guest, BlockEntry] : LookupCache.BlockList) {
|
|
static_assert(sizeof(Guest) == 8, "Breaking change in code cache data layout");
|
|
BlockList.emplace_back(Guest, &BlockEntry);
|
|
}
|
|
std::ranges::sort(BlockList);
|
|
|
|
for (auto [Guest, Host] : BlockList) {
|
|
static_assert(sizeof(Host->HostCode) == 8, "Breaking change in code cache data layout");
|
|
static_assert(sizeof(Host->CodePages[0]) == 8, "Breaking change in code cache data layout");
|
|
|
|
Guest -= SourceBinary.FileStartVA;
|
|
::write(fd, &Guest, sizeof(Guest));
|
|
uint64_t HostCode = Host->HostCode - reinterpret_cast<uintptr_t>(CodeBuffer->GetBufferBase());
|
|
::write(fd, &HostCode, sizeof(HostCode));
|
|
uint64_t NumCodePages = Host->CodePages.size();
|
|
::write(fd, &NumCodePages, sizeof(NumCodePages));
|
|
LOGMAN_THROW_A_FMT(std::ranges::is_sorted(Host->CodePages), "Code pages aren't sorted");
|
|
for (auto CodePage : Host->CodePages) {
|
|
CodePage -= SourceBinary.FileStartVA;
|
|
::write(fd, &CodePage, sizeof(CodePage));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dump relocations
|
|
static_assert(sizeof(Relocations[0]) == 48, "Breaking change in code cache data layout");
|
|
::write(fd, Relocations.data(), Relocations.size() * sizeof(Relocations[0]));
|
|
|
|
// Pad to next page in file so that the CodeBuffer can be mmap'ed into process on load
|
|
{
|
|
auto AlignedSize = AlignUp(lseek(fd, 0, SEEK_CUR), Utils::FEX_PAGE_SIZE);
|
|
::ftruncate(fd, AlignedSize);
|
|
lseek(fd, AlignedSize, SEEK_SET);
|
|
}
|
|
|
|
// Dump the host code (relocated for position-independent serialization)
|
|
std::span CodeBufferData(reinterpret_cast<std::byte*>(CodeBuffer->GetBufferBase()),
|
|
reinterpret_cast<std::byte*>(CodeBuffer->GetBufferBase()) + CodeBuffer->AllocatedSpaceUsed());
|
|
if (!ApplyCodeRelocations(SerializedBaseAddress, CodeBufferData, Relocations, true)) {
|
|
LOGMAN_THROW_A_FMT(false, "Failed to apply code relocations");
|
|
return false;
|
|
}
|
|
::write(fd, CodeBufferData.data(), CodeBufferData.size());
|
|
// Pad to next page in file for mmap
|
|
{
|
|
auto PaddedSize = AlignUp(lseek(fd, 0, SEEK_CUR), Utils::FEX_PAGE_SIZE);
|
|
::ftruncate(fd, PaddedSize);
|
|
lseek(fd, PaddedSize, SEEK_SET);
|
|
}
|
|
|
|
// Dump code pages
|
|
static_assert(OrderedContainer<decltype(LookupCache.CodePages)>, "Non-deterministic data source");
|
|
for (const auto& [PageIndex, Entrypoints] : LookupCache.CodePages) {
|
|
uint64_t PageAddr = (PageIndex << 12) - SourceBinary.FileStartVA;
|
|
::write(fd, &PageAddr, sizeof(PageAddr));
|
|
uint64_t NumEntrypoints = Entrypoints.size();
|
|
::write(fd, &NumEntrypoints, sizeof(NumEntrypoints));
|
|
for (uint64_t Entrypoint : Entrypoints) {
|
|
Entrypoint -= SourceBinary.FileStartVA;
|
|
::write(fd, &Entrypoint, sizeof(Entrypoint));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CodeCache::Validate(const ExecutableFileSectionInfo& Section, fextl::set<uint64_t> GuestBlocks, const fextl::set<uint64_t>& HostBlocks,
|
|
std::span<std::byte> CachedCode) {
|
|
LOGMAN_THROW_A_FMT(!HostBlocks.empty(), "Tried to validate without any host blocks");
|
|
// Skip any cached data before the first host block
|
|
CachedCode = CachedCode.subspan(*HostBlocks.begin() - sizeof(CPU::CPUBackend::JITCodeHeader));
|
|
|
|
if (!ValidationCTX) {
|
|
ValidationCTX.reset(static_cast<ContextImpl*>(FEXCore::Context::Context::CreateNewContext(CTX.HostFeatures).release()));
|
|
ValidationCTX->SetSignalDelegator(CTX.SignalDelegation);
|
|
ValidationCTX->SetSyscallHandler(CTX.SyscallHandler);
|
|
ValidationCTX->SetThunkHandler(CTX.ThunkHandler);
|
|
if (!ValidationCTX->InitCore()) {
|
|
ERROR_AND_DIE_FMT("Failed to create cache load validation context");
|
|
}
|
|
|
|
ValidationThread.reset(ValidationCTX->CreateThread(nullptr));
|
|
|
|
auto Frame = ValidationThread->CurrentFrame;
|
|
Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT] = &ValidationGDT[0];
|
|
Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_LDT] = &ValidationGDT[0];
|
|
Frame->State.cs_idx = 0;
|
|
Frame->State.cs_cached = 0;
|
|
|
|
if (ValidationCTX->Config.Is64BitMode()) {
|
|
ValidationGDT[0].L = 1; // L = Long Mode = 64-bit
|
|
ValidationGDT[0].D = 0; // D = Default Operand Size = Reserved
|
|
} else {
|
|
ValidationGDT[0].L = 0; // L = Long Mode = 32-bit
|
|
ValidationGDT[0].D = 1; // D = Default Operand Size = 32-bit
|
|
}
|
|
}
|
|
|
|
auto NewCodeBuffer = ValidationCTX->GetLatest();
|
|
while (CachedCode.size_bytes() > NewCodeBuffer->UsableSize()) {
|
|
ValidationCTX->ClearCodeCache(ValidationThread.get());
|
|
NewCodeBuffer = ValidationCTX->GetLatest();
|
|
LogMan::Msg::IFmt("Increased cache validation code buffer size to {} MiB", NewCodeBuffer->TotalAllocationSize() / 1024 / 1024);
|
|
}
|
|
|
|
std::span<std::byte> CodeBufferRangeRef =
|
|
std::as_writable_bytes(std::span {NewCodeBuffer->GetBufferBase(), NewCodeBuffer->GetBufferBase() + NewCodeBuffer->UsableSize()})
|
|
.subspan(0, CachedCode.size_bytes());
|
|
|
|
while (!GuestBlocks.empty()) {
|
|
auto [CompiledBlocks, _, _2, _3, _4] = ValidationCTX->CompileCode(ValidationThread.get(), *GuestBlocks.begin(), 0 /* TODO: Set MaxInst? */);
|
|
for (auto& Entry : CompiledBlocks.EntryPoints) {
|
|
GuestBlocks.erase(Entry.first);
|
|
}
|
|
}
|
|
|
|
// Patch FEX-internal function addresses with values from the main Context to ensure the code blocks are comparable
|
|
auto NewRelocations = ValidationThread->CPUBackend->TakeRelocations(Section.FileStartVA);
|
|
NewRelocations.erase(std::remove_if(NewRelocations.begin(), NewRelocations.end(), [](const CPU::Relocation& Reloc) {
|
|
return Reloc.Header.Type != CPU::RelocationTypes::RELOC_NAMED_SYMBOL_LITERAL && Reloc.Header.Type != CPU::RelocationTypes::RELOC_NAMED_THUNK_MOVE;
|
|
}));
|
|
(void)ApplyCodeRelocations(Section.FileStartVA, CodeBufferRangeRef, NewRelocations, false);
|
|
|
|
if (NewCodeBuffer->AllocatedSpaceUsed() <= CodeBufferRangeRef.size()) {
|
|
// Reference compilation produced fewer bytes than our cache, so validation is going to fail.
|
|
// Make sure we don't output any garbage bytes though.
|
|
CodeBufferRangeRef = CodeBufferRangeRef.subspan(0, NewCodeBuffer->AllocatedSpaceUsed());
|
|
}
|
|
|
|
auto [Mismatch, _] = std::mismatch(CodeBufferRangeRef.begin(), CodeBufferRangeRef.end(), CachedCode.begin());
|
|
if (Mismatch != CodeBufferRangeRef.end()) {
|
|
// Align down to instruction size
|
|
auto Idx = AlignDown(std::distance(CodeBufferRangeRef.begin(), Mismatch), 4);
|
|
|
|
auto BlockIt = std::prev(HostBlocks.lower_bound(*HostBlocks.begin() + Idx + 1));
|
|
std::optional<uint64_t> GuestBlockAddr;
|
|
std::optional<uint64_t> GuestBlockAddrRef;
|
|
if (BlockIt != HostBlocks.end()) {
|
|
for (int i : {0, 1}) {
|
|
std::span Buffer = (i == 0 ? CachedCode : CodeBufferRangeRef);
|
|
|
|
// Second instruction is always a constant load for relative offset to the (multi)block start
|
|
int32_t addr = (*reinterpret_cast<uint32_t*>(&Buffer[*BlockIt - *HostBlocks.begin() + 4]) & 0x3ff'ffe0) << 11;
|
|
addr >>= 14;
|
|
auto header = reinterpret_cast<CPU::CPUBackend::JITCodeHeader*>(&Buffer[*BlockIt - *HostBlocks.begin() + 4 + addr]);
|
|
auto tail = reinterpret_cast<CPU::CPUBackend::JITCodeTail*>(reinterpret_cast<uintptr_t>(header) + header->OffsetToBlockTail);
|
|
(i == 0 ? GuestBlockAddr : GuestBlockAddrRef) = tail->RIP - Section.FileStartVA;
|
|
LogMan::Msg::EFmt("Recorded rip {}: {:#x} (offset {:#x})", i, tail->RIP, tail->RIP - Section.FileStartVA);
|
|
|
|
if (i == 1) {
|
|
if (tail->RIP >= Section.BeginVA && tail->RIP < Section.EndVA) {
|
|
auto [IRView, TotalInstructions, TotalInstructionsLength, StartAddr, Length, _] =
|
|
ValidationCTX->GenerateIR(ValidationThread.get(), tail->RIP, false, FEXCore::Config::Get_MAXINST());
|
|
fextl::ostringstream ss;
|
|
FEXCore::IR::Dump(&ss, &*IRView);
|
|
LogMan::Msg::EFmt("IR:\n{}", ss.str());
|
|
} else {
|
|
LogMan::Msg::EFmt("Can't dump IR for out-of-range RIP {:#x}", tail->RIP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fextl::string GuestBlockInfo = "UNKNOWN";
|
|
if (GuestBlockAddr) {
|
|
GuestBlockInfo = fextl::fmt::format("{:#x}", GuestBlockAddr.value());
|
|
}
|
|
if (GuestBlockAddr != GuestBlockAddrRef) {
|
|
GuestBlockInfo += " (MISMATCH)";
|
|
}
|
|
ERROR_AND_DIE_FMT("Cache validation failed at offset {:#x}: {:02x} <-> {:02x} (at {} <-> {}, guest block {})", Idx,
|
|
fmt::join(CachedCode.subspan(Idx, 4), ""), fmt::join(CodeBufferRangeRef.subspan(Idx, 4), ""),
|
|
fmt::ptr(CachedCode.data()), fmt::ptr(CodeBufferRangeRef.data()), GuestBlockInfo);
|
|
}
|
|
|
|
// Reset Context state for next validation
|
|
ValidationThread->LookupCache->ClearCache(ValidationThread->LookupCache->AcquireWriteLock());
|
|
NewCodeBuffer->Reset();
|
|
|
|
LogMan::Msg::IFmt(" successfully validated cache");
|
|
}
|
|
|
|
static inline void ApplySymbolLiteralRelocation(ContextImpl& CTX, const CPU::RelocNamedSymbolLiteral::NamedSymbol Symbol,
|
|
uint64_t GuestEntry, CPU::Arm64Emitter& Emitter, bool ForStorage) {
|
|
// Generate a literal so we can place it
|
|
uint64_t Pointer = ForStorage ? 0 : GetNamedSymbolLiteral(CTX, Symbol);
|
|
Emitter.dc64(Pointer);
|
|
}
|
|
|
|
static inline bool
|
|
ApplyThunkMoveRelocation(ContextImpl& CTX, const IR::SHA256Sum* Symbol, uint32_t RegisterIndex, CPU::Arm64Emitter& Emitter, bool ForStorage) {
|
|
uint64_t Pointer = ForStorage ? 0 : reinterpret_cast<uint64_t>(CTX.ThunkHandler->LookupThunk(*Symbol));
|
|
if (Pointer == ~0ULL) {
|
|
return false;
|
|
}
|
|
// TODO: Pointers are required to fit within 48-bit VA space.
|
|
// But forcing 6-byte broke relocations.
|
|
Emitter.LoadConstant(ARMEmitter::Size::i64Bit, ARMEmitter::Register(RegisterIndex), Pointer, CPU::Arm64Emitter::PadType::DOPAD);
|
|
return true;
|
|
}
|
|
|
|
static inline void ApplyRIPLiteralRelocation(ContextImpl& CTX, uint64_t GuestRIP, uint64_t GuestEntry, CPU::Arm64Emitter& Emitter) {
|
|
Emitter.dc64(GuestEntry + GuestRIP);
|
|
}
|
|
|
|
static inline void
|
|
ApplyRIPMoveRelocation(ContextImpl& CTX, uint64_t GuestRIP, uint8_t RegisterIndex, uint64_t GuestEntry, CPU::Arm64Emitter& Emitter) {
|
|
uint64_t Pointer = GuestRIP + GuestEntry;
|
|
// TODO: Pointers are required to fit within 48-bit VA space.
|
|
// But forcing 6-byte broke relocations.
|
|
Emitter.LoadConstant(ARMEmitter::Size::i64Bit, ARMEmitter::Register(RegisterIndex), Pointer, CPU::Arm64Emitter::PadType::DOPAD);
|
|
}
|
|
|
|
static inline int64_t ReadLiveGuestData(uint64_t SiteAddress, uint8_t ValueSize) {
|
|
uint64_t Raw = 0;
|
|
memcpy(&Raw, reinterpret_cast<const void*>(SiteAddress), ValueSize);
|
|
// manual sign-extension from guest live bytes
|
|
if (ValueSize == 1) {
|
|
return (int8_t)Raw;
|
|
} else if (ValueSize == 2) {
|
|
return (int16_t)Raw;
|
|
} else if (ValueSize == 4) {
|
|
return (int32_t)Raw;
|
|
} else {
|
|
return (int64_t)Raw;
|
|
}
|
|
}
|
|
|
|
static inline void ApplyPatchableDataRelocation(uint64_t SiteAddress, uint8_t ValueSize, uint8_t RegisterIndex, CPU::Arm64Emitter& Emitter) {
|
|
Emitter.LoadConstant(ARMEmitter::Size::i64Bit, ARMEmitter::Register(RegisterIndex), ReadLiveGuestData(SiteAddress, ValueSize),
|
|
CPU::Arm64Emitter::PadType::DOPAD);
|
|
}
|
|
|
|
static inline void ApplyPatchableRIPLiteralRelocation(uint64_t SiteAddress, uint8_t ValueSize, CPU::Arm64Emitter& Emitter) {
|
|
Emitter.dc64(SiteAddress + ValueSize + ReadLiveGuestData(SiteAddress, ValueSize));
|
|
}
|
|
|
|
static inline void ApplyPatchableRIPMoveRelocation(uint64_t SiteAddress, uint8_t ValueSize, uint8_t RegisterIndex, CPU::Arm64Emitter& Emitter) {
|
|
const uint64_t Target = SiteAddress + ValueSize + ReadLiveGuestData(SiteAddress, ValueSize);
|
|
Emitter.LoadConstant(ARMEmitter::Size::i64Bit, ARMEmitter::Register(RegisterIndex), Target, CPU::Arm64Emitter::PadType::DOPAD);
|
|
}
|
|
|
|
static inline void ApplyPatchableCRCMoveRelocation(uint64_t SiteAddress, uint8_t ValueSize, uint8_t RegisterIndex, CPU::Arm64Emitter& Emitter) {
|
|
const uint64_t Target = FEXCore::Utils::crc32(reinterpret_cast<const uint8_t*>(SiteAddress), ValueSize);
|
|
Emitter.LoadConstant(ARMEmitter::Size::i64Bit, ARMEmitter::Register(RegisterIndex), Target, CPU::Arm64Emitter::PadType::DOPAD);
|
|
}
|
|
|
|
bool CodeCache::ApplyPackedCodeRelocations(uint64_t GuestEntry, std::span<std::byte> Code,
|
|
std::span<const DiskCache::BlobSmallRelocation> SmallRelocs,
|
|
std::span<const DiskCache::BlobThunkRelocation> ThunkRelocs) {
|
|
CPU::Arm64Emitter Emitter(&CTX, Code.data(), Code.size_bytes());
|
|
for (auto& Reloc : SmallRelocs) {
|
|
LOGMAN_THROW_A_FMT(Reloc.Offset < Code.size_bytes(), "Invalid relocation offset");
|
|
Emitter.SetCursorOffset(Reloc.Offset);
|
|
switch ((CPU::RelocationTypes)Reloc.Type) {
|
|
case FEXCore::CPU::RelocationTypes::RELOC_NAMED_SYMBOL_LITERAL: {
|
|
ApplySymbolLiteralRelocation(CTX, (CPU::RelocNamedSymbolLiteral::NamedSymbol)Reloc.Named.Symbol, GuestEntry, Emitter, false);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_RIP_LITERAL: {
|
|
ApplyRIPLiteralRelocation(CTX, Reloc.RIPLiteral.GuestRIP, GuestEntry, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_RIP_MOVE: {
|
|
ApplyRIPMoveRelocation(CTX, Reloc.RIPMove.GuestRIP, Reloc.RIPMove.RegisterIndex, GuestEntry, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_PATCHABLE_DATA_MOVE: {
|
|
ApplyPatchableDataRelocation(GuestEntry + Reloc.PatchableData.SiteOffset, Reloc.PatchableData.ValueSize,
|
|
Reloc.PatchableData.RegisterIndex, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_PATCHABLE_RIP_LITERAL: {
|
|
ApplyPatchableRIPLiteralRelocation(GuestEntry + Reloc.PatchableData.SiteOffset, Reloc.PatchableData.ValueSize, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_PATCHABLE_RIP_MOVE: {
|
|
ApplyPatchableRIPMoveRelocation(GuestEntry + Reloc.PatchableData.SiteOffset, Reloc.PatchableData.ValueSize,
|
|
Reloc.PatchableData.RegisterIndex, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_PATCHABLE_CRC_MOVE: {
|
|
ApplyPatchableCRCMoveRelocation(GuestEntry + Reloc.PatchableData.SiteOffset, Reloc.PatchableData.ValueSize,
|
|
Reloc.PatchableData.RegisterIndex, Emitter);
|
|
break;
|
|
}
|
|
default: ERROR_AND_DIE_FMT("Unknown packed relocation type {}", ToUnderlying((CPU::RelocationTypes)Reloc.Type));
|
|
}
|
|
}
|
|
for (auto& Reloc : ThunkRelocs) {
|
|
LOGMAN_THROW_A_FMT(Reloc.Offset < Code.size_bytes(), "Invalid relocation offset");
|
|
Emitter.SetCursorOffset(Reloc.Offset);
|
|
if (!ApplyThunkMoveRelocation(CTX, (const IR::SHA256Sum*)Reloc.SymbolHash, Reloc.RegisterIndex, Emitter, false)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CodeCache::ApplyCodeRelocations(uint64_t GuestEntry, std::span<std::byte> Code,
|
|
std::span<const FEXCore::CPU::Relocation> EntryRelocations, bool ForStorage) {
|
|
CPU::Arm64Emitter Emitter(&CTX, Code.data(), Code.size_bytes());
|
|
for (size_t j = 0; j < EntryRelocations.size(); ++j) {
|
|
const FEXCore::CPU::Relocation& Reloc = EntryRelocations[j];
|
|
LOGMAN_THROW_A_FMT(Reloc.Header.Offset < Code.size_bytes(), "Invalid relocation offset");
|
|
Emitter.SetCursorOffset(Reloc.Header.Offset);
|
|
|
|
switch (Reloc.Header.Type) {
|
|
case FEXCore::CPU::RelocationTypes::RELOC_NAMED_SYMBOL_LITERAL: {
|
|
ApplySymbolLiteralRelocation(CTX, Reloc.NamedSymbolLiteral.Symbol, GuestEntry, Emitter, ForStorage);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_NAMED_THUNK_MOVE: {
|
|
if (!ApplyThunkMoveRelocation(CTX, &Reloc.NamedThunkMove.Symbol, Reloc.NamedThunkMove.RegisterIndex, Emitter, ForStorage)) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_RIP_LITERAL: {
|
|
ApplyRIPLiteralRelocation(CTX, Reloc.GuestRIP.GuestRIP, GuestEntry, Emitter);
|
|
break;
|
|
}
|
|
case FEXCore::CPU::RelocationTypes::RELOC_GUEST_RIP_MOVE: {
|
|
ApplyRIPMoveRelocation(CTX, Reloc.GuestRIP.GuestRIP, Reloc.GuestRIP.RegisterIndex, GuestEntry, Emitter);
|
|
break;
|
|
}
|
|
|
|
default: ERROR_AND_DIE_FMT("Unknown relocation type {}", ToUnderlying(Reloc.Header.Type));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
fextl::unique_ptr<MappedCodeCacheFile>
|
|
CodeCache::LoadCache(std::span<std::byte> CacheFile, const ExecutableFileInfo& FileInfo, uint64_t FileStartVA) {
|
|
if (!EnableCodeCaching) {
|
|
return nullptr;
|
|
}
|
|
|
|
FEXCORE_PROFILE_SCOPED("LoadCache");
|
|
|
|
// Read file header
|
|
CodeCacheHeader header {};
|
|
::memcpy(&header, CacheFile.data(), sizeof(header));
|
|
|
|
if (!std::ranges::equal(header.Magic, header.ExpectedMagic)) {
|
|
LogMan::Msg::EFmt("Invalid cache file header");
|
|
return nullptr;
|
|
}
|
|
|
|
if (!std::ranges::equal(header.FEXVersion, GIT_HASH)) {
|
|
LogMan::Msg::IFmt("Cache generated from old FEX version {:02x}, current is {:02x}; skipping", fmt::join(header.FEXVersion, ""),
|
|
fmt::join(GIT_HASH, ""));
|
|
return nullptr;
|
|
}
|
|
|
|
if (header.NumBlocks == 0) {
|
|
// Valid caches are never empty
|
|
LogMan::Msg::IFmt("Code cache empty, aborting");
|
|
return nullptr;
|
|
}
|
|
|
|
// Skip over BlockEntry data since it won't be used until EnableLoadedSection
|
|
// TODO: Store direct offset to relocations in the header
|
|
auto* BlockListStart = CacheFile.data() + sizeof(header);
|
|
auto* Cursor = BlockListStart;
|
|
for (uint32_t i = 0; i < header.NumBlocks; ++i) {
|
|
Cursor += sizeof(uint64_t); // guest address
|
|
Cursor += sizeof(uint64_t); // host code address
|
|
uint64_t NumGuestCodePages;
|
|
::memcpy(&NumGuestCodePages, Cursor, sizeof(NumGuestCodePages));
|
|
Cursor += sizeof(NumGuestCodePages);
|
|
Cursor += NumGuestCodePages * sizeof(uint64_t);
|
|
}
|
|
|
|
auto Relocations = std::span {reinterpret_cast<const FEXCore::CPU::Relocation*>(Cursor), header.NumRelocations};
|
|
Cursor += Relocations.size_bytes();
|
|
|
|
// Pad to next page to get the code buffer data
|
|
Cursor = reinterpret_cast<std::byte*>(AlignUp(reinterpret_cast<uintptr_t>(Cursor), Utils::FEX_PAGE_SIZE));
|
|
auto CodeDataInFile = std::span {Cursor, header.CodeBufferSize};
|
|
|
|
#ifndef _WIN32
|
|
// Allocate target memory for post-relocation code. This is PROT_NONE until
|
|
// the first execution, so that contents can be lazily populated in a
|
|
// frontend-provided segfault handler.
|
|
void* CodeBufferAllocation = Allocator::mmap(nullptr, header.CodeBufferSize, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (CodeBufferAllocation == MAP_FAILED) {
|
|
LogMan::Msg::EFmt("Failed to reserve target memory for code cache");
|
|
return nullptr;
|
|
}
|
|
auto CodeBuffer = std::span {static_cast<std::byte*>(CodeBufferAllocation), header.CodeBufferSize};
|
|
#elif defined(_M_ARM64EC)
|
|
// TODO: Implement lazy mapping on Windows
|
|
// NOTE: The executed code must have MEM_EXTENDED_PARAMETER_EC_CODE set, so we can't operate on the mapped cache file directly
|
|
void* CodeBufferAllocation = Allocator::VirtualAlloc(header.CodeBufferSize, true);
|
|
if (!CodeBufferAllocation) {
|
|
LogMan::Msg::EFmt("Failed to allocate code cache memory");
|
|
return nullptr;
|
|
}
|
|
auto CodeBuffer = std::span {reinterpret_cast<std::byte*>(CodeBufferAllocation), header.CodeBufferSize};
|
|
#else // WoW64
|
|
// TODO: Implement lazy mapping on Windows
|
|
auto CodeBuffer = CodeDataInFile;
|
|
#endif
|
|
|
|
// Group relocations by page
|
|
size_t NumPages = header.CodeBufferSize / Utils::FEX_PAGE_SIZE;
|
|
fextl::vector<MappedCodeCacheFile::PageRelocationRange> PageRelocationRanges(NumPages, {0, 0});
|
|
auto RelocBaseOffset = std::as_bytes(Relocations).data() - CacheFile.data();
|
|
auto RelocIt = Relocations.begin();
|
|
for (size_t Page = 0; Page < NumPages; ++Page) {
|
|
auto EndRelocIt = std::upper_bound(RelocIt, Relocations.end(), Page,
|
|
[](auto& Page, auto& Reloc) { return Page < Reloc.Header.Offset / Utils::FEX_PAGE_SIZE; });
|
|
PageRelocationRanges.at(Page) = {static_cast<uint32_t>(RelocBaseOffset + (RelocIt - Relocations.begin()) * sizeof(CPU::Relocation)),
|
|
static_cast<uint32_t>(EndRelocIt - RelocIt)};
|
|
RelocIt = EndRelocIt;
|
|
}
|
|
|
|
auto Storage = FEXCore::Allocator::aligned_alloc(alignof(MappedCodeCacheFile), sizeof(MappedCodeCacheFile));
|
|
return fextl::unique_ptr<MappedCodeCacheFile>(
|
|
new (Storage) MappedCodeCacheFile {this, CacheFile, CodeDataInFile, CodeBuffer, BlockListStart, header.NumBlocks, header.NumCodePages,
|
|
std::move(PageRelocationRanges), fextl::vector<bool>(NumPages), FileStartVA});
|
|
}
|
|
|
|
bool CodeCache::EnableLoadedSection(Core::InternalThreadState* Thread, MappedCodeCacheFile& Code, const ExecutableFileSectionInfo& BinarySection) {
|
|
if (!EnableCodeCaching) {
|
|
return true;
|
|
}
|
|
|
|
namespace ranges = std::ranges;
|
|
|
|
FEXCORE_PROFILE_SCOPED("EnableLoadedSection");
|
|
|
|
// Read block list from cache file
|
|
// TODO: Store section-ized BlockLists in cache file
|
|
using BlockListEntry = decltype(GuestToHostMap::BlockList)::value_type;
|
|
fextl::vector<BlockListEntry> BlockList(Code.NumBlocks);
|
|
{
|
|
auto* Cursor = Code.BlockListInFile;
|
|
for (auto& BlockPtr : BlockList) {
|
|
::memcpy(&BlockPtr.first, Cursor, sizeof(BlockPtr.first));
|
|
Cursor += sizeof(BlockPtr.first);
|
|
::memcpy(&BlockPtr.second.HostCode, Cursor, sizeof(BlockPtr.second.HostCode));
|
|
Cursor += sizeof(BlockPtr.second.HostCode);
|
|
uint64_t NumGuestPages;
|
|
::memcpy(&NumGuestPages, Cursor, sizeof(NumGuestPages));
|
|
Cursor += sizeof(NumGuestPages);
|
|
|
|
BlockPtr.second.CodePages.resize(NumGuestPages);
|
|
::memcpy(BlockPtr.second.CodePages.data(), Cursor, std::span {BlockPtr.second.CodePages}.size_bytes());
|
|
Cursor += std::span {BlockPtr.second.CodePages}.size_bytes();
|
|
}
|
|
|
|
// Constrain BlockList to the given ExecutableFileSectionInfo
|
|
LOGMAN_THROW_A_FMT(ranges::is_sorted(BlockList, [](auto& a, auto& b) { return a.first < b.first; }), "Expected sorted block list");
|
|
auto begin = ranges::lower_bound(BlockList, BinarySection.BeginVA - BinarySection.FileStartVA, std::less {}, &BlockListEntry::first);
|
|
auto end =
|
|
ranges::upper_bound(begin, BlockList.end(), BinarySection.EndVA - BinarySection.FileStartVA - 1, std::less {}, &BlockListEntry::first);
|
|
if (begin == end) {
|
|
LogMan::Msg::IFmt("No blocks cached in this range, aborting");
|
|
return true;
|
|
}
|
|
BlockList.erase(end, BlockList.end());
|
|
BlockList.erase(BlockList.begin(), begin);
|
|
}
|
|
|
|
LogMan::Msg::IFmt("Cache load: {:5} blocks; base={:#14x}; off={:#9x}-{:#09x}; {:016x} {}", BlockList.size(), BinarySection.FileStartVA,
|
|
BinarySection.BeginVA - BinarySection.FileStartVA, BinarySection.EndVA - BinarySection.FileStartVA,
|
|
BinarySection.FileInfo.FileId, BinarySection.FileInfo.Filename);
|
|
|
|
if (EnableLazyCodeCaching) {
|
|
LogMan::Msg::IFmt(" lazy mapping: base={:#14x} -> host={}; cache_source={}", BinarySection.FileStartVA,
|
|
fmt::ptr(Code.CodeBuffer.data()), fmt::ptr(Code.MappedFile.data()));
|
|
}
|
|
// Register blocks to LookupCache.
|
|
// The host addresses will point into the protected code buffer, so that FEX
|
|
// can lazily apply relocations on first execution of each page.
|
|
auto CodeBuffer = CTX.GetLatest();
|
|
{
|
|
FEXCORE_PROFILE_SCOPED("Decode");
|
|
auto& LookupCache = *CodeBuffer->LookupCache;
|
|
auto WriteLock = LookupCache.AcquireWriteLock();
|
|
|
|
for (auto& [Guest, Block] : BlockList) {
|
|
for (auto& CodePage : Block.CodePages) {
|
|
CodePage += BinarySection.FileStartVA;
|
|
}
|
|
LOGMAN_THROW_A_FMT(Block.HostCode < Code.CodeBuffer.size_bytes(), "Host offset {:#x} out of range ({:#x})", Block.HostCode,
|
|
Code.CodeBuffer.size_bytes());
|
|
auto HostCode = &Code.CodeBuffer[Block.HostCode];
|
|
LookupCache.AddBlockMapping(Guest + BinarySection.FileStartVA, std::move(Block.CodePages), HostCode, WriteLock);
|
|
}
|
|
|
|
// Guest code pages
|
|
auto* Cursor = Code.CodeBufferInFile.data() + Code.CodeBufferInFile.size_bytes();
|
|
fextl::vector<uint64_t> Entrypoints;
|
|
for (uint32_t i = 0; i < Code.NumCodePages; ++i) {
|
|
uint64_t CodePage;
|
|
memcpy(&CodePage, Cursor, sizeof(CodePage));
|
|
CodePage += BinarySection.FileStartVA;
|
|
Cursor += sizeof(CodePage);
|
|
|
|
uint64_t NumEntrypoints;
|
|
memcpy(&NumEntrypoints, Cursor, sizeof(NumEntrypoints));
|
|
Cursor += sizeof(NumEntrypoints);
|
|
|
|
Entrypoints.resize(NumEntrypoints);
|
|
memcpy(Entrypoints.data(), Cursor, std::span {Entrypoints}.size_bytes());
|
|
Cursor += std::span {Entrypoints}.size_bytes();
|
|
for (auto& Entrypoint : Entrypoints) {
|
|
Entrypoint += BinarySection.FileStartVA;
|
|
}
|
|
|
|
if (LookupCache.AddBlockExecutableRange(Entrypoints, CodePage, FEXCore::Utils::FEX_PAGE_SIZE, WriteLock)) {
|
|
CTX.SyscallHandler->MarkGuestExecutableRange(Thread, CodePage, FEXCore::Utils::FEX_PAGE_SIZE);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
if (!EnableLazyCodeCaching || EnableCodeCacheValidation) {
|
|
#else
|
|
// TODO: Implement lazy mapping on Windows
|
|
if (true) {
|
|
#endif
|
|
auto Range = SelectCodeRangeToFinalize(Code, 0, Code.CodeBuffer.size_bytes() / Utils::FEX_PAGE_SIZE);
|
|
FinalizeCodePages(Code, Range);
|
|
}
|
|
|
|
if (EnableCodeCacheValidation) {
|
|
fextl::set<uint64_t> GuestBlocks, HostBlocks;
|
|
for (auto& [Guest, Host] : BlockList) {
|
|
GuestBlocks.insert(Guest + BinarySection.FileStartVA);
|
|
HostBlocks.insert(Host.HostCode);
|
|
}
|
|
|
|
Validate(BinarySection, std::move(GuestBlocks), HostBlocks, Code.CodeBuffer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace FEXCore::Context
|
|
|
|
namespace FEXCore {
|
|
|
|
static std::span<CPU::Relocation> SpanPageRelocations(const MappedCodeCacheFile& Code, size_t PageIndex) {
|
|
auto [Offset, Count] = Code.PageRelocationRanges.at(PageIndex);
|
|
return std::span {reinterpret_cast<FEXCore::CPU::Relocation*>(Code.MappedFile.data() + Offset), Count};
|
|
}
|
|
|
|
std::span<std::byte> AbstractCodeCache::SelectCodeRangeToFinalize(MappedCodeCacheFile& Code, size_t StartPage, size_t EndPage) {
|
|
// First, check if we were racing another thread in loading this range
|
|
if (std::find(Code.LoadedPages.begin() + StartPage, Code.LoadedPages.begin() + EndPage, false) == Code.LoadedPages.begin() + EndPage) {
|
|
return {};
|
|
}
|
|
|
|
LOGMAN_THROW_A_FMT(StartPage < EndPage, "Invalid page range [{}, {})", StartPage, EndPage);
|
|
LOGMAN_THROW_A_FMT(EndPage <= Code.NumPages(), "End page {} out of range ({})", EndPage, Code.NumPages());
|
|
|
|
// Include any pages that have relocations or block link records crossing
|
|
// into the current page range. This ensures we don't attempt to finalize
|
|
// any page twice, partially apply FEX relocations, or trigger page loads
|
|
// during block linking.
|
|
while (EndPage < Code.NumPages()) {
|
|
auto PageRelocs = SpanPageRelocations(Code, EndPage - 1);
|
|
if (!PageRelocs.empty()) {
|
|
auto It = std::prev(PageRelocs.end());
|
|
size_t RelocEnd = It->Header.Offset + 16 /* Upper bound for relocation size */;
|
|
if (RelocEnd > EndPage * Utils::FEX_PAGE_SIZE) {
|
|
++EndPage;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check for trailing block link
|
|
{
|
|
auto PageRelocs = SpanPageRelocations(Code, EndPage);
|
|
if (!PageRelocs.empty() && PageRelocs.begin()->Header.Offset < EndPage * Utils::FEX_PAGE_SIZE + 0x18) {
|
|
++EndPage;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
};
|
|
while (StartPage != 0) {
|
|
auto PageRelocs = SpanPageRelocations(Code, StartPage - 1);
|
|
if (!PageRelocs.empty()) {
|
|
auto It = std::prev(PageRelocs.end());
|
|
size_t RelocEnd = It->Header.Offset + 16 /* Upper bound for relocation size */;
|
|
if (RelocEnd > StartPage * Utils::FEX_PAGE_SIZE) {
|
|
--StartPage;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check for trailing block link
|
|
{
|
|
auto PageRelocs = SpanPageRelocations(Code, StartPage);
|
|
if (!PageRelocs.empty() && PageRelocs.begin()->Header.Offset < StartPage * Utils::FEX_PAGE_SIZE + 0x18) {
|
|
--StartPage;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
};
|
|
|
|
return Code.CodeBuffer.subspan(StartPage * Utils::FEX_PAGE_SIZE, (EndPage - StartPage) * Utils::FEX_PAGE_SIZE);
|
|
}
|
|
} // namespace FEXCore
|
|
|
|
namespace FEXCore::Context {
|
|
|
|
void CodeCache::FinalizeCodePages(MappedCodeCacheFile& Code, std::span<std::byte> CodeRange) {
|
|
const size_t StartOffset = CodeRange.data() - Code.CodeBuffer.data();
|
|
const auto StartPage = StartOffset / Utils::FEX_PAGE_SIZE;
|
|
const auto EndPage = StartPage + CodeRange.size_bytes() / Utils::FEX_PAGE_SIZE;
|
|
const size_t Size = CodeRange.size_bytes();
|
|
|
|
// None of the selected pages should be loaded at all; otherwise, SelectCodeRangeToFinalize returned inconsistent ranges
|
|
LOGMAN_THROW_A_FMT(std::find(Code.LoadedPages.begin() + StartPage, Code.LoadedPages.begin() + EndPage, true) == Code.LoadedPages.begin() + EndPage,
|
|
"Inconsistent page load state");
|
|
|
|
FEXCORE_PROFILE_SCOPED("FinalizeCodePages");
|
|
|
|
#ifndef _WIN32
|
|
// Atomicity is critical when making the finalized code data visible.
|
|
// We ensure this by remapping a temporary buffer onto the PROT_NONE
|
|
// placeholder page in CodeBuffer. Some constraints to keep in mind are:
|
|
// 1. Pages can't be write-only (readability is implicitly added), so
|
|
// we can't change CodeBuffer from PROT_NONE to PROT_WRITE even for just
|
|
// a short duration
|
|
// 2. Naive mremap from CodeBufferInFile to CodeBuffer would leave a gap in
|
|
// the former, which would make cleanup overly complicated
|
|
//
|
|
// Due to (1), we can't apply relocations in place (CodeBufferInFile); at
|
|
// least a secondary buffer is needed for execution (CodeBuffer).
|
|
// Due to (2), a third buffer is temporarily allocated here and freed on
|
|
// completion. The final code data is computed here and then the memory
|
|
// is remapped onto CodeBuffer.
|
|
auto* Staging = reinterpret_cast<std::byte*>(Allocator::VirtualAlloc(nullptr, Size, true));
|
|
if (!Staging) {
|
|
ERROR_AND_DIE_FMT("Failed to allocate {} bytes of staging memory for code-cache finalization", Size);
|
|
}
|
|
|
|
// Copy code from the cache file to the staging buffer
|
|
memcpy(Staging, Code.CodeBufferInFile.data() + StartOffset, Size);
|
|
|
|
// Apply relocations
|
|
auto StagingSpan = std::span {Staging, Size};
|
|
for (size_t i = StartPage; i < EndPage; ++i) {
|
|
auto PageRelocations = SpanPageRelocations(Code, i);
|
|
(void)ApplyCodeRelocations(Code.GuestBase, StagingSpan, PageRelocations, false);
|
|
Code.LoadedPages[i] = true;
|
|
}
|
|
|
|
// Atomically make the finalized code data visible by remapping the staging
|
|
// buffer onto the requested CodeBuffer window. MREMAP_DONTUNMAP is used to
|
|
// leave the old VA range reserved so that we can cleanly deallocate it
|
|
// through Allocator.
|
|
void* RemapResult = ::mremap(Staging, Size, Size, MREMAP_FIXED | MREMAP_MAYMOVE | MREMAP_DONTUNMAP, CodeRange.data());
|
|
if (RemapResult == MAP_FAILED) {
|
|
ERROR_AND_DIE_FMT("{}: mremap failed: {}", __FUNCTION__, errno);
|
|
}
|
|
Allocator::VirtualFree(Staging, Size);
|
|
|
|
// Release resident file pages that will no longer be needed. The VA range is left allocated to allow cleanup with a single VirtualFree.
|
|
Allocator::VirtualDontNeed(Code.CodeBufferInFile.data() + StartOffset, Size);
|
|
#else
|
|
// TODO: Implement lazy mapping on Windows
|
|
#ifdef _M_ARM64EC
|
|
memcpy(Code.CodeBuffer.data() + StartOffset, Code.CodeBufferInFile.data() + StartOffset, Size);
|
|
#endif
|
|
for (size_t i = StartPage; i < EndPage; ++i) {
|
|
auto PageRelocations = SpanPageRelocations(Code, i);
|
|
(void)ApplyCodeRelocations(Code.GuestBase, Code.CodeBuffer, PageRelocations, false);
|
|
Code.LoadedPages[i] = true;
|
|
}
|
|
#endif
|
|
|
|
ARMEmitter::Emitter::ClearICache(CodeRange.data(), Size);
|
|
}
|
|
|
|
} // namespace FEXCore::Context
|