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 |