#pragma once #include "mods/svc/hook.h" #include #include #include #include #include #include namespace dusk::mods { template T arg(void* argsRaw, int n) noexcept { void** args = static_cast(argsRaw); return *static_cast > >(args[n]); } template std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { void** args = static_cast(argsRaw); return *static_cast > >(args[n]); } template void* mfp_addr(F fn) noexcept { void* p = nullptr; static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size"); std::memcpy(&p, &fn, sizeof(void*)); return p; } /* A string usable as a template argument: carries the hook target's symbol name and * makes each NamedHook instantiation's static state unique. */ template struct FixedString { char chars[N]{}; constexpr FixedString(const char (&s)[N]) noexcept { for (size_t i = 0; i < N; ++i) { chars[i] = s[i]; } } }; namespace detail { template constexpr std::string_view class_name() { #if defined(__clang__) || defined(__GNUC__) // "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]" constexpr std::string_view fn = __PRETTY_FUNCTION__; constexpr size_t start = fn.find("T = ") + 4; return fn.substr(start, fn.find_first_of(";]", start) - start); #elif defined(_MSC_VER) // "... class_name(void)" constexpr std::string_view fn = __FUNCSIG__; constexpr size_t start = fn.find("class_name<") + 11; constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start); if constexpr (name.starts_with("class ")) { return name.substr(6); } else if constexpr (name.starts_with("struct ")) { return name.substr(7); } else { return name; } #else #error "unsupported compiler" #endif } /* The manifest name of C's vtable. Only unscoped, non-template class names are * supported (an empty result fails resolution and the install reports it). */ template constexpr auto vtable_symbol() { constexpr std::string_view name = class_name(); constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos; // "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated std::array out{}; if constexpr (!simple) { return out; } size_t n = 0; #if defined(_WIN32) for (char c : {'?', '?', '_', '7'}) { out[n++] = c; } for (char c : name) { out[n++] = c; } for (char c : {'@', '@', '6', 'B', '@'}) { out[n++] = c; } #else for (char c : {'_', 'Z', 'T', 'V'}) { out[n++] = c; } size_t len = name.size(); char digits[8]{}; size_t d = 0; while (len != 0) { digits[d++] = static_cast('0' + len % 10); len /= 10; } while (d != 0) { out[n++] = digits[--d]; } for (char c : name) { out[n++] = c; } #endif return out; } #if defined(_WIN32) /* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at. * Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */ inline size_t vcall_slot_offset(const void*& fn) noexcept { constexpr size_t npos = static_cast(-1); #if defined(_M_X64) || defined(__x86_64__) const auto* p = static_cast(fn); for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs int32_t rel; std::memcpy(&rel, p + 1, 4); p += 5 + rel; } fn = p; // The vptr load. Unoptimized clang-cl thunks spill/reload rcx first // (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window. const uint8_t* q = nullptr; for (int i = 0; i <= 12; ++i) { if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx] q = p + i + 3; break; } } if (q == nullptr) { return npos; } if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC) return 0; } if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8] return static_cast(q[2]); } if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32] int32_t off; std::memcpy(&off, q + 2, 4); return off; } // clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax // distinguishes the thunk from an ordinary getter that begins the same way. if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) { size_t off = 0; const uint8_t* r = q + 3; if (q[2] == 0x40) { off = static_cast(q[3]); r = q + 4; } else if (q[2] == 0x80) { int32_t off32; std::memcpy(&off32, q + 3, 4); off = off32; r = q + 7; } for (int i = 0; i <= 8; ++i) { if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional) return off; } } } return npos; #elif defined(_M_ARM64) || defined(__aarch64__) const auto* p = static_cast(fn); uint32_t insn[3]; for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs std::memcpy(insn, p, 4); if ((insn[0] & 0xFC000000u) != 0x14000000u) { break; } const auto imm26 = static_cast(insn[0] << 6) >> 6; p += static_cast(imm26) * 4; } fn = p; std::memcpy(insn, p, 12); // ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) { return npos; } const uint32_t t = insn[0] & 0x1Fu; if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) { return npos; } const uint32_t s = insn[1] & 0x1Fu; if (insn[2] != (0xD61F0000u | (s << 5))) { return npos; } return ((insn[1] >> 10) & 0xFFFu) * 8; #else (void)fn; return npos; #endif } #endif /* Code address of the member function a mfp designates. Virtual mfps don't carry * one; recover it from the class's vtable (resolved from the symbol manifest), so * Hook works uniformly on virtual and non-virtual members. */ template ModResult member_target(const HookService* hooks, F mfp, void** out) { *out = nullptr; uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {}; std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F)); #if defined(_WIN32) const void* fn = reinterpret_cast(words[0]); const size_t slot = vcall_slot_offset(fn); if (slot == static_cast(-1)) { // not a vcall thunk: direct address *out = const_cast(fn); return MOD_OK; } void* vtable = nullptr; const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); if (resolved != MOD_OK) { return resolved; } // ??_7 points at the first slot. *out = *reinterpret_cast(static_cast(vtable) + slot); #else #if defined(__aarch64__) || defined(__arm__) // AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function // addresses can't spare their low bit), and ptr holds the slot offset directly. const bool isVirtual = (words[1] & 1) != 0; const uintptr_t thisAdjust = words[1] >> 1; const uintptr_t slotOffset = words[0]; #else // Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1. const bool isVirtual = (words[0] & 1) != 0; const uintptr_t thisAdjust = words[1]; const uintptr_t slotOffset = words[0] - 1; #endif if (!isVirtual) { // non-virtual: the address itself *out = reinterpret_cast(words[0]); return MOD_OK; } if (thisAdjust != 0) { // this-adjusting mfp (member of a secondary base): the slot offset is // relative to a vtable we can't locate. Hook the overrider by name instead. return MOD_UNSUPPORTED; } void* vtable = nullptr; const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); if (resolved != MOD_OK) { return resolved; } // _ZTV points at the offset-to-top slot; the address point mfps index from is // two pointers in (past offset-to-top and the typeinfo pointer). *out = *reinterpret_cast(static_cast(vtable) + 2 * sizeof(void*) + slotOffset); #endif return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE; } } // namespace detail /* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes * each hooked target's statics distinct; the target address is filled in at install. */ template struct HookImpl { static inline R (*g_orig)(A...) = nullptr; static inline const HookService* hooks = nullptr; static inline void* target = nullptr; static bool dispatch_pre(void* args, void* retval) { if (hooks == nullptr) { return false; } int skipOriginal = 0; const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal); return result == MOD_OK && skipOriginal != 0; } static void dispatch_post(void* args, void* retval) { if (hooks != nullptr) { hooks->dispatch_post(mod_ctx, target, args, retval); } } static R trampoline(A... args) { if constexpr (sizeof...(A) == 0) { if constexpr (std::is_void_v) { const bool skipOriginal = dispatch_pre(nullptr, nullptr); if (!skipOriginal) { g_orig(args...); } dispatch_post(nullptr, nullptr); } else { R result{}; const bool skipOriginal = dispatch_pre(nullptr, static_cast(std::addressof(result))); if (!skipOriginal) { result = g_orig(args...); } dispatch_post(nullptr, static_cast(std::addressof(result))); return result; } } else { void* ptrs[] = {static_cast(std::addressof(args))...}; if constexpr (std::is_void_v) { const bool skipOriginal = dispatch_pre(static_cast(ptrs), nullptr); if (!skipOriginal) { g_orig(args...); } dispatch_post(static_cast(ptrs), nullptr); } else { R result{}; const bool skipOriginal = dispatch_pre( static_cast(ptrs), static_cast(std::addressof(result))); if (!skipOriginal) { result = g_orig(args...); } dispatch_post(static_cast(ptrs), static_cast(std::addressof(result))); return result; } } } }; namespace detail { template using TargetTag = std::integral_constant; template struct NameTag {}; } // namespace detail /* * Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn). * Member functions may be virtual: the install decodes the member function pointer and hooks the * class's own overrider. */ template struct Hook; template struct Hook : HookImpl, R, C*, A...> { static ModResult resolve_target(const HookService* hooks, void** out) { return detail::member_target(hooks, Target, out); } }; template struct Hook : HookImpl, R, const C*, A...> { static ModResult resolve_target(const HookService* hooks, void** out) { return detail::member_target(hooks, Target, out); } }; template struct Hook : HookImpl, R, A...> { static ModResult resolve_target(const HookService*, void** out) { *out = mfp_addr(Target); return MOD_OK; } }; /* * Typed hook on a function by its symbol name, for targets you can't name in C++: file-local * statics, private members, or symbols without a header. The signature is written free-style with * the receiver first and is *not* compiler-checked. * * using HookshotHit = dusk::mods::NamedHook< * "daAlink_hookshotAtHitCallBack", * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>; * dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit); */ template struct NamedHook; template struct NamedHook : HookImpl, R, A...> { static ModResult resolve_target(const HookService* hooks, void** out) { HookSymbolFlags flags{}; const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags); if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) { *out = nullptr; return MOD_INVALID_ARGUMENT; } return resolved; } }; template ModResult hook_install(const HookService* hooks) { if (hooks == nullptr) { return MOD_UNAVAILABLE; } Entry::hooks = hooks; if (Entry::target == nullptr) { const ModResult resolved = Entry::resolve_target(hooks, &Entry::target); if (resolved != MOD_OK) { return resolved; } } return hooks->install(mod_ctx, Entry::target, reinterpret_cast(Entry::trampoline), reinterpret_cast(&Entry::g_orig)); } template ModResult hook_install(const HookService* hooks) { return hook_install >(hooks); } template ModResult hook_add_pre( const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { const ModResult installed = hook_install(hooks); if (installed != MOD_OK) { return installed; } return hooks->add_pre(mod_ctx, Entry::target, callback, options); } template ModResult hook_add_pre( const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { return hook_add_pre >(hooks, callback, options); } template ModResult hook_add_post( const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { const ModResult installed = hook_install(hooks); if (installed != MOD_OK) { return installed; } return hooks->add_post(mod_ctx, Entry::target, callback, options); } template ModResult hook_add_post( const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { return hook_add_post >(hooks, callback, options); } template ModResult hook_replace( const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { const ModResult installed = hook_install(hooks); if (installed != MOD_OK) { return installed; } return hooks->replace(mod_ctx, Entry::target, callback, options); } template ModResult hook_replace( const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { return hook_replace >(hooks, callback, options); } } // namespace dusk::mods