ldls: being the PCI core, with offsets nobody wrote down

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
This commit is contained in:
Nexoniarz 2026-09-14 08:14:57 +02:00
commit 2d9a0ec6e3
8 changed files with 978 additions and 9 deletions

View file

@ -926,6 +926,7 @@ crashqr:
# -----------------------------------------------------------------------
LDLS_INSPECT = $(BUILD)/tools/ldls-inspect$(EXE)
LDLS_RUN = $(BUILD)/tools/ldls-run$(EXE)
LDLS_TYPES = $(BUILD)/tools/ldls-types$(EXE)
LDLS_REFMOD = $(BUILD)/ldls/refmod.o
$(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h
@ -935,10 +936,11 @@ $(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h
# L1. Calls a module's init with every symbol stubbed, to read off the call
# order. -fno-strict-aliasing because this deliberately treats loaded bytes as
# code, and no optimiser assumption about that is one we want.
$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/ko.h ldls/stub.h
$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \
ldls/ko.h ldls/stub.h ldls/btf.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \
ldls/host/run.c ldls/ko.c ldls/stub.c
ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c
# The test object, compiled and never linked. Ours, because docs/plan/ldls.md
# rule 1 means no .ko is committed here -- see the header of refmod.c.
@ -950,11 +952,17 @@ $(LDLS_REFMOD): ldls/test/refmod.c
@mkdir -p $(BUILD)/ldls
$(HOSTCC) -c -O1 -fno-pic -fno-asynchronous-unwind-tables -o $@ $<
ldls: $(LDLS_INSPECT) $(LDLS_RUN)
# L2. Reads struct layouts out of a kernel's own BTF, so they are never in our
# source. See ldls/btf.h.
$(LDLS_TYPES): ldls/host/types.c ldls/btf.c ldls/btf.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/types.c ldls/btf.c
ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES)
# Load our own object and check the loader got it right. No network, no QEMU,
# no module from anywhere.
ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_REFMOD)
ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES) $(LDLS_REFMOD)
$(PYTHON) tools/python/ldls_check.py $(LDLS_INSPECT) $(LDLS_REFMOD) $(LDLS_RUN)
$(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h

View file

@ -95,7 +95,7 @@ The galling ones. The work exists, is tested, and nothing uses it.
|---|---|
| **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** |
| ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". |
| **`ldls.md`** | L0 and L1 done — the loader handles 2,440 stock modules, and 449 of 500 run their `init_module` to completion against stubs. L2 (the hardware) and L3 (a packet) open. Every symbol is still a stub that logs and returns zero. |
| **`ldls.md`** | L0, L1 and L2 done — the loader handles 2,440 stock modules; 449 of 500 run `init_module`; and `8139too`'s `probe()` runs to completion and returns 0, reached through struct offsets read from the kernel's own BTF. L3 (a packet, and the `/dev/pci` back end) open. The hardware is still invented. |
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
| **`debugger.md`** | D0 through D4 built and described in [../architecture/debugging.md](../architecture/debugging.md). Only the network transport is open — a userspace daemon bridging TCP to `/dev/dbg`, which is deliberately last. |

View file

@ -180,7 +180,7 @@ One device, one process, claimed and released, on the `/dev/pci` discipline.
|---|---|---|
| **L0** | **The loader.** Parse a real `.ko`, lay it out, relocate it, and **print every unresolved symbol**. Nothing executes. The output is the measured API surface. | **done** — `ldls/ko.c`, `make ldls-check` |
| **L1** | **Stubs.** Every symbol resolves, to a function that logs its name. `init_module` runs, which yields the *call order* — the second thing you need and the other thing you cannot guess. | **done** — `ldls/stub.c`, `ldls-run` |
| **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | |
| **L2** | **The hardware.** `probe()` receives a working `struct pci_dev`, maps its BARs, allocates coherent DMA and registers an interrupt, all through `/dev/pci`. The driver reaches its device. | **done**, except `/dev/pci` — see below |
| **L3** | **A packet.** The process-backed netdev, wired to the driver's transmit and receive paths. **`ping` over a Linux driver.** This is the milestone that proves the whole idea and the one that cannot be skipped. | |
| **L4** | **Kill it.** The host process dies, the kernel clears bus mastering and reclaims the DMA, a new host starts and the interface comes back. | free — H2 built all of this |
@ -373,6 +373,99 @@ the kernel, not the module, is supposed to call. Reaching that is L2.
---
## L2, and being the PCI core
In Linux, `__pci_register_driver` ends at the PCI core, which matches the
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 itself.
To do that it has to 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.
```
$ ldls-types /sys/kernel/btf/vmlinux --need
pci_driver probe offset 16 size 8
pci_driver id_table offset 8 size 8
pci_dev vendor offset 60 size 2
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.
### What the driver then did
```
pci_driver at 0x41662400, from BTF: probe +16, id_table +8
driver name "8139too"
devices it claims:
10ec:8139 class 000000/000000
... and 16 more
calling probe(pci_dev 10ec:8139, ...)
1 alloc_etherdev_mqs(0x368, 0x1, 0x1)
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(...)
| %s at 0x%p, %pM, IRQ %d
probe returned 0 after 552 stub call(s)
```
`10ec:8139` is the real RTL8139 id. The driver mapped **BAR 1**, which is the
one we marked `IORESOURCE_MEM`. And those 538 alternating byte accesses with
values stepping `0x88, 0x8a, 0x8c, 0x8e` are **serial EEPROM clocking** — the
driver bit-banging the chip to read its MAC address, then handing the result to
`dev_addr_mod`. It registered a NAPI instance, started a watchdog timer,
registered the interface, printed its "found it" line, and **returned 0**.
That is a complete driver bring-up, on shape alone.
### Fifteen symbols
The whole of that probe touches **15 distinct symbols**, and 538 of the 552
calls are two of them:
| | |
|---|---|
| `ioread8` / `iowrite8` / `ioread32` | 540 calls, 3 symbols |
| PCI setup | `pci_enable_device`, `pci_request_regions`, `pci_set_master`, `pci_iomap` |
| net | `alloc_etherdev_mqs`, `dev_addr_mod`, `netif_napi_add_weight_locked`, `register_netdev`, `netdev_info` |
| other | `mutex_lock`, `mutex_unlock`, `timer_init_key` |
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
out of running the thing rather than reading about it.
### What is not done
`probe()` receives a `pci_dev` whose *shape* is right and whose *contents are
invented*. The BARs are numbers this harness made up; `pci_iomap` returns a
zeroed page, not a mapping; nothing has touched hardware and nothing could,
because this runs on the build machine.
**The half of L2 that says "all through `/dev/pci`" belongs to the SchOS
side**, where those calls have somewhere real to land. What is settled is the
part that had to come first: that probe can be reached at all, with a struct
the driver accepts, built from offsets nobody wrote down.
---
## What it will not do
- **It is not a security boundary, and here that is worse than usual.** There
@ -418,6 +511,15 @@ the kernel, not the module, is supposed to call. Reaching that is L2.
- **`ldls-run` is a development harness, not the deliverable.** It runs on the
build machine so that loader failures cost a second instead of a boot. The
thing that ships is the SchOS-side host.
- **BTF has to come from somewhere.** On Linux it is `/sys/kernel/btf/vmlinux`.
SchOS has no such file and never will — so the user brings the BTF along with
the module, from the kernel the module was built for. Both are theirs,
neither is shipped, and a kernel built without `CONFIG_DEBUG_INFO_BTF` has
neither.
- **`probe()` is reached with invented hardware.** The BARs are numbers the
harness made up and `pci_iomap` returns a zeroed page. A driver that reads a
status register gets whatever was there, which is why 8139too read a MAC
address of zeroes and was perfectly happy about it.
- **It will not support "Linux drivers".** It will support *one driver, on one
kernel version, that has been tried*. Anything beyond that is a claim, and
the list of what has actually been run belongs in the architecture document

283
ldls/btf.c Normal file
View file

@ -0,0 +1,283 @@
// SPDX-License-Identifier: Apache-2.0
//
// Reading BTF. See ldls/btf.h.
//
// Implemented from Documentation/bpf/btf.rst. The format is small enough to
// hold in your head: a header, a run of variable-length type records, and a
// string table. The only fiddly part is that a record's length depends on its
// kind, so iterating means knowing how much trails each one -- which is the
// table in trailing_bytes() below and nothing more.
#include "btf.h"
static void
btf_fail(struct btf *b, const char *msg, const char *what)
{
int i = 0, j = 0;
while (msg[i] && i < (int)sizeof(b->err) - 1) { b->err[i] = msg[i]; i++; }
if (what)
while (what[j] && i < (int)sizeof(b->err) - 1) b->err[i++] = what[j++];
b->err[i] = 0;
}
static int
btf_streq(const char *a, const char *c)
{
while (*a && *a == *c) { a++; c++; }
return *a == 0 && *c == 0;
}
const char *btf_err(const struct btf *b) { return b->err[0] ? b->err : "no error"; }
// How many bytes follow a type record, given its kind and vlen.
static unsigned int
trailing_bytes(unsigned int info)
{
unsigned int kind = BTF_INFO_KIND(info);
unsigned int vlen = BTF_INFO_VLEN(info);
switch (kind) {
case BTF_KIND_INT: return 4;
case BTF_KIND_ARRAY: return 12; /* type, index_type, nelems */
case BTF_KIND_STRUCT:
case BTF_KIND_UNION: return vlen * 12; /* struct btf_member */
case BTF_KIND_ENUM: return vlen * 8;
case BTF_KIND_ENUM64: return vlen * 12;
case BTF_KIND_FUNC_PROTO: return vlen * 8; /* struct btf_param */
case BTF_KIND_VAR: return 4;
case BTF_KIND_DATASEC: return vlen * 12;
case BTF_KIND_DECL_TAG: return 4;
default: return 0; /* PTR, TYPEDEF, CONST, ... */
}
}
long
btf_count(const void *image, unsigned long long len)
{
const struct btf_header *h = (const struct btf_header *)image;
const unsigned char *types;
unsigned int at = 0;
long n = 0;
if (len < sizeof(struct btf_header))
return -1;
if (h->magic != BTF_MAGIC || h->version != 1)
return -1;
if ((unsigned long long)h->hdr_len + h->type_off + h->type_len > len)
return -1;
types = (const unsigned char *)image + h->hdr_len + h->type_off;
while (at + sizeof(struct btf_type) <= h->type_len) {
const struct btf_type *t = (const struct btf_type *)(types + at);
unsigned int step = sizeof(struct btf_type) + trailing_bytes(t->info);
if (step == 0 || at + step > h->type_len)
break;
at += step;
n++;
if (n > BTF_MAXTYPES)
return -1;
}
return n;
}
int
btf_open(struct btf *b, const void *image, unsigned long long len,
unsigned int *off, unsigned int noff)
{
const struct btf_header *h = (const struct btf_header *)image;
unsigned int at = 0, id = 1;
int i;
for (i = 0; i < (int)sizeof(b->err); i++)
b->err[i] = 0;
b->img = (const unsigned char *)image;
b->len = len;
b->off = off;
b->ntypes = 0;
if (len < sizeof(struct btf_header))
{ btf_fail(b, "too short to be BTF", 0); return -1; }
if (h->magic != BTF_MAGIC)
{ btf_fail(b, "not BTF: wrong magic", 0); return -1; }
if (h->version != 1)
{ btf_fail(b, "BTF version is not 1", 0); return -1; }
if ((unsigned long long)h->hdr_len + h->type_off + h->type_len > len ||
(unsigned long long)h->hdr_len + h->str_off + h->str_len > len)
{ btf_fail(b, "BTF sections run off the end of the file", 0); return -1; }
b->h = h;
b->types = b->img + h->hdr_len + h->type_off;
b->types_len = h->type_len;
b->str = (const char *)(b->img + h->hdr_len + h->str_off);
b->str_len = h->str_len;
// Index every type. Ids are assigned in order starting at 1; id 0 is void
// and is not stored.
while (at + sizeof(struct btf_type) <= b->types_len) {
const struct btf_type *t = (const struct btf_type *)(b->types + at);
unsigned int step = sizeof(struct btf_type) + trailing_bytes(t->info);
if (at + step > b->types_len)
break;
if (id >= noff)
{ btf_fail(b, "the caller's type index is too small", 0); return -1; }
off[id++] = at;
at += step;
}
b->ntypes = id - 1;
return 0;
}
static const struct btf_type *
btf_at(struct btf *b, int id)
{
if (id <= 0 || (unsigned int)id > b->ntypes)
return 0;
return (const struct btf_type *)(b->types + b->off[id]);
}
const char *
btf_name_of(struct btf *b, int id)
{
const struct btf_type *t = btf_at(b, id);
if (!t || t->name_off >= b->str_len)
return "";
return b->str + t->name_off;
}
int
btf_kind_of(struct btf *b, int id)
{
const struct btf_type *t = btf_at(b, id);
return t ? (int)BTF_INFO_KIND(t->info) : -1;
}
int
btf_find(struct btf *b, const char *name, int kind)
{
unsigned int id;
for (id = 1; id <= b->ntypes; id++) {
const struct btf_type *t = (const struct btf_type *)(b->types + b->off[id]);
if ((int)BTF_INFO_KIND(t->info) != kind)
continue;
if (t->name_off >= b->str_len)
continue;
if (btf_streq(b->str + t->name_off, name))
return (int)id;
}
return 0;
}
// Follow the things that wrap a type without changing its layout.
static int
btf_strip(struct btf *b, int id)
{
int guard = 0;
while (id > 0 && guard++ < 32) {
const struct btf_type *t = btf_at(b, id);
if (!t)
return 0;
switch (BTF_INFO_KIND(t->info)) {
case BTF_KIND_TYPEDEF:
case BTF_KIND_CONST:
case BTF_KIND_VOLATILE:
case BTF_KIND_RESTRICT:
case BTF_KIND_TYPE_TAG:
id = (int)t->u.type;
continue;
default:
return id;
}
}
return 0;
}
unsigned int
btf_type_size(struct btf *b, int id)
{
const struct btf_type *t;
id = btf_strip(b, id);
t = btf_at(b, id);
if (!t)
return 0;
switch (BTF_INFO_KIND(t->info)) {
case BTF_KIND_INT:
case BTF_KIND_STRUCT:
case BTF_KIND_UNION:
case BTF_KIND_ENUM:
case BTF_KIND_ENUM64:
case BTF_KIND_FLOAT:
return t->u.size;
case BTF_KIND_PTR:
return 8; // x86-64; this reader is built for one ABI
case BTF_KIND_ARRAY: {
const unsigned int *a = (const unsigned int *)(t + 1);
return btf_type_size(b, (int)a[0]) * a[2];
}
default:
return 0;
}
}
int
btf_member_off(struct btf *b, int struct_id, const char *member,
unsigned int *off_bytes, unsigned int *size_bytes)
{
const struct btf_type *t;
const struct btf_member *m;
unsigned int i, vlen, kflag;
struct_id = btf_strip(b, struct_id);
t = btf_at(b, struct_id);
if (!t)
{ btf_fail(b, "no such type", 0); return -1; }
if (BTF_INFO_KIND(t->info) != BTF_KIND_STRUCT &&
BTF_INFO_KIND(t->info) != BTF_KIND_UNION)
{ btf_fail(b, "not a struct or union", 0); return -1; }
vlen = BTF_INFO_VLEN(t->info);
kflag = BTF_INFO_KFLAG(t->info);
m = (const struct btf_member *)(t + 1);
for (i = 0; i < vlen; i++, m++) {
const char *nm = (m->name_off < b->str_len) ? b->str + m->name_off : "";
unsigned int bit;
// An unnamed member is an anonymous struct or union, and what looks like
// a field of the outer type is really a field of that. Recurse, adding
// its own offset -- without this, every kernel struct with an anonymous
// union in it reports its later fields as missing.
if (nm[0] == 0) {
unsigned int sub_off, sub_size;
unsigned int here = kflag ? (m->offset & 0xffffff) : m->offset;
if (btf_member_off(b, (int)m->type, member, &sub_off, &sub_size) == 0) {
*off_bytes = here / 8 + sub_off;
*size_bytes = sub_size;
return 0;
}
continue;
}
if (!btf_streq(nm, member))
continue;
if (kflag && (m->offset >> 24) != 0)
{ btf_fail(b, "that member is a bitfield: ", member); return -1; }
bit = kflag ? (m->offset & 0xffffff) : m->offset;
if (bit % 8)
{ btf_fail(b, "that member is not byte-aligned: ", member); return -1; }
*off_bytes = bit / 8;
*size_bytes = btf_type_size(b, (int)m->type);
return 0;
}
btf_fail(b, "no such member: ", member);
return -1;
}

134
ldls/btf.h Normal file
View file

@ -0,0 +1,134 @@
// SPDX-License-Identifier: Apache-2.0
//
// Reading BTF: struct layouts and function prototypes, from the kernel that
// the driver was built for.
//
// docs/plan/ldls.md milestone L2, and the thing that makes rule 3 comfortable.
// The shim has to agree with a compiled driver about where `pci_driver.probe`
// sits, and a compiled driver is not negotiable about it. Writing those
// offsets into our source would mean a field-for-field match with Linux's
// headers, which *looks* like transcription however it was arrived at.
//
// It does not have to be in our source. A kernel built with
// CONFIG_DEBUG_INFO_BTF publishes `/sys/kernel/btf/vmlinux`: every struct
// layout and every function prototype, as generated binary type data
// describing the kernel that is installed. LDLS reads it at load time, on the
// user's machine, from the user's own kernel.
//
// Two things follow. The offsets are never ours to have copied, and they are
// right for the kernel actually present rather than one we pinned by guessing
// -- which is most of what rule 4 was worried about.
//
// The format is described in Documentation/bpf/btf.rst, which is a published
// specification; implementing it from that is the case ../docs/licensing.md §5
// covers. Freestanding, like the rest of ldls/.
// --- the format -------------------------------------------------------------
#define BTF_MAGIC 0xeB9F
struct btf_header {
unsigned short magic;
unsigned char version;
unsigned char flags;
unsigned int hdr_len;
// Both offsets are from the end of the header, not from the start of it.
unsigned int type_off;
unsigned int type_len;
unsigned int str_off;
unsigned int str_len;
};
struct btf_type {
unsigned int name_off;
// vlen in bits 0-15, kind in bits 24-28, kind_flag in bit 31.
unsigned int info;
union {
unsigned int size; // STRUCT, UNION, ENUM, INT, DATASEC, FLOAT
unsigned int type; // PTR, TYPEDEF, CONST, FUNC, VAR, ...
} u;
};
struct btf_member {
unsigned int name_off;
unsigned int type;
// With kind_flag set this is a bitfield: size in bits 24-31, bit offset in
// bits 0-23. Without it, a plain bit offset.
unsigned int offset;
};
struct btf_param {
unsigned int name_off;
unsigned int type;
};
#define BTF_KIND_INT 1
#define BTF_KIND_PTR 2
#define BTF_KIND_ARRAY 3
#define BTF_KIND_STRUCT 4
#define BTF_KIND_UNION 5
#define BTF_KIND_ENUM 6
#define BTF_KIND_FWD 7
#define BTF_KIND_TYPEDEF 8
#define BTF_KIND_VOLATILE 9
#define BTF_KIND_CONST 10
#define BTF_KIND_RESTRICT 11
#define BTF_KIND_FUNC 12
#define BTF_KIND_FUNC_PROTO 13
#define BTF_KIND_VAR 14
#define BTF_KIND_DATASEC 15
#define BTF_KIND_FLOAT 16
#define BTF_KIND_DECL_TAG 17
#define BTF_KIND_TYPE_TAG 18
#define BTF_KIND_ENUM64 19
#define BTF_INFO_KIND(i) (((i) >> 24) & 0x1f)
#define BTF_INFO_VLEN(i) ((i) & 0xffff)
#define BTF_INFO_KFLAG(i) (((i) >> 31) & 1)
// --- the reader ---------------------------------------------------------------
#define BTF_MAXTYPES 262144 // type ids we will index; vmlinux is ~150k
struct btf {
const unsigned char *img;
unsigned long long len;
const struct btf_header *h;
const unsigned char *types; // start of the type section
unsigned int types_len;
const char *str;
unsigned int str_len;
// Type id -> byte offset into the type section. Id 0 is void and has no
// entry, so this is 1-based and index 0 is unused.
unsigned int *off; // the caller's, ntypes + 1 entries
unsigned int ntypes;
char err[128];
};
// How many type ids this BTF holds, so the caller can size the index. -1 if
// the image is not BTF at all.
long btf_count(const void *image, unsigned long long len);
// Index it. `off` must have btf_count()+1 entries.
int btf_open(struct btf *b, const void *image, unsigned long long len,
unsigned int *off, unsigned int noff);
// The type id of a named type of a given kind, or 0.
int btf_find(struct btf *b, const char *name, int kind);
// Byte offset and size of a named member of a struct or union.
//
// Returns 0 on success. Bitfields are refused rather than rounded: a driver
// that reads one needs the bit position, and a byte offset would be a
// plausible-looking wrong answer.
int btf_member_off(struct btf *b, int struct_id, const char *member,
unsigned int *off_bytes, unsigned int *size_bytes);
// Size in bytes of a type, following typedefs and qualifiers. 0 if unknown.
unsigned int btf_type_size(struct btf *b, int id);
const char *btf_name_of(struct btf *b, int id);
int btf_kind_of(struct btf *b, int id);
const char *btf_err(const struct btf *b);

View file

@ -30,6 +30,7 @@
#include "../ko.h"
#include "../stub.h"
#include "../btf.h"
// --- what the stubs report ---------------------------------------------------
@ -38,6 +39,21 @@ static int g_count;
static int g_calls;
static int g_verbose;
// What init_module handed to __pci_register_driver.
//
// In Linux that call ends at the PCI core, which matches the driver's id_table
// against the bus and calls probe(). There is no PCI core here, so LDLS keeps
// the pointer and plays that part itself -- which is the whole of L2.
static unsigned long g_pci_driver;
static int g_answer; // give allocators real memory
static int
is_register_driver(const char *n)
{
return strcmp(n, "__pci_register_driver") == 0 ||
strcmp(n, "pci_register_driver") == 0;
}
// A handful of symbols are worth actually implementing rather than logging,
// because what they do is print -- and a driver printing its own banner
// through our code is the clearest evidence there is that the load worked.
@ -80,6 +96,40 @@ printk_like(const char *fmt, unsigned long *args)
printf("\n");
}
// The smallest set of answers that lets probe() get past its first allocation.
//
// **This is not the shim.** It is six answers chosen to stop the driver
// bailing out at the first call so that the *rest* of its sequence becomes
// visible -- the same reason L1's stubs return zero instead of stopping.
// Implementing these properly, against /dev/pci, is L3.
//
// sizeof(struct net_device) comes from BTF like everything else: a driver
// reaches its private area with netdev_priv(), which is the net_device
// rounded up to NETDEV_ALIGN, so getting it wrong hands the driver a pointer
// into the middle of its own control block.
static unsigned int g_netdev_size;
static unsigned long
answer(const char *name, unsigned long *args)
{
if (strcmp(name, "alloc_etherdev_mqs") == 0) {
unsigned long n = (g_netdev_size + 31u) & ~31u;
return (unsigned long)calloc(1, n + args[0] + 64);
}
if (strcmp(name, "pci_iomap") == 0 ||
strcmp(name, "pci_ioremap_bar") == 0 ||
strcmp(name, "ioremap") == 0)
return (unsigned long)calloc(1, 4096);
if (strcmp(name, "__kmem_cache_create_args") == 0 ||
strcmp(name, "kmem_cache_create") == 0)
return (unsigned long)calloc(1, 256);
if (strcmp(name, "dma_alloc_attrs") == 0)
return (unsigned long)calloc(1, args[1] ? args[1] : 4096);
// Success is zero for most of the kernel's int-returning calls, which is
// already what a stub returns -- so nothing is listed for them here.
return 0;
}
unsigned long
ldls_stub(long idx, unsigned long *args)
{
@ -93,6 +143,17 @@ ldls_stub(long idx, unsigned long *args)
if (is_printk(name))
printk_like((const char *)args[0], args);
// netdev_info(dev, fmt, ...) and dev_info(dev, fmt, ...) put the format in
// the *second* argument. Worth handling for the same reason as _printk: the
// line a driver prints when it has found its hardware is the one that says
// whether it really did.
if (strcmp(name, "netdev_info") == 0 || strcmp(name, "netdev_err") == 0 ||
strcmp(name, "_dev_info") == 0 || strcmp(name, "_dev_err") == 0)
printk_like((const char *)args[1], args + 1);
if (is_register_driver(name) && !g_pci_driver)
g_pci_driver = args[0];
if (g_verbose)
printf(" args: %#lx %#lx %#lx %#lx %#lx %#lx\n",
args[0], args[1], args[2], args[3], args[4], args[5]);
@ -100,7 +161,7 @@ ldls_stub(long idx, unsigned long *args)
// Zero, so the driver carries on and shows the next call. Stopping at the
// first tells you one name; continuing tells you the sequence, which is what
// this milestone is for.
return 0;
return g_answer ? answer(name, args) : 0;
}
// --- surviving the module -----------------------------------------------------
@ -115,6 +176,98 @@ on_fault(int sig)
siglongjmp(g_jump, 1);
}
// --- playing the PCI core -----------------------------------------------------
//
// Every offset here is read from the kernel's own BTF rather than written
// down. That is the point of doing it this way: LDLS never claims to know
// where `pci_driver.probe` is, it asks the kernel that the driver was built
// for. See ldls/btf.h.
struct pcilayout {
unsigned int drv_probe, drv_idtable, drv_name;
unsigned int id_size, id_vendor, id_device, id_class, id_class_mask;
unsigned int dev_size, dev_vendor, dev_device, dev_devfn, dev_irq,
dev_resource;
unsigned int res_size, res_start, res_end, res_flags;
};
static int
lay(struct btf *b, const char *type, const char *member, unsigned int *out)
{
unsigned int off, size;
int id = btf_find(b, type, BTF_KIND_STRUCT);
if (!id) {
fprintf(stderr, "ldls-run: this BTF has no struct %s\n", type);
return -1;
}
if (!member) { *out = btf_type_size(b, id); return 0; }
if (btf_member_off(b, id, member, &off, &size) < 0) {
fprintf(stderr, "ldls-run: %s.%s: %s\n", type, member, btf_err(b));
return -1;
}
*out = off;
return 0;
}
static int
pci_layout(struct btf *b, struct pcilayout *L)
{
return lay(b, "pci_driver", "probe", &L->drv_probe) ||
lay(b, "pci_driver", "id_table", &L->drv_idtable) ||
lay(b, "pci_driver", "name", &L->drv_name) ||
lay(b, "pci_device_id", 0, &L->id_size) ||
lay(b, "pci_device_id", "vendor", &L->id_vendor) ||
lay(b, "pci_device_id", "device", &L->id_device) ||
lay(b, "pci_device_id", "class", &L->id_class) ||
lay(b, "pci_device_id", "class_mask", &L->id_class_mask) ||
lay(b, "pci_dev", 0, &L->dev_size) ||
lay(b, "pci_dev", "vendor", &L->dev_vendor) ||
lay(b, "pci_dev", "device", &L->dev_device) ||
lay(b, "pci_dev", "devfn", &L->dev_devfn) ||
lay(b, "pci_dev", "irq", &L->dev_irq) ||
lay(b, "pci_dev", "resource", &L->dev_resource)||
lay(b, "resource", 0, &L->res_size) ||
lay(b, "resource", "start", &L->res_start) ||
lay(b, "resource", "end", &L->res_end) ||
lay(b, "resource", "flags", &L->res_flags) ? -1 : 0;
}
static unsigned int
rd32(unsigned long base, unsigned int off)
{
unsigned int v;
memcpy(&v, (void *)(base + off), 4);
return v;
}
static unsigned long
rd64(unsigned long base, unsigned int off)
{
unsigned long v;
memcpy(&v, (void *)(base + off), 8);
return v;
}
static void
wr16(unsigned long base, unsigned int off, unsigned short v)
{ memcpy((void *)(base + off), &v, 2); }
static void
wr32(unsigned long base, unsigned int off, unsigned int v)
{ memcpy((void *)(base + off), &v, 4); }
static void
wr64(unsigned long base, unsigned int off, unsigned long v)
{ memcpy((void *)(base + off), &v, 8); }
// The PCI resource flag bits. These are not layout and BTF does not carry
// them -- they are #defines. They are ABI between the kernel and every driver
// ever written against it, which makes them facts in the sense of
// docs/plan/ldls.md rule 3, and there are two of them.
#define LDLS_IORESOURCE_IO 0x00000100
#define LDLS_IORESOURCE_MEM 0x00000200
// --- loading ------------------------------------------------------------------
static unsigned char *
@ -185,10 +338,16 @@ main(int argc, char **argv)
unsigned char *img, *mem, *code;
unsigned long long len, span, entry;
int nsec, i, rv = 0;
const char *btfpath = 0;
if (argc > 1 && strcmp(argv[1], "-v") == 0) { g_verbose = 1; argv++; argc--; }
if (argc > 1 && strcmp(argv[1], "-a") == 0) { g_answer = 1; argv++; argc--; }
if (argc > 2 && strcmp(argv[1], "--btf") == 0) {
btfpath = argv[2]; argv += 2; argc -= 2;
}
if (argc < 2) {
fprintf(stderr, "usage: ldls-run [-v] <object.ko>\n");
fprintf(stderr,
"usage: ldls-run [-v] [-a] [--btf <btf>] <object.ko>\n");
return 2;
}
@ -292,9 +451,117 @@ main(int argc, char **argv)
printf("\n stopped by signal %d after %d stub call(s)\n", g_signal, g_calls);
printf(" that is a result, not a crash: the driver got that far before it\n"
" needed one of those stubs to have done something real\n");
rv = 0;
goto done;
}
// --- L2: be the PCI core -------------------------------------------------
if (!btfpath)
goto done;
if (!g_pci_driver) {
printf("\n no pci_driver was registered -- nothing to probe\n");
goto done;
}
{
struct btf b;
struct pcilayout L;
unsigned char *bimg;
unsigned long long blen;
unsigned int *boff;
long nt;
unsigned long idtab, pdev;
int nid = 0;
bimg = slurp(btfpath, &blen);
if (!bimg) { fprintf(stderr, "ldls-run: cannot read %s\n", btfpath); goto done; }
nt = btf_count(bimg, blen);
if (nt < 0) { fprintf(stderr, "ldls-run: %s is not BTF\n", btfpath); goto done; }
boff = calloc((size_t)nt + 2, sizeof(unsigned int));
if (!boff || btf_open(&b, bimg, blen, boff, (unsigned int)nt + 2) < 0) {
fprintf(stderr, "ldls-run: %s\n", btf_err(&b));
goto done;
}
if (pci_layout(&b, &L) < 0)
goto done;
{
int nd = btf_find(&b, "net_device", BTF_KIND_STRUCT);
g_netdev_size = nd ? btf_type_size(&b, nd) : 4096;
}
printf("\n pci_driver at %#lx, from BTF: probe +%u, id_table +%u\n",
g_pci_driver, L.drv_probe, L.drv_idtable);
{
const char *dn = (const char *)rd64(g_pci_driver, L.drv_name);
if (dn) printf(" driver name \"%s\"\n", dn);
}
idtab = rd64(g_pci_driver, L.drv_idtable);
if (!idtab) { printf(" it has no id_table\n"); goto done; }
printf("\n devices it claims:\n");
for (;;) {
unsigned long e = idtab + (unsigned long)nid * L.id_size;
unsigned int v = rd32(e, L.id_vendor), d = rd32(e, L.id_device);
unsigned int c = rd32(e, L.id_class);
if (!v && !d && !c) break;
if (nid < 8)
printf(" %04x:%04x class %06x/%06x\n", v, d, c,
rd32(e, L.id_class_mask));
nid++;
if (nid > 4096) break;
}
if (nid > 8) printf(" ... and %d more\n", nid - 8);
if (!nid) { printf(" (none)\n"); goto done; }
// A device to hand it. Zeroed, then the few fields a driver reads before
// it has called anything: what it is, where it lives, and its BARs.
//
// The BAR values are invented. The point of L2 is that probe() is reached
// with a pci_dev whose *shape* is right; making the values real is L3's
// job and belongs to /dev/pci rather than to a fake.
pdev = (unsigned long)calloc(1, L.dev_size);
if (!pdev) goto done;
{
unsigned long e = idtab; /* the first entry it claims */
unsigned long r0 = pdev + L.dev_resource;
unsigned long r1 = r0 + L.res_size;
wr16(pdev, L.dev_vendor, (unsigned short)rd32(e, L.id_vendor));
wr16(pdev, L.dev_device, (unsigned short)rd32(e, L.id_device));
wr32(pdev, L.dev_devfn, 0x18); /* 03.0 */
wr32(pdev, L.dev_irq, 11);
wr64(r0, L.res_start, 0xc000);
wr64(r0, L.res_end, 0xc0ff);
wr64(r0, L.res_flags, LDLS_IORESOURCE_IO);
wr64(r1, L.res_start, 0xfebc0000);
wr64(r1, L.res_end, 0xfebc00ff);
wr64(r1, L.res_flags, LDLS_IORESOURCE_MEM);
printf("\n calling probe(pci_dev %04x:%04x at %#lx, id %#lx)\n\n",
rd32(e, L.id_vendor), rd32(e, L.id_device), pdev, idtab);
g_calls = 0;
if (sigsetjmp(g_jump, 1) == 0) {
int (*probe)(void *, void *) =
(int (*)(void *, void *))(void *)rd64(g_pci_driver, L.drv_probe);
if (!probe) { printf(" the driver has no probe\n"); goto done; }
{
int r = probe((void *)pdev, (void *)idtab);
printf("\n probe returned %d after %d stub call(s)\n", r, g_calls);
}
} else {
printf("\n probe stopped by signal %d after %d stub call(s)\n",
g_signal, g_calls);
printf(" which is how far a driver gets before one of those stubs\n"
" has to do something real. That list is L3's work queue.\n");
}
}
}
done:
;
free(sec);
free(img);
return rv;

140
ldls/host/types.c Normal file
View file

@ -0,0 +1,140 @@
// SPDX-License-Identifier: Apache-2.0
//
// ldls-types -- ask a kernel's BTF where its fields are.
//
// docs/plan/ldls.md milestone L2. This is the tool that proves the layouts LDLS
// needs can be read from the kernel the user already has, rather than written
// into our source from theirs.
//
// ldls-types /sys/kernel/btf/vmlinux pci_driver probe id_table
// ldls-types /sys/kernel/btf/vmlinux --need
//
// `--need` prints every offset the PCI path needs at once, which is both a
// test and the thing L2's driver code consumes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../btf.h"
static unsigned char *
slurp(const char *path, unsigned long long *len)
{
FILE *f = fopen(path, "rb");
unsigned char *buf;
long n;
if (!f) return 0;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
n = ftell(f);
if (n <= 0) { fclose(f); return 0; }
rewind(f);
buf = malloc((size_t)n);
if (!buf) { fclose(f); return 0; }
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return 0; }
fclose(f);
*len = (unsigned long long)n;
return buf;
}
static int
show(struct btf *b, const char *type, const char *member)
{
unsigned int off, size;
int id = btf_find(b, type, BTF_KIND_STRUCT);
if (!id)
id = btf_find(b, type, BTF_KIND_UNION);
if (!id) {
printf(" %-22s %-22s (no such struct)\n", type, member ? member : "");
return 1;
}
if (!member) {
printf(" %-22s %-22s sizeof = %u\n", type, "", btf_type_size(b, id));
return 0;
}
if (btf_member_off(b, id, member, &off, &size) < 0) {
printf(" %-22s %-22s %s\n", type, member, btf_err(b));
return 1;
}
printf(" %-22s %-22s offset %5u size %u\n", type, member, off, size);
return 0;
}
// Every layout the PCI path needs to get from init_module to probe().
static const struct { const char *type, *member; } needed[] = {
{ "pci_driver", 0 },
{ "pci_driver", "name" },
{ "pci_driver", "id_table" },
{ "pci_driver", "probe" },
{ "pci_driver", "remove" },
{ "pci_device_id", 0 },
{ "pci_device_id", "vendor" },
{ "pci_device_id", "device" },
{ "pci_device_id", "subvendor" },
{ "pci_device_id", "subdevice" },
{ "pci_device_id", "class" },
{ "pci_device_id", "class_mask" },
{ "pci_device_id", "driver_data"},
{ "pci_dev", 0 },
{ "pci_dev", "vendor" },
{ "pci_dev", "device" },
{ "pci_dev", "devfn" },
{ "pci_dev", "irq" },
{ "pci_dev", "resource" },
{ "resource", 0 },
{ "resource", "start" },
{ "resource", "end" },
{ "resource", "flags" },
};
int
main(int argc, char **argv)
{
struct btf b;
unsigned char *img;
unsigned long long len;
unsigned int *off;
long n;
int i, bad = 0;
if (argc < 2) {
fprintf(stderr, "usage: ldls-types <btf-file> [--need | <struct> [member...]]\n");
return 2;
}
img = slurp(argv[1], &len);
if (!img) { fprintf(stderr, "ldls-types: cannot read %s\n", argv[1]); return 1; }
n = btf_count(img, len);
if (n < 0) { fprintf(stderr, "ldls-types: %s is not BTF\n", argv[1]); return 1; }
off = calloc((size_t)n + 2, sizeof(unsigned int));
if (!off) { fprintf(stderr, "ldls-types: out of memory\n"); return 1; }
if (btf_open(&b, img, len, off, (unsigned int)n + 2) < 0) {
fprintf(stderr, "ldls-types: %s\n", btf_err(&b));
return 1;
}
printf("%s\n %ld types, %u bytes of strings\n\n", argv[1], n, b.str_len);
if (argc == 2 || strcmp(argv[2], "--need") == 0) {
printf(" what the PCI path needs\n");
for (i = 0; i < (int)(sizeof(needed) / sizeof(needed[0])); i++)
bad += show(&b, needed[i].type, needed[i].member);
if (bad)
printf("\n %d of %d not found -- this kernel's BTF cannot drive probe()\n",
bad, (int)(sizeof(needed) / sizeof(needed[0])));
else
printf("\n all present\n");
return bad ? 1 : 0;
}
if (argc == 3)
return show(&b, argv[2], 0);
for (i = 3; i < argc; i++)
bad += show(&b, argv[2], argv[i]);
return bad ? 1 : 0;
}

View file

@ -126,6 +126,41 @@ def main():
need(m is not None and int(m.group(1), 16) != 0,
"arguments reach the stub intact, not zeroed by the thunk")
# --- L2: the layouts come from a kernel's own BTF -----------------------
#
# Conditional, and it says so when it skips. The BTF is the *host's*
# kernel type data -- not ours, not shipped, and not present on every
# machine. A check that silently passes when it did not run is worse than
# one that is honest about being unable to.
import os
types = os.path.join(os.path.dirname(inspect), "ldls-types")
btf = "/sys/kernel/btf/vmlinux"
if not (os.path.exists(types) and os.path.exists(btf)):
print("ldls-check: %d checks passed "
"(L2 skipped -- no %s on this machine)" % (checks, btf))
return 0
r5 = subprocess.run([types, btf, "--need"], capture_output=True, text=True)
need(r5.returncode == 0,
"every layout the PCI path needs is readable from the kernel's BTF")
need("all present" in r5.stdout, "none of them is missing")
# The offsets are not ours to have guessed, so the check is that they come
# back at all and are self-consistent -- pci_driver.probe after .id_table
# after .name, which is the order the struct declares them in.
o = {}
for line in r5.stdout.splitlines():
m2 = re.match(r"\s+(\w+)\s+(\w+)\s+offset\s+(\d+)", line)
if m2:
o[(m2.group(1), m2.group(2))] = int(m2.group(3))
need(o.get(("pci_driver", "name")) == 0 and
0 < o.get(("pci_driver", "id_table"), -1) <
o.get(("pci_driver", "probe"), -1),
"the pci_driver offsets come back in declaration order")
need(o.get(("resource", "start")) == 0 and
o.get(("resource", "end"), -1) == 8,
"struct resource reads as the ABI says it is laid out")
print("ldls-check: %d checks passed" % checks)
return 0