docs/plan/ldls.md milestone L2, less the half that needs /dev/pci.
In Linux, __pci_register_driver ends at the PCI core, which matches a
driver's id_table against the bus and calls probe(). There is no PCI core
here, so LDLS keeps the pointer that call was given and plays the part.
To do that it must know where pci_driver.probe is, and it asks the kernel
rather than being told by us. ldls/btf.c reads /sys/kernel/btf/vmlinux --
122,279 types, every struct layout and every function prototype, as generated
data describing the kernel that is installed. Implemented from
Documentation/bpf/btf.rst, which is a published specification.
$ ldls-types /sys/kernel/btf/vmlinux --need
pci_driver probe offset 16 size 8
pci_dev resource offset 1000 size 1088
resource start offset 0 size 8
all present
Cross-checked against pahole on the same file, field for field. No offset in
this is ours, which is rule 3's uncomfortable row disappearing rather than
being argued about.
And then the driver:
driver name "8139too"
devices it claims: 10ec:8139 ... and 16 more
calling probe(pci_dev 10ec:8139, ...)
1 alloc_etherdev_mqs(0x368, 1, 1)
2 pci_enable_device / 3 pci_request_regions / 4 pci_set_master
5 pci_iomap(pdev, 1, 0)
6..544 ioread8 / iowrite8 -- 0x88, 0x8a, 0x8c, 0x8e, over and over
545 dev_addr_mod / 547 netif_napi_add_weight_locked
549 timer_init_key / 550 register_netdev / 551 netdev_info
probe returned 0 after 552 stub call(s)
10ec:8139 is the real RTL8139 id. It mapped BAR 1, the one marked
IORESOURCE_MEM. Those 538 alternating byte accesses stepping 0x88/0x8a/0x8c/
0x8e are serial EEPROM clocking -- the driver bit-banging the chip for its MAC
address and handing the result to dev_addr_mod. Then NAPI, a watchdog timer,
register_netdev, its "found it" line, and zero.
Fifteen distinct symbols carry that entire probe, and three of them are 540 of
the 552 calls. L1 said 44 symbols carry 110 modules through init; L2 says 15
carry this one through probe. Neither is a number anyone would have guessed
and both came from running the thing.
Recorded as not done, because it is not: probe receives a pci_dev whose shape
is right and whose contents are invented. The BARs are made up, pci_iomap
returns a zeroed page, and 8139too read a MAC address of zeroes and was
perfectly happy about it. The "all through /dev/pci" half of L2 belongs to the
SchOS side. Also recorded: BTF has to come from somewhere, and SchOS has no
/sys/kernel/btf/vmlinux -- the user brings it with the module, from the kernel
the module was built for.
make ldls-check is 27 checks, four of them L2's, and it says so out loud when
it skips them on a machine with no BTF rather than passing silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
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
docs/plan/ldls.md milestone L0. ldls/ko.c indexes the sections of an ELF64
relocatable object, places the allocatable ones, resolves undefined symbols
against a caller's table and applies the relocations. Nothing is executed.
It is freestanding -- no libc, no allocation, no I/O -- so the same source
builds as a host tool now and as the SchOS-side host later. That is the
arrangement mkschfs and the kernel already share, and for the same reason:
the failures at this layer are format failures, and one found on the host
costs a second instead of a boot.
$ ldls-inspect 8139too.ko
module 8139too
license GPL
loads to 33988 bytes
relocations: 1980
R_X86_64_64 363, PC32 991, PLT32 428, 32S 169, PC64 29
undefined symbols: 94
stops at: unresolved: __fentry__
The licence is printed before anything is loaded, because it is the one
property that decides whether the object may be on the machine at all and it
is a plain string in .modinfo. Rule 1 is easier to keep when the answer is on
screen.
94 symbols is the measured API surface of 8139too on 6.18.46 -- the list,
exactly, rather than an estimate -- and it is L1's work queue. It is smaller
than it looks: __fentry__, __stack_chk_fail, __ref_stack_chk_guard,
__x86_return_thunk and __fortify_panic are compiler and instrumentation
artefacts that stub out in a line each.
Swept over every module in a stock 6.18.46 tree: 2,440 objects, all of them
laid out and relocated to completion, none with a relocation type the loader
does not handle. That sweep is also what retired the fixed section table --
nvidia-modeset has 7,069 sections and 74,305 relocations, against a median of
50 -- so the caller supplies the table and there is no limit to run into
rather than a generous one that fails later.
Loading is not running, and the doc says so: not one symbol is implemented,
and the distance between "relocates" and "transmits a packet" is the rest of
the plan.
No .ko is in this tree and .gitignore now refuses one outright. make
ldls-check runs 18 checks against ldls/test/refmod.c, which is ours, written
to contain one of each thing the loader has to get right.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Shift is the logo, Guru is the icon, SchCotie is the mascot, and the difference
between them is not stylistic. An icon works once, at one size, in one state. A
mascot has to be able to *do* things -- confused, pleased, asleep, seen from
behind -- which is why it costs more and is drawn last. Written down because
this is exactly the kind of decision that gets re-derived badly.
Assets/ becomes docs/branding/, which also fixes two things:
* Filenames had spaces in them, so every reference needed quoting. They are
lowercase and hyphenated now.
* Assets/ held hand-written SVG and generated PNG in one directory, which is
the one rule docs/guides/layout.md states.
The guide records what was previously only in people's heads:
* The constraint everything answers to -- include/logo.h is a 128x128
single-channel coverage mask, so a mark has to survive as a solid shape in
one colour and be recognisable at 24 px. Colour is the last decision, never
the first.
* Guru's purple is load-bearing. PSOGM_BG #4B1D9A is deliberately not the
accent #7C5CFF, and theme.h's rule is that the full-bleed purple belongs
entirely to failure. If Guru appears in accent violet on a sticker, the deep
purple stops meaning "the machine stopped" -- and it is the only colour in
the system that means anything.
* Why shift-mask.svg is a separate file from shift.svg. mklogo.py reads ink as
(255 - luminance) * alpha, so tones survive as coverage and the relationship
inverts: darkest here is brightest on screen. Copying the violet fills would
light the object from below.
* Why a raster is committed beside its SVG. inkscape is not a build
prerequisite, so requiring a vector rasteriser would break the build on most
hosts for a file that changes about once a year.
* That the BSD daemon is copyrighted and SchCotie must be drawn from the
concept rather than from Beastie. A daemon is a Greek daimon and a Unix
background process; neither is ownable. The pitchfork, the sneakers and that
face are.
Format rule: SVG, or PNG at high resolution with no lossy compression, nothing
else. SVG is the source of truth for every mark and a raster is generated from
it, never edited.
include/logo.h regenerates byte-identical from the new path -- only the source
comment differs. The two old wordmark SVGs were deleted outside this session and
are left deleted; their PNGs are kept as superseded-*.png for reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
tools/windows/ .ps1 (pwsh, so usable from Linux and macOS too)
tools/posix/ .sh
tools/python/ .py
tools/ .c and .pl
The .c and .pl files stay at the top level because they are not
host-specific: the build compiles or invokes them on every platform.
Call sites updated in the Makefile, ports/mk/port.mk, docs and the
project memory. The one thing that does not survive a move on its own is
a script deriving the repo root from its own location -- all nine
PowerShell tools used Split-Path $PSScriptRoot -Parent, and memory-sync.sh
used dirname/.., both of which quietly become tools/ one level deeper.
Two levels now.
Two pre-existing Windows failures surfaced while verifying, both real and
neither caused by the move:
- schfs_fsck.c did not compile under mingw at all: --extract uses
POSIX mkdir(path,mode), symlink() and link(), none of which mingw
has. That took the whole tool with it, and `make schfs-check` with
it. mkdir maps to _mkdir; symlink and link fail loudly rather than
writing a regular file in their place, because an extracted tree
that differs from the image while reporting success defeats the
point of --extract.
- the round-trip test then failed at `ln -s`, since Git Bash cannot
create symlinks without developer mode. It skips with a message now.
Not a fudge: mkschfs's mingw build cannot see a symlink either, so
the test would have compared a tree with none against an image with
none and called it a pass -- coverage reporting itself as present
while being absent.
Verified on Windows: build clean, all seven bootcheck suites green,
make schfs-check passes.