Added ARM32 support

This commit is contained in:
alejo committed 2026-11-03 15:22:24 -03:00
1 parent df792729ef
commit 1212cb9dbe
36 files changed
+2381 -783

No files matched your search

+94 -41
View File
@@ -1,12 +1,4 @@
<div align="center">
<img src="res/icon.png" alt="Logo" width="auto" height="80">
# Volkan
**C++ Hot-Reload library**
</div>
<br>
A cross-platform, cross-compiler C/C++ library that patches a running
process's own code in place, so a new build of a function takes effect
@@ -15,16 +7,24 @@ function pointer cached somewhere) picks it up automatically.
```cpp
// host.cpp -- loads a plugin, calls into it, then hot-reloads it later
HotReloadContext* ctx = hot_reload_context_create(nullptr);
hot_reload_load(ctx, "plugin", "plugin.so");
hot_reload::Context ctx;
ctx.load("plugin", "plugin.so");
auto tick = reinterpret_cast<int (*)(int)>(hot_reload_get_symbol(ctx, "plugin", "tick"));
tick(1); // runs V1's code
auto tick = reinterpret_cast<int (*)(int)>(ctx.findSymbol("plugin", "tick"));
tick(1); // runs V1's code
hot_reload_reload(ctx, "plugin", "plugin.so"); // plugin.so was rebuilt in between
tick(1); // same pointer, now runs V2's code
ctx.reload("plugin", "plugin.so"); // plugin.so was rebuilt in between
tick(1); // same pointer, now runs V2's code
```
This is `hot_reload::Context`, the primary, modern C++ API
([`Context.hpp`](source/HotReload/include/hot_reload/Context.hpp)) --
RAII, a `Result` instead of a status-code-plus-side-channel, and safe to
call from multiple threads at once. A separate, independently stable C
API ([`hot_reload.h`](source/HotReload/include/hot_reload/hot_reload.h))
wraps it for anything that needs plain C linkage; see ["Pick an
API"](#pick-an-api) below.
## Features
- **Works on a real function pointer, not just a lookup table.** Patching
@@ -59,9 +59,20 @@ tick(1); // same pointer, now runs V2's c
reader trusts is bounds-checked against the file's actual size, confirmed
by feeding it 1500 randomly corrupted variants of a real plugin — see
"What's actually been tested" below.
- **x86, x86-64, and ARM64, across GCC, Clang, MinGW, and MSVC.** See
"What's actually been tested" below for exactly how each combination was
verified — natively, under QEMU, or under Wine, not just reasoned about.
- **x86, x86-64, ARM64, and ARM32 (both its ARM and Thumb-2 instruction
sets), across GCC, Clang, MinGW, and MSVC.** See "What's actually been
tested" below for exactly how each combination was verified —
natively, under QEMU, or under Wine, not just reasoned about.
- **Safe to call from multiple threads at once, verified under
ThreadSanitizer, not just locked and hoped.** `hot_reload::Context`
uses a reader-writer lock internally: concurrent `findSymbol()` calls
run in parallel with each other, while `load()`/`reload()` briefly
take exclusive access. This covers the library's own bookkeeping —
it does not, and architecturally cannot, make it safe to reload a
module some other thread is at that exact moment executing inside;
see ["Pick an API"](#pick-an-api) for the precise guarantee and
`source/HotReload/README.md`'s "What this cannot detect" for why that
one specific case is out of reach for any tool like this one.
## Documentation
@@ -69,9 +80,22 @@ tick(1); // same pointer, now runs V2's c
full reference: exactly how patching works, both safety guarantees in
detail, the complete C++ interoperability story, per-platform scope and
how each was verified, and a growing list of real bugs found (and
fixed) by actually running this — on Windows/MSVC, under fuzzing, and
on every other toolchain in the table below — read this before relying
on any specific edge case.
fixed) by actually running this — on Windows/MSVC, under fuzzing, under
ThreadSanitizer, and on every other toolchain in the table below — read
this before relying on any specific edge case.
- **[`Context.hpp`](source/HotReload/include/hot_reload/Context.hpp) is
the primary, modern C++ API**, with the rest of its surface --
[`Result`](source/HotReload/include/hot_reload/Result.hpp),
[`Status`](source/HotReload/include/hot_reload/Status.hpp),
[`Options`](source/HotReload/include/hot_reload/Options.hpp),
[`SymbolEvent`](source/HotReload/include/hot_reload/SymbolEvent.hpp),
[`SymbolEventInfo`](source/HotReload/include/hot_reload/SymbolEventInfo.hpp),
[`demangle()`](source/HotReload/include/hot_reload/Demangle.hpp) --
one type or function per header, each with its own matching `.cpp`.
`hot_reload.h`'s C API is a separate, loosely coupled wrapper built on
top of this one's public methods only (`c-api/HotReloadApi.cpp`), not
the other way around -- see that file's own file comment, and ["Pick
an API"](#pick-an-api) below for which to reach for.
- **Every source file, not just the header, is Doxygen-documented** —
`hot_reload.h`'s public API (every function, status code, and event
kind has a `@brief`/`@param`/`@return`) and every internal class and
@@ -103,12 +127,41 @@ scripts\build.bat
scripts\build.bat Release -DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON
```
This produces `hotreload_lib`, a `STATIC` library with a plain C API
(`hot_reload.h`) over a C++ implementation. Link your host application
against it (`target_link_libraries(your_app PRIVATE hotreload_lib)` if
you're using CMake; see [`source/HotReload/CMakeLists.txt`](source/HotReload/CMakeLists.txt)
This produces `hotreload_lib`, a `STATIC` library exposing both a
modern C++ API (`Context.hpp` and the headers alongside it) and a
separate, independently stable plain C API (`hot_reload.h`) built on
top of it. Link your host application against it
(`target_link_libraries(your_app PRIVATE hotreload_lib)` if you're
using CMake; see [`source/HotReload/CMakeLists.txt`](source/HotReload/CMakeLists.txt)
for what it links on each platform if you're not).
### Pick an API
Both are first-class, both stay supported, and both end up patching the
exact same way underneath (the C API calls the C++ one's own public
methods, nothing more privileged) -- this is about which fits what
you're writing:
- **`hot_reload::Context` (`Context.hpp`)** — a C++ host. RAII (no
separate create/destroy calls), a `Result` returned directly from
`load()`/`reload()` instead of a status code plus a separate
`last_error()` side-channel to remember to check, `std::function`
callbacks instead of a raw function pointer plus manually-threaded
`void*` user data, and the thread-safety guarantee described above —
there is no equivalent of the C API's stateful `last_error()` to race
on in the first place, since every call's outcome comes back in its
own `Result`. Prefer this for any new C++ code.
- **`hot_reload.h`** — plain C linkage itself is the requirement: a C
host, or a different language's FFI boundary (most FFI story the way
a C header does; few can call a C++ class directly at all). The
opaque `HotReloadContext*` it hands out is a small wrapper struct
(`c-api/HotReloadApi.cpp`) that owns a real `hot_reload::Context` by
composition — never a cast between unrelated types — and
`hot_reload_last_error()` keeps working exactly as documented by
tracking each calling thread's own most recent error internally, so
two threads' errors can never be confused with each other even
though the underlying `Context` itself keeps none of that state.
### See it work — a scripted, deterministic demo
```sh
@@ -154,12 +207,12 @@ for bugs the first time you hit them.
it in your own module" for the full version of this list, including
why this specific step doesn't (and can't) quiet `std::string`/
`std::vector` noise, and why that's fine.
4. Have your host `hot_reload_load()` it once at startup and
`hot_reload_reload()` it after every rebuild, from a point where
nothing else is actively calling into it. A path with no `/` in it is
still resolved against the current working directory (never a system
library path), so a plain `plugin.so` next to your host works exactly
as you'd expect.
4. Have your host `load()` it once at startup (`hot_reload::Context`,
or `hot_reload_load()` through the C API) and `reload()` it after
every rebuild, from a point where nothing else is actively calling
into it. A path with no `/` in it is still resolved against the
current working directory (never a system library path), so a plain
`plugin.so` next to your host works exactly as you'd expect.
## Limitations
@@ -193,12 +246,15 @@ than assumed:
| Toolchain | How it was verified |
|---|---|
| GCC + Clang, native Linux/x86-64 | Both compilers, full demo suite and test suite run to completion, including a CET (`endbr64`) landing pad this toolchain turns out to emit by default, and again explicitly forcing one with `-fcf-protection=full` |
| GCC + Clang, native Linux/x86-64 | Both compilers, the full demo and both test suites (`hotreload_tests` for the C API, `context_tests` for `hot_reload::Context` directly) run to completion, including a CET (`endbr64`) landing pad this toolchain turns out to emit by default, and again explicitly forcing one with `-fcf-protection=full` |
| GCC, native Linux/x86 (32-bit) | Full demo and test suite run to completion under `-m32`. This is also where the suite's own "too small to patch" plugin function turned out not to be small enough at `-O2` on this one target — a real, previously-unverified gap between what the test assumed and what the compiler actually emitted, closed by making that function require no instructions at all |
| ARM64 (GCC cross-compiled) | Run under `qemu-aarch64` — twice, once plain and once with `-mbranch-protection=standard`, to exercise the BTI landing-pad path on real ARM64 semantics |
| ARM64 (GCC cross-compiled) | Run under `qemu-aarch64` — twice, once plain and once with `-mbranch-protection=standard`, to exercise the BTI landing-pad path on real ARM64 semantics. Re-run, both test suites included, after `hot_reload::Context` and its reader-writer lock were introduced, to confirm a genuinely multi-threaded C++ host works correctly under real (if emulated) ARM64 hardware too, not just natively |
| ARM32 (GCC cross-compiled, hard-float ABI) | Run under `qemu-arm` — both instruction sets (the default Thumb-2, and ARM state forced with `-marm`), across `-O0` through `-O3`. This architecture needed a genuinely new backend, not just a recompile: a CPU in this family is always in one of two incompatible instruction-decoding states, detected from the same bit the ARM EABI already marks every function symbol with. The one case that depends on runtime layout rather than just compiler flags was built and run deliberately, not just reasoned about: Thumb-2's literal-load jump needs its own address 4-byte aligned, which a Thumb function is only ever guaranteed to be 2-byte aligned for, so a plugin with a function confirmed landing 2 bytes off that boundary was built and reloaded under qemu to prove the one-instruction realignment this backend adds for exactly that case actually works. Also found here: the suite's own "too small to patch" function needed a third implementation (hand-written, not compiler-generated) — even an empty C++ body's unoptimized frame-pointer prologue reaches this architecture's own jump size at `-O0` |
| MinGW | Cross-compiled and run under Wine, including the PE export-table parser and a real `LoadLibrary` failure (confirmed `hot_reload_last_error()` reports "Bad EXE format" correctly) |
| MSVC | Built and live-tested on real Windows by someone using this library — not by the environment that wrote it. Four real fixes came out of that and are folded in: `windows.h`/`dbghelp.h` include order, `NOMINMAX`, explicit `dbghelp` linking, and a reload-retry widening for Windows' slower post-rebuild file-availability timing |
| AddressSanitizer + UndefinedBehaviorSanitizer, and Valgrind with self-modifying-code checking enabled | Full test suite, clean under all three — the self-modifying-code checking matters specifically here, since patching a function's own bytes at runtime is exactly the pattern a memory checker's JIT otherwise mishandles by default |
| MSVC | Built and live-tested on real Windows by someone using this library — not by the environment that wrote it. Four real fixes came out of that and are folded in: `windows.h`/`dbghelp.h` include order, `NOMINMAX`, explicit `dbghelp` linking, and a reload-retry widening for Windows' slower post-rebuild file-availability timing. The same person has since reported it still working after the rewrite to `hot_reload::Context` (reported, not independently re-run here) |
| Clang (GNU-style `clang++`) targeting the MSVC ABI, Windows | **Two real build failures reported against this configuration (Clang 22.1, Windows SDK 10.0.26100), both fixed — but not yet confirmed by a successful end-to-end build on that exact toolchain.** Nothing in the environment this was written in can build for Windows natively, so everything below is a proxy check, not that confirmation. (1) `Demangle.cpp` tested `__clang__` before `_MSC_VER`, but Clang defines both when targeting the MSVC ABI, so it went looking for `<cxxabi.h>`, which that configuration does not have; it now keys on `_MSC_VER` first, since that tracks the mangling scheme in use rather than which compiler frontend produced it. (2) `windows.h` without `WIN32_LEAN_AND_MEAN` pulls in COM headers (`ole2.h` → `combaseapi.h` → …) that failed to compile there; all four files that include it now define it. A third was found by reading the same build log rather than waiting for it to fail: the CMake condition that links `dbghelp` asked whether the compiler takes MSVC *command-line syntax* (the `MSVC` variable, false for GNU-style `clang++`) when it needed to ask whether it produces the MSVC *ABI* — `CMAKE_CXX_SIMULATE_ID`, which CMake sets from the very `_MSC_VER` the C++ code now keys on. What was actually checked: each branch condition under simulated `_MSC_VER` and under plain Clang/GCC; every library source compiled for Windows with mingw-w64 (plus `Demangle.cpp` with its MSVC branch forced on); that `WIN32_LEAN_AND_MEAN` removes exactly the `ole2.h`/`combaseapi.h`/`objidlbase.h`/`servprov.h`/`urlmon.h` chain from the failing log (against mingw-w64's headers, not the real SDK's); the new CMake condition under four compiler configurations, including a `clang++` made to define `_MSC_VER` so CMake reports `MSVC` false and `SIMULATE_ID` `MSVC`, as that log implies; and that Clang accepts `-fpatchable-function-entry=32,0` for the Windows/MSVC target and emits the entry padding this library needs (while `-fms-hotpatch` emits none on x64, so it is not a substitute). The actual `__uuidof` failure itself could not be reproduced here |
| AddressSanitizer + UndefinedBehaviorSanitizer, and Valgrind with self-modifying-code checking enabled | Both test suites, clean under all three — the self-modifying-code checking matters specifically here, since patching a function's own bytes at runtime is exactly the pattern a memory checker's JIT otherwise mishandles by default |
| ThreadSanitizer | `context_tests`' own concurrency stress test (several threads calling `findSymbol()` against others calling `reload()`, some on the very same module name as each other, plus several more each `load()`-ing their own distinct module name, all against one shared `Context`) -- five separate runs, to cover more than one possible thread interleaving, zero races reported each time. Exactly what `hot_reload::Context`'s reader-writer lock exists to make true, confirmed rather than assumed, the same standard this project holds every other claim in this table to |
| Fuzzing (ELF section-header parsing) | 1500 randomly corrupted variants of a real plugin's section header table — each still a file the OS loader itself accepts via `dlopen()` — all handled cleanly. An earlier version of this same reader crashed on 69 of an initial 300 |
Three more real, reproducible bugs came out of this same pass, each
@@ -212,10 +268,7 @@ staging path used to make each reload load from a fresh copy; and the
ELF section-header hardening above.
**Deliberately not implemented, rather than implemented and untested:**
32-bit ARM (its ARM/Thumb instruction-mode ambiguity fails silently if
gotten wrong, and there was no way to verify it on real or emulated
hardware) and macOS/Mach-O (never requested, and would need a third image
format reader alongside the existing ELF and PE ones). Both are
documented, bounded gaps — see `source/HotReload/README.md`'s "Scope"
section for the reasoning behind each, and the extension point if you
need either.
macOS/Mach-O (never requested, and would need a third image format
reader alongside the existing ELF and PE ones) — a documented, bounded
gap; see `source/HotReload/README.md`'s "Scope" section for the
reasoning and the extension point if you need it.