418 lines
20 KiB
C++
418 lines
20 KiB
C++
/**
|
|
* @file Architecture.cpp
|
|
* @brief Per-architecture jump-patch backends for x86, x86-64, ARM64, and ARM32 (both the ARM
|
|
* and Thumb-2 instruction sets).
|
|
*/
|
|
#include "Architecture.hpp"
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
/* WHY THERE CAN BE A "LANDING PAD" IN FRONT OF A FUNCTION
|
|
* -----------------------------------------------------------
|
|
* Modern toolchains can emit a marker instruction as the very first
|
|
* instruction of a function, required by a CPU-level indirect-branch
|
|
* protection feature: x86 CET ("Control-flow Enforcement Technology",
|
|
* ENDBR64/ENDBR32) and ARM64 BTI ("Branch Target Identification").
|
|
* When the CPU enforces either, an indirect call/jump (which is
|
|
* exactly how a call through a function pointer, or a call across a
|
|
* shared-library boundary via the PLT/GOT, actually reaches a
|
|
* function) is only allowed to land on one of these marker
|
|
* instructions -- landing anywhere else raises a fault.
|
|
*
|
|
* This matters here because it is genuinely on by default in
|
|
* mainstream environments, not a rare opt-in: this exact toolchain
|
|
* (Ubuntu 24.04's system GCC 13, x86-64) emits ENDBR64 at every
|
|
* function's entry with no extra flags beyond -fpatchable-function-entry,
|
|
* confirmed empirically while building this library --
|
|
* `gcc -O2 -fpatchable-function-entry=16,0` alone was enough to produce
|
|
* an `endbr64` as the very first instruction, ahead of the requested
|
|
* NOP padding. Overwriting it unconditionally would silently work on
|
|
* a CPU/kernel combination that does not enforce IBT and silently
|
|
* crash on one that does -- exactly the kind of environment-dependent
|
|
* failure this library exists to avoid.
|
|
*
|
|
* So every backend below first checks the live bytes at the function's
|
|
* entry for its architecture's landing-pad encoding and, if present,
|
|
* treats it as a fixed, immovable prefix: the jump is written
|
|
* immediately after it rather than over it. Since a landing pad is
|
|
* defined to otherwise behave as a NOP, falling through it into our
|
|
* jump is always correct whether or not the CPU actually enforces the
|
|
* protection it marks.
|
|
*
|
|
* WRITE ORDERING (AND WHAT IT DOES / DOES NOT PROTECT AGAINST)
|
|
* -----------------------------------------------------------
|
|
* Every backend below writes the target-address bytes first and the
|
|
* leading opcode/instruction word that "arms" the jump last, with a
|
|
* release fence in between. This protects the realistic case this
|
|
* library is designed for: hot_reload_reload() runs at a quiescent
|
|
* point (e.g. your main thread between frames) where nothing is
|
|
* actively calling INTO the function while it is being patched, and
|
|
* this ordering guarantees that any call arriving right at that
|
|
* boundary sees either the fully-old or the fully-new jump, never a
|
|
* target address that is half-old-half-new. It does NOT make the
|
|
* write safe against a thread that is concurrently, actively
|
|
* executing through the exact bytes being patched -- doing that in
|
|
* general on a variable-length ISA needs a signal-handler-based retry
|
|
* protocol (a much bigger mechanism, well beyond a dev-loop tool's
|
|
* requirements). See the "Limitations" section of the top-level README
|
|
* for this precondition stated plainly.
|
|
*/
|
|
|
|
namespace hot_reload::detail
|
|
{
|
|
namespace
|
|
{
|
|
#if defined(__x86_64__) || defined(_M_X64)
|
|
|
|
/// Size in bytes of the CET landing pad (`endbr64`) a function may start with.
|
|
constexpr std::size_t kLandingPadBytes = 4;
|
|
|
|
/// Encoding of `endbr64`, the x86-64 CET indirect-branch landing pad.
|
|
constexpr unsigned char kEndbr64[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFA};
|
|
|
|
/// Size in bytes of the jump written over a function's entry:
|
|
/// `jmp qword ptr [rip+0]` followed by an 8-byte absolute address. That reaches any
|
|
/// 64-bit address regardless of how far apart the old and new modules were loaded,
|
|
/// unlike a 5-byte relative jump (`E9 rel32`) which only reaches +/-2GB. Verified
|
|
/// byte-for-byte against `as`/objdump output while building this library.
|
|
constexpr std::size_t kJumpBytes = 14;
|
|
|
|
/// Whether the function at @p entry starts with an `endbr64` landing pad.
|
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
|
/// @return True if the first bytes are `endbr64`.
|
|
bool hasLandingPad(const unsigned char* entry)
|
|
{
|
|
return std::memcmp(entry, kEndbr64, kLandingPadBytes) == 0;
|
|
}
|
|
|
|
/// Writes the x86-64 absolute jump at @p site, payload first and the opcode last.
|
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
|
/// @param target Address the jump transfers control to.
|
|
void writeJumpAt(unsigned char* site, const void* target)
|
|
{
|
|
std::uint64_t targetValue = 0;
|
|
std::memcpy(&targetValue, &target, sizeof(target));
|
|
|
|
// Payload first: the absolute address (site+6) and the
|
|
// constant zero rip-displacement (site+2) are only ever
|
|
// consumed as DATA once the opcode below is in place, so
|
|
// writing them early cannot be misinterpreted as a
|
|
// different, fresh instruction stream by anything that
|
|
// might fall through this region in its old, all-NOP state.
|
|
std::memcpy(site + 6, &targetValue, sizeof(targetValue));
|
|
std::memcpy(site + 2, "\x00\x00\x00\x00", 4);
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
|
|
// Arm last: until these 2 bytes land, site+0 still reads as
|
|
// whatever it was (a NOP, on first patch; a previous FF 25
|
|
// on a later reload -- either way harmless/inert).
|
|
unsigned char opcode[2] = {0xFF, 0x25};
|
|
std::memcpy(site, opcode, sizeof(opcode));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
}
|
|
|
|
#elif defined(__i386__) || defined(_M_IX86)
|
|
|
|
/// Size in bytes of the CET landing pad (`endbr32`) a function may start with.
|
|
constexpr std::size_t kLandingPadBytes = 4;
|
|
|
|
/// Encoding of `endbr32`, the 32-bit x86 CET indirect-branch landing pad.
|
|
constexpr unsigned char kEndbr32[kLandingPadBytes] = {0xF3, 0x0F, 0x1E, 0xFB};
|
|
|
|
/// Size in bytes of the jump written over a function's entry: `e9 rel32`, a plain
|
|
/// relative near jump. On 32-bit x86 the entire address space fits in the signed 32-bit
|
|
/// displacement (mod 2^32 wraparound gives the right answer regardless of "distance"),
|
|
/// so unlike x86-64 there is no need for an absolute/indirect form here.
|
|
constexpr std::size_t kJumpBytes = 5;
|
|
|
|
/// Whether the function at @p entry starts with an `endbr32` landing pad.
|
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
|
/// @return True if the first bytes are `endbr32`.
|
|
bool hasLandingPad(const unsigned char* entry)
|
|
{
|
|
return std::memcmp(entry, kEndbr32, kLandingPadBytes) == 0;
|
|
}
|
|
|
|
/// Writes the x86 relative jump at @p site, displacement first and the opcode last.
|
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
|
/// @param target Address the jump transfers control to.
|
|
void writeJumpAt(unsigned char* site, const void* target)
|
|
{
|
|
std::int32_t rel32 = static_cast<std::int32_t>(
|
|
reinterpret_cast<std::intptr_t>(target) - reinterpret_cast<std::intptr_t>(site) - 5);
|
|
|
|
// Payload (the displacement) first, opcode byte last --
|
|
// same reasoning as the x86-64 backend above.
|
|
std::memcpy(site + 1, &rel32, sizeof(rel32));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
site[0] = 0xE9;
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
}
|
|
|
|
#elif defined(__aarch64__) || defined(_M_ARM64)
|
|
|
|
/// Size in bytes of the BTI landing pad a function may start with (one instruction word).
|
|
constexpr std::size_t kLandingPadBytes = 4;
|
|
|
|
/// Whether the instruction word at @p entry is any BTI variant (plain/C/J/JC): bits
|
|
/// [31:6] are fixed at 0b010101000000001001001, differing only in bits [6:5] (the
|
|
/// qualifier) -- equivalently, the low byte is one of 0x1F/0x5F/0x9F/0xDF and the next 3
|
|
/// bytes are always 24 03 D5. Verified with `as`/objdump for all 4 variants while
|
|
/// building this library.
|
|
/// @param entry The function's entry address (at least 4 readable bytes).
|
|
/// @return True if the first instruction is a BTI landing pad.
|
|
bool isBti(const unsigned char* entry)
|
|
{
|
|
return (entry[0] & 0x3Fu) == 0x1Fu && entry[1] == 0x24 && entry[2] == 0x03 && entry[3] == 0xD5;
|
|
}
|
|
|
|
/// Whether the function at @p entry starts with a BTI landing pad.
|
|
/// @param entry The function's entry address (at least kLandingPadBytes readable bytes).
|
|
/// @return True if the first instruction is a BTI landing pad.
|
|
bool hasLandingPad(const unsigned char* entry)
|
|
{
|
|
return isBti(entry);
|
|
}
|
|
|
|
/// Size in bytes of the jump written over a function's entry:
|
|
/// `ldr x17, #8 ; br x17 ; <8-byte absolute address>`. x16/x17 are AArch64's designated
|
|
/// "intra-procedure-call" scratch registers (IP0/IP1) -- the AAPCS64 explicitly reserves
|
|
/// them for exactly this kind of linker/veneer-style long branch, so clobbering them at
|
|
/// a function's entry point, before any of its real prologue has executed, is always
|
|
/// safe. Byte values verified with `as`/objdump while building this library.
|
|
constexpr std::size_t kJumpBytes = 16;
|
|
|
|
/// Encoding of `ldr x17, #8` (loads the literal address stored 8 bytes further on).
|
|
constexpr unsigned char kLdrX17Pc8[4] = {0x51, 0x00, 0x00, 0x58};
|
|
|
|
/// Encoding of `br x17` (branches to the address `ldr x17, #8` loaded).
|
|
constexpr unsigned char kBrX17[4] = {0x20, 0x02, 0x1F, 0xD6};
|
|
|
|
/// Writes the ARM64 `ldr`/`br` pair plus literal address at @p site, payload first and
|
|
/// the leading `ldr` last.
|
|
/// @param site Where the jump goes (just past the landing pad, if there is one).
|
|
/// @param target Address the jump transfers control to.
|
|
void writeJumpAt(unsigned char* site, const void* target)
|
|
{
|
|
std::uint64_t targetValue = 0;
|
|
std::memcpy(&targetValue, &target, sizeof(target));
|
|
|
|
// Payload (the literal address) first. ARM64 instructions
|
|
// are fixed-width and naturally aligned, so -- unlike the
|
|
// variable-length x86 case -- each 4-byte slot here is
|
|
// unambiguous: it is either the old NOP, the old
|
|
// instruction, or the new one, never a byte-misaligned
|
|
// mix. Writing high-to-low keeps the two real instructions
|
|
// (which is what any in-flight fall-through NOP execution
|
|
// could actually reach) inert for as long as possible.
|
|
std::memcpy(site + 8, &targetValue, sizeof(targetValue));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
std::memcpy(site + 4, kBrX17, sizeof(kBrX17));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
|
|
// Arm last: until this word lands, site+0 still reads as a
|
|
// NOP (or a previous ldr, on a later reload) and execution
|
|
// never reaches the br at site+4 via fall-through.
|
|
std::memcpy(site, kLdrX17Pc8, sizeof(kLdrX17Pc8));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
}
|
|
|
|
#elif defined(__arm__) || defined(_M_ARM)
|
|
|
|
/* 32-bit ARM is a genuinely different shape from the three backends above: every
|
|
* other architecture here picks one instruction encoding and a single fixed
|
|
* kJumpBytes. ARM32 cannot, for a reason specific to this architecture: a CPU in
|
|
* this family is always in one of two incompatible instruction-decoding states, ARM
|
|
* (fixed 4-byte instructions) or Thumb (2-byte and 4-byte Thumb-2 instructions), and
|
|
* a function's own compiled state -- never a free choice this library gets to make,
|
|
* only something to detect and match -- decides which encoding is even legal to
|
|
* write at its address. The ARM EABI records this on every function symbol: bit 0 of
|
|
* its value is set if and only if the function is Thumb code (so that loading the
|
|
* symbol's own value into pc, exactly what this backend's jump does, switches the
|
|
* CPU to the right state automatically) -- SymbolTable.cpp's `loadedBase + st_value`
|
|
* already preserves that bit unmodified for every architecture, so detecting the
|
|
* mode here is as simple as reading it back off the address this backend is handed.
|
|
*
|
|
* A second, smaller wrinkle is specific to Thumb state alone: its `ldr pc, [...]`
|
|
* literal-load form can only read from a 4-byte-aligned address, computed from the
|
|
* instruction's OWN address rounded down to 4 bytes -- but a Thumb function is only
|
|
* ever guaranteed 2-byte alignment, not 4 (confirmed directly: building a plugin
|
|
* with -falign-functions=1 reliably produces functions starting 2 bytes off a
|
|
* 4-byte boundary). So Thumb state alone needs a one-instruction fork: a 2-byte NOP
|
|
* realigns first when the entry isn't already on a 4-byte boundary, nothing extra
|
|
* when it already is. ARM state needs no such fork -- that state requires 4-byte
|
|
* alignment architecture-wide, so every ARM-mode function already has it.
|
|
*
|
|
* No landing pad exists on this architecture in any mainstream toolchain or CPU
|
|
* extension in common use (unlike x86 CET or ARM64 BTI) -- confirmed directly:
|
|
* -fpatchable-function-entry's reserved padding here is plain NOPs in both states,
|
|
* nothing else, so there is never anything to detect or preserve in front of a
|
|
* function's real body.
|
|
*
|
|
* Every byte sequence below was derived the same way as the other three backends:
|
|
* assembled with `as` from the exact mnemonics named in each comment, and the
|
|
* resulting bytes read back with objdump -- including, for the Thumb realignment
|
|
* case specifically, assembling it at both a 4-byte-aligned and a 2-mod-4 address
|
|
* and confirming objdump's own computed literal-load target lands exactly on the
|
|
* embedded address both times.
|
|
*/
|
|
|
|
/// No landing pad exists on this architecture (see the file comment above), so unlike
|
|
/// the three backends above, there is no hasLandingPad()/kLandingPadBytes pair here:
|
|
/// the outer planPatch() below sets PatchLayout::patchOffset to 0 directly instead.
|
|
|
|
/// @param address A function's address as this library carries it: `loadedBase +
|
|
/// st_value`, Thumb's mode bit (bit 0) included unmodified.
|
|
/// @return True if @p address names Thumb code, per the ARM EABI convention.
|
|
bool isThumb(const void* address)
|
|
{
|
|
return (reinterpret_cast<std::uintptr_t>(address) & std::uintptr_t{1}) != 0;
|
|
}
|
|
|
|
/// @param address A function's address, Thumb mode bit possibly included.
|
|
/// @return The real, even byte address of its first instruction -- bit 0 is a mode
|
|
/// marker on this architecture, never part of the actual memory location.
|
|
unsigned char* realAddress(const void* address)
|
|
{
|
|
return reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(address) & ~std::uintptr_t{1});
|
|
}
|
|
|
|
/// `ldr pc, [pc, #-4]`: ARM state's literal-load jump. ARM state is always 4-byte
|
|
/// aligned architecture-wide, so this one fixed form always applies; the embedded
|
|
/// address follows immediately, 8 bytes total.
|
|
constexpr unsigned char kArmJump[4] = {0x04, 0xF0, 0x1F, 0xE5};
|
|
|
|
/// `ldr.w pc, [pc, #0]`: Thumb-2's literal-load jump, valid only when this
|
|
/// instruction's own address is 4-byte aligned (see the file comment above); the
|
|
/// embedded address follows immediately, 8 bytes total from wherever this is placed.
|
|
constexpr unsigned char kThumbJump[4] = {0xDF, 0xF8, 0x00, 0xF0};
|
|
|
|
/// The Thumb-2 `nop` hint instruction -- matches the padding GCC's own
|
|
/// -fpatchable-function-entry emits on this target -- used only to realign kThumbJump
|
|
/// onto a 4-byte boundary when the function's own entry isn't already on one.
|
|
constexpr unsigned char kThumbNop[2] = {0x00, 0xBF};
|
|
|
|
/// @param entryAddress The function's entry address, Thumb bit included.
|
|
/// @return 8 for ARM state or an already-4-byte-aligned Thumb entry; 10 for a Thumb
|
|
/// entry starting 2 bytes off a 4-byte boundary (room for the one extra
|
|
/// realigning NOP in front of kThumbJump -- see the file comment above).
|
|
/// Unlike every other backend in this file, this one genuinely depends on
|
|
/// the entry address itself, not just the architecture.
|
|
std::size_t jumpBytes(const void* entryAddress)
|
|
{
|
|
if (!isThumb(entryAddress))
|
|
{
|
|
return 8;
|
|
}
|
|
bool misaligned = (reinterpret_cast<std::uintptr_t>(realAddress(entryAddress)) & std::uintptr_t{2}) != 0;
|
|
return misaligned ? 10 : 8;
|
|
}
|
|
|
|
/// Writes the ARM32 jump at @p site: the ARM-state form if @p site is ARM code, the
|
|
/// Thumb-2 form (with a realigning NOP first, if needed) if it is Thumb -- @p site's
|
|
/// own bit 0 says which, exactly as isThumb() reads it, and there is no separate
|
|
/// landing pad to skip past on this architecture (see hasLandingPad()), so @p site is
|
|
/// always the function's own entry address, unmodified.
|
|
/// @param site Where the jump goes.
|
|
/// @param target Address the jump transfers control to, Thumb bit preserved so the
|
|
/// `ldr` that loads it into pc correctly switches state at the destination,
|
|
/// too, exactly as calling this address normally would.
|
|
void writeJumpAt(const void* site, const void* target)
|
|
{
|
|
std::uint32_t targetValue = static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(target));
|
|
unsigned char* bytes = realAddress(site);
|
|
|
|
if (!isThumb(site))
|
|
{
|
|
// Payload first, opcode last -- same reasoning as every other backend above.
|
|
std::memcpy(bytes + 4, &targetValue, sizeof(targetValue));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
std::memcpy(bytes, kArmJump, sizeof(kArmJump));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
return;
|
|
}
|
|
|
|
bool misaligned = (reinterpret_cast<std::uintptr_t>(bytes) & std::uintptr_t{2}) != 0;
|
|
unsigned char* jumpAt = misaligned ? bytes + 2 : bytes;
|
|
|
|
// The realigning NOP, if there is one, is pure filler -- inert whether a reader
|
|
// sees it before or after this write, and byte-for-byte identical on every
|
|
// reload of this same (fixed) address, so it is written first, unconditionally,
|
|
// with no fence of its own: it never participates in the old-jump/new-jump
|
|
// transition the fences below protect.
|
|
if (misaligned)
|
|
{
|
|
std::memcpy(bytes, kThumbNop, sizeof(kThumbNop));
|
|
}
|
|
|
|
// Payload next, opcode last -- same reasoning as every other backend above: until
|
|
// kThumbJump lands, jumpAt still reads as whatever it was (old NOP padding, or a
|
|
// previous ldr.w, on a later reload), and the fully-written literal sitting
|
|
// ahead of it is inert until something actually loads it.
|
|
std::memcpy(jumpAt + 4, &targetValue, sizeof(targetValue));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
std::memcpy(jumpAt, kThumbJump, sizeof(kThumbJump));
|
|
std::atomic_thread_fence(std::memory_order_release);
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
bool isArchitectureSupported()
|
|
{
|
|
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
|
defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
PatchLayout planPatch(const void* entryAddress)
|
|
{
|
|
#if defined(__arm__) || defined(_M_ARM)
|
|
// ARM32's own kJumpBytes genuinely depends on entryAddress (ARM state vs. Thumb state,
|
|
// and for Thumb, alignment) -- see Architecture.cpp's ARM32 file comment above -- so it
|
|
// cannot share the single-constant shape the other three architectures below do; there
|
|
// is never a landing pad to skip past on this architecture either (hasLandingPad() is
|
|
// always false), so patchOffset is always 0.
|
|
return PatchLayout{jumpBytes(entryAddress), 0};
|
|
#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
|
defined(_M_ARM64)
|
|
const unsigned char* entry = static_cast<const unsigned char*>(entryAddress);
|
|
PatchLayout layout;
|
|
layout.patchOffset = hasLandingPad(entry) ? kLandingPadBytes : 0;
|
|
layout.totalBytes = layout.patchOffset + kJumpBytes;
|
|
return layout;
|
|
#else
|
|
(void) entryAddress;
|
|
return PatchLayout{};
|
|
#endif
|
|
}
|
|
|
|
bool writeJump(void* entryAddress, const void* targetAddress)
|
|
{
|
|
#if defined(__arm__) || defined(_M_ARM)
|
|
// entryAddress is passed through unmodified (not cast to unsigned char* the way the
|
|
// generic path below does): its own bit 0, the Thumb/ARM mode marker, is exactly what
|
|
// writeJumpAt() needs to pick the right encoding -- see Architecture.cpp's ARM32 file
|
|
// comment above. There is no landing pad on this architecture to offset past first.
|
|
writeJumpAt(entryAddress, targetAddress);
|
|
return true;
|
|
#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
|
defined(_M_ARM64)
|
|
PatchLayout layout = planPatch(entryAddress);
|
|
unsigned char* site = static_cast<unsigned char*>(entryAddress) + layout.patchOffset;
|
|
writeJumpAt(site, targetAddress);
|
|
return true;
|
|
#else
|
|
(void) entryAddress;
|
|
(void) targetAddress;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
}
|