| 17fbd3c6 | 12-Dec-2025 |
Chris Fallin <[email protected]> |
Debug: implement breakpoints and single-stepping. (#12133)
* Debug: implement breakpoints and single-stepping.
This is a PR that puts together a bunch of earlier pieces (patchable calls in #12061 a
Debug: implement breakpoints and single-stepping. (#12133)
* Debug: implement breakpoints and single-stepping.
This is a PR that puts together a bunch of earlier pieces (patchable calls in #12061 and #12101, private copies of code in #12051, and all the prior debug event and instrumentation infrastructure) to implement breakpoints in the guest debugger.
These are implemented in the way we have planned in #11964: each sequence point (location prior to a Wasm opcode) is now a patchable call instruction, patched out (replaced with NOPs) by default. When patched in, the breakpoint callsite calls a trampoline with the `patchable` ABI which then invokes the `breakpoint` hostcall. That hostcall emits the debug event and nothing else.
A few of the interesting bits in this PR include: - Implementations of "unpublish" (switch permissions back to read/write from read/execute) for mmap'd code memory on all our platforms. - Infrastructure in the frame-tables (debug info) metadata producer and parser to record "breakpoint patches". - A tweak to the NOP metadata packaged with the `MachBuffer` to allow multiple NOP sizes. This lets us use one 5-byte NOP on x86-64, for example (did you know x86-64 had these?!) rather than five 1-byte NOPs.
This PR also implements single-stepping with a global-per-`Store` flag, because at this point why not; it's a small additional bit of logic to do *all* patches in all modules registered in the `Store` when that flag is enabled.
A few realizations for future work: - The need for an introspection API available to a debugger to see the modules within a component is starting to become clear; either that, or the "module and PC" location identifier for a breakpoint switches to a "module or component" sum type. Right now, the tests for this feature use only core modules. Extending to components should not actually be hard at all, we just need to build the API for it. - The interaction between inlining and `patchable_call` is interesting: what happens if we inline a `patchable_call` at a `try_call` callsite? Right now, we do *not* update the `patchable_call` to a `try_call`, because there is no `patchable_try_call`; this is fine in the Wasmtime embedding in practice because we never (today!) throw exceptions from a breakpoint handler. This does suggest to me that maybe we should make patchability a property of any callsite, and allow try-calls to be patchable too (with the same restriction about no return values as the only restriction); but happy to discuss that one further.
* Add missing debug.wat disas test.
* Review feedback.
* Fix comment on `CodeMemory::text_mut`.
* Review feedback.
* Review feedback: abort process on failure to re-apply executable permissions.
* Implement icache flush for aarch64.
This appears to be necessary as we otherwise see a failure in CI on macOS/aarch64 that is consistent with patched-in breakpoint calls still being incorrectly cached after we remove them and republish the code.
There is a longstanding issue in #3310 tracking proper icache coherence handling on aarch64. We implemented this for Linux with the `membarrier` syscall but never did so for macOS. Maybe this is the first point at which it matters, because code was always loaded at new addresses (hence did not have coherence issues because nothing would have been cached) previously.
prtest:full
* Review feedback: use `next_multiple_of`.
show more ...
|
| c00e9ea2 | 02-Dec-2025 |
Chris Fallin <[email protected]> |
Cranelift: add patchable call instructions. (#12101)
* Cranelift: add patchable call instructions.
The new `patchable_call` CLIF instruction pairs with the `patchable` ABI, and emits a callsite wit
Cranelift: add patchable call instructions. (#12101)
* Cranelift: add patchable call instructions.
The new `patchable_call` CLIF instruction pairs with the `patchable` ABI, and emits a callsite with one new key property: the MachBuffer carries metadata that describes exactly which byte range to "NOP out" (overwrite with NOP instructions) to disable that callsite. Doing so is semantically valid and explicitly supported.
This enables patching of code at runtime to dynamically turn on and off features such as instrumentation or debugging hooks. We plan to use this to implement breakpoints in Wasmtime's guest debugging support.
As part of this change, I added a notion of "unit of NOP bytes" to the MachBuffer so that the consumer (e.g., Wasmtime's Cranelift-based code compilation pipeline and metadata-producing logic) can handle patchable callsites without any other special knowledge of the ISA.
For the "real metal" ISAs there are perfectly well-defined NOPs to use, but for Pulley, where all opcodes are assigned at compile time by macro magic, I explicitly defined NOP as opcode byte 0 by moving `Nop`'s definition to the top of the list and adding a unit test asserting its encoding.
A design note: in principle it would be possible, as an alternative, to treat "patchability" as an orthogonal dimension of all callsites, and emit the metadata describing the instruction-offset range for any callsite with the flag set. The only truly necessary semantic restriction is that there are no return values (because if we turn the callsite off, nothing writes to them); we could support patchability for other ABIs and for the other kinds of call instructions. The `patchable` ABI would then be better described as something like the "no clobbers ABI". I opted not to generalize in this way because it creates some less-tested corners and the generalized form, at least at the MachInst level, is not really much simpler in the end.
A testing note: I opted not to implement actual code patching in the `cranelift-tools` filetest runner and test patching callsites in/out via some actuation (e.g. a magic hostcall, like we do for throws) because (i) that's a lot of new plumbing and (ii) we are going to test this very shortly in Wasmtime anyway and (iii) the correctness (or not) of the location-and-length metadata is easy enough to verify in the disassemblies in the compile-tests.
* Review feedback: remove dependence on (and test for) NOP being the literal byte 0.
show more ...
|
| 08927ba9 | 22-Nov-2025 |
Chris Fallin <[email protected]> |
Cranelift: add a "patchable call" ABI. (#12061)
This ABI is intended for use in scenarios where we want a very lightweight callsite that can be turned on and off by patching in one instruction. (The
Cranelift: add a "patchable call" ABI. (#12061)
This ABI is intended for use in scenarios where we want a very lightweight callsite that can be turned on and off by patching in one instruction. (The actual patchable call instruction is not in this PR; that will be a separate PR.)
The idea is that we define a call to clobber *no* registers -- not even the arguments! And we restrict signatures such that on all of our supported architectures, all arguments go into registers only. Those two requirements together mean that all callsites for this ABI should have only a raw call instruction, with no loads/stores to stackslots; and have the minimum possible impact on regalloc, by only imposing constraints on args to ensure they are in certain registers but not altering those registers.
Given this, we could implement, e.g., breakpoints with patchable callsites (off by default) at every sequence point in compiled code. In a typical use-case with Wasmtime-compiled Wasm, that would put a bunch of uses of vmctx constrained to the first argument register in every code path, but vmctx likely already sits there most of the time anyway (for any call to other Wasm functions or for libcalls). Thus, the impact is just the one instruction and nothing else.
This PR adds the calling convention itself and tests that show that *two* consecutive callsites can be compiled with no register setup re-occurring from one call to the next (thus demonstrating no clobbers).
show more ...
|