Fix documentation
This commit is contained in:
1 parent
a317c07124
commit
ab21b3f30d
27 files changed
+2137
-1043
No files matched your search
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file Architecture.cpp
|
||||
* @brief Per-architecture jump-patch backends for x86, x86-64, and ARM64.
|
||||
*/
|
||||
#include "Architecture.hpp"
|
||||
|
||||
#include <atomic>
|
||||
@@ -51,7 +55,8 @@
|
||||
* 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 README for this precondition stated plainly.
|
||||
* requirements). See the "Limitations" section of the top-level README
|
||||
* for this precondition stated plainly.
|
||||
*/
|
||||
|
||||
namespace hot_reload::detail
|
||||
@@ -60,20 +65,30 @@ namespace hot_reload::detail
|
||||
{
|
||||
#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};
|
||||
// jmp qword ptr [rip+0]; <8-byte absolute address> -- 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.
|
||||
|
||||
/// 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;
|
||||
@@ -99,20 +114,29 @@ namespace hot_reload::detail
|
||||
|
||||
#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};
|
||||
// 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.
|
||||
|
||||
/// 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>(
|
||||
@@ -128,35 +152,47 @@ namespace hot_reload::detail
|
||||
|
||||
#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;
|
||||
|
||||
// Matches 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.
|
||||
/// 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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
/// 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;
|
||||
|
||||
@@ -2,56 +2,91 @@
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
/**
|
||||
* @namespace hot_reload::detail
|
||||
* @brief Internal building blocks of the hot-reload runtime -- not part of the public API.
|
||||
*
|
||||
* The jump-patch backends (`Architecture.hpp`), the page-protection helper
|
||||
* (`MemoryProtection.hpp`), the loader wrapper (`ModuleHandle.hpp`) and the symbol-table readers
|
||||
* (`SymbolTable.hpp`) all live here. They are implementation details of
|
||||
* hot_reload::HotReloadContext and may change freely; the supported surface is the C API in
|
||||
* `hot_reload.h`.
|
||||
*/
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// What planPatch() found at one specific function's entry point --
|
||||
/// how many bytes from the entry address must be safe to overwrite,
|
||||
/// and how many of those (if any) are a landing pad that must be
|
||||
/// preserved rather than overwritten. See Architecture.cpp's file
|
||||
/// comment for why a landing pad can be present at all.
|
||||
/**
|
||||
* @brief What planPatch() found at one specific function's entry point.
|
||||
*
|
||||
* How many bytes from the entry address must be safe to overwrite, and
|
||||
* how many of those (if any) are a landing pad that must be preserved
|
||||
* rather than overwritten. See `Architecture.cpp`'s file comment for
|
||||
* why a landing pad can be present at all.
|
||||
*/
|
||||
struct PatchLayout
|
||||
{
|
||||
/// Total bytes from the function's entry address that this
|
||||
/// patch touches, landing pad included -- compare this against
|
||||
/// the symbol's compiled size before writing anything.
|
||||
/**
|
||||
* Total bytes from the function's entry address that this patch
|
||||
* touches, landing pad included -- compare this against the
|
||||
* symbol's compiled size before writing anything.
|
||||
*/
|
||||
std::size_t totalBytes = 0;
|
||||
|
||||
/// How many leading bytes are a landing pad and must be left
|
||||
/// untouched; the jump itself is written starting at
|
||||
/// entryAddress + patchOffset.
|
||||
/**
|
||||
* How many leading bytes are a landing pad and must be left
|
||||
* untouched; the jump itself is written starting at
|
||||
* `entryAddress + patchOffset`.
|
||||
*/
|
||||
std::size_t patchOffset = 0;
|
||||
};
|
||||
|
||||
/// True if this build has a jump-patch backend for the CPU
|
||||
/// architecture it was compiled for (decided at compile time --
|
||||
/// see the top of Architecture.cpp). x86, x86-64, and ARM64 are
|
||||
/// supported; anything else (including 32-bit ARM, whose ARM/Thumb
|
||||
/// interworking this library does not attempt -- see the README)
|
||||
/// returns false here, and HotReloadContext surfaces that as
|
||||
/// HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
||||
/**
|
||||
* @brief Whether this build has a jump-patch backend for the current CPU architecture.
|
||||
*
|
||||
* Decided at compile time -- see the top of `Architecture.cpp`. x86,
|
||||
* x86-64, and ARM64 are supported; anything else (including 32-bit
|
||||
* ARM, whose ARM/Thumb interworking this library does not attempt --
|
||||
* see the "Scope" section of `source/HotReload/README.md`) returns false here, and
|
||||
* `HotReloadContext` surfaces that as
|
||||
* ::HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE.
|
||||
*
|
||||
* @return True if writeJump() can succeed on this architecture.
|
||||
*/
|
||||
bool isArchitectureSupported();
|
||||
|
||||
/// Inspects the bytes already sitting at entryAddress to determine
|
||||
/// the real patch layout for THIS function, accounting for a
|
||||
/// possible leading landing pad. Safe to call even when
|
||||
/// isArchitectureSupported() is false (returns a zeroed PatchLayout).
|
||||
/**
|
||||
* @brief Determines the real patch layout for one specific function.
|
||||
*
|
||||
* Inspects the bytes already sitting at @p entryAddress, accounting
|
||||
* for a possible leading landing pad. Safe to call even when
|
||||
* isArchitectureSupported() is false (returns a zeroed PatchLayout).
|
||||
*
|
||||
* @param entryAddress The function's entry address.
|
||||
* @return The layout to check against the symbol's compiled size
|
||||
* before writing anything, and to pass to writeJump().
|
||||
*/
|
||||
PatchLayout planPatch(const void* entryAddress);
|
||||
|
||||
/// Overwrites entryAddress so that control transfers unconditionally
|
||||
/// to targetAddress, following the layout planPatch() would return
|
||||
/// for this same entryAddress. The caller must already have made
|
||||
/// [entryAddress, entryAddress + planPatch(entryAddress).totalBytes)
|
||||
/// writable (see MemoryProtection.hpp) and must flush the
|
||||
/// instruction cache over that same range afterwards -- this
|
||||
/// function does neither.
|
||||
///
|
||||
/// Only meaningful precondition: the caller has already checked the
|
||||
/// target symbol's compiled size against planPatch(entryAddress)
|
||||
/// .totalBytes; this function does not re-check it and will happily
|
||||
/// overwrite whatever comes next in memory if asked to.
|
||||
///
|
||||
/// Returns false only if isArchitectureSupported() is false.
|
||||
/**
|
||||
* @brief Overwrites a function's entry so it jumps to another address.
|
||||
*
|
||||
* Follows the layout planPatch() would return for this same
|
||||
* @p entryAddress.
|
||||
*
|
||||
* @param entryAddress The function entry to overwrite.
|
||||
* @param targetAddress Where control should transfer to instead.
|
||||
* @return False only if isArchitectureSupported() is false.
|
||||
*
|
||||
* @pre The caller has already made
|
||||
* `[entryAddress, entryAddress + planPatch(entryAddress).totalBytes)`
|
||||
* writable (see `MemoryProtection.hpp`) and will flush the
|
||||
* instruction cache over that same range afterwards -- this
|
||||
* function does neither.
|
||||
* @pre The caller has already checked the target symbol's compiled
|
||||
* size against `planPatch(entryAddress).totalBytes`; this
|
||||
* function does not re-check it and will happily overwrite
|
||||
* whatever comes next in memory if asked to.
|
||||
*/
|
||||
bool writeJump(void* entryAddress, const void* targetAddress);
|
||||
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file HotReloadContext.cpp
|
||||
* @brief The diff-and-patch orchestration logic: load, reload, and per-symbol policy.
|
||||
*/
|
||||
#include "HotReloadContext.hpp"
|
||||
|
||||
#include "Architecture.hpp"
|
||||
@@ -12,6 +16,12 @@ namespace hot_reload
|
||||
{
|
||||
namespace
|
||||
{
|
||||
/// Copies a file byte for byte, overwriting the destination.
|
||||
/// @param from The file to read.
|
||||
/// @param to The file to create (or truncate).
|
||||
/// @return True if the whole copy succeeded. An empty source counts as a failure (inserting
|
||||
/// zero characters into an output stream sets its failbit) -- harmless, since an
|
||||
/// empty file is never a loadable module anyway.
|
||||
bool copyFile(const std::string& from, const std::string& to)
|
||||
{
|
||||
std::ifstream in(from, std::ios::binary);
|
||||
@@ -28,32 +38,67 @@ namespace hot_reload
|
||||
return static_cast<bool>(out);
|
||||
}
|
||||
|
||||
// Best-effort: deletes any staging copy left over from a
|
||||
// PREVIOUS run of this same host/module -- see
|
||||
// stagePathForLoad()'s own comment for why those copies exist
|
||||
// at all. On POSIX this normally finds nothing (an active
|
||||
// run's own copies are already gone by the time it makes its
|
||||
// next one -- see the std::remove() calls after each
|
||||
// ModuleHandle::load() below). On Windows, a copy backing a
|
||||
// still-loaded module cannot be deleted for as long as that
|
||||
// module stays mapped -- which, by this library's own design,
|
||||
// is for the rest of the CURRENT process's lifetime (see
|
||||
// HotReloadContext.hpp's LoadedModule comment) -- so within a
|
||||
// single long-running session some accumulation is expected
|
||||
// and unavoidable; this specifically cleans up the PREVIOUS
|
||||
// session's leftovers once that process has exited and
|
||||
// released whatever lock Windows held. Called once, here, at
|
||||
// the first hot_reload_load() for a given name -- not on every
|
||||
// reload() -- since a copy from earlier in the SAME session
|
||||
// would still be locked for the same reason and retrying
|
||||
// wouldn't change that.
|
||||
/// The module name as it appears inside a staging copy's file name.
|
||||
///
|
||||
/// module_name is an arbitrary caller-chosen key (hot_reload.h promises it need not match
|
||||
/// anything on disk), so it may contain characters that are not legal in a file name --
|
||||
/// a '/' in "engine/renderer", say. Everything outside a conservative set is replaced by
|
||||
/// '_'. stagePathForLoad() and cleanupOrphanedStagingFiles() must agree on the result, so
|
||||
/// both call this. Names made only of letters, digits, '-' and '_' are unchanged.
|
||||
///
|
||||
/// @param moduleName The logical module name.
|
||||
/// @return @p moduleName with every character outside `[A-Za-z0-9_-]` replaced by '_'.
|
||||
std::string stagingTag(const std::string& moduleName)
|
||||
{
|
||||
std::string tag = moduleName;
|
||||
for (char& character : tag)
|
||||
{
|
||||
const bool isLower = character >= 'a' && character <= 'z';
|
||||
const bool isUpper = character >= 'A' && character <= 'Z';
|
||||
const bool isDigit = character >= '0' && character <= '9';
|
||||
const bool isDashOrUnderscore = character == '-' || character == '_';
|
||||
const bool safe = isLower || isUpper || isDigit || isDashOrUnderscore;
|
||||
if (!safe)
|
||||
{
|
||||
character = '_';
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/// Best-effort: deletes any staging copy left over from a
|
||||
/// PREVIOUS run of this same host/module -- see
|
||||
/// stagePathForLoad()'s own comment for why those copies exist
|
||||
/// at all.
|
||||
///
|
||||
/// On POSIX this normally finds nothing (an active
|
||||
/// run's own copies are already gone by the time it makes its
|
||||
/// next one -- see the std::remove() calls after each
|
||||
/// ModuleHandle::load() below). On Windows, a copy backing a
|
||||
/// still-loaded module cannot be deleted for as long as that
|
||||
/// module stays mapped -- which, by this library's own design,
|
||||
/// is for as long as the owning context lives, usually the rest
|
||||
/// of the CURRENT process's lifetime (see HotReloadContext.hpp's
|
||||
/// LoadedModule::versions) -- so within a
|
||||
/// single long-running session some accumulation is expected
|
||||
/// and unavoidable; this specifically cleans up the PREVIOUS
|
||||
/// session's leftovers once that process has exited and
|
||||
/// released whatever lock Windows held. Called once, at
|
||||
/// the first hot_reload_load() for a given name -- not on every
|
||||
/// reload() -- since a copy from earlier in the SAME session
|
||||
/// would still be locked for the same reason and retrying
|
||||
/// wouldn't change that.
|
||||
///
|
||||
/// @param sourcePath The path the module is being loaded from; the copies live next to it.
|
||||
/// @param moduleName The module's logical name.
|
||||
void cleanupOrphanedStagingFiles(const std::string& sourcePath, const std::string& moduleName)
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path source(sourcePath);
|
||||
fs::path dir = source.has_parent_path() ? source.parent_path() : fs::path(".");
|
||||
std::string prefix = source.filename().string() + ".hotreload." + moduleName + ".";
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
std::string prefix = source.filename().string() + ".hotreload." + tag + ".";
|
||||
|
||||
std::error_code dirError;
|
||||
fs::directory_iterator entries(dir, dirError);
|
||||
@@ -120,7 +165,9 @@ namespace hot_reload
|
||||
// name guaranteed never reused sidesteps it regardless of how
|
||||
// the build produced sourcePath.
|
||||
++module.reloadCounter;
|
||||
std::string staged = sourcePath + ".hotreload." + moduleName + "." + std::to_string(module.reloadCounter);
|
||||
const std::string tag = stagingTag(moduleName);
|
||||
const std::string counter = std::to_string(module.reloadCounter);
|
||||
std::string staged = sourcePath + ".hotreload." + tag + "." + counter;
|
||||
if (!copyFile(sourcePath, staged))
|
||||
{
|
||||
return {};
|
||||
|
||||
@@ -8,62 +8,160 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* @namespace hot_reload
|
||||
* @brief The C++ implementation behind the C API declared in `hot_reload.h`.
|
||||
*
|
||||
* The one type meant to be used from outside is HotReloadContext, and even that only through the
|
||||
* opaque ::HotReloadContext pointer the C API hands out. The building blocks it is made of live in
|
||||
* hot_reload::detail.
|
||||
*/
|
||||
namespace hot_reload
|
||||
{
|
||||
|
||||
/// The real type behind the opaque ::HotReloadContext the public C
|
||||
/// API hands out -- see HotReloadApi.cpp for the extern "C" shim
|
||||
/// that casts between the two, following the same opaque-pointer
|
||||
/// PIMPL pattern as cpp-style-guide.md's own NetManager example.
|
||||
/**
|
||||
* @brief The real type behind the opaque ::HotReloadContext the public C API hands out.
|
||||
*
|
||||
* See `HotReloadApi.cpp` for the `extern "C"` shim that casts between
|
||||
* the two, following the same opaque-pointer PIMPL pattern as
|
||||
* `cpp-style-guide.md`'s own `NetManager` example.
|
||||
*
|
||||
* A context owns every version of every module loaded through it. Destroying the context
|
||||
* closes all of them, after which any function or data pointer obtained from any of those
|
||||
* modules -- including old, patched entry points that now only jump into a newer version --
|
||||
* is dangling.
|
||||
*
|
||||
* @note Not thread-safe: the class holds no locks, so all calls on one context (and any
|
||||
* reload it performs) must be serialized by the caller.
|
||||
*/
|
||||
class HotReloadContext
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Creates an empty context.
|
||||
* @param options Callbacks and user data to use, copied out immediately; may be null,
|
||||
* meaning "no callbacks".
|
||||
*/
|
||||
explicit HotReloadContext(const HotReloadOptions* options);
|
||||
|
||||
HotReloadContext(const HotReloadContext&) = delete;
|
||||
HotReloadContext& operator=(const HotReloadContext&) = delete;
|
||||
|
||||
/// See hot_reload_load()'s doc comment in hot_reload.h.
|
||||
/**
|
||||
* @brief Implements hot_reload_load().
|
||||
* @param moduleName The logical name to register the module under.
|
||||
* @param path Path to the shared library to load.
|
||||
* @return The status hot_reload_load() reports; see its doc comment in `hot_reload.h`.
|
||||
*/
|
||||
HotReloadStatus load(const std::string& moduleName, const std::string& path);
|
||||
|
||||
/// See hot_reload_reload()'s doc comment in hot_reload.h.
|
||||
/**
|
||||
* @brief Implements hot_reload_reload().
|
||||
* @param moduleName The logical name the module was loaded under.
|
||||
* @param newPath Path to the new build of the shared library.
|
||||
* @return The status hot_reload_reload() reports; see its doc comment in `hot_reload.h`.
|
||||
*/
|
||||
HotReloadStatus reload(const std::string& moduleName, const std::string& newPath);
|
||||
|
||||
/// See hot_reload_get_symbol()'s doc comment in hot_reload.h.
|
||||
/**
|
||||
* @brief Implements hot_reload_get_symbol().
|
||||
* @param moduleName The logical name the module was loaded under.
|
||||
* @param symbolName The symbol's real (possibly mangled) name.
|
||||
* @return The symbol's address in the module's newest build, or null if the module or
|
||||
* symbol is unknown; see hot_reload_get_symbol()'s doc comment in `hot_reload.h`.
|
||||
*/
|
||||
void* getSymbol(const std::string& moduleName, const std::string& symbolName) const;
|
||||
|
||||
/// See hot_reload_last_error()'s doc comment in hot_reload.h.
|
||||
/**
|
||||
* @brief Implements hot_reload_last_error().
|
||||
* @return The platform's message for the most recent load()/reload() failure on this
|
||||
* context; empty if there was none. Valid until the next load()/reload().
|
||||
*/
|
||||
const std::string& lastError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Everything tracked for one logical module name. Has no
|
||||
/// meaning outside HotReloadContext, so it lives here rather
|
||||
/// than in its own header -- see cpp-style-guide.md's carve-out
|
||||
/// for small types with no use outside their owning type.
|
||||
/// Everything tracked for one logical module name. Has no meaning
|
||||
/// outside HotReloadContext, so it lives here rather than in its
|
||||
/// own header -- see cpp-style-guide.md's carve-out for small
|
||||
/// types with no use outside their owning type.
|
||||
struct LoadedModule
|
||||
{
|
||||
// Every version ever loaded for this name, kept alive for
|
||||
// the rest of the process's lifetime: once a function is
|
||||
// patched, anything that already holds its old address
|
||||
// must stay valid memory forever, even though it now holds
|
||||
// nothing but a jump. versions.back() is the active one.
|
||||
/// Every version ever loaded for this name, kept alive for as long as the owning
|
||||
/// context lives (in practice usually the rest of the process): once a function is
|
||||
/// patched, anything that already holds its old address must stay valid memory,
|
||||
/// even though it now holds nothing but a jump. versions.back() is the active one.
|
||||
std::vector<detail::ModuleHandle> versions;
|
||||
|
||||
/// The exported symbols of the newest build (versions.back()), as reported by
|
||||
/// detail::readExportedSymbols(); what the next reload() diffs against.
|
||||
std::vector<detail::ExportedSymbol> currentSymbols;
|
||||
|
||||
/// How many staging copies have been made for this name so far; makes each copy's
|
||||
/// file name unique (see stagePathForLoad()).
|
||||
int reloadCounter = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stages a source file through a fresh, uniquely-named copy.
|
||||
*
|
||||
* Called before a path is ever passed to detail::ModuleHandle::load() -- see the .cpp
|
||||
* definition's comment for why (a dlopen caching gotcha on POSIX; a Windows file-locking
|
||||
* side effect that turns out to be handled by the same fix).
|
||||
*
|
||||
* @param module The module the copy is for; its reloadCounter is advanced.
|
||||
* @param moduleName The module's logical name (used, sanitized, in the copy's file name).
|
||||
* @param sourcePath The file to copy.
|
||||
* @return The copy's path, or an empty string if copying failed.
|
||||
*/
|
||||
std::string stagePathForLoad(LoadedModule& module, const std::string& moduleName, const std::string& sourcePath);
|
||||
|
||||
/**
|
||||
* @brief Redirects one matched function pair, or emits a SKIPPED_* event instead if that
|
||||
* isn't safe.
|
||||
*
|
||||
* Patches @p oldSymbol's entry to jump to @p newSymbol's address; emits exactly one event
|
||||
* describing the outcome either way.
|
||||
*
|
||||
* @param oldSymbol The function in the previous build.
|
||||
* @param newSymbol The same-named function in the new build.
|
||||
*/
|
||||
void patchOne(const detail::ExportedSymbol& oldSymbol, const detail::ExportedSymbol& newSymbol);
|
||||
|
||||
/**
|
||||
* @brief Handles a symbol present in the old build but missing from the new one.
|
||||
*
|
||||
* Emits REMOVED, and first redirects to a caller-supplied stub if
|
||||
* HotReloadOptions::on_deleted_symbol provides one and it's safe to do so.
|
||||
*
|
||||
* @param oldSymbol The symbol the new build no longer exports.
|
||||
*/
|
||||
void handleRemoved(const detail::ExportedSymbol& oldSymbol);
|
||||
|
||||
/**
|
||||
* @brief Invokes HotReloadOptions::on_symbol_event, if one was supplied.
|
||||
* @param kind What happened to the symbol.
|
||||
* @param name The symbol's real (possibly mangled) name.
|
||||
* @param oldAddress The symbol's address in the previous build, or null.
|
||||
* @param newAddress The symbol's address in the new build, or null.
|
||||
*/
|
||||
void emitEvent(HotReloadSymbolEvent kind, const std::string& name, void* oldAddress, void* newAddress);
|
||||
|
||||
/// Every module loaded so far, by logical name.
|
||||
std::unordered_map<std::string, LoadedModule> m_modules;
|
||||
|
||||
/// HotReloadOptions::on_symbol_event, or null.
|
||||
HotReloadSymbolEventFn m_onSymbolEvent;
|
||||
|
||||
/// HotReloadOptions::on_deleted_symbol, or null.
|
||||
HotReloadDeletedSymbolFn m_onDeletedSymbol;
|
||||
|
||||
/// HotReloadOptions::user_data, handed back to both callbacks untouched.
|
||||
void* m_userData;
|
||||
|
||||
/// The message lastError() returns.
|
||||
std::string m_lastError;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file MemoryProtection.cpp
|
||||
* @brief Code-page write-access RAII guard implementation (mprotect/VirtualProtect).
|
||||
*/
|
||||
#include "MemoryProtection.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -20,6 +24,8 @@ namespace hot_reload::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
/// The operating system's virtual-memory page size.
|
||||
/// @return The page size in bytes (4096 if the platform could not report one).
|
||||
std::size_t systemPageSize()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
|
||||
@@ -5,50 +5,89 @@
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// RAII guard: makes [address, address + length) writable for its
|
||||
/// lifetime (the OS only grants permissions in whole pages, so the
|
||||
/// guard rounds out to the enclosing page range internally) and
|
||||
/// restores the original protection when it goes out of scope.
|
||||
///
|
||||
/// Assumes the range started out READ+EXECUTE, as every platform's
|
||||
/// loader maps a .text/code section -- restoring to exactly that,
|
||||
/// rather than querying and restoring whatever the previous
|
||||
/// protection actually was, since POSIX mprotect() has no portable
|
||||
/// way to ask for the current protection of a range and code pages
|
||||
/// are never anything else in practice.
|
||||
/**
|
||||
* @brief RAII guard that temporarily makes a code range writable.
|
||||
*
|
||||
* Makes `[address, address + length)` writable for its lifetime (the
|
||||
* OS only grants permissions in whole pages, so the guard rounds out
|
||||
* to the enclosing page range internally) and restores the original
|
||||
* protection when it goes out of scope.
|
||||
*
|
||||
* @note Assumes the range started out READ+EXECUTE, as every
|
||||
* platform's loader maps a `.text`/code section -- restoring to
|
||||
* exactly that, rather than querying and restoring whatever the
|
||||
* previous protection actually was, since POSIX `mprotect()`
|
||||
* has no portable way to ask for the current protection of a
|
||||
* range and code pages are never anything else in practice.
|
||||
*
|
||||
* @note The change applies to whole pages and is process-wide: every thread sees the
|
||||
* enclosing pages as writable (and still executable) while the guard is alive.
|
||||
*/
|
||||
class ScopedCodeWriteAccess
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Attempts to make a range writable immediately.
|
||||
*
|
||||
* Check isValid() afterwards: the platform call can be refused (for example by a
|
||||
* hardened environment that denies writable+executable transitions).
|
||||
*
|
||||
* @param address Start of the range.
|
||||
* @param length Length of the range in bytes.
|
||||
*/
|
||||
ScopedCodeWriteAccess(void* address, std::size_t length);
|
||||
|
||||
/**
|
||||
* @brief Restores the range's original protection (does nothing if isValid() is false).
|
||||
*/
|
||||
~ScopedCodeWriteAccess();
|
||||
|
||||
ScopedCodeWriteAccess(const ScopedCodeWriteAccess&) = delete;
|
||||
ScopedCodeWriteAccess& operator=(const ScopedCodeWriteAccess&) = delete;
|
||||
|
||||
/// False if the platform call to relax protection failed --
|
||||
/// when false, nothing was changed and the range must not be
|
||||
/// written to (the destructor is still safe to run either way).
|
||||
/**
|
||||
* @brief Whether the platform call to relax protection succeeded.
|
||||
*
|
||||
* When false, nothing was changed and the range must not be
|
||||
* written to (the destructor is still safe to run either way).
|
||||
*
|
||||
* @return True if the range is currently writable.
|
||||
*/
|
||||
bool isValid() const
|
||||
{
|
||||
return m_valid;
|
||||
}
|
||||
|
||||
/// Call exactly once, after writing the new bytes and before
|
||||
/// this guard is destroyed: flushes the instruction cache over
|
||||
/// the original [address, address + length) range so every
|
||||
/// core sees the new instructions rather than a stale fetch
|
||||
/// (required on ARM64, where instruction and data caches are
|
||||
/// not automatically coherent; effectively a formality on
|
||||
/// x86/x86-64, called anyway for correctness on that
|
||||
/// architecture too).
|
||||
/**
|
||||
* @brief Flushes the instruction cache over the patched range.
|
||||
*
|
||||
* Call exactly once, after writing the new bytes and before this
|
||||
* guard is destroyed, so every core sees the new instructions
|
||||
* rather than a stale fetch (required on ARM64, where instruction
|
||||
* and data caches are not automatically coherent; effectively a
|
||||
* formality on x86/x86-64, called anyway for correctness on that
|
||||
* architecture too).
|
||||
*/
|
||||
void finishWrite();
|
||||
|
||||
private:
|
||||
/// Start of the page-aligned region whose protection was changed.
|
||||
void* m_pageAddress;
|
||||
|
||||
/// Length in bytes of that page-aligned region (a whole number of pages).
|
||||
std::size_t m_pageLength;
|
||||
|
||||
/// Start of the range the caller actually asked to write to.
|
||||
void* m_rangeAddress;
|
||||
|
||||
/// Length in bytes of the range the caller actually asked to write to.
|
||||
std::size_t m_rangeLength;
|
||||
|
||||
/// The protection to restore on destruction, in the platform's own encoding
|
||||
/// (`PROT_*` flags on POSIX, a `PAGE_*` constant on Windows).
|
||||
unsigned long m_previousProtection;
|
||||
|
||||
/// Whether the constructor's protection change succeeded.
|
||||
bool m_valid;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* @file ModuleHandle.cpp
|
||||
* @brief RAII dlopen()/LoadLibrary() wrapper implementation.
|
||||
*/
|
||||
#include "ModuleHandle.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#if defined(_WIN32)
|
||||
// NOMINMAX: see MemoryProtection.cpp's identical comment -- same
|
||||
@@ -23,9 +28,12 @@ namespace hot_reload::detail
|
||||
{
|
||||
/// GetLastError() alone is just a number; this renders it the
|
||||
/// way a person would want to read it, the same information
|
||||
/// dlerror() gives directly on POSIX. Written against
|
||||
/// documented Win32 behavior; not exercised on real Windows --
|
||||
/// see source/HotReload/README.md's verification notes.
|
||||
/// dlerror() gives directly on POSIX. Exercised under Wine (MinGW cross-build) -- see
|
||||
/// "What's actually been tested" in the top-level README; not verified against a real
|
||||
/// Windows/MSVC build.
|
||||
/// @return The system's message for the calling thread's last Win32 error, without
|
||||
/// trailing line breaks, or a generic message naming the error code if the
|
||||
/// system has none.
|
||||
std::string formatLastWindowsError()
|
||||
{
|
||||
DWORD errorCode = GetLastError();
|
||||
@@ -71,6 +79,7 @@ namespace hot_reload::detail
|
||||
ModuleHandle::ModuleHandle(ModuleHandle&& other) noexcept
|
||||
: m_native(other.m_native)
|
||||
, m_loadedBase(other.m_loadedBase)
|
||||
, m_errorMessage(std::move(other.m_errorMessage))
|
||||
{
|
||||
other.m_native = nullptr;
|
||||
other.m_loadedBase = nullptr;
|
||||
@@ -90,6 +99,7 @@ namespace hot_reload::detail
|
||||
}
|
||||
m_native = other.m_native;
|
||||
m_loadedBase = other.m_loadedBase;
|
||||
m_errorMessage = std::move(other.m_errorMessage);
|
||||
other.m_native = nullptr;
|
||||
other.m_loadedBase = nullptr;
|
||||
}
|
||||
@@ -129,7 +139,18 @@ namespace hot_reload::detail
|
||||
// first runs under RTLD_LAZY's deferred binding. For a
|
||||
// long-running host, a clean, catchable load failure is far
|
||||
// preferable to a delayed, uncatchable one.
|
||||
void* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
//
|
||||
// The "./" prefix: dlopen() only treats its argument as a file
|
||||
// path if it contains a '/'. A bare name (e.g. "plugin.so") is
|
||||
// instead searched for like a library name -- LD_LIBRARY_PATH,
|
||||
// ld.so.cache, the default directories -- and is never looked
|
||||
// for in the current directory, so a plain relative file name
|
||||
// would fail with a baffling "cannot open shared object file"
|
||||
// for a file that is sitting right there. Everything this
|
||||
// library loads is a real file on disk (the staged copy
|
||||
// HotReloadContext makes), so always force path semantics.
|
||||
const std::string loadPath = path.find('/') == std::string::npos ? "./" + path : path;
|
||||
void* module = dlopen(loadPath.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (module == nullptr)
|
||||
{
|
||||
const char* error = dlerror();
|
||||
|
||||
@@ -5,53 +5,103 @@
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// RAII wrapper around dlopen()/LoadLibrary(). Move-only, closes
|
||||
/// the underlying module on destruction (or on being replaced by a
|
||||
/// move-assignment).
|
||||
/**
|
||||
* @brief RAII wrapper around `dlopen()`/`LoadLibrary()`.
|
||||
*
|
||||
* Move-only, closes the underlying module on destruction (or on
|
||||
* being replaced by a move-assignment).
|
||||
*
|
||||
* @note Closing a module unmaps its code (unless the platform keeps it resident, e.g. a
|
||||
* library that defines C++ unique symbols). Any function pointer into a closed module
|
||||
* -- including old, patched entry points that jump into it -- dangles from then on.
|
||||
*/
|
||||
class ModuleHandle
|
||||
{
|
||||
public:
|
||||
/** @brief Creates an empty handle that owns no module (isValid() is false). */
|
||||
ModuleHandle() = default;
|
||||
|
||||
/** @brief Closes the module, if this handle owns one. */
|
||||
~ModuleHandle();
|
||||
|
||||
ModuleHandle(const ModuleHandle&) = delete;
|
||||
ModuleHandle& operator=(const ModuleHandle&) = delete;
|
||||
|
||||
/**
|
||||
* @brief Takes ownership of @p other's module (and its error message), leaving @p other
|
||||
* empty.
|
||||
* @param other The handle to move from.
|
||||
*/
|
||||
ModuleHandle(ModuleHandle&& other) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Closes this handle's current module, then takes ownership of @p other's module
|
||||
* (and its error message), leaving @p other empty.
|
||||
* @param other The handle to move from.
|
||||
* @return This handle.
|
||||
*/
|
||||
ModuleHandle& operator=(ModuleHandle&& other) noexcept;
|
||||
|
||||
/// Loads path. On failure the returned handle's isValid() is
|
||||
/// false and loadedBase() is null.
|
||||
/**
|
||||
* @brief Loads a shared library.
|
||||
*
|
||||
* @param path Path to the shared library to load. Always treated as a filesystem path: a
|
||||
* bare file name such as `plugin.so` means the file of that name in the current
|
||||
* working directory (on POSIX it is loaded as `./plugin.so`, because `dlopen()`
|
||||
* would otherwise search the library path for it instead).
|
||||
* @return The loaded handle. On failure, isValid() is false and
|
||||
* loadedBase() is null -- check errorMessage() for why.
|
||||
*/
|
||||
static ModuleHandle load(const std::string& path);
|
||||
|
||||
/**
|
||||
* @brief Whether load() succeeded.
|
||||
* @return True if this handle owns a loaded module.
|
||||
*/
|
||||
bool isValid() const
|
||||
{
|
||||
return m_native != nullptr;
|
||||
}
|
||||
|
||||
/// The address to pass as SymbolTable's loadedBase: on Windows
|
||||
/// this is simply the HMODULE; on POSIX it is the runtime load
|
||||
/// bias (struct link_map::l_addr) that turns an ELF symbol's
|
||||
/// file-relative st_value into a real address.
|
||||
/**
|
||||
* @brief The address to pass as SymbolTable's `loadedBase`.
|
||||
*
|
||||
* On Windows this is simply the `HMODULE`; on POSIX it is the
|
||||
* runtime load bias (`struct link_map::l_addr`) that turns an
|
||||
* ELF symbol's file-relative `st_value` into a real address.
|
||||
*
|
||||
* @return The value described above, or null if isValid() is false.
|
||||
*/
|
||||
void* loadedBase() const
|
||||
{
|
||||
return m_loadedBase;
|
||||
}
|
||||
|
||||
/// Only meaningful when isValid() is false: the underlying
|
||||
/// platform's own description of why loading failed --
|
||||
/// dlerror() on POSIX, the formatted message for
|
||||
/// GetLastError() on Windows. Captured immediately inside
|
||||
/// load()'s failing branch, before any other dl*/Win32 call
|
||||
/// that could otherwise overwrite it first. Empty if load()
|
||||
/// was never attempted, or if the platform gave no message.
|
||||
/**
|
||||
* @brief Why load() failed, if isValid() is false.
|
||||
*
|
||||
* The underlying platform's own description of why loading
|
||||
* failed -- `dlerror()` on POSIX, the formatted message for
|
||||
* `GetLastError()` on Windows. Captured immediately inside
|
||||
* load()'s failing branch, before any other POSIX dl-family or
|
||||
* Win32 call that could otherwise overwrite it first.
|
||||
*
|
||||
* @return The platform's message, or empty if load() was never
|
||||
* attempted or the platform gave no message.
|
||||
*/
|
||||
const std::string& errorMessage() const
|
||||
{
|
||||
return m_errorMessage;
|
||||
}
|
||||
|
||||
private:
|
||||
/// The platform's own handle (`HMODULE` or the `dlopen()` handle); null if none.
|
||||
void* m_native = nullptr;
|
||||
|
||||
/// See loadedBase().
|
||||
void* m_loadedBase = nullptr;
|
||||
|
||||
/// See errorMessage().
|
||||
std::string m_errorMessage;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file SymbolTable.cpp
|
||||
* @brief Reads and classifies a module's exported symbols (ELF and PE).
|
||||
*/
|
||||
#include "SymbolTable.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -5,6 +9,7 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
/* WHY "IS THIS A FUNCTION" IS DECIDED HERE, MECHANICALLY
|
||||
@@ -49,13 +54,18 @@ namespace hot_reload::detail
|
||||
|
||||
namespace
|
||||
{
|
||||
/// One PE section, reduced to what the function/data classification needs.
|
||||
struct SectionInfo
|
||||
{
|
||||
std::uint32_t rvaBegin;
|
||||
std::uint32_t rvaEnd;
|
||||
bool executable;
|
||||
std::uint32_t rvaBegin; /**< First RVA covered by the section. */
|
||||
std::uint32_t rvaEnd; /**< One past the last RVA covered by the section. */
|
||||
bool executable; /**< Whether the section is marked as containing code. */
|
||||
};
|
||||
|
||||
/// Turns a relative virtual address inside the mapped image into a string pointer.
|
||||
/// @param base The image's base address.
|
||||
/// @param rva A relative virtual address inside the image.
|
||||
/// @return `base + rva`, viewed as a C string.
|
||||
const char* rvaToPointer(std::uint8_t* base, std::uint32_t rva)
|
||||
{
|
||||
return reinterpret_cast<const char*>(base + rva);
|
||||
@@ -170,9 +180,11 @@ namespace hot_reload::detail
|
||||
// large real function) -- it only ever gives a hard "not
|
||||
// enough room" answer when the gap itself is already small.
|
||||
// PE has no per-export size field the way ELF does (see
|
||||
// below), so /hotpatch (applied automatically by
|
||||
// cmake/HotReload.cmake) is the primary safety mechanism on
|
||||
// Windows; this is a secondary backstop, not a substitute.
|
||||
// below), so /hotpatch (applied by calling
|
||||
// hot_reload_enable_patchable_functions() from
|
||||
// cmake/HotReload.cmake on the plugin target) is the primary
|
||||
// safety mechanism on Windows; this is a secondary backstop,
|
||||
// not a substitute.
|
||||
std::vector<std::uint32_t> functionRvasSorted;
|
||||
for (const Candidate& candidate : candidates)
|
||||
{
|
||||
@@ -236,6 +248,10 @@ namespace hot_reload::detail
|
||||
|
||||
namespace
|
||||
{
|
||||
/// Reads a whole file into memory.
|
||||
/// @param path The file to read.
|
||||
/// @return The file's bytes, or an empty vector if it could not be opened, is empty, or
|
||||
/// could not be read completely.
|
||||
std::vector<char> readWholeFile(const std::string& path)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
@@ -257,83 +273,191 @@ namespace hot_reload::detail
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// Whether a range read out of an ELF file lies entirely inside that file.
|
||||
///
|
||||
/// Every offset and length in an ELF header is untrusted input until proven otherwise:
|
||||
/// the dynamic loader validates the program headers it maps, but nothing validates the
|
||||
/// section header table this reader walks. Written so the sum can never overflow.
|
||||
///
|
||||
/// @param offset Start of the range, as read from the file.
|
||||
/// @param length Length of the range, as read from the file.
|
||||
/// @param fileSize Size of the whole file in bytes.
|
||||
/// @return True if `[offset, offset + length)` is inside `[0, fileSize)`.
|
||||
bool rangeInFile(std::uint64_t offset, std::uint64_t length, std::size_t fileSize)
|
||||
{
|
||||
return offset <= fileSize && length <= fileSize - offset;
|
||||
}
|
||||
|
||||
/// Copies one `T` out of the file, if it lies entirely inside it.
|
||||
///
|
||||
/// A `memcpy` rather than a `reinterpret_cast`, so an offset read from a corrupt file
|
||||
/// can never produce a misaligned access.
|
||||
///
|
||||
/// @tparam T A trivially copyable ELF structure (`Elf64_Ehdr`, `Elf32_Sym`, ...).
|
||||
/// @param file The whole file.
|
||||
/// @param offset Where the structure starts.
|
||||
/// @param[out] out Receives the structure.
|
||||
/// @return False (leaving @p out untouched) if the structure would extend past the end.
|
||||
template <typename T>
|
||||
bool readStruct(const std::vector<char>& file, std::uint64_t offset, T& out)
|
||||
{
|
||||
if (!rangeInFile(offset, sizeof(T), file.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&out, file.data() + offset, sizeof(T));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Looks up a NUL-terminated string in an ELF string table, without ever reading past it.
|
||||
/// @param table Start of the string table.
|
||||
/// @param tableSize Size of the string table in bytes.
|
||||
/// @param offset The string's offset within the table, as read from the file.
|
||||
/// @return The string, or an empty view if @p offset is outside the table or the string
|
||||
/// is not NUL-terminated inside it.
|
||||
std::string_view stringAt(const char* table, std::uint64_t tableSize, std::uint64_t offset)
|
||||
{
|
||||
if (offset >= tableSize)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const char* begin = table + offset;
|
||||
const auto remaining = static_cast<std::size_t>(tableSize - offset);
|
||||
const void* end = std::memchr(begin, '\0', remaining);
|
||||
if (end == nullptr)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const auto* endPtr = static_cast<const char*>(end);
|
||||
return std::string_view(begin, static_cast<std::size_t>(endPtr - begin));
|
||||
}
|
||||
|
||||
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
|
||||
/// @return The symbol's type (`STT_*`).
|
||||
unsigned char elfSymType(unsigned char info)
|
||||
{
|
||||
return static_cast<unsigned char>(ELF32_ST_TYPE(info));
|
||||
}
|
||||
|
||||
/// @param info An ELF32 or ELF64 `st_info` byte (the encoding is identical for both).
|
||||
/// @return The symbol's binding (`STB_*`).
|
||||
unsigned char elfSymBind(unsigned char info)
|
||||
{
|
||||
return static_cast<unsigned char>(ELF32_ST_BIND(info));
|
||||
}
|
||||
|
||||
/// Parses one ELF class (32 or 64-bit); Ehdr/Shdr/Sym is
|
||||
/// Elf32_* or Elf64_* depending on the caller, matched against
|
||||
/// e_ident[EI_CLASS] before this is ever instantiated.
|
||||
template <typename Ehdr, typename Shdr, typename Sym, typename SymType, typename SymBind>
|
||||
std::vector<ExportedSymbol> parseElf(
|
||||
const std::vector<char>& file, void* loadedBase, SymType symType, SymBind symBind)
|
||||
///
|
||||
/// Every offset, length and index read from the file is bounds-checked first (see
|
||||
/// rangeInFile()), so a damaged image produces a shorter result rather than an
|
||||
/// out-of-bounds read.
|
||||
///
|
||||
/// @tparam Ehdr The ELF file header type (`Elf32_Ehdr` or `Elf64_Ehdr`).
|
||||
/// @tparam Shdr The ELF section header type (`Elf32_Shdr` or `Elf64_Shdr`).
|
||||
/// @tparam Sym The ELF symbol type (`Elf32_Sym` or `Elf64_Sym`).
|
||||
/// @param file The whole shared object, as read from disk.
|
||||
/// @param loadedBase The runtime load bias to add to every symbol's `st_value`.
|
||||
/// @return Every defined, global or weak symbol in `.dynsym`, classified; empty if the
|
||||
/// file's section headers or symbol tables are missing or inconsistent.
|
||||
template <typename Ehdr, typename Shdr, typename Sym>
|
||||
std::vector<ExportedSymbol> parseElf(const std::vector<char>& file, void* loadedBase)
|
||||
{
|
||||
std::vector<ExportedSymbol> result;
|
||||
if (file.size() < sizeof(Ehdr))
|
||||
|
||||
Ehdr ehdr{};
|
||||
if (!readStruct(file, 0, ehdr))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto* ehdr = reinterpret_cast<const Ehdr*>(file.data());
|
||||
if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0)
|
||||
if (ehdr.e_shoff == 0 || ehdr.e_shnum == 0)
|
||||
{
|
||||
return result; // no section header table -- see file comment above
|
||||
}
|
||||
if (static_cast<std::uint64_t>(ehdr->e_shoff) + static_cast<std::uint64_t>(ehdr->e_shnum) * sizeof(Shdr) >
|
||||
file.size())
|
||||
const auto sectionTableBytes = static_cast<std::uint64_t>(ehdr.e_shnum) * sizeof(Shdr);
|
||||
const bool sectionsFit = rangeInFile(ehdr.e_shoff, sectionTableBytes, file.size());
|
||||
if (ehdr.e_shentsize != sizeof(Shdr) || !sectionsFit)
|
||||
{
|
||||
return result; // truncated/corrupt file
|
||||
}
|
||||
|
||||
const auto* sections = reinterpret_cast<const Shdr*>(file.data() + ehdr->e_shoff);
|
||||
std::vector<Shdr> sections(ehdr.e_shnum);
|
||||
const auto sectionBytes = sections.size() * sizeof(Shdr);
|
||||
std::memcpy(sections.data(), file.data() + ehdr.e_shoff, sectionBytes);
|
||||
|
||||
// Section header string table, to find sections by name.
|
||||
if (ehdr->e_shstrndx >= ehdr->e_shnum)
|
||||
if (ehdr.e_shstrndx >= ehdr.e_shnum)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
const char* shstrtab = file.data() + sections[ehdr->e_shstrndx].sh_offset;
|
||||
const Shdr& shstrSection = sections[ehdr.e_shstrndx];
|
||||
if (!rangeInFile(shstrSection.sh_offset, shstrSection.sh_size, file.size()))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
const char* shstrtab = file.data() + shstrSection.sh_offset;
|
||||
|
||||
const Shdr* dynsymSection = nullptr;
|
||||
const Shdr* dynstrSection = nullptr;
|
||||
for (std::size_t i = 0; i < ehdr->e_shnum; ++i)
|
||||
for (const Shdr& section : sections)
|
||||
{
|
||||
const char* name = shstrtab + sections[i].sh_name;
|
||||
if (std::strcmp(name, ".dynsym") == 0)
|
||||
const auto nameOffset = section.sh_name;
|
||||
const std::string_view name = stringAt(shstrtab, shstrSection.sh_size, nameOffset);
|
||||
if (name == ".dynsym")
|
||||
{
|
||||
dynsymSection = §ions[i];
|
||||
dynsymSection = §ion;
|
||||
}
|
||||
else if (std::strcmp(name, ".dynstr") == 0)
|
||||
else if (name == ".dynstr")
|
||||
{
|
||||
dynstrSection = §ions[i];
|
||||
dynstrSection = §ion;
|
||||
}
|
||||
}
|
||||
if (dynsymSection == nullptr || dynstrSection == nullptr)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
if (!rangeInFile(dynsymSection->sh_offset, dynsymSection->sh_size, file.size()) ||
|
||||
!rangeInFile(dynstrSection->sh_offset, dynstrSection->sh_size, file.size()))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* dynstr = file.data() + dynstrSection->sh_offset;
|
||||
std::size_t symbolCount = dynsymSection->sh_size / sizeof(Sym);
|
||||
const auto* symbols = reinterpret_cast<const Sym*>(file.data() + dynsymSection->sh_offset);
|
||||
const auto symBytes = static_cast<std::size_t>(dynsymSection->sh_size);
|
||||
const std::size_t symbolCount = symBytes / sizeof(Sym);
|
||||
|
||||
for (std::size_t i = 0; i < symbolCount; ++i)
|
||||
{
|
||||
const Sym& sym = symbols[i];
|
||||
Sym sym{};
|
||||
const char* symPtr = file.data() + dynsymSection->sh_offset + i * sizeof(Sym);
|
||||
std::memcpy(&sym, symPtr, sizeof(Sym));
|
||||
if (sym.st_shndx == SHN_UNDEF || sym.st_name == 0)
|
||||
{
|
||||
continue; // imported/unresolved, not defined here
|
||||
}
|
||||
unsigned char bind = symBind(sym.st_info);
|
||||
unsigned char bind = elfSymBind(sym.st_info);
|
||||
if (bind != STB_GLOBAL && bind != STB_WEAK)
|
||||
{
|
||||
continue; // not part of this module's visible surface
|
||||
}
|
||||
const auto symNameOffset = sym.st_name;
|
||||
const auto dynstrSize = dynstrSection->sh_size;
|
||||
const std::string_view name = stringAt(dynstr, dynstrSize, symNameOffset);
|
||||
if (name.empty())
|
||||
{
|
||||
continue; // name offset outside .dynstr (corrupt file)
|
||||
}
|
||||
|
||||
ExportedSymbol exported;
|
||||
exported.name = dynstr + sym.st_name;
|
||||
exported.name = std::string(name);
|
||||
exported.address = static_cast<char*>(loadedBase) + sym.st_value;
|
||||
exported.size = static_cast<std::size_t>(sym.st_size);
|
||||
|
||||
bool isFuncType = symType(sym.st_info) == STT_FUNC;
|
||||
bool isFuncType = elfSymType(sym.st_info) == STT_FUNC;
|
||||
bool sectionExecutable = false;
|
||||
if (sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON && sym.st_shndx < ehdr->e_shnum)
|
||||
const bool realSection = sym.st_shndx != SHN_ABS && sym.st_shndx != SHN_COMMON;
|
||||
if (realSection && sym.st_shndx < ehdr.e_shnum)
|
||||
{
|
||||
sectionExecutable = (sections[sym.st_shndx].sh_flags & SHF_EXECINSTR) != 0;
|
||||
}
|
||||
@@ -354,18 +478,17 @@ namespace hot_reload::detail
|
||||
return {};
|
||||
}
|
||||
|
||||
// ELF32_ST_TYPE/BIND and ELF64_ST_TYPE/BIND decode the same bit layout (the System V ABI
|
||||
// defines st_info identically for both classes) -- elfSymType()/elfSymBind() above use the
|
||||
// ELF32 macros for both, so one pair of extractors serves both instantiations below.
|
||||
unsigned char elfClass = static_cast<unsigned char>(file[EI_CLASS]);
|
||||
if (elfClass == ELFCLASS64)
|
||||
{
|
||||
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(
|
||||
file, loadedBase, [](unsigned char info) { return ELF64_ST_TYPE(info); },
|
||||
[](unsigned char info) { return ELF64_ST_BIND(info); });
|
||||
return parseElf<Elf64_Ehdr, Elf64_Shdr, Elf64_Sym>(file, loadedBase);
|
||||
}
|
||||
if (elfClass == ELFCLASS32)
|
||||
{
|
||||
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(
|
||||
file, loadedBase, [](unsigned char info) { return ELF32_ST_TYPE(info); },
|
||||
[](unsigned char info) { return ELF32_ST_BIND(info); });
|
||||
return parseElf<Elf32_Ehdr, Elf32_Shdr, Elf32_Sym>(file, loadedBase);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -7,45 +7,60 @@
|
||||
namespace hot_reload::detail
|
||||
{
|
||||
|
||||
/// One exported symbol found in a loaded module, already classified.
|
||||
/**
|
||||
* @brief One exported symbol found in a loaded module, already classified.
|
||||
*/
|
||||
struct ExportedSymbol
|
||||
{
|
||||
std::string name;
|
||||
void* address = nullptr;
|
||||
std::string name; /**< The symbol's real (possibly mangled) name. */
|
||||
void* address = nullptr; /**< The symbol's runtime address. */
|
||||
|
||||
/// The single load-bearing safety gate behind the "only
|
||||
/// executable code changes" guarantee in hot_reload.h: false
|
||||
/// for anything not mechanically identified as code (a data
|
||||
/// object, a forwarder export, or a symbol kind this reader
|
||||
/// does not recognize at all). HotReloadContext never patches
|
||||
/// a symbol where this is false, on either side of a reload.
|
||||
/**
|
||||
* The single load-bearing safety gate behind the "only
|
||||
* executable code changes" guarantee in `hot_reload.h`: false
|
||||
* for anything not mechanically identified as code (a data
|
||||
* object, a forwarder export, or a symbol kind this reader does
|
||||
* not recognize at all). `HotReloadContext` never patches a
|
||||
* symbol where this is false, on either side of a reload.
|
||||
*/
|
||||
bool isFunction = false;
|
||||
|
||||
/// Compiled size in bytes if known, 0 if not. ELF gives this
|
||||
/// exactly (the symbol table's st_size field); PE gives no
|
||||
/// such field, so this holds a conservative estimate derived
|
||||
/// from the distance to the next exported function in the
|
||||
/// same section -- see the file comment in SymbolTable.cpp
|
||||
/// for exactly what that heuristic can and cannot catch.
|
||||
/**
|
||||
* Compiled size in bytes if known, 0 if not. ELF gives this
|
||||
* exactly (the symbol table's `st_size` field); PE gives no such
|
||||
* field, so this holds a conservative estimate derived from the
|
||||
* distance to the next exported function in the same section --
|
||||
* see the file comment in `SymbolTable.cpp` for exactly what
|
||||
* that heuristic can and cannot catch.
|
||||
*/
|
||||
std::size_t size = 0;
|
||||
};
|
||||
|
||||
/// Reads every exported, defined symbol belonging to the module
|
||||
/// mapped at loadedBase (the base address dlopen()/LoadLibrary()
|
||||
/// returned for it), classifying each one as code or data.
|
||||
///
|
||||
/// On POSIX this parses imagePath on disk (its ELF .dynsym /
|
||||
/// .dynstr / section-header table) and adds loadedBase as the
|
||||
/// runtime load bias. On Windows this ignores imagePath and reads
|
||||
/// directly from the already-mapped image at loadedBase (its PE
|
||||
/// export directory and section table) -- see the per-platform
|
||||
/// implementation blocks in SymbolTable.cpp for why each platform
|
||||
/// reads from a different place.
|
||||
///
|
||||
/// Returns an empty vector, never throws, if the image could not
|
||||
/// be parsed as this platform's native format (including the rare
|
||||
/// case of a .so with its section header table deliberately
|
||||
/// stripped -- see the file comment in SymbolTable.cpp).
|
||||
/**
|
||||
* @brief Reads and classifies every exported, defined symbol in a loaded module.
|
||||
*
|
||||
* On POSIX this parses @p imagePath on disk (its ELF `.dynsym` /
|
||||
* `.dynstr` / section-header table) and adds @p loadedBase as the
|
||||
* runtime load bias. On Windows this ignores @p imagePath and reads
|
||||
* directly from the already-mapped image at @p loadedBase (its PE
|
||||
* export directory and section table) -- see the per-platform
|
||||
* implementation blocks in `SymbolTable.cpp` for why each platform
|
||||
* reads from a different place.
|
||||
*
|
||||
* A file that is truncated or otherwise corrupt in the parts this reader relies on (the
|
||||
* section header table and the tables it points at) is treated like any other unparseable
|
||||
* image: every offset and length read from the file is range-checked against the file's size
|
||||
* before it is used, so a damaged image yields fewer (or no) symbols rather than an
|
||||
* out-of-bounds read.
|
||||
*
|
||||
* @param imagePath Path to the module on disk (POSIX only -- see above).
|
||||
* @param loadedBase The base address `dlopen()`/`LoadLibrary()` returned for it.
|
||||
* @return Every exported symbol, classified via ExportedSymbol::isFunction.
|
||||
* Empty (never throws) if the image could not be parsed as
|
||||
* this platform's native format (including the rare case of
|
||||
* a `.so` with its section header table deliberately
|
||||
* stripped -- see the file comment in `SymbolTable.cpp`).
|
||||
*/
|
||||
std::vector<ExportedSymbol> readExportedSymbols(const std::string& imagePath, void* loadedBase);
|
||||
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
/* Demangle.cpp -- hot_reload_demangle() / hot_reload_free_string().
|
||||
/**
|
||||
* @file Demangle.cpp
|
||||
* @brief hot_reload_demangle() / hot_reload_free_string() implementation.
|
||||
*
|
||||
* Pure string utility, no HotReloadContext involved, so it gets its
|
||||
* own file alongside HotReloadApi.cpp rather than crowding that one's
|
||||
@@ -15,6 +17,7 @@
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
@@ -24,9 +27,9 @@
|
||||
// COMPILER implements (Itanium here), not the operating system --
|
||||
// MinGW is still GCC/Clang generating Itanium-mangled names even
|
||||
// though the OS is Windows. MSVC, on the same OS, uses its own,
|
||||
// unrelated scheme (the #elif below). See README.md's "Hot
|
||||
// reloading C++ symbols" section for why this distinction matters
|
||||
// beyond just this function.
|
||||
// unrelated scheme (the #elif below). See the "Hot reloading C++
|
||||
// symbols" section of source/HotReload/README.md for why this
|
||||
// distinction matters beyond just this function.
|
||||
#include <cxxabi.h>
|
||||
#elif defined(_MSC_VER)
|
||||
// windows.h must come first: dbghelp.h uses types (HANDLE, DWORD,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @file HotReloadApi.cpp
|
||||
* @brief The extern "C" shim implementing hot_reload.h by delegating to hot_reload::HotReloadContext.
|
||||
*/
|
||||
#include "hot_reload/hot_reload.h"
|
||||
|
||||
#include "../HotReloadContext.hpp"
|
||||
@@ -9,6 +13,8 @@ namespace
|
||||
/// is a hot_reload::HotReloadContext, following the same
|
||||
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
|
||||
/// example uses. This is the one place that cast happens.
|
||||
/// @param ctx A context previously returned by hot_reload_context_create(), or null.
|
||||
/// @return The same object viewed as the C++ type it really is (null if @p ctx is null).
|
||||
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
|
||||
{
|
||||
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
|
||||
|
||||
Reference in new issue
Block a user