GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 39 / 0 / 39
Functions: 100.0% 4 / 0 / 4
Branches: 93.3% 28 / 0 / 30

src/x86_decode.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_X86_DECODE_HPP
2 #define DETOURMODKIT_X86_DECODE_HPP
3
4 #include "DetourModKit/memory.hpp"
5
6 #include "internal/memory_guarded.hpp"
7
8 #include <array>
9 #include <cstdint>
10 #include <cstring>
11 #include <optional>
12
13 /**
14 * @file x86_decode.hpp
15 * @brief Minimal jump-shape decoders for the nested-hook detection and prologue-recovery paths.
16 * @details Each decoder copies the candidate instruction bytes into a local buffer under a single fault guard
17 * (guarded_read_bytes), then inspects the copy. Read-then-test on a local buffer closes the time-of-check to
18 * time-of-use gap that is_readable + a raw dereference leaves open: on these paths the target page is most
19 * likely to change protection or unmap between the probe and the access. A faulting read returns nullopt
20 * rather than faulting the host. These recognise a single jump shape each; they are not a general decoder.
21 */
22
23 namespace DetourModKit::detail
24 {
25 /**
26 * @brief Decodes an E9 rel32 near JMP at @p address and returns its absolute destination.
27 * @param address Absolute address of the candidate instruction.
28 * @return The absolute jump destination, or std::nullopt when the bytes are unreadable or the opcode is not E9.
29 */
30 25 [[nodiscard]] inline std::optional<std::uintptr_t> decode_e9_rel32(std::uintptr_t address) noexcept
31 {
32 25 std::array<std::uint8_t, 5> code{};
33
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 23 times.
50 if (!guarded_read_bytes(address, code.data(), code.size()))
34 {
35 2 return std::nullopt;
36 }
37
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 22 times.
23 if (code[0] != 0xE9)
38 {
39 1 return std::nullopt;
40 }
41 22 std::int32_t disp = 0;
42 22 std::memcpy(&disp, code.data() + 1, sizeof(disp));
43 22 return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 5 + disp);
44 }
45
46 /**
47 * @brief Decodes an EB rel8 short JMP at @p address and returns its absolute destination.
48 * @param address Absolute address of the candidate instruction.
49 * @return The absolute jump destination (rel8 sign-extended), or std::nullopt when the bytes are unreadable or the
50 * opcode is not EB.
51 */
52 4 [[nodiscard]] inline std::optional<std::uintptr_t> decode_eb_rel8(std::uintptr_t address) noexcept
53 {
54 4 std::array<std::uint8_t, 2> code{};
55
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 3 times.
8 if (!guarded_read_bytes(address, code.data(), code.size()))
56 {
57 1 return std::nullopt;
58 }
59
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 2 times.
3 if (code[0] != 0xEB)
60 {
61 1 return std::nullopt;
62 }
63 2 const auto disp = static_cast<std::int8_t>(code[1]);
64 2 return static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 2 + disp);
65 }
66
67 /**
68 * @brief Decodes an FF 25 disp32 indirect JMP at @p address and returns the target stored in its memory slot.
69 * @details FF 25 disp32 on x86-64 is RIP-relative: the 32-bit signed displacement is added to the address of the
70 * next instruction. On x86 (32-bit) the same encoding is absolute, which this decoder does not handle.
71 * @param address Absolute address of the candidate instruction.
72 * @return The final indirect target read from the slot, or std::nullopt when the instruction bytes or the slot are
73 * unreadable or the opcode is not FF 25.
74 */
75 10 [[nodiscard]] inline std::optional<std::uintptr_t> decode_ff25_indirect(std::uintptr_t address) noexcept
76 {
77 static_assert(sizeof(void *) == 8, "decode_ff25_indirect assumes x86-64 RIP-relative semantics");
78 10 std::array<std::uint8_t, 6> code{};
79
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 9 times.
20 if (!guarded_read_bytes(address, code.data(), code.size()))
80 {
81 1 return std::nullopt;
82 }
83
6/6
✓ Branch 10 → 11 taken 8 times.
✓ Branch 10 → 13 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 7 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 7 times.
9 if (code[0] != 0xFF || code[1] != 0x25)
84 {
85 2 return std::nullopt;
86 }
87 7 std::int32_t disp = 0;
88 7 std::memcpy(&disp, code.data() + 2, sizeof(disp));
89 7 const auto slot_addr = static_cast<std::uintptr_t>(static_cast<std::int64_t>(address) + 6 + disp);
90 // The slot stores the final indirect target; read it under the same fault guard rather than is_readable + a raw
91 // dereference.
92 7 const auto indirect_destination = guarded_read<std::uintptr_t>(slot_addr);
93
2/2
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 6 times.
7 if (!indirect_destination)
94 {
95 1 return std::nullopt;
96 }
97 6 return *indirect_destination;
98 }
99
100 /**
101 * @brief Decodes a `mov rax, imm64; jmp rax` absolute-jump pair at @p address and returns the imm64 destination.
102 * @details Some inline hooks emit this 12-byte absolute jump when the detour trampoline is beyond rel32 reach and
103 * the hooking library does not use the FF 25 RIP-relative form. The pair is `48 B8 <imm64>` (REX.W mov
104 * rax, imm64) immediately followed by `FF E0` (jmp rax). Unlike FF 25 the absolute target is the imm64
105 * baked directly into the instruction, so no pointer-slot dereference is needed.
106 * @param address Absolute address of the candidate instruction pair.
107 * @return The absolute jump destination (the imm64), or std::nullopt when the bytes are unreadable or the opcodes
108 * are not `48 B8 ... FF E0`.
109 */
110 13 [[nodiscard]] inline std::optional<std::uintptr_t> decode_mov_rax_imm64_jmp_rax(std::uintptr_t address) noexcept
111 {
112 static_assert(sizeof(void *) == 8, "decode_mov_rax_imm64_jmp_rax assumes a 64-bit absolute target");
113 13 std::array<std::uint8_t, 12> code{};
114
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 11 times.
26 if (!guarded_read_bytes(address, code.data(), code.size()))
115 {
116 2 return std::nullopt;
117 }
118 // 48 B8 = REX.W + mov rax, imm64; bytes [2..9] hold the imm64; FF E0 = jmp rax.
119
8/10
✓ Branch 10 → 11 taken 11 times.
✗ Branch 10 → 17 not taken.
✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 17 taken 1 time.
✓ Branch 14 → 15 taken 10 times.
✗ Branch 14 → 17 not taken.
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 9 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 9 times.
11 if (code[0] != 0x48 || code[1] != 0xB8 || code[10] != 0xFF || code[11] != 0xE0)
120 {
121 2 return std::nullopt;
122 }
123 9 std::uintptr_t destination = 0;
124 9 std::memcpy(&destination, code.data() + 2, sizeof(destination));
125 9 return destination;
126 }
127 } // namespace DetourModKit::detail
128
129 #endif // DETOURMODKIT_X86_DECODE_HPP
130