diff --git a/FEXCore/Source/Interface/Core/CodeCache.cpp b/FEXCore/Source/Interface/Core/CodeCache.cpp index 4db9b5212..83dd3b375 100644 --- a/FEXCore/Source/Interface/Core/CodeCache.cpp +++ b/FEXCore/Source/Interface/Core/CodeCache.cpp @@ -2,20 +2,20 @@ #include "Utils/SpinWaitLock.h" #include +#include #include +#include +#include #include #include -#include -#include +#include #include -#include - -#include +#include namespace FEXCore { @@ -226,12 +226,106 @@ uint64_t CodeCache::ComputeCodeMapId(std::string_view Filename, int FD) { return XXH3_64bits(Filename.data(), Filename.size()); } +struct CodeCacheHeader { + char Magic[4] = {'F', 'X', 'C', 'C'}; + uint32_t FormatVersion = 1; + char FEXVersion[8] = {}; + uint32_t NumBlocks; + uint32_t NumCodePages; + uint32_t CodeBufferSize; + uint32_t NumRelocations; + uint64_t SerializedBaseAddress; + // TODO: Consider including information from LookupCache.BlockLinks +}; + void CodeCache::LoadData(Core::InternalThreadState& Thread, std::byte* MappedCacheFile, const ExecutableFileSectionInfo& GuestRIPLookup) { // TODO } +template +static constexpr auto IsOrderedContainer(const T&) -> std::false_type; +template +static constexpr auto IsOrderedContainer(const std::map&) -> std::true_type; +template +static constexpr auto IsOrderedContainer(const std::set&) -> std::true_type; + bool CodeCache::SaveData(Core::InternalThreadState& Thread, int fd, const ExecutableFileSectionInfo& SourceBinary, uint64_t SerializedBaseAddress) { - // TODO + auto CodeBuffer = CTX.GetLatest(); + auto& LookupCache = *Thread.LookupCache->Shared; + + auto Relocations = Thread.CPUBackend->TakeRelocations(SourceBinary.FileStartVA); + + // Write file header + CodeCacheHeader header; + memcpy(&header.FEXVersion[0], GIT_SHORT_HASH, strlen(GIT_SHORT_HASH)); + header.NumBlocks = LookupCache.BlockList.size(); + header.NumCodePages = LookupCache.CodePages.size(); + header.CodeBufferSize = CTX.LatestOffset; + 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(!decltype(IsOrderedContainer(LookupCache.BlockList))::value, "Already deterministic; drop temporary container"); + fextl::vector> 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(CodeBuffer->Ptr); + ::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 + char Zero[64] {}; + auto Off = lseek(fd, 0, SEEK_CUR); + while (Off != AlignUp(Off, Utils::FEX_PAGE_SIZE)) { + auto BytesToWrite = std::min(AlignUp(Off, Utils::FEX_PAGE_SIZE) - Off, sizeof(Zero)); + ::write(fd, Zero, BytesToWrite); + Off += BytesToWrite; + } + + // Dump the host code (relocated for position-independent serialization) + std::vector CodeBufferData(reinterpret_cast(CodeBuffer->Ptr), reinterpret_cast(CodeBuffer->Ptr) + CTX.LatestOffset); + if (!ApplyCodeRelocations(SerializedBaseAddress, CodeBufferData, Relocations, true)) { + LOGMAN_THROW_A_FMT(false, "Failed to apply code relocations"); + return false; + } + ::write(fd, CodeBufferData.data(), CodeBufferData.size()); + + // Dump code pages + static_assert(decltype(IsOrderedContainer(LookupCache.CodePages))::value, "Non-deterministic data source"); + for (auto& [Page, Entrypoints] : LookupCache.CodePages) { + static_assert(sizeof(Page) == 8, "Breaking change in code cache data layout"); + ::write(fd, &Page, sizeof(Page)); + uint64_t NumEntrypoints = Entrypoints.size(); + ::write(fd, &NumEntrypoints, sizeof(NumEntrypoints)); + ::write(fd, Entrypoints.data(), Entrypoints.size() * sizeof(Entrypoints[0])); + } + return true; } diff --git a/Source/Tools/CMakeLists.txt b/Source/Tools/CMakeLists.txt index cd900de5d..85bbad364 100644 --- a/Source/Tools/CMakeLists.txt +++ b/Source/Tools/CMakeLists.txt @@ -18,6 +18,7 @@ if (NOT MINGW_BUILD) add_subdirectory(FEXGetConfig/) add_subdirectory(FEXServer/) add_subdirectory(FEXBash/) + add_subdirectory(FEXOfflineCompiler/) add_subdirectory(CodeSizeValidation/) add_subdirectory(LinuxEmulation/) diff --git a/Source/Tools/FEXOfflineCompiler/CMakeLists.txt b/Source/Tools/FEXOfflineCompiler/CMakeLists.txt new file mode 100644 index 000000000..7407f354a --- /dev/null +++ b/Source/Tools/FEXOfflineCompiler/CMakeLists.txt @@ -0,0 +1,25 @@ +add_executable(FEXOfflineCompiler Main.cpp) + +target_link_libraries(FEXOfflineCompiler + PRIVATE + Common + CommonTools + cpp-optparse + FEXCore + JemallocLibs + LinuxEmulation + ${PTHREAD_LIB} + fmt::fmt) + +if (CMAKE_BUILD_TYPE MATCHES "RELEASE") + target_link_options(FEXOfflineCompiler + PRIVATE + "LINKER:--gc-sections" + "LINKER:--strip-all" + "LINKER:--as-needed") +endif() + +install(TARGETS FEXOfflineCompiler + RUNTIME + DESTINATION bin + COMPONENT Runtime) diff --git a/Source/Tools/FEXOfflineCompiler/Main.cpp b/Source/Tools/FEXOfflineCompiler/Main.cpp new file mode 100644 index 000000000..fb854c6b1 --- /dev/null +++ b/Source/Tools/FEXOfflineCompiler/Main.cpp @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: MIT +#include "../FEXInterpreter/ELFCodeLoader.h" +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#include +#include + +class AOTSyscallHandler : public FEXCore::HLE::SyscallHandler, public FEX::HLE::SyscallMmapInterface { +public: + AOTSyscallHandler(FEXCore::HLE::SyscallOSABI SyscallOSABI) { + OSABI = SyscallOSABI; + } + + uint64_t HandleSyscall(FEXCore::Core::CpuStateFrame* Frame, FEXCore::HLE::SyscallArguments* Args) override { + // Don't do anything + return 0; + } + + FEXCore::ExecutableFileInfo FileInfo; + + uintptr_t VAFileStart = 0; + + // These are no-ops implementations of the SyscallHandler API + std::optional LookupExecutableFileSection(FEXCore::Core::InternalThreadState&, uint64_t) override { + return FEXCore::ExecutableFileSectionInfo {FileInfo, VAFileStart}; + } + + FEXCore::HLE::ExecutableRangeInfo QueryGuestExecutableRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Address) override { + return {0, UINT64_MAX, true}; + } + + void* GuestMmap(FEXCore::Core::InternalThreadState*, void* addr, size_t Size, int prot, int Flags, int fd, off_t offset) override { + auto Ret = mmap(addr, Size, prot, Flags, fd, offset); + if (Ret != MAP_FAILED && VAFileStart == 0) { + VAFileStart = reinterpret_cast(Ret); + } + return Ret; + } + + uint64_t GuestMunmap(FEXCore::Core::InternalThreadState*, void* addr, uint64_t length) override { + return munmap(addr, length); + } +}; + +static void MsgHandler(LogMan::DebugLevels Level, const char* Message) { + fmt::print("[{}] {}\n", LogMan::DebugLevelStr(Level), Message); +} + +static void AssertHandler(const char* Message) { + fmt::print("[A] {}\n", Message); +} + +namespace FEXCore { +inline bool operator<(const ExecutableFileInfo& a, const ExecutableFileInfo& b) noexcept { + return a.FileId < b.FileId; +} +} // namespace FEXCore + +template<> +struct std::hash { + std::size_t operator()(const FEXCore::ExecutableFileInfo& Val) const noexcept { + return Val.FileId; + } +}; + +// Placeholder data to ensure the compile thread doesn't de-reference nullptr data +static FEXCore::Core::CPUState::gdt_segment gdt[32] {}; + +static FEXCore::Core::InternalThreadState* SetupCompileThread(FEXCore::Context::Context& CTX, bool Is64Bit) { + auto Thread = CTX.CreateThread(0, 0); + + auto Frame = Thread->CurrentFrame; + Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_GDT] = &gdt[0]; + Frame->State.segment_arrays[FEXCore::Core::CPUState::SEGMENT_ARRAY_INDEX_LDT] = &gdt[0]; + + Frame->State.cs_idx = FEXCore::Core::CPUState::DEFAULT_USER_CS << 3; + auto GDT = FEXCore::Core::CPUState::GetSegmentFromIndex(Frame->State, Frame->State.cs_idx); + FEXCore::Core::CPUState::SetGDTBase(GDT, 0); + FEXCore::Core::CPUState::SetGDTLimit(GDT, 0xFFFFFU); + Frame->State.cs_cached = + FEXCore::Core::CPUState::CalculateGDTBase(*FEXCore::Core::CPUState::GetSegmentFromIndex(Frame->State, Frame->State.cs_idx)); + + if (Is64Bit) { + GDT->L = 1; // L = Long Mode = 64-bit + GDT->D = 0; // D = Default Operand SIze = Reserved + } else { + GDT->L = 0; // L = Long Mode = 32-bit + GDT->D = 1; // D = Default Operand Size = 32-bit + } + + return Thread; +} + +// Returns filename of generated cache on success +static std::optional +GenerateSingleCache(const FEXCore::ExecutableFileInfo& Binary, fextl::set BlockList, std::string_view OutDir) { + uint64_t CodeCacheConfigId = 0; // TODO: Make unique to active configuration + + ELFCodeLoader Loader(Binary.Filename.c_str(), -1, "", fextl::vector {Binary.Filename.c_str()}, + fextl::vector {}, nullptr, nullptr, true /* skip interpreter */); + if (!Loader.ELFWasLoaded()) { + fmt::print("Invalid or unsupported ELF file.\n"); + return std::nullopt; + } + const bool Is64Bit = Loader.Is64BitMode(); + FEXCore::Config::Set(FEXCore::Config::CONFIG_IS64BIT_MODE, Is64Bit ? "1" : "0"); + + // Load HostFeatures + auto HostFeatures = FEX::FetchHostFeatures(); + + if (!std::filesystem::exists(Binary.Filename)) { + fmt::print("File {} does not exist\n", Binary.Filename); + // TODO: Pressure vessel hits this + return /*EXIT_FAILURE*/ std::nullopt; + } + + auto CTX = FEXCore::Context::Context::CreateNewContext(HostFeatures); + + auto SignalDelegation = std::make_unique(); + + auto SyscallOSABI = Is64Bit ? FEXCore::HLE::SyscallOSABI::OS_LINUX64 : FEXCore::HLE::SyscallOSABI::OS_LINUX32; + auto SyscallHandler = std::make_unique(SyscallOSABI); + + Loader.CalculateHWCaps(CTX.get()); + + CTX->SetSignalDelegator(SignalDelegation.get()); + CTX->SetSyscallHandler(SyscallHandler.get()); + auto ThunkHandler = FEX::HLE::CreateThunkHandler(); + CTX->SetThunkHandler(ThunkHandler.get()); + + if (!CTX->InitCore()) { + return std::nullopt; + } + + { + if (!Is64Bit) { + // Block upper address space + FEXCore::Allocator::SetupHooks(); + } + auto ElfBase = Loader.LoadMainElfFile(nullptr, SyscallHandler.get()); + if (!ElfBase.has_value()) { + ERROR_AND_DIE_FMT("Failed to load ELF file {} ({})", Binary.Filename, Binary.FileId); + } + } + + auto Thread = SetupCompileThread(*CTX, Is64Bit); + + CTX->GetCodeCache().InitiateCacheGeneration(); + + { + std::vector> LoaderMem; + + fmt::print(stderr, "Compiling code...\n"); + for (auto Addr : BlockList) { + CTX->CompileRIP(Thread, Addr + SyscallHandler->VAFileStart); + } + + auto Filename = fmt::format("{}{}-{:016x}", OutDir, FEXCore::CodeMap::GetBaseFilename(Binary, false), CodeCacheConfigId); + auto FilenameNew = Filename + ".new"; + int fd = open(FilenameNew.c_str(), O_CREAT | O_WRONLY, 0644); + { + auto Entry = SyscallHandler->LookupExecutableFileSection(*Thread, Loader.GetMainElfBase()).value(); + CTX->GetCodeCache().SaveData(*Thread, fd, Entry, 0 /* TODO: Use static base address information if available */); + } + std::filesystem::rename(FilenameNew.c_str(), Filename.c_str()); + close(fd); + return Filename; + } +} + +// Command handler that parses the given code map and generates a code cache for the selected x86 binary. +// If no binary is selected explicitly, it is inferred from the code map ExecutableFileId block. +static int GenerateCache(int argc, const char** argv) { + optparse::OptionParser Parser {}; + Parser.add_option("--outdir").set_default(FEX::Config::GetCacheDirectory() + "cache").help("Output directory for generated cache files"); + Parser.add_option("--fileid").help("Select binary to generate cache for"); + + optparse::Values Options = Parser.parse_args(argc, argv); + if (Parser.args().size() != 1) { + Parser.print_usage(); + return 1; + } + const fextl::string CodeMapPath = Parser.args()[0]; + + std::ifstream Codemap(CodeMapPath.c_str(), std::ios_base::binary); + if (!Codemap) { + fmt::print("Could not open {}\n", CodeMapPath); + return 1; + } + + FEXCore::ExecutableFileInfo ProgramName; + std::map> Data; + { + auto Parsed = FEXCore::CodeMap::ParseCodeMap(Codemap); + + // If an explicit file id is selected, use it. + // Otherwise, fall back to an IsExecutable marker (or pick the first entry if there's only one) + auto ExplicitFileId = strtoull(((fextl::string)Options.get("fileid")).data(), nullptr, 16); + if (ExplicitFileId) { + ProgramName.FileId = ExplicitFileId; + ProgramName.Filename = Parsed.at(ExplicitFileId).Filename; + } + + for (auto& [FileId, Contents] : Parsed) { + if (!ExplicitFileId && (Contents.IsExecutable || Parsed.size() == 1)) { + ProgramName.FileId = FileId; + ProgramName.Filename = Contents.Filename; + } + Data.emplace(std::piecewise_construct, std::forward_as_tuple(nullptr, FileId, std::move(Contents.Filename)), + std::forward_as_tuple(std::move(Contents.Blocks))); + } + } + if (!ProgramName.FileId) { + fmt::print("Cannot generate cache from unsanitized code map {}", CodeMapPath); + return 1; + } + + for (auto& [File, Blocks] : Data) { + if (!Blocks.empty()) { + fmt::print("Parsed {} codemap entries for {} ({:016x})\n", Blocks.size(), File.Filename, File.FileId); + } else { + fmt::print("Found dependency {} ({:016x})\n", File.Filename, File.FileId); + } + } + + if (!Data.contains(ProgramName)) { + throw std::runtime_error(fmt::format("Input code map {} did not contain {} ({:016x})", CodeMapPath, ProgramName.Filename, ProgramName.FileId)); + } + + fextl::string OutDir(Options.get("outdir")); + if (!OutDir.ends_with('/')) { + OutDir.push_back('/'); + } + std::filesystem::create_directories(OutDir); + + const auto PortableInfo = FEX::ReadPortabilityInformation(); + char* envp[] = {nullptr}; + FEX::Config::LoadConfig("", envp, PortableInfo); + + auto NumBlocks = Data.at(ProgramName).size(); + auto GeneratedCache = GenerateSingleCache(ProgramName, Data.at(ProgramName), OutDir); + if (GeneratedCache) { + fmt::print("Successfully populated cache {} ({} blocks) via {}\n\n", GeneratedCache.value(), NumBlocks, + std::filesystem::path {CodeMapPath}.filename().string()); + } + return GeneratedCache ? 0 : 1; +} + +int main(int argc, char** argv) { + LogMan::Throw::InstallHandler(AssertHandler); + LogMan::Msg::InstallHandler(MsgHandler); + + std::vector Args {argv + 1, argv + argc}; + auto CommandName = std::string {basename(argv[0])} + " " + (argc > 1 ? argv[1] : ""); + Args[0] = CommandName.c_str(); + + if (argc >= 2 && argv[1] == std::string_view {"generate"}) { + return GenerateCache(argc - 1, Args.data()); + } else { + fmt::print("Usage: {} \n\n", basename(argv[0])); + fmt::print("Commands:\n"); + fmt::print(" generate\tTrigger cache generation from combined code map\n"); + return EXIT_FAILURE; + } +}