#include "util/types.hpp" #include "util/sysinfo.hpp" #include "JIT.h" #include "StrFmt.h" #include "File.h" #include "util/logs.hpp" #include "mutex.h" #include "util/vm.hpp" #include "util/asm.hpp" #include "Crypto/unzip.h" #include LOG_CHANNEL(jit_log, "JIT"); #ifdef LLVM_AVAILABLE #include #ifdef _MSC_VER #pragma warning(push, 0) #else #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wall" #pragma GCC diagnostic ignored "-Wextra" #pragma GCC diagnostic ignored "-Wold-style-cast" #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wstrict-aliasing" #pragma GCC diagnostic ignored "-Wredundant-decls" #pragma GCC diagnostic ignored "-Weffc++" #pragma GCC diagnostic ignored "-Wmissing-noreturn" #endif #include #include "llvm/Support/TargetSelect.h" #include "llvm/TargetParser/Host.h" #include "llvm/ExecutionEngine/ExecutionEngine.h" #include "llvm/ExecutionEngine/RTDyldMemoryManager.h" #include "llvm/ExecutionEngine/ObjectCache.h" #include "llvm/ExecutionEngine/JITEventListener.h" #include "llvm/Object/ObjectFile.h" #include "llvm/Object/SymbolSize.h" #ifdef _MSC_VER #pragma warning(pop) #else #pragma GCC diagnostic pop #endif const bool jit_initialize = []() -> bool { llvm::InitializeNativeTarget(); llvm::InitializeNativeTargetAsmPrinter(); llvm::InitializeNativeTargetAsmParser(); LLVMLinkInMCJIT(); return true; }(); [[noreturn]] static void null(const char* name) { fmt::throw_exception("Null function: %s", name); } namespace vm { extern u8* const g_sudo_addr; } static shared_mutex null_mtx; static std::unordered_map null_funcs; static u64 make_null_function(const std::string& name) { if (name.starts_with("__0x")) { u32 addr = -1; auto res = std::from_chars(name.c_str() + 4, name.c_str() + name.size(), addr, 16); if (res.ec == std::errc() && res.ptr == name.c_str() + name.size() && addr < 0x8000'0000) { // Point the garbage to reserved, non-executable memory return reinterpret_cast(vm::g_sudo_addr + addr); } } std::lock_guard lock(null_mtx); if (u64& func_ptr = null_funcs[name]) [[likely]] { // Already exists return func_ptr; } else { using namespace asmjit; // Build a "null" function that contains its name const auto func = build_function_asm("NULL", [&](native_asm& c, auto& args) { #if defined(ARCH_X64) Label data = c.newLabel(); c.lea(args[0], x86::qword_ptr(data, 0)); c.jmp(Imm(&null)); c.align(AlignMode::kCode, 16); c.bind(data); // Copy function name bytes for (char ch : name) c.db(ch); c.db(0); c.align(AlignMode::kData, 16); #else // AArch64 implementation Label jmp_address = c.newLabel(); Label data = c.newLabel(); // Force absolute jump to prevent out of bounds PC-rel jmp c.ldr(args[0], arm::ptr(jmp_address)); c.br(args[0]); c.align(AlignMode::kCode, 16); c.bind(data); c.embed(name.c_str(), name.size()); c.embedUInt8(0U); c.bind(jmp_address); c.embedUInt64(reinterpret_cast(&null)); c.align(AlignMode::kData, 16); #endif }); func_ptr = reinterpret_cast(func); return func_ptr; } } struct JITAnnouncer : llvm::JITEventListener { void notifyObjectLoaded(u64, const llvm::object::ObjectFile& obj, const llvm::RuntimeDyld::LoadedObjectInfo& info) override { using namespace llvm; object::OwningBinary debug_obj_ = info.getObjectForDebug(obj); if (!debug_obj_.getBinary()) { #ifdef __linux__ jit_log.error("LLVM: Failed to announce JIT events (no debug object)"); #endif return; } const object::ObjectFile& debug_obj = *debug_obj_.getBinary(); for (const auto& [sym, size] : computeSymbolSizes(debug_obj)) { Expected type_ = sym.getType(); if (!type_ || *type_ != object::SymbolRef::ST_Function) continue; Expected name = sym.getName(); if (!name) continue; Expected addr = sym.getAddress(); if (!addr) continue; jit_announce(*addr, size, {name->data(), name->size()}); } } }; // Simple memory manager struct MemoryManager1 : llvm::RTDyldMemoryManager { // 256 MiB for code or data static constexpr u64 c_max_size = 0x20000000 / 2; // Allocation unit (2M) static constexpr u64 c_page_size = 2 * 1024 * 1024; // Reserve 512 MiB u8* const ptr = static_cast(utils::memory_reserve(c_max_size * 2)); u64 code_ptr = 0; u64 data_ptr = c_max_size; MemoryManager1() = default; MemoryManager1(const MemoryManager1&) = delete; MemoryManager1& operator=(const MemoryManager1&) = delete; ~MemoryManager1() override { // Hack: don't release to prevent reuse of address space, see jit_announce utils::memory_decommit(ptr, c_max_size * 2); } llvm::JITSymbol findSymbol(const std::string& name) override { u64 addr = RTDyldMemoryManager::getSymbolAddress(name); if (!addr) { addr = make_null_function(name); if (!addr) { fmt::throw_exception("Failed to link '%s'", name); } } return {addr, llvm::JITSymbolFlags::Exported}; } u8* allocate(u64& oldp, uptr size, uint align, utils::protection prot) { if (align > c_page_size) { jit_log.fatal("Unsupported alignment (size=0x%x, align=0x%x)", size, align); return nullptr; } const u64 olda = utils::align(oldp, align); const u64 newp = utils::align(olda + size, align); if ((newp - 1) / c_max_size != oldp / c_max_size) { jit_log.fatal("Out of memory (size=0x%x, align=0x%x)", size, align); return nullptr; } if ((oldp - 1) / c_page_size != (newp - 1) / c_page_size) { // Allocate pages on demand const u64 pagea = utils::align(oldp, c_page_size); const u64 psize = utils::align(newp - pagea, c_page_size); utils::memory_commit(this->ptr + pagea, psize, prot); } // Update allocation counter oldp = newp; return this->ptr + olda; } u8* allocateCodeSection(uptr size, uint align, uint /*sec_id*/, llvm::StringRef /*sec_name*/) override { return allocate(code_ptr, size, align, utils::protection::wx); } u8* allocateDataSection(uptr size, uint align, uint /*sec_id*/, llvm::StringRef /*sec_name*/, bool /*is_ro*/) override { return allocate(data_ptr, size, align, utils::protection::rw); } bool finalizeMemory(std::string* = nullptr) override { return false; } void registerEHFrames(u8*, u64, usz) override { } void deregisterEHFrames() override { } }; // Simple memory manager struct MemoryManager2 : llvm::RTDyldMemoryManager { MemoryManager2() = default; ~MemoryManager2() override { } llvm::JITSymbol findSymbol(const std::string& name) override { u64 addr = RTDyldMemoryManager::getSymbolAddress(name); if (!addr) { addr = make_null_function(name); if (!addr) { fmt::throw_exception("Failed to link '%s' (MM2)", name); } } return {addr, llvm::JITSymbolFlags::Exported}; } u8* allocateCodeSection(uptr size, uint align, uint /*sec_id*/, llvm::StringRef /*sec_name*/) override { return jit_runtime::alloc(size, align, true); } u8* allocateDataSection(uptr size, uint align, uint /*sec_id*/, llvm::StringRef /*sec_name*/, bool /*is_ro*/) override { return jit_runtime::alloc(size, align, false); } bool finalizeMemory(std::string* = nullptr) override { return false; } void registerEHFrames(u8*, u64, usz) override { } void deregisterEHFrames() override { } }; // Helper class class ObjectCache final : public llvm::ObjectCache { const std::string& m_path; const std::add_pointer_t m_compiler = nullptr; public: ObjectCache(const std::string& path, jit_compiler* compiler = nullptr) : m_path(path) , m_compiler(compiler) { } ~ObjectCache() override = default; void notifyObjectCompiled(const llvm::Module* _module, llvm::MemoryBufferRef obj) override { std::string name = m_path; name.append(_module->getName().data()); //fs::file(name, fs::rewrite).write(obj.getBufferStart(), obj.getBufferSize()); name.append(".gz"); if (!obj.getBufferSize()) { jit_log.error("LLVM: Nothing to write: %s", name); return; } ensure(m_compiler); fs::file module_file(name, fs::rewrite); if (!module_file) { jit_log.error("LLVM: Failed to create module file: %s (%s)", name, fs::g_tls_error); return; } // Bold assumption about upper limit of space consumption const usz max_size = obj.getBufferSize() * 4; if (!m_compiler->add_sub_disk_space(0 - max_size)) { jit_log.error("LLVM: Failed to create module file: %s (not enough disk space left)", name); return; } if (!zip(obj.getBufferStart(), obj.getBufferSize(), module_file)) { jit_log.error("LLVM: Failed to compress module: %s", _module->getName().data()); module_file.close(); fs::remove_file(name); return; } jit_log.notice("LLVM: Created module: %s", _module->getName().data()); // Restore space that was overestimated ensure(m_compiler->add_sub_disk_space(max_size - module_file.size())); } static std::unique_ptr load(const std::string& path) { if (fs::file cached{path + ".gz", fs::read}) { const std::vector cached_data = cached.to_vector(); if (cached_data.empty()) [[unlikely]] { return nullptr; } const std::vector out = unzip(cached_data); if (out.empty()) { jit_log.error("LLVM: Failed to unzip module: '%s'", path); return nullptr; } auto buf = llvm::WritableMemoryBuffer::getNewUninitMemBuffer(out.size()); std::memcpy(buf->getBufferStart(), out.data(), out.size()); return buf; } if (fs::file cached{path, fs::read}) { if (cached.size() == 0) [[unlikely]] { return nullptr; } auto buf = llvm::WritableMemoryBuffer::getNewUninitMemBuffer(cached.size()); cached.read(buf->getBufferStart(), buf->getBufferSize()); return buf; } return nullptr; } std::unique_ptr getObject(const llvm::Module* _module) override { std::string path = m_path; path.append(_module->getName().data()); if (auto buf = load(path)) { jit_log.notice("LLVM: Loaded module: %s", _module->getName().data()); return buf; } return nullptr; } }; std::string jit_compiler::cpu(const std::string& _cpu) { std::string m_cpu = _cpu; if (m_cpu.empty()) { m_cpu = llvm::sys::getHostCPUName().str(); if (m_cpu == "sandybridge" || m_cpu == "ivybridge" || m_cpu == "haswell" || m_cpu == "broadwell" || m_cpu == "skylake" || m_cpu == "skylake-avx512" || m_cpu == "cascadelake" || m_cpu == "cooperlake" || m_cpu == "cannonlake" || m_cpu == "icelake" || m_cpu == "icelake-client" || m_cpu == "icelake-server" || m_cpu == "tigerlake" || m_cpu == "rocketlake" || m_cpu == "alderlake" || m_cpu == "raptorlake" || m_cpu == "meteorlake") { // Downgrade if AVX is not supported by some chips if (!utils::has_avx()) { m_cpu = "nehalem"; } } if (m_cpu == "skylake-avx512" || m_cpu == "cascadelake" || m_cpu == "cooperlake" || m_cpu == "cannonlake" || m_cpu == "icelake" || m_cpu == "icelake-client" || m_cpu == "icelake-server" || m_cpu == "tigerlake" || m_cpu == "rocketlake") { // Downgrade if AVX-512 is disabled or not supported if (!utils::has_avx512()) { m_cpu = "skylake"; } } if (m_cpu == "znver1" && utils::has_clwb()) { // Upgrade m_cpu = "znver2"; } if ((m_cpu == "znver3" || m_cpu == "goldmont" || m_cpu == "alderlake" || m_cpu == "raptorlake" || m_cpu == "meteorlake") && utils::has_avx512_icl()) { // Upgrade m_cpu = "icelake-client"; } if (m_cpu == "goldmont" && utils::has_avx2()) { // Upgrade m_cpu = "alderlake"; } } return m_cpu; } std::string jit_compiler::triple1() { #if defined(_WIN32) return llvm::Triple::normalize(llvm::sys::getProcessTriple()); #elif defined(__APPLE__) && defined(ARCH_X64) return llvm::Triple::normalize("x86_64-unknown-linux-gnu"); #elif defined(__APPLE__) && defined(ARCH_ARM64) return llvm::Triple::normalize("aarch64-unknown-linux-gnu"); #else return llvm::Triple::normalize(llvm::sys::getProcessTriple()); #endif } std::string jit_compiler::triple2() { #if defined(_WIN32) && defined(ARCH_X64) return llvm::Triple::normalize("x86_64-unknown-linux-gnu"); #elif defined(_WIN32) && defined(ARCH_ARM64) return llvm::Triple::normalize("aarch64-unknown-linux-gnu"); #elif defined(__APPLE__) && defined(ARCH_X64) return llvm::Triple::normalize("x86_64-unknown-linux-gnu"); #elif defined(__APPLE__) && defined(ARCH_ARM64) return llvm::Triple::normalize("aarch64-unknown-linux-gnu"); #else return llvm::Triple::normalize(llvm::sys::getProcessTriple()); #endif } bool jit_compiler::add_sub_disk_space(ssz space) { if (space >= 0) { ensure(m_disk_space.fetch_add(space) < ~static_cast(space)); return true; } return m_disk_space.fetch_op([sub_size = static_cast(0 - space)](usz& val) { if (val >= sub_size) { val -= sub_size; return true; } return false; }).second; } jit_compiler::jit_compiler(const std::unordered_map& _link, const std::string& _cpu, u32 flags) : m_context(new llvm::LLVMContext) , m_cpu(cpu(_cpu)) { std::string result; auto null_mod = std::make_unique ("null_", *m_context); null_mod->setTargetTriple(jit_compiler::triple1()); std::unique_ptr mem; if (_link.empty()) { // Auxiliary JIT (does not use custom memory manager, only writes the objects) if (flags & 0x1) { mem = std::make_unique(); } else { mem = std::make_unique(); null_mod->setTargetTriple(jit_compiler::triple2()); } } else { mem = std::make_unique(); } { m_engine.reset(llvm::EngineBuilder(std::move(null_mod)) .setErrorStr(&result) .setEngineKind(llvm::EngineKind::JIT) .setMCJITMemoryManager(std::move(mem)) #if LLVM_VERSION_MAJOR < 18 .setOptLevel(llvm::CodeGenOpt::Aggressive) #else .setOptLevel(llvm::CodeGenOptLevel::Aggressive) #endif .setCodeModel(flags & 0x2 ? llvm::CodeModel::Large : llvm::CodeModel::Small) #ifdef __APPLE__ //.setCodeModel(llvm::CodeModel::Large) #endif .setRelocationModel(llvm::Reloc::Model::PIC_) .setMCPU(m_cpu) .create()); } if (!_link.empty()) { for (auto&& [name, addr] : _link) { m_engine->updateGlobalMapping(name, addr); } } if (!_link.empty() || !(flags & 0x1)) { m_engine->RegisterJITEventListener(llvm::JITEventListener::createIntelJITEventListener()); m_engine->RegisterJITEventListener(new JITAnnouncer); } if (!m_engine) { fmt::throw_exception("LLVM: Failed to create ExecutionEngine: %s", result); } fs::device_stat stats{}; if (fs::statfs(fs::get_cache_dir(), stats)) { m_disk_space = stats.avail_free / 4; } } jit_compiler::~jit_compiler() { } void jit_compiler::add(std::unique_ptr _module, const std::string& path) { ObjectCache cache{path, this}; m_engine->setObjectCache(&cache); const auto ptr = _module.get(); m_engine->addModule(std::move(_module)); m_engine->generateCodeForModule(ptr); m_engine->setObjectCache(nullptr); for (auto& func : ptr->functions()) { // Delete IR to lower memory consumption func.deleteBody(); } } void jit_compiler::add(std::unique_ptr _module) { const auto ptr = _module.get(); m_engine->addModule(std::move(_module)); m_engine->generateCodeForModule(ptr); for (auto& func : ptr->functions()) { // Delete IR to lower memory consumption func.deleteBody(); } } bool jit_compiler::add(const std::string& path) { auto cache = ObjectCache::load(path); if (!cache) { jit_log.error("ObjectCache: Failed to read file. (path='%s', error=%s)", path, fs::g_tls_error); return false; } if (auto object_file = llvm::object::ObjectFile::createObjectFile(*cache)) { m_engine->addObjectFile(llvm::object::OwningBinary(std::move(*object_file), std::move(cache))); jit_log.trace("ObjectCache: Successfully added %s", path); return true; } else { jit_log.error("ObjectCache: Adding failed: %s", path); return false; } } bool jit_compiler::check(const std::string& path) { if (auto cache = ObjectCache::load(path)) { if (auto object_file = llvm::object::ObjectFile::createObjectFile(*cache)) { return true; } if (fs::remove_file(path)) { jit_log.error("ObjectCache: Removed damaged file: %s", path); } } return false; } void jit_compiler::update_global_mapping(const std::string& name, u64 addr) { m_engine->updateGlobalMapping(name, addr); } void jit_compiler::fin() { m_engine->finalizeObject(); } u64 jit_compiler::get(const std::string& name) { return m_engine->getGlobalValueAddress(name); } #endif // LLVM_AVAILABLE