GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.5% 37 / 0 / 40
Functions: 100.0% 2 / 0 / 2
Branches: 69.6% 32 / 0 / 46

src/code_constant.cpp
Line Branch Exec Source
1 /**
2 * @file code_constant.cpp
3 * @brief Zydis-backed extraction of a constant encoded in engine machine code.
4 *
5 * The code-side twin of the RTTI self-heal: an AOB cascade lands on an instruction, the instruction is decoded, and the
6 * requested operand's immediate or memory displacement is returned as the CURRENT value. The caller's nominal is never
7 * a short-circuit, so a same-shape / different-value drift (for example an array stride 232 -> 240) is reported as the
8 * new value, which is the entire point of re-deriving it.
9 *
10 * Zydis is confined to this translation unit: no public DetourModKit header exposes a Zydis type. An installed-package
11 * consumer therefore only links
12 * DetourModKit (which already links Zydis statically) and never needs Zydis headers on its own include path.
13 */
14
15 #include "DetourModKit/scanner.hpp"
16 #include "DetourModKit/memory.hpp"
17
18 #include <Zydis/Zydis.h>
19
20 #include <cstddef>
21 #include <cstdint>
22
23 namespace DetourModKit
24 {
25 namespace
26 {
27 // Narrows an already-64-bit-sign-extended value to @p byte_width bytes and re-sign-extends from that width, so
28 // a deliberately narrowed negative value (for example a disp8 of -1) stays negative instead of becoming a large
29 // positive number. @p byte_width 0 returns the value verbatim, since
30 // Zydis has already sign-extended immediates and displacements to 64 bits.
31 30 std::int64_t narrow_signed(std::int64_t value, std::uint8_t byte_width) noexcept
32 {
33
3/4
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 27 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
30 if (byte_width == 0 || byte_width >= sizeof(std::int64_t))
34 {
35 27 return value;
36 }
37 3 const unsigned bits = static_cast<unsigned>(byte_width) * 8u;
38 3 const std::uint64_t mask = (std::uint64_t{1} << bits) - 1u;
39 3 const std::uint64_t masked = static_cast<std::uint64_t>(value) & mask;
40 3 const std::uint64_t sign_bit = std::uint64_t{1} << (bits - 1u);
41 // Two's-complement sign-extension from the top bit of the chosen width.
42 3 const std::uint64_t extended = (masked ^ sign_bit) - sign_bit;
43 3 return static_cast<std::int64_t>(extended);
44 }
45 } // anonymous namespace
46
47 38 std::expected<std::int64_t, Scanner::ResolveError> Scanner::read_code_constant(const CodeConstant &cc,
48 Memory::ModuleRange range)
49 {
50 // Resolve the instruction site through the existing module-scoped cascade and propagate its typed failure
51 // verbatim (EmptyCandidates, NoMatch, InvalidRange, ...).
52
1/2
✓ Branch 3 → 4 taken 38 times.
✗ Branch 3 → 69 not taken.
38 const auto hit = resolve_cascade_in_module(cc.site, "read_code_constant", range);
53
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 10 taken 35 times.
38 if (!hit)
54 {
55 3 return std::unexpected(hit.error());
56 }
57 35 const std::uintptr_t site = hit->address;
58
59 // Read a full maximum-length instruction window, clamped to the module so the read never runs past the end of
60 // the image, behind a fault guard. A truncated window that fails to decode is reported as DecodeFailed below.
61 std::byte buf[ZYDIS_MAX_INSTRUCTION_LENGTH];
62 35 std::size_t avail = sizeof(buf);
63
3/6
✓ Branch 12 → 13 taken 35 times.
✗ Branch 12 → 15 not taken.
✓ Branch 13 → 14 taken 35 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 35 times.
✗ Branch 16 → 19 not taken.
35 if (range.valid() && site < range.end)
64 {
65 35 const std::uintptr_t to_end = range.end - site;
66
2/2
✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 19 taken 34 times.
35 if (to_end < avail)
67 {
68 1 avail = static_cast<std::size_t>(to_end);
69 }
70 }
71
3/6
✓ Branch 19 → 20 taken 35 times.
✗ Branch 19 → 22 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 35 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 28 taken 35 times.
35 if (avail == 0 || !Memory::seh_read_bytes(site, buf, avail))
72 {
73 return std::unexpected(ResolveError::DecodeFailed);
74 }
75
76 ZydisDecoder decoder;
77
2/4
✓ Branch 28 → 29 taken 35 times.
✗ Branch 28 → 71 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 33 taken 35 times.
35 if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)))
78 {
79 return std::unexpected(ResolveError::DecodeFailed);
80 }
81
82 ZydisDecodedInstruction insn;
83 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
84
3/4
✓ Branch 33 → 34 taken 35 times.
✗ Branch 33 → 71 not taken.
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 38 taken 34 times.
35 if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, buf, avail, &insn, operands)))
85 {
86 1 return std::unexpected(ResolveError::DecodeFailed);
87 }
88
89 // Index the VISIBLE operands -- the ones a human counts in a disassembler. operand_count includes
90 // implicit/hidden operands (flags, implicit registers, stack writes), which would make a fixed operand_index
91 // drift between mnemonics.
92
2/2
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 42 taken 33 times.
34 if (cc.operand_index >= insn.operand_count_visible)
93 {
94 1 return std::unexpected(ResolveError::OperandOutOfRange);
95 }
96 33 const ZydisDecodedOperand &operand = operands[cc.operand_index];
97
98
2/2
✓ Branch 42 → 43 taken 27 times.
✓ Branch 42 → 50 taken 6 times.
33 if (cc.kind == OperandKind::Immediate)
99 {
100
2/2
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 47 taken 26 times.
27 if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE)
101 {
102 1 return std::unexpected(ResolveError::UnexpectedShape);
103 }
104 // imm.value.s is already 64-bit sign-extended by Zydis.
105 26 return narrow_signed(static_cast<std::int64_t>(operand.imm.value.s), cc.byte_width);
106 }
107
108 // MemoryDisplacement. A register-indirect operand with no displacement (for example plain `[rcx]`) carries no
109 // constant to read.
110
3/4
✓ Branch 50 → 51 taken 6 times.
✗ Branch 50 → 52 not taken.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 55 taken 5 times.
6 if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || !operand.mem.disp.has_displacement)
111 {
112 1 return std::unexpected(ResolveError::UnexpectedShape);
113 }
114
115
2/2
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 64 taken 4 times.
5 if (operand.mem.base == ZYDIS_REGISTER_RIP)
116 {
117 // RIP-relative: the raw displacement is measured from the next
118 // instruction, not the absolute constant the caller wants. Resolve it to the absolute target so the return
119 // value is meaningful rather than a misleading relative offset.
120 1 ZyanU64 absolute = 0;
121
2/4
✓ Branch 56 → 57 taken 1 time.
✗ Branch 56 → 70 not taken.
✗ Branch 57 → 58 not taken.
✓ Branch 57 → 61 taken 1 time.
1 if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(site), &absolute)))
122 {
123 return std::unexpected(ResolveError::DecodeFailed);
124 }
125 1 return static_cast<std::int64_t>(absolute);
126 }
127
128 // disp.value is already 64-bit sign-extended.
129 4 return narrow_signed(static_cast<std::int64_t>(operand.mem.disp.value), cc.byte_width);
130 }
131 } // namespace DetourModKit
132