./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
315 / 546
57.7%
Branches:
-
-%
Functions:
45 / 78
57.7%

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