From b69905525e5fb8592473751d122287b1b0a74e43 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Sun, 5 Jan 2025 15:14:26 -0500 Subject: [PATCH] Implement function hooking for functions replaced by mods --- N64Recomp | 2 +- librecomp/CMakeLists.txt | 1 + librecomp/include/librecomp/mods.hpp | 42 +++++++- librecomp/src/mod_hooks.cpp | 51 +++++++++ librecomp/src/mod_manifest.cpp | 2 + librecomp/src/mods.cpp | 151 ++++++++++++++++++++++++--- librecomp/src/recomp.cpp | 8 +- 7 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 librecomp/src/mod_hooks.cpp diff --git a/N64Recomp b/N64Recomp index fc69604..58b2f86 160000 --- a/N64Recomp +++ b/N64Recomp @@ -1 +1 @@ -Subproject commit fc696046da3e703450559154d9370ca74c197f8b +Subproject commit 58b2f8698fb7c9adef4b8cd18ef50154c3ac416b diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index c12c651..9f78d4f 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/math_routines.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/mods.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_events.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 48d22a7..65e5c85 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -28,6 +28,30 @@ namespace N64Recomp { struct LiveGeneratorOutput; }; +namespace recomp { + namespace mods { + struct HookDefinition { + uint32_t section_rom; + uint32_t function_vram; + bool at_return; + bool operator==(const HookDefinition& rhs) const = default; + }; + } +} + +template <> +struct std::hash +{ + std::size_t operator()(const recomp::mods::HookDefinition& def) const { + // This hash packing only works if the resulting value is 64 bits. + static_assert(sizeof(std::size_t) == 8); + // Combine the three values into a single 64-bit value. + // The lower 2 bits of a function address will always be zero, so pack + // the value of at_return into the lowest bit. + return (size_t(def.section_rom) << 32) | size_t(def.function_vram) | size_t(def.at_return ? 1 : 0); + } +}; + namespace recomp { namespace mods { enum class ModOpenError { @@ -81,6 +105,7 @@ namespace recomp { BaseRecompConflict, ModConflict, DuplicateExport, + OfflineModHooked, NoSpecifiedApiVersion, UnsupportedApiVersion, }; @@ -231,7 +256,8 @@ namespace recomp { ModOpenError open_mod(const std::filesystem::path& mod_path, std::string& error_param, const std::vector& supported_content_types, bool requires_manifest); ModLoadError load_mod(recomp::mods::ModHandle& mod, std::string& error_param); void check_dependencies(recomp::mods::ModHandle& mod, std::vector>& errors); - CodeModLoadError load_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, recomp::mods::ModHandle& mod, int32_t load_address, uint32_t& ram_used, std::string& error_param); + CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, recomp::mods::ModHandle& mod, int32_t load_address, uint32_t& ram_used, std::string& error_param); + CodeModLoadError load_mod_code(uint8_t* rdram, recomp::mods::ModHandle& mod, uint32_t base_event_index, std::string& error_param); CodeModLoadError resolve_code_dependencies(recomp::mods::ModHandle& mod, const std::unordered_set base_patched_funcs, std::string& error_param); void add_opened_mod(ModManifest&& manifest, std::vector&& game_indices, std::vector&& detected_content_types); void close_mods(); @@ -249,6 +275,11 @@ namespace recomp { std::unordered_map patched_funcs; std::unordered_map loaded_mods_by_id; std::vector loaded_code_mods; + // Map of hook definition to the entry hook slot's index. + std::unordered_map hook_slots; + // Tracks which hook slots have already been processed. Used to regenerate vanilla functions as needed + // to add hooks to any functions that weren't already replaced by a mod. + std::vector processed_hook_slots; size_t num_events = 0; ModContentTypeId code_content_type_id; size_t active_game = (size_t)-1; @@ -368,7 +399,8 @@ namespace recomp { class LiveRecompilerCodeHandle : public ModCodeHandle { public: - LiveRecompilerCodeHandle(const N64Recomp::Context& context, const ModCodeHandleInputs& inputs); + LiveRecompilerCodeHandle(const N64Recomp::Context& context, const ModCodeHandleInputs& inputs, + std::unordered_map&& entry_func_hooks, std::unordered_map&& return_func_hooks); ~LiveRecompilerCodeHandle() = default; @@ -398,6 +430,12 @@ namespace recomp { void setup_events(size_t num_events); void register_event_callback(size_t event_index, GenericFunction callback); void reset_events(); + + void setup_hooks(size_t num_hook_slots); + void register_hook(size_t hook_slot_index, GenericFunction callback); + void reset_hooks(); + void run_hook(uint8_t* rdram, recomp_context* ctx, size_t hook_slot_index); + CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param); void initialize_mod_recompiler(); diff --git a/librecomp/src/mod_hooks.cpp b/librecomp/src/mod_hooks.cpp new file mode 100644 index 0000000..768d02a --- /dev/null +++ b/librecomp/src/mod_hooks.cpp @@ -0,0 +1,51 @@ +#include +#include "librecomp/mods.hpp" +#include "librecomp/overlays.hpp" +#include "ultramodern/error_handling.hpp" + +template +struct overloaded : Ts... { using Ts::operator()...; }; +template +overloaded(Ts...) -> overloaded; + +// Vector of individual hooks for each hook slot. +std::vector> hook_table{}; + +void recomp::mods::run_hook(uint8_t* rdram, recomp_context* ctx, size_t hook_slot_index) { + // Sanity check the hook slot index. + if (hook_slot_index >= hook_table.size()) { + printf("Hook slot %zu triggered, but only %zu hook slots have been registered!\n", hook_slot_index, hook_table.size()); + assert(false); + ultramodern::error_handling::message_box("Encountered an error with loaded mods: hook slot out of bounds"); + ULTRAMODERN_QUICK_EXIT(); + } + + // Copy the initial context state to restore it after running each callback. + recomp_context initial_context = *ctx; + + // Call every hook attached to the hook slot. + const std::vector& hooks = hook_table[hook_slot_index]; + for (recomp::mods::GenericFunction func : hooks) { + // Run the hook. + std::visit(overloaded { + [rdram, ctx](recomp_func_t* native_func) { + native_func(rdram, ctx); + }, + }, func); + + // Restore the original context. + *ctx = initial_context; + } +} + +void recomp::mods::setup_hooks(size_t num_hook_slots) { + hook_table.resize(num_hook_slots); +} + +void recomp::mods::register_hook(size_t hook_slot_index, GenericFunction callback) { + hook_table[hook_slot_index].emplace_back(callback); +} + +void recomp::mods::reset_hooks() { + hook_table.clear(); +} diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 83b7133..aec8260 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -589,6 +589,8 @@ std::string recomp::mods::error_to_string(CodeModLoadError error) { return "Conflicts with other mod"; case CodeModLoadError::DuplicateExport: return "Duplicate exports in mod"; + case CodeModLoadError::OfflineModHooked: + return "Offline recompiled mod has a function hooked by another mod"; case CodeModLoadError::NoSpecifiedApiVersion: return "Mod DLL does not specify an API version"; case CodeModLoadError::UnsupportedApiVersion: diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 21c7fb1..61d544e 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -418,7 +418,10 @@ recomp::mods::CodeModLoadError recomp::mods::DynamicLibraryCodeHandle::populate_ return CodeModLoadError::Good; } -recomp::mods::LiveRecompilerCodeHandle::LiveRecompilerCodeHandle(const N64Recomp::Context& context, const ModCodeHandleInputs& inputs) { +recomp::mods::LiveRecompilerCodeHandle::LiveRecompilerCodeHandle( + const N64Recomp::Context& context, const ModCodeHandleInputs& inputs, + std::unordered_map&& entry_func_hooks, std::unordered_map&& return_func_hooks) +{ section_addresses = std::make_unique(context.sections.size()); base_event_index = inputs.base_event_index; @@ -434,6 +437,9 @@ recomp::mods::LiveRecompilerCodeHandle::LiveRecompilerCodeHandle(const N64Recomp .trigger_event = inputs.recomp_trigger_event, .reference_section_addresses = inputs.reference_section_addresses, .local_section_addresses = section_addresses.get(), + .run_hook = run_hook, + .entry_func_hooks = std::move(entry_func_hooks), + .return_func_hooks = std::move(return_func_hooks) }; N64Recomp::LiveGenerator generator{ context.functions.size(), recompiler_inputs }; @@ -815,12 +821,15 @@ std::vector recomp::mods::ModContext::load_mo return ret; } - // Load the code and exports from all mods. + std::vector base_event_indices; + base_event_indices.resize(opened_mods.size()); + + // Parse the code mods and load their binary data. for (size_t mod_index : loaded_code_mods) { uint32_t cur_ram_used = 0; auto& mod = opened_mods[mod_index]; std::string cur_error_param; - CodeModLoadError cur_error = load_mod_code(rdram, section_vrom_map, mod, load_address, cur_ram_used, cur_error_param); + CodeModLoadError cur_error = init_mod_code(rdram, section_vrom_map, mod, load_address, cur_ram_used, cur_error_param); if (cur_error != CodeModLoadError::Good) { if (cur_error_param.empty()) { ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error)); @@ -832,6 +841,7 @@ std::vector recomp::mods::ModContext::load_mo else { load_address += cur_ram_used; ram_used += cur_ram_used; + base_event_indices[mod_index] = static_cast(num_events); } } @@ -843,6 +853,34 @@ std::vector recomp::mods::ModContext::load_mo // Set up the event callbacks based on the number of events allocated. recomp::mods::setup_events(num_events); + + // Set up the hook slots based on the number of unique hooks. + recomp::mods::setup_hooks(hook_slots.size()); + + // Allocate room for tracking the processed hook slots. + processed_hook_slots.clear(); + processed_hook_slots.resize(hook_slots.size()); + + // Load the code and exports from all mods. + for (size_t mod_index : loaded_code_mods) { + auto& mod = opened_mods[mod_index]; + std::string cur_error_param; + CodeModLoadError cur_error = load_mod_code(rdram, mod, base_event_indices[mod_index], cur_error_param); + if (cur_error != CodeModLoadError::Good) { + if (cur_error_param.empty()) { + ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error)); + } + else { + ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error) + ":" + cur_error_param); + } + } + } + + // Exit early if errors were found. + if (!ret.empty()) { + unload_mods(); + return ret; + } // Resolve code dependencies for all mods. for (size_t mod_index : loaded_code_mods) { @@ -865,6 +903,9 @@ std::vector recomp::mods::ModContext::load_mo return ret; } + // Regenerate any remaining hook slots that weren't handled during mod recompilation. + // TODO + active_game = mod_game_index; return ret; } @@ -899,7 +940,7 @@ void recomp::mods::ModContext::check_dependencies(recomp::mods::ModHandle& mod, } } -recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, recomp::mods::ModHandle& mod, int32_t load_address, uint32_t& ram_used, std::string& error_param) { +recomp::mods::CodeModLoadError recomp::mods::ModContext::init_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, recomp::mods::ModHandle& mod, int32_t load_address, uint32_t& ram_used, std::string& error_param) { // Load the mod symbol data from the file provided in the manifest. bool binary_syms_exists = false; std::vector syms_data = mod.manifest.file_handle->read_file(std::string{ modpaths::binary_syms_path }, binary_syms_exists); @@ -998,10 +1039,69 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* ram_used = cur_section_addr - load_address; + // Allocate the event indices used by the mod. + num_events += mod.num_events(); + + // Read the mod's hooks and allocate hook slots as needed. + for (const N64Recomp::FunctionHook& hook : mod.recompiler_context->hooks) { + // Get the definition of this hook. + HookDefinition def { + .section_rom = hook.original_section_vrom, + .function_vram = hook.original_vram, + .at_return = (hook.flags & N64Recomp::HookFlags::AtReturn) == N64Recomp::HookFlags::AtReturn + }; + // Check if the hook definition already exists in the hook slots. + auto find_it = hook_slots.find(def); + if (find_it == hook_slots.end()) { + // The hook definition is new, so assign a hook slot index and add it to the slots. + hook_slots.emplace(def, hook_slots.size()); + } + } + + // Copy the mod's binary into the recompiler context so it can be analyzed during code loading. + // TODO move it instead, right now the move can't be done because of a signedness difference in the types. + mod.recompiler_context->rom.assign(binary_span.begin(), binary_span.end()); + + return CodeModLoadError::Good; +} + +recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* rdram, recomp::mods::ModHandle& mod, uint32_t base_event_index, std::string& error_param) { + // Build the hook list for this mod. Maps function index within mod to hook slot index. + std::unordered_map entry_func_hooks{}; + std::unordered_map return_func_hooks{}; + + // Scan the replacements and check for any + for (const auto& replacement : mod.recompiler_context->replacements) { + // Check if there's a hook slot for the entry of this function. + HookDefinition entry_def { + .section_rom = replacement.original_section_vrom, + .function_vram = replacement.original_vram, + .at_return = false + }; + auto find_entry_it = hook_slots.find(entry_def); + if (find_entry_it != hook_slots.end()) { + entry_func_hooks.emplace(replacement.func_index, find_entry_it->second); + processed_hook_slots[find_entry_it->second] = true; + } + + // Check if there's a hook slot for the return of this function. + HookDefinition return_def { + .section_rom = replacement.original_section_vrom, + .function_vram = replacement.original_vram, + .at_return = true + }; + auto find_return_it = hook_slots.find(return_def); + if (find_return_it != hook_slots.end()) { + return_func_hooks.emplace(replacement.func_index, find_return_it->second); + processed_hook_slots[find_return_it->second] = true; + } + } + + // Build the inputs for the mod code handle. std::string cur_error_param; CodeModLoadError cur_error; ModCodeHandleInputs handle_inputs{ - .base_event_index = static_cast(num_events), + .base_event_index = base_event_index, .recomp_trigger_event = recomp_trigger_event, .get_function = get_function, .cop0_status_write = cop0_status_write, @@ -1011,17 +1111,15 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* .reference_section_addresses = section_addresses, }; - // Allocate the event indices used by the mod. - num_events += mod.num_events(); - - // Copy the mod's binary into the recompiler context so it can be analyzed during code loading. - // TODO move it instead, right now the move can't be done because of a signedness difference in the types. - mod.recompiler_context->rom.assign(binary_span.begin(), binary_span.end()); - // Use a dynamic library code handle. This feature isn't meant to be used by end users, but provides a more debuggable // experience than the live recompiler for mod developers. // Enabled if the mod's filename ends with ".offline.dll". if (mod.manifest.mod_root_path.filename().string().ends_with(".offline.nrm")) { + // Hooks can't be generated for native mods, so return an error if any of the functions this mod replaces are also hooked by another mod. + if (!entry_func_hooks.empty() || !return_func_hooks.empty()) { + return CodeModLoadError::OfflineModHooked; + } + std::filesystem::path dll_path = mod.manifest.mod_root_path; dll_path.replace_extension(DynamicLibrary::PlatformExtension); mod.code_handle = std::make_unique(dll_path, *mod.recompiler_context, handle_inputs); @@ -1045,7 +1143,7 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* } // Live recompiler code handle. else { - mod.code_handle = std::make_unique(*mod.recompiler_context, handle_inputs); + mod.code_handle = std::make_unique(*mod.recompiler_context, handle_inputs, std::move(entry_func_hooks), std::move(return_func_hooks)); if (!mod.code_handle->good()) { mod.code_handle.reset(); @@ -1064,6 +1162,8 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* } } + const std::vector& mod_sections = mod.recompiler_context->sections; + // Add each function from the mod into the function lookup table. for (size_t func_index = 0; func_index < mod.recompiler_context->functions.size(); func_index++) { const auto& func = mod.recompiler_context->functions[func_index]; @@ -1176,6 +1276,28 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::resolve_code_dependenci recomp::mods::register_event_callback(event_index, func); } + // Register hooks. + for (const auto& cur_hook : mod.recompiler_context->hooks) { + // Get the definition of this hook. + HookDefinition def { + .section_rom = cur_hook.original_section_vrom, + .function_vram = cur_hook.original_vram, + .at_return = (cur_hook.flags & N64Recomp::HookFlags::AtReturn) == N64Recomp::HookFlags::AtReturn + }; + + // Find the hook's slot from the definition. + auto find_it = hook_slots.find(def); + if (find_it == hook_slots.end()) { + error_param = "Failed to register hook"; + // This should never happen, as hooks are scanned earlier to generate hook_slots. + return CodeModLoadError::InternalError; + } + + // Register the function handle for this hook slot. + GenericFunction func = mod.code_handle->get_function_handle(cur_hook.func_index); + recomp::mods::register_hook(find_it->second, func); + } + // Populate the relocated section addresses for the mod. for (size_t section_index = 0; section_index < mod.section_load_addresses.size(); section_index++) { mod.code_handle->set_local_section_address(section_index, mod.section_load_addresses[section_index]); @@ -1232,7 +1354,10 @@ void recomp::mods::ModContext::unload_mods() { } patched_funcs.clear(); loaded_mods_by_id.clear(); + hook_slots.clear(); + processed_hook_slots.clear(); recomp::mods::reset_events(); + recomp::mods::reset_hooks(); num_events = recomp::overlays::num_base_events(); active_game = (size_t)-1; } diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 17894e4..e18a74c 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -460,13 +460,13 @@ void init(uint8_t* rdram, recomp_context* ctx, gpr entrypoint) { // Initialize variables normally set by IPL3 constexpr int32_t osTvType = 0x80000300; - constexpr int32_t osRomType = 0x80000304; + //constexpr int32_t osRomType = 0x80000304; constexpr int32_t osRomBase = 0x80000308; constexpr int32_t osResetType = 0x8000030c; - constexpr int32_t osCicId = 0x80000310; - constexpr int32_t osVersion = 0x80000314; + //constexpr int32_t osCicId = 0x80000310; + //constexpr int32_t osVersion = 0x80000314; constexpr int32_t osMemSize = 0x80000318; - constexpr int32_t osAppNMIBuffer = 0x8000031c; + //constexpr int32_t osAppNMIBuffer = 0x8000031c; MEM_W(osTvType, 0) = 1; // NTSC MEM_W(osRomBase, 0) = 0xB0000000u; // standard rom base MEM_W(osResetType, 0) = 0; // cold reset