Added ARM32 support
This commit is contained in:
1 parent
df792729ef
commit
1212cb9dbe
36 files changed
+2381
-783
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @file Architecture.cpp
|
||||
* @brief Per-architecture jump-patch backends for x86, x86-64, and ARM64.
|
||||
* @brief Per-architecture jump-patch backends for x86, x86-64, ARM64, and ARM32 (both the ARM
|
||||
* and Thumb-2 instruction sets).
|
||||
*/
|
||||
#include "Architecture.hpp"
|
||||
|
||||
@@ -218,13 +219,151 @@ namespace hot_reload::detail
|
||||
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(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
@@ -233,7 +372,14 @@ namespace hot_reload::detail
|
||||
|
||||
PatchLayout planPatch(const void* entryAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
#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;
|
||||
@@ -248,7 +394,14 @@ namespace hot_reload::detail
|
||||
|
||||
bool writeJump(void* entryAddress, const void* targetAddress)
|
||||
{
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__) || \
|
||||
#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;
|
||||
|
||||
Reference in new issue
Block a user