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
134 lines
5.5 KiB
Python
134 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Check the LDLS loader against an object we compiled ourselves.
|
|
#
|
|
# docs/plan/ldls.md milestone L0. The thing being tested is ldls/ko.c; the
|
|
# input is ldls/test/refmod.c compiled with -c, which is exactly what a `.ko`
|
|
# is. **No kernel module is involved and none is in this tree**, which is rule
|
|
# 1 applied to the repository as well as to the image.
|
|
#
|
|
# This is a test rather than a demo, so it asserts. `ldls-inspect` printing
|
|
# something plausible is not evidence; the specific things the loader has to
|
|
# get right are listed below and each one is checked by name.
|
|
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def fail(msg):
|
|
print("ldls-check: FAIL -- %s" % msg)
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print("usage: ldls_check.py <ldls-inspect> <object.o>")
|
|
return 2
|
|
|
|
inspect, obj = sys.argv[1], sys.argv[2]
|
|
r = subprocess.run([inspect, obj], capture_output=True, text=True)
|
|
out = r.stdout
|
|
|
|
if r.returncode != 0:
|
|
fail("ldls-inspect exited %d\n%s%s" % (r.returncode, out, r.stderr))
|
|
|
|
checks = 0
|
|
|
|
def need(cond, what):
|
|
nonlocal checks
|
|
checks += 1
|
|
if not cond:
|
|
fail("%s\n--- output ---\n%s" % (what, out))
|
|
print(" [ok] %s" % what)
|
|
|
|
# It is recognised as a module at all, and its declared licence is read
|
|
# straight out of .modinfo. That is the first question anyone should be
|
|
# able to ask about an object before loading it.
|
|
need("module refmod" in out, "the module names itself from .modinfo")
|
|
need("license Apache-2.0" in out,
|
|
"the declared licence is read without loading anything")
|
|
|
|
# Every kind of section the loader has to place.
|
|
for sec, why in (
|
|
(".text", "text is laid out"),
|
|
(".data", "initialised data is laid out"),
|
|
(".bss", "bss is laid out, and zeroed rather than copied"),
|
|
):
|
|
need(re.search(r"^\s+%s\b" % re.escape(sec), out, re.M) is not None, why)
|
|
|
|
need("(zeroed)" in out, "bss is marked NOBITS and gets zeroed, not copied")
|
|
|
|
# The undefined symbols -- L0's entire output, and the work queue for L1.
|
|
m = re.search(r"undefined symbols: (\d+)", out)
|
|
need(m is not None, "the undefined symbols are counted")
|
|
need(int(m.group(1)) >= 4, "it found the object's undefined symbols")
|
|
|
|
for sym in ("ldls_test_alloc", "ldls_test_free", "ldls_test_print",
|
|
"ldls_test_counter"):
|
|
need(sym in out, "reports the undefined symbol %s" % sym)
|
|
|
|
# And the one that matters most: with a resolver that knows nothing, a real
|
|
# object cannot relocate, and the loader must say which name stopped it
|
|
# rather than quietly producing a broken image.
|
|
need("stops at: unresolved: ldls_test_" in out,
|
|
"relocation stops at a named unresolved symbol")
|
|
|
|
need("loads to" in out, "the object reports a load size")
|
|
|
|
# The relocations are counted by type, and none of them is one the loader
|
|
# cannot handle. That check matters more than it looks: an unhandled type
|
|
# is invisible until a relocation of that type is reached, which for a real
|
|
# driver can be the last one in the file.
|
|
m = re.search(r"relocations: (\d+)", out)
|
|
need(m is not None and int(m.group(1)) > 0,
|
|
"the relocations are counted by type")
|
|
need("does not handle" not in out,
|
|
"every relocation type in the object is one the loader handles")
|
|
|
|
# And with everything resolved, relocation must run to the end. This is a
|
|
# different question from the list of undefined symbols and it is the one
|
|
# that says the loader is finished rather than merely parsing.
|
|
r2 = subprocess.run([inspect, "-q", "-r", obj], capture_output=True,
|
|
text=True)
|
|
need(r2.returncode == 0 and "relocated in full against stubs" in r2.stdout,
|
|
"with every symbol resolved, relocation runs to the end")
|
|
|
|
# A file that is not a relocatable object is refused rather than parsed.
|
|
r3 = subprocess.run([inspect, sys.argv[0]], capture_output=True, text=True)
|
|
need(r3.returncode != 0, "a non-ELF file is refused, not parsed")
|
|
|
|
# --- L1: the object is loaded and its init actually runs ----------------
|
|
if len(sys.argv) < 4:
|
|
print("ldls-check: %d checks passed (L0 only -- no ldls-run given)"
|
|
% checks)
|
|
return 0
|
|
|
|
run = sys.argv[3]
|
|
r4 = subprocess.run([run, obj], capture_output=True, text=True)
|
|
out = r4.stdout
|
|
need(r4.returncode == 0, "ldls-run loads the object and returns cleanly")
|
|
need("calling init_module" in out, "it finds and calls init_module")
|
|
need("init_module returned 0" in out,
|
|
"init runs to the end and returns what it meant to")
|
|
|
|
# The call *order*, which is the whole point of L1: a symbol list is a set
|
|
# and says nothing about sequence.
|
|
seq = re.findall(r"^\s+\d+\s+(\w+)\(", out, re.M)
|
|
need(seq == ["ldls_test_print", "ldls_test_alloc", "ldls_test_free"],
|
|
"the stub calls are reported in the order the code makes them")
|
|
|
|
# Arguments survive the trampoline and the thunk. If the register save were
|
|
# wrong this is where it would show, because the first argument is a real
|
|
# pointer into the loaded object rather than a small integer.
|
|
m = re.search(r"ldls_test_print\((0x[0-9a-f]+)", out)
|
|
need(m is not None and int(m.group(1), 16) != 0,
|
|
"arguments reach the stub intact, not zeroed by the thunk")
|
|
|
|
print("ldls-check: %d checks passed" % checks)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|