Implement Symbol Hooking

Symbol hooking is required for HLE implementations of certain features in the future such as `nvdec` and for more in-depth debugging of games as we can inspect them on a SDK function level which allows us to debug issues far more easily.
This commit is contained in:
PixelyIon 2022-11-07 23:46:34 +05:30
parent 8892eb08e6
commit f4a8328cef
13 changed files with 412 additions and 77 deletions

View File

@ -232,6 +232,7 @@ add_library(skyline SHARED
${source_DIR}/skyline/loader/nca.cpp
${source_DIR}/skyline/loader/xci.cpp
${source_DIR}/skyline/loader/nsp.cpp
${source_DIR}/skyline/hle/symbol_hooks.cpp
${source_DIR}/skyline/vfs/partition_filesystem.cpp
${source_DIR}/skyline/vfs/ctr_encrypted_backing.cpp
${source_DIR}/skyline/vfs/rom_filesystem.cpp

View File

@ -11,6 +11,7 @@
PERFETTO_DEFINE_CATEGORIES(
perfetto::Category("scheduler").SetDescription("Events from the HLE scheduler"),
perfetto::Category("kernel").SetDescription("Events from parts of the HLE kernel"),
perfetto::Category("hook").SetDescription("Events from HLE hooks"),
perfetto::Category("guest").SetDescription("Events relating to guest code"),
perfetto::Category("host").SetDescription("Events relating to host code"),
perfetto::Category("gpu").SetDescription("Events from the emulated GPU"),

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include "symbol_hooks.h"
namespace skyline::hle {
struct HookTableEntry {
std::string_view name; //!< The name of the symbol
HookType hook; //!< The hook that the symbol should include
HookTableEntry(std::string_view name, HookType hook) : name{name}, hook{std::move(hook)} {}
};
static std::array<HookTableEntry, 0> HookedSymbols{};
}

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <cxxabi.h>
#include "symbol_hooks.h"
namespace skyline::hle {
std::string Demangle(const std::string_view mangledName) {
int status{};
size_t length{};
std::unique_ptr<char, decltype(&std::free)> demangled{abi::__cxa_demangle(mangledName.data(), nullptr, &length, &status), std::free};
return std::string{status == 0 ? std::string_view{demangled.get()} : mangledName};
}
HookedSymbol::HookedSymbol(std::string pName, HookType hook) : name{std::move(pName)}, prettyName{Demangle(name)}, hook{std::move(hook)} {}
}

View File

@ -0,0 +1,29 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include <common.h>
namespace skyline::hle {
struct HookedSymbol;
using HookFunction = std::function<void(const DeviceState &, const HookedSymbol &)>;
using OverrideHook = HookFunction; //!< A function that overrides the execution of a hooked function
struct EntryExitHook {
HookFunction entry; //!< The hook to be called when the function is entered
HookFunction exit; //!< The hook to be called when the function is exited
};
using HookType = std::variant<OverrideHook, EntryExitHook>;
struct HookedSymbol {
std::string name; //!< The name of the symbol
std::string prettyName; //!< The demangled name of the symbol
HookType hook; //!< The hook that the symbol should include
HookedSymbol(std::string name, HookType hook);
};
}

View File

@ -6,6 +6,27 @@
#include <common.h>
namespace skyline::loader {
/**
* @brief The MOD header embeds metadata about an executable into it
* @url https://switchbrew.org/wiki/NSO#MOD
*/
struct MOD {
u32 reserved;
u32 magicOffset;
u32 magic;
u32 dynamicOffset;
u32 bssStart;
u32 bssEnd;
u32 ehFrameHdrStart;
u32 ehFrameHdrEnd;
u32 moduleOffset;
bool IsValid() {
constexpr u32 MODMagic{util::MakeMagic<u32>("MOD0")};
return magic == MODMagic;
}
};
/**
* @brief The contents of an executable binary abstracted away from the derivatives of Loader
*/

View File

@ -7,55 +7,111 @@
#include <os.h>
#include <kernel/types/KProcess.h>
#include <kernel/memory.h>
#include <hle/symbol_hook_table.h>
#include "loader.h"
namespace skyline::loader {
Loader::ExecutableLoadInfo Loader::LoadExecutable(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, Executable &executable, size_t offset, const std::string &name) {
Loader::ExecutableLoadInfo Loader::LoadExecutable(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, Executable &executable, size_t offset, const std::string &name, bool dynamicallyLinked) {
u8 *base{reinterpret_cast<u8 *>(process->memory.base.data() + offset)};
u64 textSize{executable.text.contents.size()};
u64 roSize{executable.ro.contents.size()};
u64 dataSize{executable.data.contents.size() + executable.bssSize};
size_t textSize{executable.text.contents.size()};
size_t roSize{executable.ro.contents.size()};
size_t dataSize{executable.data.contents.size() + executable.bssSize};
if (!util::IsPageAligned(textSize) || !util::IsPageAligned(roSize) || !util::IsPageAligned(dataSize))
throw exception("LoadProcessData: Sections are not aligned with page size: 0x{:X}, 0x{:X}, 0x{:X}", textSize, roSize, dataSize);
throw exception("Sections are not aligned with page size: 0x{:X}, 0x{:X}, 0x{:X}", textSize, roSize, dataSize);
if (executable.text.offset != 0)
throw exception("Executable's .text offset is not 0: 0x{:X}", executable.text.offset);
if (!util::IsPageAligned(executable.text.offset) || !util::IsPageAligned(executable.ro.offset) || !util::IsPageAligned(executable.data.offset))
throw exception("LoadProcessData: Section offsets are not aligned with page size: 0x{:X}, 0x{:X}, 0x{:X}", executable.text.offset, executable.ro.offset, executable.data.offset);
throw exception("Section offsets are not aligned with page size: 0x{:X}, 0x{:X}, 0x{:X}", executable.text.offset, executable.ro.offset, executable.data.offset);
auto patch{state.nce->GetPatchData(executable.text.contents)};
auto size{patch.size + textSize + roSize + dataSize};
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{base, patch.size}, memory::Permission{false, false, false}, memory::states::Reserved); // ---
Logger::Debug("Successfully mapped section .patch @ 0x{:X}, Size = 0x{:X}", base, patch.size);
span dynsym{reinterpret_cast<Elf64_Sym *>(executable.ro.contents.data() + executable.dynsym.offset), executable.dynsym.size / sizeof(Elf64_Sym)};
span dynstr{reinterpret_cast<char *>(executable.ro.contents.data() + executable.dynstr.offset), executable.dynstr.size};
std::vector<nce::NCE::HookedSymbolEntry> executableSymbols;
size_t hookSize{};
if (dynamicallyLinked) {
for (auto &symbol : dynsym) {
if (symbol.st_name == 0 || symbol.st_value == 0)
continue;
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{base + patch.size + executable.text.offset, textSize}, memory::Permission{true, false, true}, memory::states::CodeStatic); // R-X
Logger::Debug("Successfully mapped section .text @ 0x{:X}, Size = 0x{:X}", base + patch.size + executable.text.offset, textSize);
if (ELF64_ST_TYPE(symbol.st_info) != STT_FUNC || ELF64_ST_BIND(symbol.st_info) != STB_GLOBAL || symbol.st_shndx == SHN_UNDEF)
continue;
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{base + patch.size + executable.ro.offset, roSize}, memory::Permission{true, false, false}, memory::states::CodeStatic); // R--
Logger::Debug("Successfully mapped section .rodata @ 0x{:X}, Size = 0x{:X}", base + patch.size + executable.ro.offset, roSize);
std::string_view symbolName{dynstr.data() + symbol.st_name};
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{base + patch.size + executable.data.offset, dataSize}, memory::Permission{true, true, false}, memory::states::CodeMutable); // RW-
Logger::Debug("Successfully mapped section .data + .bss @ 0x{:X}, Size = 0x{:X}", base + patch.size + executable.data.offset, dataSize);
auto item{std::find_if(hle::HookedSymbols.begin(), hle::HookedSymbols.end(), [&symbolName](const auto &item) {
return item.name == symbolName;
})};
if (item != hle::HookedSymbols.end()) {
executableSymbols.emplace_back(std::string{symbolName}, item->hook, &symbol.st_value);
continue;
}
state.nce->PatchCode(executable.text.contents, reinterpret_cast<u32 *>(base), patch.size, patch.offsets);
std::memcpy(base + patch.size + executable.text.offset, executable.text.contents.data(), textSize);
std::memcpy(base + patch.size + executable.ro.offset, executable.ro.contents.data(), roSize);
std::memcpy(base + patch.size + executable.data.offset, executable.data.contents.data(), dataSize - executable.bssSize);
#ifdef PRINT_HOOK_ALL
if (symbolName == "memcpy" || symbolName == "memcmp" || symbolName == "memset" || symbolName == "strcmp" || symbolName == "strlen")
// If symbol is from libc (such as memcpy, strcmp, strlen, etc), we don't need to hook it
continue;
auto rodataOffset{base + patch.size + executable.ro.offset};
ExecutableSymbolicInfo symbolicInfo{
.patchStart = base,
.programStart = base + patch.size,
.programEnd = base + size,
.name = name,
.patchName = name + ".patch",
.symbols = span(reinterpret_cast<Elf64_Sym *>(rodataOffset + executable.dynsym.offset), executable.dynsym.size / sizeof(Elf64_Sym)),
.symbolStrings = span(reinterpret_cast<char *>(rodataOffset + executable.dynstr.offset), executable.dynstr.size),
};
executables.insert(std::upper_bound(executables.begin(), executables.end(), base, [](void *ptr, const ExecutableSymbolicInfo &it) { return ptr < it.patchStart; }), symbolicInfo);
executableSymbols.emplace_back(std::string{symbolName}, hle::EntryExitHook{
.entry = [](const DeviceState &, const hle::HookedSymbol &symbol) {
Logger::Error("Entering \"{}\" ({})", symbol.prettyName, symbol.name);
},
.exit = [](const DeviceState &, const hle::HookedSymbol &symbol) {
Logger::Error("Exiting \"{}\"", symbol.prettyName);
},
}, &symbol.st_value);
#endif
}
return {base, size, base + patch.size};
hookSize = util::AlignUp(state.nce->GetHookSectionSize(executableSymbols), PAGE_SIZE);
}
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{base, patch.size + hookSize}, memory::Permission{false, false, false}, memory::states::Reserved); // ---
Logger::Error("Successfully mapped section .patch @ 0x{:X}, Size = 0x{:X}", base, patch.size);
if (hookSize > 0)
Logger::Error("Successfully mapped section .hook @ 0x{:X}, Size = 0x{:X}", base + patch.size, hookSize);
u8 *executableBase{base + patch.size + hookSize};
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{executableBase + executable.text.offset, textSize}, memory::Permission{true, false, true}, memory::states::CodeStatic); // R-X
Logger::Error("Successfully mapped section .text @ 0x{:X}, Size = 0x{:X}", executableBase, textSize);
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{executableBase + executable.ro.offset, roSize}, memory::Permission{true, false, false}, memory::states::CodeStatic); // R--
Logger::Error("Successfully mapped section .rodata @ 0x{:X}, Size = 0x{:X}", executableBase + executable.ro.offset, roSize);
process->NewHandle<kernel::type::KPrivateMemory>(span<u8>{executableBase + executable.data.offset, dataSize}, memory::Permission{true, true, false}, memory::states::CodeMutable); // RW-
Logger::Error("Successfully mapped section .data + .bss @ 0x{:X}, Size = 0x{:X}", executableBase + executable.data.offset, dataSize);
size_t size{patch.size + hookSize + textSize + roSize + dataSize};
{
// Note: We need to copy out the symbols here as it'll be overwritten by any hooks
ExecutableSymbolicInfo symbolicInfo{
.patchStart = base,
.hookStart = base + patch.size,
.programStart = executableBase,
.programEnd = base + size,
.name = name,
.patchName = name + ".patch",
.hookName = name + ".hook",
.symbols = {dynsym.begin(), dynsym.end()},
.symbolStrings = {dynstr.begin(), dynstr.end()},
};
executables.insert(std::upper_bound(executables.begin(), executables.end(), base, [](void *ptr, const ExecutableSymbolicInfo &it) { return ptr < it.patchStart; }), std::move(symbolicInfo));
}
state.nce->PatchCode(executable.text.contents, reinterpret_cast<u32 *>(base), patch.size, patch.offsets, hookSize);
if (hookSize)
state.nce->WriteHookSection(executableSymbols, span<u8>{base + patch.size, hookSize}.cast<u32>());
std::memcpy(executableBase, executable.text.contents.data(), executable.text.contents.size());
std::memcpy(executableBase + executable.ro.offset, executable.ro.contents.data(), roSize);
std::memcpy(executableBase + executable.data.offset, executable.data.contents.data(), dataSize - executable.bssSize);
Logger::EmulationContext.Flush();
return {base, size, executableBase + executable.text.offset};
}
Loader::SymbolInfo Loader::ResolveSymbol(void *ptr) {
@ -69,6 +125,8 @@ namespace skyline::loader {
} else {
return {.executableName = executable->name};
}
} else if (ptr >= executable->hookStart) {
return {.executableName = executable->hookName};
} else {
return {.executableName = executable->patchName};
}

View File

@ -54,12 +54,14 @@ namespace skyline::loader {
*/
struct ExecutableSymbolicInfo {
void *patchStart; //!< A pointer to the start of this executable's patch section
void *hookStart; //!< A pointer to the start of this executable's hook section
void *programStart; //!< A pointer to the start of the executable
void *programEnd; //!< A pointer to the end of the executable
std::string name; //!< The name of the executable
std::string patchName; //!< The name of the patch section
span<Elf64_Sym> symbols; //!< A span over the .dynsym section
span<char> symbolStrings; //!< A span over the .dynstr section
std::string hookName; //!< The name of the hook section
std::vector<Elf64_Sym> symbols; //!< A span over the .dynsym section
std::vector<char> symbolStrings; //!< A span over the .dynstr section
};
std::vector<ExecutableSymbolicInfo> executables;
@ -80,7 +82,7 @@ namespace skyline::loader {
* @param name An optional name for the executable, used for symbol resolution
* @return An ExecutableLoadInfo struct containing the load base and size
*/
ExecutableLoadInfo LoadExecutable(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, Executable &executable, size_t offset = 0, const std::string &name = {});
ExecutableLoadInfo LoadExecutable(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, Executable &executable, size_t offset = 0, const std::string &name = {}, bool dynamicallyLinked = false);
std::optional<vfs::NACP> nacp;
std::shared_ptr<vfs::Backing> romFs;

View File

@ -36,7 +36,7 @@ namespace skyline::loader {
else
continue;
loadInfo = NsoLoader::LoadNso(loader, nsoFile, process, state, offset, nso + std::string(".nso"));
loadInfo = NsoLoader::LoadNso(loader, nsoFile, process, state, offset, nso + std::string(".nso"), true);
Logger::Info("Loaded '{}.nso' at 0x{:X} (.text @ 0x{:X})", nso, base + offset, loadInfo.entry);
offset += loadInfo.size;
}

View File

@ -29,7 +29,7 @@ namespace skyline::loader {
return outputBuffer;
}
Loader::ExecutableLoadInfo NsoLoader::LoadNso(Loader *loader, const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, size_t offset, const std::string &name) {
Loader::ExecutableLoadInfo NsoLoader::LoadNso(Loader *loader, const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, size_t offset, const std::string &name, bool dynamicallyLinked) {
auto header{backing->Read<NsoHeader>()};
if (header.magic != util::MakeMagic<u32>("NSO0"))
@ -56,7 +56,7 @@ namespace skyline::loader {
executable.dynstr = {header.dynstr.offset, header.dynstr.size};
}
return loader->LoadExecutable(process, state, executable, offset, name);
return loader->LoadExecutable(process, state, executable, offset, name, dynamicallyLinked);
}
void *NsoLoader::LoadProcessData(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state) {

View File

@ -87,7 +87,7 @@ namespace skyline::loader {
* @param name An optional name for the NSO, used for symbol resolution
* @return An ExecutableLoadInfo struct containing the load base and size
*/
static ExecutableLoadInfo LoadNso(Loader *loader, const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, size_t offset = 0, const std::string &name = {});
static ExecutableLoadInfo LoadNso(Loader *loader, const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, size_t offset = 0, const std::string &name = {}, bool dynamicallyLinked = false);
void *LoadProcessData(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state) override;
};

View File

@ -92,6 +92,67 @@ namespace skyline::nce {
TRACE_EVENT_BEGIN("guest", "Guest");
}
void NCE::HookHandler(HookId hookId, ThreadContext *ctx) {
const auto &state{*ctx->state};
auto hookedSymbol{state.nce->hookedSymbols[hookId.index]};
try {
std::visit(VariantVisitor{
[&](const hle::OverrideHook &hook) {
TRACE_EVENT("hook", nullptr, [&](perfetto::EventContext ctx) {
ctx.event()->set_name(hookedSymbol.prettyName);
});
hook(state, hookedSymbol);
},
[&](const hle::EntryExitHook &hook) {
if (!hookId.isExit) {
TRACE_EVENT_BEGIN("hook", nullptr, [&](perfetto::EventContext ctx) {
ctx.event()->set_name(hookedSymbol.prettyName);
});
hook.entry(state, hookedSymbol);
} else {
hook.exit(state, hookedSymbol);
TRACE_EVENT_END("hook");
}
},
}, hookedSymbol.hook);
while (kernel::Scheduler::YieldPending) [[unlikely]] {
state.scheduler->Rotate(false);
kernel::Scheduler::YieldPending = false;
state.scheduler->WaitSchedule();
}
} catch (const signal::SignalException &e) {
if (e.signal != SIGINT) {
Logger::ErrorNoPrefix("{} (Hook: {})\nStack Trace:{}", e.what(), hookedSymbol.prettyName, state.loader->GetStackTrace(e.frames));
Logger::EmulationContext.Flush();
if (state.thread->id) {
signal::BlockSignal({SIGINT});
state.process->Kill(false);
}
} else {
Logger::EmulationContext.Flush();
}
abi::__cxa_end_catch();
std::longjmp(state.thread->originalCtx, true);
} catch (const exception &e) {
Logger::ErrorNoPrefix("{}\nStack Trace:{}", e.what(), state.loader->GetStackTrace(e.frames));
Logger::EmulationContext.Flush();
if (state.thread->id) {
signal::BlockSignal({SIGINT});
state.process->Kill(false);
}
abi::__cxa_end_catch();
std::longjmp(state.thread->originalCtx, true);
} catch (const std::exception &e) {
Logger::ErrorNoPrefix("{} (Hook: {})\nStack Trace:{}", e.what(), hookedSymbol.prettyName, state.loader->GetStackTrace());
Logger::EmulationContext.Flush();
}
}
void NCE::SignalHandler(int signal, siginfo *info, ucontext *ctx, void **tls) {
if (*tls) { // If TLS was restored then this occurred in guest code
auto &mctx{ctx->uc_mcontext};
@ -195,7 +256,47 @@ namespace skyline::nce {
staticNce = nullptr;
}
constexpr u8 MainSvcTrampolineSize{17}; // Size of the main SVC trampoline function in u32 units
constexpr size_t TrampolineSize{17}; // Size of the main SVC trampoline function in u32 units
/**
* @brief Writes a trampoline to the given target address that saves the current context and calls the given function
*/
u32 *WriteTrampoline(u32 *code, u64 target) {
/* Hook Trampoline */
/* Store LR in 16B of pre-allocated stack */
*code++ = 0xF90007FE; // STR LR, [SP, #8]
/* Replace Skyline TLS with host TLS */
*code++ = 0xD53BD041; // MRS X1, TPIDR_EL0
*code++ = 0xF9415022; // LDR X2, [X1, #0x2A0] (ThreadContext::hostTpidrEl0)
*code++ = 0xD51BD042; // MSR TPIDR_EL0, X2
/* Replace guest stack with host stack */
*code++ = 0x910003E2; // MOV X2, SP
*code++ = 0xF9415423; // LDR X3, [X1, #0x2A8] (ThreadContext::hostSp)
*code++ = 0x9100007F; // MOV SP, X3
/* Store Skyline TLS + guest SP on stack */
*code++ = 0xA9BF0BE1; // STP X1, X2, [SP, #-16]!
/* Jump to SvcHandler */
for (const auto &mov : instructions::MoveRegister(registers::X2, target))
if (mov)
*code++ = mov;
*code++ = 0xD63F0040; // BLR X2
/* Restore Skyline TLS + guest SP */
*code++ = 0xA8C10BE1; // LDP X1, X2, [SP], #16
*code++ = 0xD51BD041; // MSR TPIDR_EL0, X1
*code++ = 0x9100005F; // MOV SP, X2
/* Restore LR and Return */
*code++ = 0xF94007FE; // LDR LR, [SP, #8]
*code++ = 0xD65F03C0; // RET
return code;
}
constexpr u32 TpidrEl0{0x5E82}; // ID of TPIDR_EL0 in MRS
constexpr u32 TpidrroEl0{0x5E83}; // ID of TPIDRRO_EL0 in MRS
constexpr u32 CntfrqEl0{0x5F00}; // ID of CNTFRQ_EL0 in MRS
@ -204,7 +305,7 @@ namespace skyline::nce {
constexpr u32 TegraX1Freq{19200000}; // The clock frequency of the Tegra X1 (19.2 MHz)
NCE::PatchData NCE::GetPatchData(const std::vector<u8> &text) {
size_t size{guest::SaveCtxSize + guest::LoadCtxSize + MainSvcTrampolineSize};
size_t size{guest::SaveCtxSize + guest::LoadCtxSize + TrampolineSize};
std::vector<size_t> offsets;
u64 frequency;
@ -246,46 +347,14 @@ namespace skyline::nce {
return {util::AlignUp(size * sizeof(u32), constant::PageSize), offsets};
}
void NCE::PatchCode(std::vector<u8> &text, u32 *patch, size_t patchSize, const std::vector<size_t> &offsets) {
void NCE::PatchCode(std::vector<u8> &text, u32 *patch, size_t patchSize, const std::vector<size_t> &offsets, size_t textOffset) {
u32 *start{patch};
u32 *end{patch + (patchSize / sizeof(u32))};
std::memcpy(patch, reinterpret_cast<void *>(&guest::SaveCtx), guest::SaveCtxSize * sizeof(u32));
patch += guest::SaveCtxSize;
{
/* Main SVC Trampoline */
/* Store LR in 16B of pre-allocated stack */
*patch++ = 0xF90007FE; // STR LR, [SP, #8]
/* Replace Skyline TLS with host TLS */
*patch++ = 0xD53BD041; // MRS X1, TPIDR_EL0
*patch++ = 0xF9415022; // LDR X2, [X1, #0x2A0] (ThreadContext::hostTpidrEl0)
*patch++ = 0xD51BD042; // MSR TPIDR_EL0, X2
/* Replace guest stack with host stack */
*patch++ = 0x910003E2; // MOV X2, SP
*patch++ = 0xF9415423; // LDR X3, [X1, #0x2A8] (ThreadContext::hostSp)
*patch++ = 0x9100007F; // MOV SP, X3
/* Store Skyline TLS + guest SP on stack */
*patch++ = 0xA9BF0BE1; // STP X1, X2, [SP, #-16]!
/* Jump to SvcHandler */
for (const auto &mov : instructions::MoveRegister(registers::X2, reinterpret_cast<u64>(&NCE::SvcHandler)))
if (mov)
*patch++ = mov;
*patch++ = 0xD63F0040; // BLR X2
/* Restore Skyline TLS + guest SP */
*patch++ = 0xA8C10BE1; // LDP X1, X2, [SP], #16
*patch++ = 0xD51BD041; // MSR TPIDR_EL0, X1
*patch++ = 0x9100005F; // MOV SP, X2
/* Restore LR and Return */
*patch++ = 0xF94007FE; // LDR LR, [SP, #8]
*patch++ = 0xD65F03C0; // RET
}
patch = WriteTrampoline(patch, reinterpret_cast<u64>(&NCE::SvcHandler));
std::memcpy(patch, reinterpret_cast<void *>(&guest::LoadCtx), guest::LoadCtxSize * sizeof(u32));
patch += guest::LoadCtxSize;
@ -299,7 +368,7 @@ namespace skyline::nce {
auto svc{*reinterpret_cast<instructions::Svc *>(instruction)};
auto mrs{*reinterpret_cast<instructions::Mrs *>(instruction)};
auto msr{*reinterpret_cast<instructions::Msr *>(instruction)};
auto endOffset{[&] { return static_cast<size_t>(end - patch); }};
auto endOffset{[&] { return static_cast<size_t>(end - patch) + (textOffset / sizeof(u32)); }};
auto startOffset{[&] { return static_cast<size_t>(start - patch); }};
if (svc.Verify()) {
@ -318,7 +387,7 @@ namespace skyline::nce {
patch++;
/* Restore Context and Return */
*patch = instructions::BL(static_cast<i32>(startOffset() + guest::SaveCtxSize + MainSvcTrampolineSize)).raw;
*patch = instructions::BL(static_cast<i32>(startOffset() + guest::SaveCtxSize + TrampolineSize)).raw;
patch++;
*patch++ = 0xF84107FE; // LDR LR, [SP], #16
*patch = instructions::B(static_cast<i32>(endOffset() + offset + 1)).raw;
@ -406,6 +475,95 @@ namespace skyline::nce {
}
}
size_t NCE::GetHookSectionSize(span<HookedSymbolEntry> entries) {
size_t size{guest::SaveCtxSize + guest::LoadCtxSize + TrampolineSize};
for (const auto &entry : entries) {
constexpr size_t EmitTrampolineSize{10};
if (std::holds_alternative<hle::OverrideHook>(entry.hook))
size += EmitTrampolineSize + 1;
else if (std::holds_alternative<hle::EntryExitHook>(entry.hook))
size += 4 + EmitTrampolineSize + 1 + EmitTrampolineSize + 4 + 1;
}
return size * sizeof(u32);
}
void NCE::WriteHookSection(span<HookedSymbolEntry> entries, span<u32> hookSection) {
u32 *start{hookSection.data()};
u32 *end{hookSection.end().base()};
u32 *hook{start};
std::memcpy(hook, reinterpret_cast<void *>(&guest::SaveCtx), guest::SaveCtxSize * sizeof(u32));
hook += guest::SaveCtxSize;
hook = WriteTrampoline(hook, reinterpret_cast<u64>(&NCE::HookHandler));
std::memcpy(hook, reinterpret_cast<void *>(&guest::LoadCtx), guest::LoadCtxSize * sizeof(u32));
hook += guest::LoadCtxSize;
u64 hookIndex{static_cast<u64>(hookedSymbols.size())};
hookedSymbols.reserve(entries.size());
for (const auto &entry : entries) {
auto startOffset{[&] { return static_cast<size_t>(start - hook); }};
auto endOffset{[&] { return static_cast<size_t>(end - hook); }};
auto emitTrampoline{[&](HookId hookId) {
/* Save Context */
*hook++ = 0xF81F0FFE; // STR LR, [SP, #-16]!
*hook = instructions::BL(static_cast<i32>(startOffset())).raw; // BL SaveCtx
hook++;
/* Jump to entry trampoline */
for (const auto &mov : instructions::MoveRegister(registers::X0, hookId.raw))
if (mov)
*hook++ = mov;
*hook = instructions::BL(static_cast<i32>(startOffset() + guest::SaveCtxSize)).raw; // BL HookTrampoline
hook++;
/* Restore Context */
*hook = instructions::BL(static_cast<i32>(startOffset() + guest::SaveCtxSize + TrampolineSize)).raw; // BL LoadCtx
hook++;
*hook++ = 0xF84107FE; // LDR LR, [SP], #16
}};
Elf64_Addr originalOffset{*entry.offset};
*entry.offset = -(endOffset() * sizeof(u32));
if (std::holds_alternative<hle::OverrideHook>(entry.hook)) {
/* Override Hook */
emitTrampoline(HookId{hookIndex, false});
} else if (std::holds_alternative<hle::EntryExitHook>(entry.hook)) {
/* TLS LR Store */
*hook++ = 0xA9BF07E0; // STP X0, X1, [SP, #-16]!
*hook++ = 0xD53BD040; // MRS X0, TPIDR_EL0
*hook++ = 0xF9415401; // LDR X1, [X0, #0x2A8] (ThreadContext::hostSp)
*hook++ = 0xF81F0C3E; // STR LR, [X1, #-16]!
*hook++ = 0xF9015401; // STR X1, [X0, #0x2A8] (ThreadContext::hostSp)
*hook++ = 0xA8C107E0; // LDP X0, X1, [SP], #16
/* Entry Hook */
emitTrampoline(HookId{hookIndex, false});
/* Function Proxy */
*hook++ = instructions::BL(static_cast<i32>(endOffset() + (originalOffset / sizeof(u32)))).raw;
/* Exit Hook */
emitTrampoline(HookId{hookIndex, true});
/* TLS LR Load */
*hook++ = 0xA9BF07E0; // STP X0, X1, [SP, #-16]!
*hook++ = 0xD53BD040; // MRS X0, TPIDR_EL0
*hook++ = 0xF9415401; // LDR X1, [X0, #0x2A8] (ThreadContext::hostSp)
*hook++ = 0xF841043E; // LDR LR, [X1], #16
*hook++ = 0xF9015401; // STR X1, [X0, #0x2A8] (ThreadContext::hostSp)
*hook++ = 0xA8C107E0; // LDP X0, X1, [SP], #16
}
*hook++ = 0xD65F03C0; // RET
hookedSymbols.emplace_back(entry);
hookIndex++;
}
}
NCE::CallbackEntry::CallbackEntry(TrapProtection protection, LockCallback lockCallback, TrapCallback readCallback, TrapCallback writeCallback) : protection{protection}, lockCallback{std::move(lockCallback)}, readCallback{std::move(readCallback)}, writeCallback{std::move(writeCallback)} {}
void NCE::ReprotectIntervals(const std::vector<TrapMap::Interval> &intervals, TrapProtection protection) {

View File

@ -4,7 +4,9 @@
#pragma once
#include <sys/wait.h>
#include <linux/elf.h>
#include "common.h"
#include "hle/symbol_hooks.h"
#include "common/interval_map.h"
namespace skyline::nce {
@ -15,6 +17,8 @@ namespace skyline::nce {
private:
const DeviceState &state;
std::vector<hle::HookedSymbol> hookedSymbols; //!< The list of symbols that are hooked, these have a specific ordering that is hardcoded into the hooked functions
/**
* @brief The level of protection that is required for a callback entry
*/
@ -48,6 +52,23 @@ namespace skyline::nce {
static void SvcHandler(u16 svcId, ThreadContext *ctx);
/**
* @brief A parameter packed into a 64-bit register denoting the hook which is being called
*/
struct HookId {
union {
struct {
u64 index : 63;
u64 isExit : 1;
};
u64 raw;
};
constexpr HookId(u64 index, bool isExit) : index{index}, isExit{isExit} {}
};
static void HookHandler(HookId hookId, ThreadContext *ctx);
public:
/**
* @brief An exception which causes the throwing thread to exit alongside all threads optionally
@ -89,8 +110,19 @@ namespace skyline::nce {
/**
* @brief Writes the .patch section and mutates the code accordingly
* @param patch A pointer to the .patch section which should be exactly patchSize in size and located before the .text section
* @param textOffset The offset of the .text section, this must be page-aligned
*/
static void PatchCode(std::vector<u8> &text, u32 *patch, size_t patchSize, const std::vector<size_t> &offsets);
static void PatchCode(std::vector<u8> &text, u32 *patch, size_t patchSize, const std::vector<size_t> &offsets, size_t textOffset = 0);
struct HookedSymbolEntry : hle::HookedSymbol {
Elf64_Addr* offset{}; //!< A pointer to the hooked function's offset (st_value) in the ELF's dynsym, this is set by the loader and is used to resolve/update the address of the function
HookedSymbolEntry(std::string name, const hle::HookType &hook, Elf64_Addr* offset) : HookedSymbol{std::move(name), hook}, offset{offset} {}
};
static size_t GetHookSectionSize(span<HookedSymbolEntry> entries);
void WriteHookSection(span<HookedSymbolEntry> entries, span<u32> hookSection);
/**
* @brief An opaque handle to a group of trapped region