D062 - One stub per import, not one shared target

decided ยท 2026-08-19

This is the other half of D005. Relocation writes an address into a linkage table slot; a thunk is what lives at that address.

A single shared target answers "did the guest call something unimplemented?" and nothing else, which is worth very little. One stub per import answers which, in order, with counts - the entire input to the loop this project exists to run: execute it, read what it wanted, implement the frequent ones, execute it again. The cost is 32 bytes per import; the 96 MB executable spends 45 KiB across 1,411 stubs, once.

The carrier registers are forced. A stub puts its own index in r10 and the trampoline address in r11 - the only two registers System V lets a callee destroy that do not carry an argument. Any other choice corrupts an argument before the trampoline can save it, and the corruption stays invisible until some implemented function eventually reads the wrong value.

An absolute jump through a register rather than a relative one, because the table and the trampoline are separate allocations and nothing keeps them within two gigabytes.

One hand-written trampoline serves the whole table, because Rust cannot read r10. It spills the six argument registers and re-presents them as an ordinary call. Its sub rsp, 8 is not decoration: System V wants rsp % 16 == 0 at a call site, and getting it wrong does nothing at all until some callee runs an aligned vector instruction against a stack slot.

Recording obeys principle 9 - no allocation and no locks on the call path. Counters are allocated when the table is built; a bounded ring keeps the first calls in order, because a histogram loses the sequence and the sequence is what says what the guest was trying to do.