SchOS/ldls/test/refmod.c
Nexoniarz 7a7c7611e0 ldls: the stubs, and the driver's own voice
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
2026-09-14 08:02:29 +02:00

99 lines
3.9 KiB
C

// SPDX-License-Identifier: Apache-2.0
//
// The loader's test object. **Ours, on purpose.**
//
// docs/plan/ldls.md rule 1 says nothing GPL ever enters an image we build, and
// the same rule applies to the tree itself: there is no `.ko` committed here
// and there never will be. So the thing `make ldls-check` loads is this file,
// compiled with `-c`, which produces exactly what a `.ko` is -- an ELF64
// relocatable object for x86-64.
//
// It is written to contain one of each thing the loader has to get right,
// because a test object that only exercises the easy path is a test that
// passes on a loader that does not work:
//
// - undefined symbols, of both the function and the data kind
// - a `.text` with calls needing PC32/PLT32
// - `.data` with an absolute pointer needing R_X86_64_64
// - `.rodata` a pointer points into, so section-relative values matter
// - `.bss`, which must arrive zeroed and is NOBITS with no file content
// - a `.modinfo` shaped the way a module's is, so ko_modinfo() is exercised
//
// It is never linked and never run. Compiling it is the whole of its job.
// Things this object does not define. A real module's undefined list is
// hundreds of names from the kernel it was built against; five is enough to
// prove the loader reports them rather than to pretend this is a driver.
extern int ldls_test_alloc(unsigned long size);
extern void ldls_test_free(void *p);
extern int ldls_test_print(const char *s);
extern int ldls_test_counter; // undefined *data*, not a function
// .rodata, and something in .data pointing at it -- which is the case that
// needs a section-relative symbol value to come out right.
static const char greeting[] = "ldls reference module";
const char *ldls_test_greeting = greeting;
// .bss. The loader must zero this rather than copy it, because NOBITS has no
// bytes in the file to copy.
static char scratch[256];
char *ldls_test_scratch = scratch;
// .data with a known value, so a test can check the bytes survived layout.
int ldls_test_magic = 0x1D15;
// .text with calls, which is where PC32 and PLT32 come from.
int
ldls_test_entry(void)
{
int n = ldls_test_alloc(sizeof(scratch));
if (n == 0)
return -1;
ldls_test_print(greeting);
ldls_test_counter++;
scratch[0] = (char)ldls_test_magic;
ldls_test_free(scratch);
return ldls_test_counter;
}
// A second function, called by the first, so at least one relocation resolves
// inside the object and does not need the resolver at all.
int
ldls_test_twice(void)
{
return ldls_test_entry() + ldls_test_entry();
}
// Shaped like a module's `.modinfo`: NUL-terminated key=value strings in their
// own section. A real one is produced by MODULE_LICENSE() and friends; this is
// the same bytes by hand, because the macros that emit them are not ours.
#define MODINFO __attribute__((section(".modinfo"), used, aligned(1)))
MODINFO static const char mi_license[] = "license=Apache-2.0";
MODINFO static const char mi_description[] = "description=LDLS loader test object";
MODINFO static const char mi_name[] = "name=refmod";
// Modules define this; ko_name() looks for it to tell a kernel module from an
// ordinary object file.
int __this_module;
// The entry point, under the name `module_init(x)` gives it.
//
// L1 calls this, so it is written to exercise the thing L1 is for: a sequence
// of calls out to symbols this object does not define, in an order that can be
// checked. Three calls, always the same three, always in this order -- which
// is exactly what a real driver's init produces and what no amount of reading
// the symbol table would tell you.
//
// It deliberately does *not* dereference what the stubs return. A real driver
// does, faults, and that is a useful result; a test that faults every time is
// a test that cannot tell a fault it expected from one it did not.
int
init_module(void)
{
ldls_test_print(greeting);
ldls_test_alloc(sizeof(scratch));
ldls_test_free(scratch);
return 0;
}