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 |