Fix documentation

This commit is contained in:
alejo committed 2026-09-25 03:20:35 -03:00
1 parent a317c07124
commit ab21b3f30d
27 files changed
+2137 -1043

No files matched your search

+60 -24
View File
@@ -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;
+71 -36
View File
@@ -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);
}
+68 -21
View File
@@ -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 {};
+115 -17
View File
@@ -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)
+61 -22
View File
@@ -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;
};
+25 -4
View File
@@ -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();
+66 -16
View File
@@ -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;
};
+160 -37
View File
@@ -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 = &sections[i];
dynsymSection = &section;
}
else if (std::strcmp(name, ".dynstr") == 0)
else if (name == ".dynstr")
{
dynstrSection = &sections[i];
dynstrSection = &section;
}
}
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 {};
}
+46 -31
View File
@@ -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);
}
+7 -4
View File
@@ -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);