GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 83.8% 166 / 0 / 198
Functions: 100.0% 7 / 0 / 7
Branches: 69.9% 151 / 0 / 216

src/scanner.cpp
Line Branch Exec Source
1 /**
2 * @file scanner.cpp
3 * @brief Implementation of Array-of-Bytes (AOB) parsing, scanning, and RIP-relative resolution.
4 */
5
6 #include "DetourModKit/scanner.hpp"
7 #include "DetourModKit/memory.hpp"
8 #include "DetourModKit/logger.hpp"
9 #include "DetourModKit/format.hpp"
10
11 #include <windows.h>
12 #include <vector>
13 #include <string>
14 #include <sstream>
15 #include <cctype>
16 #include <stdexcept>
17 #include <cstddef>
18 #include <cstdint>
19 #include <cstring>
20
21 #if defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
22 #define DMK_HAS_SSE2 1
23 #include <emmintrin.h>
24 #endif
25
26 using namespace DetourModKit;
27 using namespace DetourModKit::String;
28
29 namespace
30 {
31 /**
32 * @brief Returns a commonality score for a byte value in typical x64 PE code sections.
33 * @details Higher scores indicate bytes that appear more frequently, making them
34 * poor candidates for anchor-based scanning.
35 */
36 276 static constexpr uint8_t byte_frequency_class(uint8_t b) noexcept
37 {
38
6/13
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 14 times.
✗ Branch 2 → 6 not taken.
✓ Branch 2 → 7 taken 10 times.
✓ Branch 2 → 8 taken 9 times.
✗ Branch 2 → 9 not taken.
✗ Branch 2 → 10 not taken.
✗ Branch 2 → 11 not taken.
✗ Branch 2 → 12 not taken.
✗ Branch 2 → 13 not taken.
✗ Branch 2 → 14 not taken.
✓ Branch 2 → 15 taken 239 times.
276 switch (b)
39 {
40 2 case 0x00:
41 2 return 10; // null padding, very common
42 2 case 0xCC:
43 2 return 9; // INT3, debug padding
44 14 case 0x90:
45 14 return 9; // NOP
46 case 0xFF:
47 return 8; // call/jmp indirect, common
48 10 case 0x48:
49 10 return 8; // REX.W prefix, ubiquitous in x64
50 9 case 0x8B:
51 9 return 7; // MOV reg, r/m
52 case 0x89:
53 return 7; // MOV r/m, reg
54 case 0x0F:
55 return 7; // two-byte opcode escape
56 case 0xE8:
57 return 6; // CALL rel32
58 case 0xE9:
59 return 6; // JMP rel32
60 case 0x83:
61 return 6; // arithmetic imm8
62 case 0xC3:
63 return 5; // RET
64 239 default:
65 239 return 0; // uncommon, ideal anchor
66 }
67 }
68 } // anonymous namespace
69
70 81 std::optional<Scanner::CompiledPattern> DetourModKit::Scanner::parse_aob(std::string_view aob_str)
71 {
72
1/2
✓ Branch 2 → 3 taken 81 times.
✗ Branch 2 → 138 not taken.
81 Logger &logger = Logger::get_instance();
73
74
2/4
✓ Branch 5 → 6 taken 81 times.
✗ Branch 5 → 90 not taken.
✓ Branch 7 → 8 taken 81 times.
✗ Branch 7 → 88 not taken.
81 std::string trimmed_aob = trim(std::string(aob_str));
75
2/2
✓ Branch 11 → 12 taken 6 times.
✓ Branch 11 → 17 taken 75 times.
81 if (trimmed_aob.empty())
76 {
77
2/2
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 16 taken 3 times.
6 if (!aob_str.empty())
78 {
79
1/2
✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 94 not taken.
3 logger.warning("AOB Parser: Input string became empty after trimming.");
80 }
81 6 return std::nullopt;
82 }
83
84 75 CompiledPattern result;
85
1/2
✓ Branch 17 → 18 taken 75 times.
✗ Branch 17 → 134 not taken.
75 std::istringstream iss(trimmed_aob);
86 75 std::string token;
87 75 size_t token_idx = 0;
88
89 75 bool offset_set = false;
90
91
4/6
✓ Branch 68 → 69 taken 497 times.
✗ Branch 68 → 130 not taken.
✓ Branch 69 → 70 taken 497 times.
✗ Branch 69 → 130 not taken.
✓ Branch 70 → 20 taken 431 times.
✓ Branch 70 → 71 taken 66 times.
497 while (iss >> token)
92 {
93 431 token_idx++;
94
3/4
✓ Branch 20 → 21 taken 431 times.
✗ Branch 20 → 130 not taken.
✓ Branch 21 → 22 taken 9 times.
✓ Branch 21 → 27 taken 422 times.
431 if (token == "|")
95 {
96
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 25 taken 8 times.
9 if (offset_set)
97 {
98
1/2
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 95 not taken.
1 logger.error("AOB Parser: Multiple '|' offset markers at position {}.", token_idx);
99 1 return std::nullopt;
100 }
101 8 result.offset = result.bytes.size();
102 8 offset_set = true;
103 }
104
8/10
✓ Branch 27 → 28 taken 422 times.
✗ Branch 27 → 130 not taken.
✓ Branch 28 → 29 taken 390 times.
✓ Branch 28 → 31 taken 32 times.
✓ Branch 29 → 30 taken 390 times.
✗ Branch 29 → 130 not taken.
✓ Branch 30 → 31 taken 2 times.
✓ Branch 30 → 32 taken 388 times.
✓ Branch 33 → 34 taken 34 times.
✓ Branch 33 → 37 taken 388 times.
422 else if (token == "??" || token == "?")
105 {
106
1/2
✓ Branch 34 → 35 taken 34 times.
✗ Branch 34 → 96 not taken.
34 result.bytes.push_back(std::byte{0x00});
107
1/2
✓ Branch 35 → 36 taken 34 times.
✗ Branch 35 → 97 not taken.
34 result.mask.push_back(std::byte{0x00});
108 }
109
9/12
✓ Branch 38 → 39 taken 386 times.
✓ Branch 38 → 44 taken 2 times.
✓ Branch 39 → 40 taken 386 times.
✗ Branch 39 → 130 not taken.
✓ Branch 40 → 41 taken 380 times.
✓ Branch 40 → 44 taken 6 times.
✓ Branch 41 → 42 taken 380 times.
✗ Branch 41 → 130 not taken.
✓ Branch 42 → 43 taken 380 times.
✗ Branch 42 → 44 not taken.
✓ Branch 45 → 46 taken 380 times.
✓ Branch 45 → 54 taken 8 times.
388 else if (token.length() == 2 && std::isxdigit(static_cast<unsigned char>(token[0])) && std::isxdigit(static_cast<unsigned char>(token[1])))
110 {
111 try
112 {
113
1/2
✓ Branch 46 → 47 taken 380 times.
✗ Branch 46 → 102 not taken.
380 unsigned long ulong_val = std::stoul(token, nullptr, 16);
114
1/2
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 51 taken 380 times.
380 if (ulong_val > 0xFF)
115 {
116 throw std::out_of_range("Value parsed exceeds byte range (0xFF).");
117 }
118
1/2
✓ Branch 51 → 52 taken 380 times.
✗ Branch 51 → 100 not taken.
380 result.bytes.push_back(static_cast<std::byte>(ulong_val));
119
1/2
✓ Branch 52 → 53 taken 380 times.
✗ Branch 52 → 101 not taken.
380 result.mask.push_back(std::byte{0xFF});
120 }
121 catch (const std::out_of_range &oor)
122 {
123 logger.error("AOB Parser: Hex conversion out of range for '{}' (Pos {}): {}", token, token_idx, oor.what());
124 return std::nullopt;
125 }
126 catch (const std::invalid_argument &ia)
127 {
128 logger.error("AOB Parser: Invalid argument for hex conversion '{}' (Pos {}): {}", token, token_idx, ia.what());
129 return std::nullopt;
130 }
131 }
132 else
133 {
134
1/2
✓ Branch 54 → 55 taken 8 times.
✗ Branch 54 → 127 not taken.
8 std::ostringstream oss_err;
135
4/8
✓ Branch 55 → 56 taken 8 times.
✗ Branch 55 → 125 not taken.
✓ Branch 56 → 57 taken 8 times.
✗ Branch 56 → 125 not taken.
✓ Branch 57 → 58 taken 8 times.
✗ Branch 57 → 125 not taken.
✓ Branch 58 → 59 taken 8 times.
✗ Branch 58 → 125 not taken.
8 oss_err << "AOB Parser: Invalid token '" << token << "' at position " << token_idx
136
1/2
✓ Branch 59 → 60 taken 8 times.
✗ Branch 59 → 125 not taken.
8 << ". Expected hex byte (e.g., FF), '?' or '?\?'.";
137
2/4
✓ Branch 60 → 61 taken 8 times.
✗ Branch 60 → 124 not taken.
✓ Branch 62 → 63 taken 8 times.
✗ Branch 62 → 122 not taken.
8 logger.log(LogLevel::Error, oss_err.str());
138 8 return std::nullopt;
139 8 }
140 }
141
142
1/2
✗ Branch 72 → 73 not taken.
✓ Branch 72 → 81 taken 66 times.
66 if (result.empty())
143 {
144 if (token_idx > 0)
145 {
146 logger.error("AOB Parser: Processed tokens but resulting pattern is empty.");
147 }
148 else if (!trimmed_aob.empty())
149 {
150 logger.warning("AOB: Parsing AOB string '{}' resulted in an empty pattern.", aob_str);
151 }
152 return std::nullopt;
153 }
154
155 66 return result;
156 81 }
157
158 261 const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size,
159 const CompiledPattern &pattern)
160 {
161 261 Logger &logger = Logger::get_instance();
162 261 const size_t pattern_size = pattern.size();
163
164
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 7 taken 260 times.
261 if (pattern_size == 0)
165 {
166
1/2
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 70 not taken.
1 logger.error("find_pattern: Pattern is empty. Cannot scan.");
167 1 return nullptr;
168 }
169
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 10 taken 258 times.
260 if (!start_address)
170 {
171
1/2
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 71 not taken.
2 logger.error("find_pattern: Start address is null. Cannot scan.");
172 2 return nullptr;
173 }
174
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 254 times.
258 if (region_size < pattern_size)
175 {
176 4 return nullptr;
177 }
178
179 // Select the best anchor byte: the non-wildcard byte with the lowest frequency score.
180 // Ties are broken by first occurrence for deterministic behavior.
181 254 size_t best_anchor = pattern_size; // invalid = all wildcards
182 254 uint8_t best_score = UINT8_MAX;
183
2/2
✓ Branch 22 → 13 taken 289 times.
✓ Branch 22 → 23 taken 15 times.
304 for (size_t i = 0; i < pattern_size; ++i)
184 {
185
2/2
✓ Branch 14 → 15 taken 276 times.
✓ Branch 14 → 21 taken 13 times.
289 if (pattern.mask[i] != std::byte{0x00})
186 {
187 276 uint8_t score = byte_frequency_class(static_cast<uint8_t>(pattern.bytes[i]));
188
4/4
✓ Branch 17 → 18 taken 23 times.
✓ Branch 17 → 19 taken 253 times.
✓ Branch 18 → 19 taken 16 times.
✓ Branch 18 → 21 taken 7 times.
276 if (best_anchor == pattern_size || score < best_score)
189 {
190 269 best_anchor = i;
191 269 best_score = score;
192
2/2
✓ Branch 19 → 20 taken 239 times.
✓ Branch 19 → 21 taken 30 times.
269 if (score == 0)
193 {
194 239 break; // Cannot improve on score 0
195 }
196 }
197 }
198 }
199
200 // All wildcards: matches immediately at start
201
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 253 times.
254 if (best_anchor == pattern_size)
202 {
203 1 return start_address;
204 }
205
206 253 const std::byte target_byte = pattern.bytes[best_anchor];
207 253 const unsigned char target_val = static_cast<unsigned char>(target_byte);
208
209 253 const std::byte *search_start = start_address + best_anchor;
210 253 const std::byte *const search_end = start_address + (region_size - pattern_size) + best_anchor;
211
212
1/2
✓ Branch 67 → 27 taken 227861 times.
✗ Branch 67 → 68 not taken.
227861 while (search_start <= search_end)
213 {
214 227861 const void *found = memchr(search_start, static_cast<int>(target_val),
215 227861 static_cast<size_t>(search_end - search_start + 1));
216
217
2/2
✓ Branch 27 → 28 taken 204 times.
✓ Branch 27 → 29 taken 227657 times.
227861 if (!found)
218 {
219 204 break;
220 }
221
222 227657 const std::byte *current_scan_ptr = static_cast<const std::byte *>(found);
223 227657 const std::byte *pattern_start = current_scan_ptr - best_anchor;
224
225 // Verify the full pattern at this position
226 227657 bool match_found = true;
227 227657 size_t j = 0;
228
229 #ifdef DMK_HAS_SSE2
230
2/2
✓ Branch 51 → 30 taken 227618 times.
✓ Branch 51 → 52 taken 51 times.
227669 for (; j + 16 <= pattern_size; j += 16)
231 {
232 227618 __m128i mem = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern_start + j));
233 227618 __m128i pat = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern.bytes.data() + j));
234 455236 __m128i msk = _mm_loadu_si128(reinterpret_cast<const __m128i *>(pattern.mask.data() + j));
235
236 227618 __m128i xored = _mm_xor_si128(mem, pat);
237 227618 __m128i masked = _mm_and_si128(xored, msk);
238 455236 __m128i cmp = _mm_cmpeq_epi8(masked, _mm_setzero_si128());
239
240
2/2
✓ Branch 48 → 49 taken 227606 times.
✓ Branch 48 → 50 taken 12 times.
227618 if (_mm_movemask_epi8(cmp) != 0xFFFF)
241 {
242 227606 match_found = false;
243 227606 break;
244 }
245 }
246 #endif // DMK_HAS_SSE2
247
248
4/4
✓ Branch 62 → 63 taken 162 times.
✓ Branch 62 → 64 taken 227608 times.
✓ Branch 63 → 53 taken 113 times.
✓ Branch 63 → 64 taken 49 times.
227770 for (; match_found && j < pattern_size; ++j)
249 {
250
6/6
✓ Branch 54 → 55 taken 103 times.
✓ Branch 54 → 58 taken 10 times.
✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 101 times.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 61 taken 111 times.
113 if (pattern.mask[j] != std::byte{0x00} && pattern_start[j] != pattern.bytes[j])
251 {
252 2 match_found = false;
253 }
254 }
255
256
2/2
✓ Branch 64 → 65 taken 49 times.
✓ Branch 64 → 66 taken 227608 times.
227657 if (match_found)
257 {
258 49 return pattern_start;
259 }
260
261 // No match, continue searching from next position
262 227608 search_start = current_scan_ptr + 1;
263 }
264
265 204 return nullptr;
266 }
267
268 10 const std::byte *DetourModKit::Scanner::find_pattern(const std::byte *start_address, size_t region_size,
269 const CompiledPattern &pattern, size_t occurrence)
270 {
271
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 9 times.
10 if (occurrence == 0)
272 {
273 1 return nullptr;
274 }
275
276 9 const std::byte *cursor = start_address;
277 9 size_t remaining = region_size;
278 9 size_t found_count = 0;
279
280
2/2
✓ Branch 12 → 5 taken 19 times.
✓ Branch 12 → 13 taken 1 time.
20 while (remaining >= pattern.size())
281 {
282 19 const std::byte *match = find_pattern(cursor, remaining, pattern);
283
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 18 times.
19 if (!match)
284 {
285 1 break;
286 }
287
2/2
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 10 taken 11 times.
18 if (++found_count == occurrence)
288 {
289 7 return match;
290 }
291 11 const size_t advance = static_cast<size_t>(match - cursor) + 1;
292 11 cursor += advance;
293 11 remaining -= advance;
294 }
295
296 2 return nullptr;
297 }
298
299 14 std::optional<uintptr_t> DetourModKit::Scanner::resolve_rip_relative(const std::byte *instruction_address,
300 size_t displacement_offset,
301 size_t instruction_length)
302 {
303
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 13 times.
14 if (!instruction_address)
304 {
305 1 return std::nullopt;
306 }
307
308 13 const std::byte *disp_ptr = instruction_address + displacement_offset;
309
2/4
✓ Branch 4 → 5 taken 13 times.
✗ Branch 4 → 11 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 13 times.
13 if (!Memory::is_readable(disp_ptr, sizeof(int32_t)))
310 {
311 return std::nullopt;
312 }
313
314 int32_t displacement;
315 13 std::memcpy(&displacement, disp_ptr, sizeof(int32_t));
316
317 13 auto base = reinterpret_cast<uintptr_t>(instruction_address);
318 13 return base + instruction_length + static_cast<uintptr_t>(static_cast<intptr_t>(displacement));
319 }
320
321 12 std::optional<uintptr_t> DetourModKit::Scanner::find_and_resolve_rip_relative(const std::byte *search_start,
322 size_t search_length,
323 std::span<const std::byte> opcode_prefix,
324 size_t instruction_length)
325 {
326
6/6
✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 10 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 10 times.
12 if (!search_start || opcode_prefix.empty())
327 {
328 2 return std::nullopt;
329 }
330
331 10 const size_t prefix_len = opcode_prefix.size();
332 10 const size_t min_bytes = prefix_len + sizeof(int32_t);
333
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 8 times.
10 if (search_length < min_bytes)
334 {
335 2 return std::nullopt;
336 }
337
338 8 const size_t scan_limit = search_length - min_bytes;
339 8 const std::byte first = opcode_prefix[0];
340
341
1/2
✓ Branch 25 → 14 taken 22 times.
✗ Branch 25 → 26 not taken.
22 for (size_t i = 0; i <= scan_limit; ++i)
342 {
343
2/2
✓ Branch 14 → 15 taken 13 times.
✓ Branch 14 → 16 taken 9 times.
22 if (search_start[i] != first)
344 {
345 13 continue;
346 }
347
348
6/6
✓ Branch 16 → 17 taken 6 times.
✓ Branch 16 → 20 taken 3 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 5 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 23 taken 8 times.
9 if (prefix_len > 1 && std::memcmp(&search_start[i + 1], opcode_prefix.data() + 1, prefix_len - 1) != 0)
349 {
350 1 continue;
351 }
352
353 8 return resolve_rip_relative(&search_start[i], prefix_len, instruction_length);
354 }
355
356 return std::nullopt;
357 }
358
359 10 const std::byte *DetourModKit::Scanner::scan_executable_regions(const CompiledPattern &pattern, size_t occurrence)
360 {
361
6/6
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 8 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 8 times.
10 if (pattern.empty() || occurrence == 0)
362 2 return nullptr;
363
364 8 constexpr DWORD EXEC_FLAGS = PAGE_EXECUTE | PAGE_EXECUTE_READ |
365 PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
366
367 8 size_t matches_remaining = occurrence;
368 8 MEMORY_BASIC_INFORMATION mbi{};
369 8 uintptr_t addr = 0;
370
371
3/4
✓ Branch 31 → 32 taken 2138 times.
✗ Branch 31 → 36 not taken.
✓ Branch 32 → 10 taken 2134 times.
✓ Branch 32 → 33 taken 4 times.
2138 while (VirtualQuery(reinterpret_cast<LPCVOID>(addr), &mbi, sizeof(mbi)))
372 {
373
2/2
✓ Branch 11 → 12 taken 206 times.
✓ Branch 11 → 16 taken 1369 times.
1575 if (mbi.State == MEM_COMMIT && (mbi.Protect & EXEC_FLAGS) != 0 &&
374
7/8
✓ Branch 10 → 11 taken 1575 times.
✓ Branch 10 → 16 taken 559 times.
✓ Branch 12 → 13 taken 205 times.
✓ Branch 12 → 16 taken 1 time.
✓ Branch 14 → 15 taken 205 times.
✗ Branch 14 → 16 not taken.
✓ Branch 17 → 18 taken 205 times.
✓ Branch 17 → 28 taken 1929 times.
3709 (mbi.Protect & PAGE_GUARD) == 0 && mbi.RegionSize >= pattern.size())
375 {
376 205 const auto *region_start = reinterpret_cast<const std::byte *>(mbi.BaseAddress);
377
378
1/2
✓ Branch 18 → 19 taken 205 times.
✗ Branch 18 → 36 not taken.
205 const std::byte *match = find_pattern(region_start, mbi.RegionSize, pattern);
379
2/2
✓ Branch 26 → 20 taken 7 times.
✓ Branch 26 → 27 taken 201 times.
208 while (match != nullptr)
380 {
381 7 --matches_remaining;
382
2/2
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 22 taken 3 times.
7 if (matches_remaining == 0)
383 4 return match + pattern.offset;
384
385 // Continue scanning past the current match
386 3 const size_t consumed = static_cast<size_t>(match - region_start) + 1;
387
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 3 times.
3 if (consumed >= mbi.RegionSize)
388 break;
389
1/2
✓ Branch 24 → 25 taken 3 times.
✗ Branch 24 → 36 not taken.
3 match = find_pattern(match + 1, mbi.RegionSize - consumed, pattern);
390 }
391 }
392
393 2130 const uintptr_t next = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
394
1/2
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 2130 times.
2130 if (next <= addr)
395 break; // Overflow guard
396 2130 addr = next;
397 }
398
399 4 return nullptr;
400 }
401