docs/plan/ldls.md milestone L1. Every undefined symbol resolves to a
trampoline -- twenty generated bytes that load the symbol's index and jump to
a shared thunk, which saves the argument registers and reports the call. Then
init_module is simply called.
$ ldls-run 8139too.ko
module 8139too
license GPL
wants 94 symbols
calling init_module at 0x416fc020
1 _printk(0x416fb6c0, 0, 0)
| 8139too: 8139too Fast Ethernet driver 0.9.28
2 __pci_register_driver(0x416fb400, 0x416fff80, 0x416fbdbb)
init_module returned 0 after 2 stub call(s)
That second line is the driver's own banner, printed through a _printk that
is ours, from a string in the module's own .rodata that a relocation put
there. There is no stronger evidence that a load worked than the loaded thing
speaking.
Stubs return zero rather than stopping, which departs from what the milestone
originally said and does so deliberately: stopping at the first call tells you
one name, carrying on tells you the sequence, and the sequence is the thing a
symbol table cannot give you. A driver that then dereferences the zero faults,
and that is a result too -- it says how far it got before it needed the kernel
to have really done something.
The first run failed on neither of those. It failed on a relocation:
ldls-run: relocate: a signed 32-bit relocation does not fit
A module is compiled -mcmodel=kernel, so the compiler emits R_X86_64_32S for
anything it believes lives in the top 2 GB, and an ordinary mmap at 0x7f...
has nowhere to put it. Linux gives modules a region within 2 GB of the kernel
text; the harness uses MAP_32BIT to reach the same window from the other end.
On SchOS this costs nothing -- a process's mmap area starts at 0x40000000 and
is already inside it -- but it is a real constraint on where the host may load
a module, and a loader that did not check its own arithmetic would have found
it much later and much more confusingly.
Swept over 500 stock modules: 449 ran init to completion, 22 faulted on a
stub's zero, 29 have no init_module, and zero hit a loader error.
Recorded honestly in "what it will not do": running init is not working. Every
symbol is a stub, so what has been shown is that drivers ask for things in a
readable order, not that anything answers. Also recorded: every undefined
symbol in a module is NOTYPE, so the stubs cannot tell jiffies from
pci_iomap and both get a trampoline -- BTF distinguishes VAR from FUNC and
that is where the fix belongs rather than in a guess.
make ldls-check is 23 checks now, five of them L1's, all against
ldls/test/refmod.c which is ours and now has an init_module of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
55 lines
2.4 KiB
C
55 lines
2.4 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Stubs: giving every undefined symbol an address that says who it was.
|
|
//
|
|
// docs/plan/ldls.md milestone L1. L0 measured what a module asks for; this is
|
|
// what answers. Every name resolves to a small block of generated code, and
|
|
// when the driver calls it the block says which name it was and what arguments
|
|
// it came with.
|
|
//
|
|
// The value is not the logging, it is the **order**. A module's undefined
|
|
// symbol list is a set and tells you nothing about sequence; what a driver
|
|
// does at init, and in what order, is the other thing that cannot be guessed
|
|
// and it falls out of running the thing.
|
|
//
|
|
// Freestanding, like ldls/ko.c, so the SchOS-side host gets the same code.
|
|
|
|
#define STUB_SIZE 32 // bytes per trampoline, generously aligned
|
|
|
|
// What a stub call reports. The embedder provides this; stub.c calls it from
|
|
// the thunk with the original argument registers intact, which is what lets a
|
|
// stub for `_printk` show the string the driver actually passed.
|
|
//
|
|
// Whatever it returns becomes the stub's return value, so returning 0 lets a
|
|
// driver carry on past a call and reveal the next one -- which is worth far
|
|
// more than stopping at the first.
|
|
unsigned long ldls_stub(long idx, unsigned long *args);
|
|
|
|
struct stubs {
|
|
unsigned char *code; // STUB_SIZE * n bytes, must be executable
|
|
int n;
|
|
};
|
|
|
|
// Build `n` trampolines into `code`. Each one, when called, ends up in
|
|
// ldls_stub() with its index.
|
|
//
|
|
// `code` has to be executable memory; this only writes the bytes.
|
|
void stub_build(struct stubs *s, unsigned char *code, int n);
|
|
|
|
// The address of stub `i`, to hand to the loader as a symbol value.
|
|
unsigned long long stub_addr(const struct stubs *s, int i);
|
|
|
|
// Some names must not be logged, because they are called so often that the log
|
|
// becomes the only thing you see, and because they have calling conventions of
|
|
// their own that a logging thunk would break.
|
|
//
|
|
// `__fentry__` is ftrace's hook, emitted at the top of every function, and it
|
|
// is required to preserve every register. `__x86_return_thunk` is the retpoline
|
|
// return. Both are correctly implemented as a bare `ret`, which is also the
|
|
// fastest thing they could possibly be.
|
|
//
|
|
// Returns 1 if the name got a bare-ret stub instead of a logging one.
|
|
int stub_is_bare(const char *name);
|
|
|
|
// Overwrite one stub with that bare `ret`.
|
|
void stub_make_bare(unsigned char *p);
|