diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index ffbda79256..b681d21a76 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -460,6 +460,8 @@ add_library(core STATIC
     hle/service/hid/controllers/touchscreen.h
     hle/service/hid/controllers/xpad.cpp
     hle/service/hid/controllers/xpad.h
+    hle/service/jit/jit_context.cpp
+    hle/service/jit/jit_context.h
     hle/service/jit/jit.cpp
     hle/service/jit/jit.h
     hle/service/lbl/lbl.cpp
diff --git a/src/core/hle/kernel/k_code_memory.cpp b/src/core/hle/kernel/k_code_memory.cpp
index 63bbe02e91..09eaf004c0 100644
--- a/src/core/hle/kernel/k_code_memory.cpp
+++ b/src/core/hle/kernel/k_code_memory.cpp
@@ -35,9 +35,14 @@ ResultCode KCodeMemory::Initialize(Core::DeviceMemory& device_memory, VAddr addr
     R_TRY(page_table.LockForCodeMemory(addr, size))
 
     // Clear the memory.
-    for (const auto& block : m_page_group.Nodes()) {
-        std::memset(device_memory.GetPointer(block.GetAddress()), 0xFF, block.GetSize());
-    }
+    //
+    // FIXME: this ends up clobbering address ranges outside the scope of the mapping within
+    // guest memory, and is not specifically required if the guest program is correctly
+    // written, so disable until this is further investigated.
+    //
+    // for (const auto& block : m_page_group.Nodes()) {
+    //     std::memset(device_memory.GetPointer(block.GetAddress()), 0xFF, block.GetSize());
+    // }
 
     // Set remaining tracking members.
     m_address = addr;
diff --git a/src/core/hle/service/jit/jit.cpp b/src/core/hle/service/jit/jit.cpp
index c8ebd2e3f2..0f9e33ef61 100644
--- a/src/core/hle/service/jit/jit.cpp
+++ b/src/core/hle/service/jit/jit.cpp
@@ -2,27 +2,256 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include "core/arm/symbols.h"
+#include "core/core.h"
 #include "core/hle/ipc_helpers.h"
+#include "core/hle/kernel/k_code_memory.h"
+#include "core/hle/kernel/k_transfer_memory.h"
 #include "core/hle/result.h"
 #include "core/hle/service/jit/jit.h"
+#include "core/hle/service/jit/jit_context.h"
 #include "core/hle/service/service.h"
+#include "core/memory.h"
 
 namespace Service::JIT {
 
+struct CodeRange {
+    u64 offset;
+    u64 size;
+};
+
 class IJitEnvironment final : public ServiceFramework<IJitEnvironment> {
 public:
-    explicit IJitEnvironment(Core::System& system_) : ServiceFramework{system_, "IJitEnvironment"} {
+    explicit IJitEnvironment(Core::System& system_, CodeRange user_rx, CodeRange user_ro)
+        : ServiceFramework{system_, "IJitEnvironment", ServiceThreadType::CreateNew},
+          context{system_.Memory()} {
         // clang-format off
         static const FunctionInfo functions[] = {
-            {0, nullptr, "GenerateCode"},
-            {1, nullptr, "Control"},
-            {1000, nullptr, "LoadPlugin"},
-            {1001, nullptr, "GetCodeAddress"},
+            {0, &IJitEnvironment::GenerateCode, "GenerateCode"},
+            {1, &IJitEnvironment::Control, "Control"},
+            {1000, &IJitEnvironment::LoadPlugin, "LoadPlugin"},
+            {1001, &IJitEnvironment::GetCodeAddress, "GetCodeAddress"},
         };
         // clang-format on
 
         RegisterHandlers(functions);
+
+        // Identity map user code range into sysmodule context
+        configuration.user_ro_memory = user_ro;
+        configuration.user_rx_memory = user_rx;
+        configuration.sys_ro_memory = user_ro;
+        configuration.sys_rx_memory = user_rx;
     }
+
+    void GenerateCode(Kernel::HLERequestContext& ctx) {
+        struct Parameters {
+            u32 data_size;
+            u64 command;
+            CodeRange cr1;
+            CodeRange cr2;
+            Struct32 data;
+        };
+
+        IPC::RequestParser rp{ctx};
+        const auto parameters{rp.PopRaw<Parameters>()};
+        std::vector<u8> input_buffer{ctx.CanReadBuffer() ? ctx.ReadBuffer() : std::vector<u8>()};
+        std::vector<u8> output_buffer(ctx.CanWriteBuffer() ? ctx.GetWriteBufferSize() : 0);
+
+        const VAddr return_ptr{context.AddHeap(0u)};
+        const VAddr cr1_in_ptr{context.AddHeap(parameters.cr1)};
+        const VAddr cr2_in_ptr{context.AddHeap(parameters.cr2)};
+        const VAddr cr1_out_ptr{
+            context.AddHeap(CodeRange{.offset = parameters.cr1.offset, .size = 0})};
+        const VAddr cr2_out_ptr{
+            context.AddHeap(CodeRange{.offset = parameters.cr2.offset, .size = 0})};
+        const VAddr input_ptr{context.AddHeap(input_buffer.data(), input_buffer.size())};
+        const VAddr output_ptr{context.AddHeap(output_buffer.data(), output_buffer.size())};
+        const VAddr data_ptr{context.AddHeap(parameters.data)};
+        const VAddr configuration_ptr{context.AddHeap(configuration)};
+
+        context.CallFunction(callbacks.GenerateCode, return_ptr, cr1_out_ptr, cr2_out_ptr,
+                             configuration_ptr, parameters.command, input_ptr, input_buffer.size(),
+                             cr1_in_ptr, cr2_in_ptr, data_ptr, parameters.data_size, output_ptr,
+                             output_buffer.size());
+
+        const s32 return_value{context.GetHeap<s32>(return_ptr)};
+
+        if (return_value == 0) {
+            system.InvalidateCpuInstructionCacheRange(configuration.user_rx_memory.offset,
+                                                      configuration.user_rx_memory.size);
+
+            if (ctx.CanWriteBuffer()) {
+                context.GetHeap(output_ptr, output_buffer.data(), output_buffer.size());
+                ctx.WriteBuffer(output_buffer.data(), output_buffer.size());
+            }
+            const auto cr1_out{context.GetHeap<CodeRange>(cr1_out_ptr)};
+            const auto cr2_out{context.GetHeap<CodeRange>(cr2_out_ptr)};
+
+            IPC::ResponseBuilder rb{ctx, 8};
+            rb.Push(ResultSuccess);
+            rb.Push<u64>(return_value);
+            rb.PushRaw(cr1_out);
+            rb.PushRaw(cr2_out);
+        } else {
+            LOG_WARNING(Service_JIT, "plugin GenerateCode callback failed");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+        }
+    };
+
+    void Control(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto command{rp.PopRaw<u64>()};
+        const auto input_buffer{ctx.ReadBuffer()};
+        std::vector<u8> output_buffer(ctx.CanWriteBuffer() ? ctx.GetWriteBufferSize() : 0);
+
+        const VAddr return_ptr{context.AddHeap(0u)};
+        const VAddr configuration_ptr{context.AddHeap(configuration)};
+        const VAddr input_ptr{context.AddHeap(input_buffer.data(), input_buffer.size())};
+        const VAddr output_ptr{context.AddHeap(output_buffer.data(), output_buffer.size())};
+        const u64 wrapper_value{
+            context.CallFunction(callbacks.Control, return_ptr, configuration_ptr, command,
+                                 input_ptr, input_buffer.size(), output_ptr, output_buffer.size())};
+        const s32 return_value{context.GetHeap<s32>(return_ptr)};
+
+        if (wrapper_value == 0 && return_value == 0) {
+            if (ctx.CanWriteBuffer()) {
+                context.GetHeap(output_ptr, output_buffer.data(), output_buffer.size());
+                ctx.WriteBuffer(output_buffer.data(), output_buffer.size());
+            }
+            IPC::ResponseBuilder rb{ctx, 3};
+            rb.Push(ResultSuccess);
+            rb.Push(return_value);
+        } else {
+            LOG_WARNING(Service_JIT, "plugin Control callback failed");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+        }
+    }
+
+    void LoadPlugin(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        const auto tmem_size{rp.PopRaw<u64>()};
+        if (tmem_size == 0) {
+            LOG_ERROR(Service_JIT, "attempted to load plugin with empty transfer memory");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        const auto tmem_handle{ctx.GetCopyHandle(0)};
+        auto tmem{system.CurrentProcess()->GetHandleTable().GetObject<Kernel::KTransferMemory>(
+            tmem_handle)};
+        if (tmem.IsNull()) {
+            LOG_ERROR(Service_JIT, "attempted to load plugin with invalid transfer memory handle");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        configuration.work_memory.offset = tmem->GetSourceAddress();
+        configuration.work_memory.size = tmem_size;
+
+        const auto nro_plugin{ctx.ReadBuffer(1)};
+        auto symbols{Core::Symbols::GetSymbols(nro_plugin, true)};
+        const auto GetSymbol{[&](std::string name) { return symbols[name].first; }};
+
+        callbacks =
+            GuestCallbacks{.rtld_fini = GetSymbol("_fini"),
+                           .rtld_init = GetSymbol("_init"),
+                           .Control = GetSymbol("nnjitpluginControl"),
+                           .ResolveBasicSymbols = GetSymbol("nnjitpluginResolveBasicSymbols"),
+                           .SetupDiagnostics = GetSymbol("nnjitpluginSetupDiagnostics"),
+                           .Configure = GetSymbol("nnjitpluginConfigure"),
+                           .GenerateCode = GetSymbol("nnjitpluginGenerateCode"),
+                           .GetVersion = GetSymbol("nnjitpluginGetVersion"),
+                           .Keeper = GetSymbol("nnjitpluginKeeper"),
+                           .OnPrepared = GetSymbol("nnjitpluginOnPrepared")};
+
+        if (callbacks.GetVersion == 0 || callbacks.Configure == 0 || callbacks.GenerateCode == 0 ||
+            callbacks.OnPrepared == 0) {
+            LOG_ERROR(Service_JIT, "plugin does not implement all necessary functionality");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        if (!context.LoadNRO(nro_plugin)) {
+            LOG_ERROR(Service_JIT, "failed to load plugin");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        context.MapProcessMemory(configuration.sys_ro_memory.offset,
+                                 configuration.sys_ro_memory.size);
+        context.MapProcessMemory(configuration.sys_rx_memory.offset,
+                                 configuration.sys_rx_memory.size);
+        context.MapProcessMemory(configuration.work_memory.offset, configuration.work_memory.size);
+
+        if (callbacks.rtld_init != 0) {
+            context.CallFunction(callbacks.rtld_init);
+        }
+
+        const auto version{context.CallFunction(callbacks.GetVersion)};
+        if (version != 1) {
+            LOG_ERROR(Service_JIT, "unknown plugin version {}", version);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        const auto resolve{context.GetHelper("_resolve")};
+        if (callbacks.ResolveBasicSymbols != 0) {
+            context.CallFunction(callbacks.ResolveBasicSymbols, resolve);
+        }
+        const auto resolve_ptr{context.AddHeap(resolve)};
+        if (callbacks.SetupDiagnostics != 0) {
+            context.CallFunction(callbacks.SetupDiagnostics, 0u, resolve_ptr);
+        }
+
+        context.CallFunction(callbacks.Configure, 0u);
+        const auto configuration_ptr{context.AddHeap(configuration)};
+        context.CallFunction(callbacks.OnPrepared, configuration_ptr);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(ResultSuccess);
+    }
+
+    void GetCodeAddress(Kernel::HLERequestContext& ctx) {
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(ResultSuccess);
+        rb.Push(configuration.user_rx_memory.offset);
+        rb.Push(configuration.user_ro_memory.offset);
+    }
+
+private:
+    using Struct32 = std::array<u8, 32>;
+
+    struct GuestCallbacks {
+        VAddr rtld_fini;
+        VAddr rtld_init;
+        VAddr Control;
+        VAddr ResolveBasicSymbols;
+        VAddr SetupDiagnostics;
+        VAddr Configure;
+        VAddr GenerateCode;
+        VAddr GetVersion;
+        VAddr Keeper;
+        VAddr OnPrepared;
+    };
+
+    struct JITConfiguration {
+        CodeRange user_rx_memory;
+        CodeRange user_ro_memory;
+        CodeRange work_memory;
+        CodeRange sys_rx_memory;
+        CodeRange sys_ro_memory;
+    };
+
+    GuestCallbacks callbacks;
+    JITConfiguration configuration;
+    JITContext context;
 };
 
 class JITU final : public ServiceFramework<JITU> {
@@ -40,9 +269,59 @@ public:
     void CreateJitEnvironment(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_JIT, "called");
 
+        struct Parameters {
+            u64 rx_size;
+            u64 ro_size;
+        };
+
+        IPC::RequestParser rp{ctx};
+        const auto parameters{rp.PopRaw<Parameters>()};
+        const auto executable_mem_handle{ctx.GetCopyHandle(1)};
+        const auto readable_mem_handle{ctx.GetCopyHandle(2)};
+
+        if (parameters.rx_size == 0 || parameters.ro_size == 0) {
+            LOG_ERROR(Service_JIT, "attempted to init with empty code regions");
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        // The copy handle at index 0 is the process handle, but handle tables are
+        // per-process, so there is no point reading it here until we are multiprocess
+        const auto& process{*system.CurrentProcess()};
+
+        auto executable_mem{
+            process.GetHandleTable().GetObject<Kernel::KCodeMemory>(executable_mem_handle)};
+        if (executable_mem.IsNull()) {
+            LOG_ERROR(Service_JIT, "executable_mem is null for handle=0x{:08X}",
+                      executable_mem_handle);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        auto readable_mem{
+            process.GetHandleTable().GetObject<Kernel::KCodeMemory>(readable_mem_handle)};
+        if (readable_mem.IsNull()) {
+            LOG_ERROR(Service_JIT, "readable_mem is null for handle=0x{:08X}", readable_mem_handle);
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ResultUnknown);
+            return;
+        }
+
+        const CodeRange user_rx{
+            .offset = executable_mem->GetSourceAddress(),
+            .size = parameters.rx_size,
+        };
+
+        const CodeRange user_ro{
+            .offset = readable_mem->GetSourceAddress(),
+            .size = parameters.ro_size,
+        };
+
         IPC::ResponseBuilder rb{ctx, 2, 0, 1};
         rb.Push(ResultSuccess);
-        rb.PushIpcInterface<IJitEnvironment>(system);
+        rb.PushIpcInterface<IJitEnvironment>(system, user_rx, user_ro);
     }
 };
 
diff --git a/src/core/hle/service/jit/jit_context.cpp b/src/core/hle/service/jit/jit_context.cpp
new file mode 100644
index 0000000000..630368fb33
--- /dev/null
+++ b/src/core/hle/service/jit/jit_context.cpp
@@ -0,0 +1,424 @@
+// Copyright 2022 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <array>
+#include <map>
+#include <span>
+#include <boost/icl/interval_set.hpp>
+#include <dynarmic/interface/A64/a64.h>
+#include <dynarmic/interface/A64/config.h>
+
+#include "common/alignment.h"
+#include "common/common_funcs.h"
+#include "common/div_ceil.h"
+#include "common/logging/log.h"
+#include "core/hle/service/jit/jit_context.h"
+#include "core/memory.h"
+
+namespace Service::JIT {
+
+constexpr std::array<u8, 4> STOP_ARM64 = {
+    0x01, 0x00, 0x00, 0xd4, // svc  #0
+};
+
+constexpr std::array<u8, 8> RESOLVE_ARM64 = {
+    0x21, 0x00, 0x00, 0xd4, // svc  #1
+    0xc0, 0x03, 0x5f, 0xd6, // ret
+};
+
+constexpr std::array<u8, 4> PANIC_ARM64 = {
+    0x41, 0x00, 0x00, 0xd4, // svc  #2
+};
+
+constexpr std::array<u8, 60> MEMMOVE_ARM64 = {
+    0x1f, 0x00, 0x01, 0xeb, // cmp  x0, x1
+    0x83, 0x01, 0x00, 0x54, // b.lo #+34
+    0x42, 0x04, 0x00, 0xd1, // sub  x2, x2, 1
+    0x22, 0x01, 0xf8, 0xb7, // tbnz x2, #63, #+36
+    0x23, 0x68, 0x62, 0x38, // ldrb w3, [x1, x2]
+    0x03, 0x68, 0x22, 0x38, // strb w3, [x0, x2]
+    0xfc, 0xff, 0xff, 0x17, // b    #-16
+    0x24, 0x68, 0x63, 0x38, // ldrb w4, [x1, x3]
+    0x04, 0x68, 0x23, 0x38, // strb w4, [x0, x3]
+    0x63, 0x04, 0x00, 0x91, // add  x3, x3, 1
+    0x7f, 0x00, 0x02, 0xeb, // cmp  x3, x2
+    0x8b, 0xff, 0xff, 0x54, // b.lt #-16
+    0xc0, 0x03, 0x5f, 0xd6, // ret
+    0x03, 0x00, 0x80, 0xd2, // mov  x3, 0
+    0xfc, 0xff, 0xff, 0x17, // b    #-16
+};
+
+constexpr std::array<u8, 28> MEMSET_ARM64 = {
+    0x03, 0x00, 0x80, 0xd2, // mov  x3, 0
+    0x7f, 0x00, 0x02, 0xeb, // cmp  x3, x2
+    0x4b, 0x00, 0x00, 0x54, // b.lt #+8
+    0xc0, 0x03, 0x5f, 0xd6, // ret
+    0x01, 0x68, 0x23, 0x38, // strb w1, [x0, x3]
+    0x63, 0x04, 0x00, 0x91, // add  x3, x3, 1
+    0xfb, 0xff, 0xff, 0x17, // b    #-20
+};
+
+struct HelperFunction {
+    const char* name;
+    const std::span<const u8> data;
+};
+
+constexpr std::array<HelperFunction, 6> HELPER_FUNCTIONS{{
+    {"_stop", STOP_ARM64},
+    {"_resolve", RESOLVE_ARM64},
+    {"_panic", PANIC_ARM64},
+    {"memcpy", MEMMOVE_ARM64},
+    {"memmove", MEMMOVE_ARM64},
+    {"memset", MEMSET_ARM64},
+}};
+
+struct Elf64_Dyn {
+    u64 d_tag;
+    u64 d_un;
+};
+
+struct Elf64_Rela {
+    u64 r_offset;
+    u64 r_info;
+    s64 r_addend;
+};
+
+static constexpr u32 Elf64_RelaType(const Elf64_Rela* rela) {
+    return static_cast<u32>(rela->r_info);
+}
+
+constexpr int DT_RELA = 7;               /* Address of Rela relocs */
+constexpr int DT_RELASZ = 8;             /* Total size of Rela relocs */
+constexpr int R_AARCH64_RELATIVE = 1027; /* Adjust by program base.  */
+
+constexpr size_t STACK_ALIGN = 16;
+
+class JITContextImpl;
+
+using IntervalSet = boost::icl::interval_set<VAddr>::type;
+using IntervalType = boost::icl::interval_set<VAddr>::interval_type;
+
+class DynarmicCallbacks64 : public Dynarmic::A64::UserCallbacks {
+public:
+    explicit DynarmicCallbacks64(Core::Memory::Memory& memory_, std::vector<u8>& local_memory_,
+                                 IntervalSet& mapped_ranges_, JITContextImpl& parent_)
+        : memory{memory_}, local_memory{local_memory_},
+          mapped_ranges{mapped_ranges_}, parent{parent_} {}
+
+    u8 MemoryRead8(u64 vaddr) override {
+        return ReadMemory<u8>(vaddr);
+    }
+    u16 MemoryRead16(u64 vaddr) override {
+        return ReadMemory<u16>(vaddr);
+    }
+    u32 MemoryRead32(u64 vaddr) override {
+        return ReadMemory<u32>(vaddr);
+    }
+    u64 MemoryRead64(u64 vaddr) override {
+        return ReadMemory<u64>(vaddr);
+    }
+    u128 MemoryRead128(u64 vaddr) override {
+        return ReadMemory<u128>(vaddr);
+    }
+    std::string MemoryReadCString(u64 vaddr) {
+        std::string result;
+        u8 next;
+
+        while ((next = MemoryRead8(vaddr++)) != 0) {
+            result += next;
+        }
+
+        return result;
+    }
+
+    void MemoryWrite8(u64 vaddr, u8 value) override {
+        WriteMemory<u8>(vaddr, value);
+    }
+    void MemoryWrite16(u64 vaddr, u16 value) override {
+        WriteMemory<u16>(vaddr, value);
+    }
+    void MemoryWrite32(u64 vaddr, u32 value) override {
+        WriteMemory<u32>(vaddr, value);
+    }
+    void MemoryWrite64(u64 vaddr, u64 value) override {
+        WriteMemory<u64>(vaddr, value);
+    }
+    void MemoryWrite128(u64 vaddr, u128 value) override {
+        WriteMemory<u128>(vaddr, value);
+    }
+
+    bool MemoryWriteExclusive8(u64 vaddr, u8 value, u8) override {
+        return WriteMemory<u8>(vaddr, value);
+    }
+    bool MemoryWriteExclusive16(u64 vaddr, u16 value, u16) override {
+        return WriteMemory<u16>(vaddr, value);
+    }
+    bool MemoryWriteExclusive32(u64 vaddr, u32 value, u32) override {
+        return WriteMemory<u32>(vaddr, value);
+    }
+    bool MemoryWriteExclusive64(u64 vaddr, u64 value, u64) override {
+        return WriteMemory<u64>(vaddr, value);
+    }
+    bool MemoryWriteExclusive128(u64 vaddr, u128 value, u128) override {
+        return WriteMemory<u128>(vaddr, value);
+    }
+
+    void CallSVC(u32 swi) override;
+    void ExceptionRaised(u64 pc, Dynarmic::A64::Exception exception) override;
+    void InterpreterFallback(u64 pc, size_t num_instructions) override;
+
+    void AddTicks(u64 ticks) override {}
+    u64 GetTicksRemaining() override {
+        return std::numeric_limits<u32>::max();
+    }
+    u64 GetCNTPCT() override {
+        return 0;
+    }
+
+    template <class T>
+    T ReadMemory(u64 vaddr) {
+        T ret{};
+        if (boost::icl::contains(mapped_ranges, vaddr)) {
+            memory.ReadBlock(vaddr, &ret, sizeof(T));
+        } else if (vaddr + sizeof(T) > local_memory.size()) {
+            LOG_CRITICAL(Service_JIT, "plugin: unmapped read @ 0x{:016x}", vaddr);
+        } else {
+            std::memcpy(&ret, local_memory.data() + vaddr, sizeof(T));
+        }
+        return ret;
+    }
+
+    template <class T>
+    bool WriteMemory(u64 vaddr, const T value) {
+        if (boost::icl::contains(mapped_ranges, vaddr)) {
+            memory.WriteBlock(vaddr, &value, sizeof(T));
+        } else if (vaddr + sizeof(T) > local_memory.size()) {
+            LOG_CRITICAL(Service_JIT, "plugin: unmapped write @ 0x{:016x}", vaddr);
+        } else {
+            std::memcpy(local_memory.data() + vaddr, &value, sizeof(T));
+        }
+        return true;
+    }
+
+private:
+    Core::Memory::Memory& memory;
+    std::vector<u8>& local_memory;
+    IntervalSet& mapped_ranges;
+    JITContextImpl& parent;
+};
+
+class JITContextImpl {
+public:
+    explicit JITContextImpl(Core::Memory::Memory& memory_) : memory{memory_} {
+        callbacks =
+            std::make_unique<DynarmicCallbacks64>(memory, local_memory, mapped_ranges, *this);
+        user_config.callbacks = callbacks.get();
+        jit = std::make_unique<Dynarmic::A64::Jit>(user_config);
+    }
+
+    bool LoadNRO(std::span<const u8> data) {
+        local_memory.clear();
+        local_memory.insert(local_memory.end(), data.begin(), data.end());
+
+        if (FixupRelocations()) {
+            InsertHelperFunctions();
+            InsertStack();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    bool FixupRelocations() {
+        const VAddr mod_offset{callbacks->MemoryRead32(4)};
+        if (callbacks->MemoryRead32(mod_offset) != Common::MakeMagic('M', 'O', 'D', '0')) {
+            return false;
+        }
+
+        VAddr dynamic_offset{mod_offset + callbacks->MemoryRead32(mod_offset + 4)};
+        VAddr rela_dyn = 0;
+        size_t num_rela = 0;
+        while (true) {
+            const auto dyn{callbacks->ReadMemory<Elf64_Dyn>(dynamic_offset)};
+            dynamic_offset += sizeof(Elf64_Dyn);
+
+            if (!dyn.d_tag) {
+                break;
+            }
+            if (dyn.d_tag == DT_RELA) {
+                rela_dyn = dyn.d_un;
+            }
+            if (dyn.d_tag == DT_RELASZ) {
+                num_rela = dyn.d_un / sizeof(Elf64_Rela);
+            }
+        }
+
+        for (size_t i = 0; i < num_rela; i++) {
+            const auto rela{callbacks->ReadMemory<Elf64_Rela>(rela_dyn + i * sizeof(Elf64_Rela))};
+            if (Elf64_RelaType(&rela) != R_AARCH64_RELATIVE) {
+                continue;
+            }
+            const VAddr contents{callbacks->MemoryRead64(rela.r_offset)};
+            callbacks->MemoryWrite64(rela.r_offset, contents + rela.r_addend);
+        }
+
+        return true;
+    }
+
+    void InsertHelperFunctions() {
+        for (const auto& [name, contents] : HELPER_FUNCTIONS) {
+            helpers[name] = local_memory.size();
+            local_memory.insert(local_memory.end(), contents.begin(), contents.end());
+        }
+    }
+
+    void InsertStack() {
+        const u64 pad_amount{Common::AlignUp(local_memory.size(), STACK_ALIGN) -
+                             local_memory.size()};
+        local_memory.insert(local_memory.end(), 0x10000 + pad_amount, 0);
+        top_of_stack = local_memory.size();
+        heap_pointer = top_of_stack;
+    }
+
+    void MapProcessMemory(VAddr dest_address, std::size_t size) {
+        mapped_ranges.add(IntervalType{dest_address, dest_address + size});
+    }
+
+    void PushArgument(const void* data, size_t size) {
+        const size_t num_words = Common::DivCeil(size, sizeof(u64));
+        const size_t current_pos = argument_stack.size();
+        argument_stack.insert(argument_stack.end(), num_words, 0);
+        std::memcpy(argument_stack.data() + current_pos, data, size);
+    }
+
+    void SetupArguments() {
+        for (size_t i = 0; i < 8 && i < argument_stack.size(); i++) {
+            jit->SetRegister(i, argument_stack[i]);
+        }
+        if (argument_stack.size() > 8) {
+            const VAddr new_sp = Common::AlignDown(
+                top_of_stack - (argument_stack.size() - 8) * sizeof(u64), STACK_ALIGN);
+            for (size_t i = 8; i < argument_stack.size(); i++) {
+                callbacks->MemoryWrite64(new_sp + (i - 8) * sizeof(u64), argument_stack[i]);
+            }
+            jit->SetSP(new_sp);
+        }
+        argument_stack.clear();
+        heap_pointer = top_of_stack;
+    }
+
+    u64 CallFunction(VAddr func) {
+        jit->SetRegister(30, helpers["_stop"]);
+        jit->SetSP(top_of_stack);
+        SetupArguments();
+
+        jit->SetPC(func);
+        jit->Run();
+        return jit->GetRegister(0);
+    }
+
+    VAddr GetHelper(const std::string& name) {
+        return helpers[name];
+    }
+
+    VAddr AddHeap(const void* data, size_t size) {
+        const size_t num_bytes{Common::AlignUp(size, STACK_ALIGN)};
+        if (heap_pointer + num_bytes > local_memory.size()) {
+            local_memory.insert(local_memory.end(),
+                                (heap_pointer + num_bytes) - local_memory.size(), 0);
+        }
+        const VAddr location{heap_pointer};
+        std::memcpy(local_memory.data() + location, data, size);
+        heap_pointer += num_bytes;
+        return location;
+    }
+
+    void GetHeap(VAddr location, void* data, size_t size) {
+        std::memcpy(data, local_memory.data() + location, size);
+    }
+
+    std::unique_ptr<DynarmicCallbacks64> callbacks;
+    std::vector<u8> local_memory;
+    std::vector<u64> argument_stack;
+    IntervalSet mapped_ranges;
+    Dynarmic::A64::UserConfig user_config;
+    std::unique_ptr<Dynarmic::A64::Jit> jit;
+    std::map<std::string, VAddr, std::less<>> helpers;
+    Core::Memory::Memory& memory;
+    VAddr top_of_stack;
+    VAddr heap_pointer;
+};
+
+void DynarmicCallbacks64::CallSVC(u32 swi) {
+    switch (swi) {
+    case 0:
+        parent.jit->HaltExecution();
+        break;
+
+    case 1: {
+        // X0 contains a char* for a symbol to resolve
+        std::string name{MemoryReadCString(parent.jit->GetRegister(0))};
+        const auto helper{parent.helpers[name]};
+
+        if (helper != 0) {
+            parent.jit->SetRegister(0, helper);
+        } else {
+            LOG_WARNING(Service_JIT, "plugin requested unknown function {}", name);
+            parent.jit->SetRegister(0, parent.helpers["_panic"]);
+        }
+        break;
+    }
+
+    case 2:
+    default:
+        LOG_CRITICAL(Service_JIT, "plugin panicked!");
+        parent.jit->HaltExecution();
+        break;
+    }
+}
+
+void DynarmicCallbacks64::ExceptionRaised(u64 pc, Dynarmic::A64::Exception exception) {
+    LOG_CRITICAL(Service_JIT, "Illegal operation PC @ {:08x}", pc);
+    parent.jit->HaltExecution();
+}
+
+void DynarmicCallbacks64::InterpreterFallback(u64 pc, size_t num_instructions) {
+    LOG_CRITICAL(Service_JIT, "Unimplemented instruction PC @ {:08x}", pc);
+    parent.jit->HaltExecution();
+}
+
+JITContext::JITContext(Core::Memory::Memory& memory)
+    : impl{std::make_unique<JITContextImpl>(memory)} {}
+
+JITContext::~JITContext() {}
+
+bool JITContext::LoadNRO(std::span<const u8> data) {
+    return impl->LoadNRO(data);
+}
+
+void JITContext::MapProcessMemory(VAddr dest_address, std::size_t size) {
+    impl->MapProcessMemory(dest_address, size);
+}
+
+u64 JITContext::CallFunction(VAddr func) {
+    return impl->CallFunction(func);
+}
+
+void JITContext::PushArgument(const void* data, size_t size) {
+    impl->PushArgument(data, size);
+}
+
+VAddr JITContext::GetHelper(const std::string& name) {
+    return impl->GetHelper(name);
+}
+
+VAddr JITContext::AddHeap(const void* data, size_t size) {
+    return impl->AddHeap(data, size);
+}
+
+void JITContext::GetHeap(VAddr location, void* data, size_t size) {
+    impl->GetHeap(location, data, size);
+}
+
+} // namespace Service::JIT
diff --git a/src/core/hle/service/jit/jit_context.h b/src/core/hle/service/jit/jit_context.h
new file mode 100644
index 0000000000..d8bf76cff4
--- /dev/null
+++ b/src/core/hle/service/jit/jit_context.h
@@ -0,0 +1,65 @@
+// Copyright 2022 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <span>
+#include <string>
+
+#include "common/common_types.h"
+
+namespace Core::Memory {
+class Memory;
+}
+
+namespace Service::JIT {
+
+class JITContextImpl;
+
+class JITContext {
+public:
+    explicit JITContext(Core::Memory::Memory& memory);
+    ~JITContext();
+
+    [[nodiscard]] bool LoadNRO(std::span<const u8> data);
+    void MapProcessMemory(VAddr dest_address, std::size_t size);
+
+    template <typename T, typename... Ts>
+    u64 CallFunction(VAddr func, T argument, Ts... rest) {
+        static_assert(std::is_trivially_copyable_v<T>);
+        PushArgument(&argument, sizeof(argument));
+
+        if constexpr (sizeof...(rest) > 0) {
+            return CallFunction(func, rest...);
+        } else {
+            return CallFunction(func);
+        }
+    }
+
+    u64 CallFunction(VAddr func);
+    VAddr GetHelper(const std::string& name);
+
+    template <typename T>
+    VAddr AddHeap(T argument) {
+        return AddHeap(&argument, sizeof(argument));
+    }
+    VAddr AddHeap(const void* data, size_t size);
+
+    template <typename T>
+    T GetHeap(VAddr location) {
+        static_assert(std::is_trivially_copyable_v<T>);
+        T result;
+        GetHeap(location, &result, sizeof(result));
+        return result;
+    }
+    void GetHeap(VAddr location, void* data, size_t size);
+
+private:
+    std::unique_ptr<JITContextImpl> impl;
+
+    void PushArgument(const void* data, size_t size);
+};
+
+} // namespace Service::JIT