include/DetourModKit/detail/counted_entry.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_DETAIL_COUNTED_ENTRY_HPP | ||
| 2 | #define DETOURMODKIT_DETAIL_COUNTED_ENTRY_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file counted_entry.hpp | ||
| 6 | * @brief The counted thunk that @ref DetourModKit::hook::inline_at installs in front of a typed detour (`[B-106]`). | ||
| 7 | * @details The thunk counts every call into and out of the detour, so a retirement can prove the detour idle before | ||
| 8 | * the image unmaps. The install decodes each instantiated thunk and refuses a shape that the proof cannot | ||
| 9 | * cover. The counts therefore use compiler intrinsics and a C array, so they need no helper call at any | ||
| 10 | * optimization level. | ||
| 11 | */ | ||
| 12 | |||
| 13 | #include "DetourModKit/defines.hpp" | ||
| 14 | |||
| 15 | #include <cstddef> | ||
| 16 | #include <cstdint> | ||
| 17 | #include <type_traits> | ||
| 18 | |||
| 19 | #if defined(_MSC_VER) | ||
| 20 | #include <intrin.h> | ||
| 21 | #endif | ||
| 22 | |||
| 23 | namespace DetourModKit::hook::detail | ||
| 24 | { | ||
| 25 | #ifdef _MSC_VER | ||
| 26 | #pragma warning(push) | ||
| 27 | // The stripe carries its cache-line alignment, so the padding that C4324 reports is intended. | ||
| 28 | #pragma warning(disable : 4324) | ||
| 29 | #endif | ||
| 30 | |||
| 31 | /// The calls that entered and left a counted detour on the threads of one stripe. | ||
| 32 | struct alignas(64) CountedStripe | ||
| 33 | { | ||
| 34 | std::uint64_t enters; | ||
| 35 | std::uint64_t exits; | ||
| 36 | }; | ||
| 37 | |||
| 38 | #ifdef _MSC_VER | ||
| 39 | #pragma warning(pop) | ||
| 40 | #endif | ||
| 41 | |||
| 42 | /// The stripe count. The thread id in the TEB selects a stripe, so concurrent callers rarely share a cache line. | ||
| 43 | inline constexpr std::size_t COUNTED_STRIPES = 16; | ||
| 44 | static_assert((COUNTED_STRIPES & (COUNTED_STRIPES - 1)) == 0, "the stripe mask needs a power of two"); | ||
| 45 | |||
| 46 | /** | ||
| 47 | * @brief The counters of one counted detour. Constant initialization fills it, and it is trivially destructible | ||
| 48 | * (`[B-47]`), so it stays valid during static destruction. | ||
| 49 | */ | ||
| 50 | struct CountedBlock | ||
| 51 | { | ||
| 52 | // A C array, because std::array::operator[] is an out-of-line call in an unoptimized consumer build. | ||
| 53 | CountedStripe stripes[COUNTED_STRIPES]; | ||
| 54 | }; | ||
| 55 | |||
| 56 | static_assert(std::is_trivially_destructible_v<CountedBlock>, "the counters must outlive static destruction"); | ||
| 57 | |||
| 58 | /// The thunk and its counters, as @ref inline_at hands them to the install. | ||
| 59 | struct CountedInstall | ||
| 60 | { | ||
| 61 | void *entry = nullptr; | ||
| 62 | CountedBlock *block = nullptr; | ||
| 63 | }; | ||
| 64 | |||
| 65 | /** | ||
| 66 | * @brief True when the thunk forwards a parameter or result of type @p T unchanged. | ||
| 67 | * @details @p T is a reference or a trivially copyable object, and it is not @p Frame. The frame passes only by | ||
| 68 | * value in the first position, so a frame anywhere else reads a game argument as a stack slot. | ||
| 69 | */ | ||
| 70 | template <class T, class Frame> | ||
| 71 | inline constexpr bool counted_value_v = | ||
| 72 | (std::is_reference_v<T> || std::is_trivially_copyable_v<T>) && !std::is_same_v<std::remove_cvref_t<T>, Frame>; | ||
| 73 | |||
| 74 | #if defined(_MSC_VER) | ||
| 75 | #define DMK_DETAIL_COUNTED_THUNK __declspec(noinline) __declspec(safebuffers) __declspec(no_sanitize_address) | ||
| 76 | #define DMK_DETAIL_COUNTED_INCREMENT(counter) \ | ||
| 77 | _InterlockedIncrement64(reinterpret_cast<volatile long long *>(&(counter))) | ||
| 78 | #define DMK_DETAIL_COUNTED_THREAD_ID(name) const std::uint64_t name = __readgsqword(0x48) | ||
| 79 | #define DMK_DETAIL_COUNTED_RETURN_SLOT() static_cast<void **>(_AddressOfReturnAddress()) | ||
| 80 | #else | ||
| 81 | // Clang has no noipa, and it changes no signature of a function whose address escapes. A stack protector check is a | ||
| 82 | // conditional branch, which the install decode refuses, so the thunk opts out as safebuffers does under MSVC. | ||
| 83 | #if defined(__clang__) | ||
| 84 | #define DMK_DETAIL_COUNTED_THUNK [[gnu::noinline, gnu::no_sanitize_address, gnu::no_stack_protector]] | ||
| 85 | #else | ||
| 86 | #define DMK_DETAIL_COUNTED_THUNK [[gnu::noipa, gnu::no_sanitize_address, gnu::no_stack_protector]] | ||
| 87 | #endif | ||
| 88 | #define DMK_DETAIL_COUNTED_INCREMENT(counter) __atomic_add_fetch(&(counter), 1, __ATOMIC_SEQ_CST) | ||
| 89 | #define DMK_DETAIL_COUNTED_THREAD_ID(name) \ | ||
| 90 | std::uint64_t name; \ | ||
| 91 | __asm__("movq %%gs:0x48, %0" : "=r"(name)) | ||
| 92 | #define DMK_DETAIL_COUNTED_RETURN_SLOT() (static_cast<void **>(__builtin_dwarf_cfa()) - 1) | ||
| 93 | #endif | ||
| 94 | |||
| 95 | /** | ||
| 96 | * @brief The counted thunk and counters of one detour. | ||
| 97 | * @tparam Detour The detour. The thunk calls it through @ref detour, a volatile read, so the compiler cannot | ||
| 98 | * inline it and the call stays one instruction. | ||
| 99 | * @tparam Frame The frame type that the detour takes first, or void for a detour without one. | ||
| 100 | * @details The relay slot jumps to @ref entry, so the thunk runs with the stack of the game caller. Its | ||
| 101 | * parameters and stack arguments therefore match the hooked function. Each enter and exit is a seq_cst | ||
| 102 | * locked increment (`[B-43]`). | ||
| 103 | */ | ||
| 104 | template <auto Detour, class Frame, class Ret, class... Args> struct CountedEntry | ||
| 105 | { | ||
| 106 | /// The counters that the drain proof reads. | ||
| 107 | static constinit inline CountedBlock block{}; | ||
| 108 | /// The detour behind a volatile read. Constant initialization fills it. | ||
| 109 | static constinit inline decltype(Detour) volatile detour = Detour; | ||
| 110 | |||
| 111 | /// The thunk. Only the relay slot of the hook reaches it. | ||
| 112 | 4536 | DMK_DETAIL_COUNTED_THUNK static Ret entry(Args... args) noexcept | |
| 113 | { | ||
| 114 | 4536 | DMK_DETAIL_COUNTED_THREAD_ID(thread_id); | |
| 115 | 4536 | CountedStripe &stripe = block.stripes[(thread_id >> 2) & (COUNTED_STRIPES - 1)]; | |
| 116 | 4536 | DMK_DETAIL_COUNTED_INCREMENT(stripe.enters); | |
| 117 | if constexpr (std::is_void_v<Ret>) | ||
| 118 | { | ||
| 119 | if constexpr (std::is_void_v<Frame>) | ||
| 120 | { | ||
| 121 | 1 | detour(static_cast<Args &&>(args)...); | |
| 122 | } | ||
| 123 | else | ||
| 124 | { | ||
| 125 | detour(Frame{DMK_DETAIL_COUNTED_RETURN_SLOT()}, static_cast<Args &&>(args)...); | ||
| 126 | } | ||
| 127 | 1 | DMK_DETAIL_COUNTED_INCREMENT(stripe.exits); | |
| 128 | } | ||
| 129 | else | ||
| 130 | { | ||
| 131 | if constexpr (std::is_void_v<Frame>) | ||
| 132 | { | ||
| 133 | 4531 | Ret value = detour(static_cast<Args &&>(args)...); | |
| 134 | 4530 | DMK_DETAIL_COUNTED_INCREMENT(stripe.exits); | |
| 135 | 4530 | return value; | |
| 136 | } | ||
| 137 | else | ||
| 138 | { | ||
| 139 | 4 | Ret value = detour(Frame{DMK_DETAIL_COUNTED_RETURN_SLOT()}, static_cast<Args &&>(args)...); | |
| 140 | 4 | DMK_DETAIL_COUNTED_INCREMENT(stripe.exits); | |
| 141 | 4 | return value; | |
| 142 | } | ||
| 143 | } | ||
| 144 | 1 | } | |
| 145 | }; | ||
| 146 | |||
| 147 | #undef DMK_DETAIL_COUNTED_RETURN_SLOT | ||
| 148 | #undef DMK_DETAIL_COUNTED_THREAD_ID | ||
| 149 | #undef DMK_DETAIL_COUNTED_INCREMENT | ||
| 150 | #undef DMK_DETAIL_COUNTED_THUNK | ||
| 151 | |||
| 152 | /** | ||
| 153 | * @brief Classifies a detour type for the counted thunk. | ||
| 154 | * @details The primary template rejects every type that is not a non-variadic noexcept function pointer. | ||
| 155 | */ | ||
| 156 | template <class Fn, class Frame> struct CountedSignature | ||
| 157 | { | ||
| 158 | static constexpr bool valid = false; | ||
| 159 | }; | ||
| 160 | |||
| 161 | /// A detour without a frame parameter. | ||
| 162 | template <class Ret, class... Args, class Frame> struct CountedSignature<Ret (*)(Args...) noexcept, Frame> | ||
| 163 | { | ||
| 164 | static constexpr bool valid = | ||
| 165 | (std::is_void_v<Ret> || counted_value_v<Ret, Frame>) && (counted_value_v<Args, Frame> && ...); | ||
| 166 | |||
| 167 | template <auto Detour> using entry = CountedEntry<Detour, void, Ret, Args...>; | ||
| 168 | }; | ||
| 169 | |||
| 170 | /// A detour that takes @p Frame first. | ||
| 171 | template <class Ret, class... Args, class Frame> struct CountedSignature<Ret (*)(Frame, Args...) noexcept, Frame> | ||
| 172 | { | ||
| 173 | static constexpr bool valid = | ||
| 174 | (std::is_void_v<Ret> || counted_value_v<Ret, Frame>) && (counted_value_v<Args, Frame> && ...); | ||
| 175 | |||
| 176 | template <auto Detour> using entry = CountedEntry<Detour, Frame, Ret, Args...>; | ||
| 177 | }; | ||
| 178 | } // namespace DetourModKit::hook::detail | ||
| 179 | |||
| 180 | #endif // DETOURMODKIT_DETAIL_COUNTED_ENTRY_HPP | ||
| 181 |