GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 50 / 0 / 50
Functions: 100.0% 35 / 0 / 35
Branches: 100.0% 8 / 0 / 8

include/DetourModKit/address.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_ADDRESS_HPP
2 #define DETOURMODKIT_ADDRESS_HPP
3
4 /**
5 * @file address.hpp
6 * @brief The Address value type, the single addressing vocabulary of the public surface.
7 * @details Address is exactly one machine pointer in size and alignment, and trivially copyable. The static_asserts
8 * at the bottom of this file pin both. The arithmetic and the comparisons are `constexpr` and touch no
9 * memory. Pointer punning is confined to the templated pointer constructor, `as<T>()`, `ptr<T>()`, and
10 * `rip()`. Only `rip()` reads process memory, a disp32 at the caller-supplied offset.
11 */
12
13 #include "DetourModKit/defines.hpp"
14
15 #include <compare>
16 #include <cstddef>
17 #include <cstdint>
18 #include <cstring>
19 #include <type_traits>
20
21 namespace DetourModKit
22 {
23 /**
24 * @class Address
25 * @brief A strongly-typed machine address with constexpr arithmetic and an explicit cast surface.
26 * @details Constructs from a raw integer, from `nullptr`, or from any object/function pointer, and converts back
27 * out only through the explicit `as<T>()` / `ptr<T>()` accessors. Comparisons and the boolean test follow
28 * pointer intuition (null is false; ordering is by numeric address). The type is trivially copyable and
29 * occupies exactly one pointer, so it is free to pass by value and to store in the scan/hook result
30 * structs at no layout cost.
31 */
32 class Address
33 {
34 208 std::uintptr_t m_value{0};
35
36 public:
37 /// Constructs a null address (numeric value zero).
38 2840 constexpr Address() noexcept = default;
39
40 /**
41 * @brief Constructs from a raw integral address.
42 * @param value The numeric address.
43 * @details Explicit so an arbitrary integer never silently becomes an Address; this is the canonical entry
44 * point for an address that arrives as a number (a scan hit, a serialized offset applied to a base).
45 */
46 13838 constexpr explicit Address(std::uintptr_t value) noexcept : m_value{value} {}
47
48 /**
49 * @brief Constructs a null address from `nullptr`.
50 * @details A dedicated overload so `Address{nullptr}` is well-formed: the templated pointer constructor below
51 * does not bind `std::nullptr_t` (it has no pointee type to deduce), so without this overload the
52 * literal would be ambiguous or ill-formed.
53 */
54 7 constexpr Address(std::nullptr_t) noexcept : m_value{0} {}
55
56 /**
57 * @brief Constructs from any object or function pointer.
58 * @tparam T The pointee type, deduced from the argument.
59 * @param pointer The pointer to capture as an address.
60 * @details The `T*` parameter only deduces against an actual pointer argument, so a non-pointer is a deduction
61 * failure and never competes here, and `std::nullptr_t` is taken by the overload above.
62 */
63 295627 template <class T> explicit Address(T *pointer) noexcept : m_value{reinterpret_cast<std::uintptr_t>(pointer)} {}
64
65 /// Returns the underlying numeric address.
66 322269 [[nodiscard]] constexpr std::uintptr_t raw() const noexcept { return m_value; }
67
68 /// Tests whether the address is non-null, matching pointer truthiness; explicit to avoid accidental int use.
69 6852 [[nodiscard]] constexpr explicit operator bool() const noexcept { return m_value != 0; }
70
71 /**
72 * @brief Returns this address advanced by a signed byte delta.
73 * @param delta The byte offset to add (may be negative).
74 * @details Wrapping unsigned arithmetic, so a negative delta walks backwards without invoking signed overflow.
75 * Pure value math: it never dereferences, so it is valid on any address including a null base used as
76 * an offset origin.
77 */
78 3733 [[nodiscard]] constexpr Address offset(std::ptrdiff_t delta) const noexcept
79 {
80 3733 return Address{m_value + static_cast<std::uintptr_t>(delta)};
81 }
82
83 /**
84 * @brief Returns this address rounded up to the next multiple of @p alignment.
85 * @param alignment The alignment in bytes; must be a power of two.
86 * @details Branch-free power-of-two round-up. The caller owns the power-of-two precondition (an alignment of 0
87 * or a non-power-of-two yields a meaningless result rather than a diagnostic), matching how alignment
88 * helpers are used on the scan/page paths where the value is always a known constant. An address
89 * within `alignment - 1` bytes of the address-space top wraps modulo 2^64, so the result is then
90 * numerically below this address.
91 */
92 3 [[nodiscard]] constexpr Address align_up(std::size_t alignment) const noexcept
93 {
94 3 const std::uintptr_t mask = static_cast<std::uintptr_t>(alignment) - 1U;
95 3 return Address{(m_value + mask) & ~mask};
96 }
97
98 /**
99 * @brief Resolves a RIP-relative reference whose displacement is embedded in the instruction at this address.
100 * @param displacement_at Byte offset from this address to the signed 4-byte displacement field.
101 * @param instruction_length Total length of the instruction in bytes.
102 * @return The absolute target: (this + instruction_length) + sign-extended disp32.
103 * @details The RAW, unchecked resolve for an instruction that is already located and validated. It reads the
104 * disp32 straight out of process memory and assumes that the bytes are mapped. The bounds-checked,
105 * fault-tolerant resolver lives in the scan and memory layer. The x86-64 convention measures the
106 * displacement from the END of the instruction, which is the next IP.
107 */
108 1 [[nodiscard]] Address rip(std::ptrdiff_t displacement_at, std::size_t instruction_length) const noexcept
109 {
110 // Load the disp32 with memcpy rather than a typed dereference. A displacement field sits at an arbitrary
111 // byte offset inside an instruction and is almost never 4-byte aligned, so forming an `int32_t *` to it
112 // and dereferencing would be undefined behaviour. memcpy of a fixed 4 bytes is the well-defined unaligned
113 // load and the compiler folds it to a single (unaligned) mov on x86-64.
114 1 std::int32_t displacement = 0;
115 1 std::memcpy(
116 &displacement,
117 1 reinterpret_cast<const void *>(m_value + static_cast<std::uintptr_t>(displacement_at)),
118 sizeof(displacement)
119 );
120 1 const std::uintptr_t next_instruction = m_value + static_cast<std::uintptr_t>(instruction_length);
121 1 return Address{next_instruction + static_cast<std::uintptr_t>(static_cast<std::intptr_t>(displacement))};
122 }
123
124 /**
125 * @brief Reinterprets the address as a value of type @p T.
126 * @tparam T A pointer / function-pointer type, or a pointer-width integer (`std::uintptr_t`, `std::intptr_t`,
127 * and same-width aliases such as `std::size_t`). Narrower integrals, references, and non-pointer
128 * object types are rejected by the constraint below.
129 * @details The `requires` clause matches the two casts this accessor performs:
130 * - A pointer or function-pointer @p T takes the `reinterpret_cast` path.
131 * - An integral @p T takes the `static_cast` path. The width constraint prevents a truncation of the
132 * pointer-sized representation, so a narrower integral such as `int` is a compile error instead of
133 * a lossy conversion.
134 * A reference @p T is excluded, because it reinterprets this Address object's own storage, never the
135 * memory the address names. For a narrowed integer use @ref raw(). For a typed view of the addressed
136 * bytes use @ref ptr() and dereference it.
137 * @note Callback-safe: a pure cast, no allocation, locking, or I/O.
138 */
139 template <class T>
140 requires(std::is_pointer_v<T> || (std::is_integral_v<T> && sizeof(T) >= sizeof(std::uintptr_t)))
141 2264 [[nodiscard]] T as() const noexcept
142 {
143 if constexpr (std::is_integral_v<T>)
144 {
145 5 return static_cast<T>(m_value);
146 }
147 else
148 {
149 2259 return reinterpret_cast<T>(m_value);
150 }
151 }
152
153 /// Reinterprets the address as a `T*`; the typed-pointer shorthand for `as<T*>()`.
154 29 template <class T> [[nodiscard]] T *ptr() const noexcept { return reinterpret_cast<T *>(m_value); }
155
156 /// Numeric three-way ordering, so `<`, `<=`, `==`, ... all compare by raw address.
157
6/6
✓ Branch 2 → 3 taken 31 times.
✓ Branch 2 → 6 taken 7 times.
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 16 times.
✓ Branch 8 → 9 taken 31 times.
✓ Branch 8 → 10 taken 7 times.
45 [[nodiscard]] constexpr auto operator<=>(const Address &) const noexcept = default;
158
2/2
✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 4 taken 153 times.
170 [[nodiscard]] constexpr bool operator==(const Address &) const noexcept = default;
159 };
160
161 // An Address must be a drop-in, zero-cost replacement for a raw pointer everywhere it is passed or stored; if it
162 // ever grew past a machine pointer the "free to pass by value" assumption and the reinterpret_cast round-trips
163 // would both break. Pin it at compile time.
164 static_assert(
165 sizeof(Address) == sizeof(void *) && alignof(Address) == alignof(void *),
166 "Address must be exactly a machine pointer in size and alignment."
167 );
168 static_assert(std::is_trivially_copyable_v<Address>, "Address must stay trivially copyable.");
169
170 } // namespace DetourModKit
171
172 #endif // DETOURMODKIT_ADDRESS_HPP
173