ldls: the loader, and what it measured

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
This commit is contained in:
Nexoniarz 2026-09-14 07:45:31 +02:00
commit 1be5f3a704
9 changed files with 1296 additions and 2 deletions

7
.gitignore vendored
View file

@ -41,3 +41,10 @@ projects/schcom/schcom-demo
# A built ISO dropped next to the site for download.html. 62 MB; not source.
docs/Website/releases/
# Kernel modules, ever. docs/plan/ldls.md rule 1: LDLS ships the layer and
# never the thing it loads, and that applies to this tree as well as to an
# image. The loader's test input is built from ldls/test/refmod.c, which is
# ours; a .ko here would be someone else's GPL code in an Apache-2.0 repo.
*.ko
*.ko.xz

View file

@ -916,6 +916,38 @@ crashshot:
crashqr:
$(PYTHON) tools/python/crashqr.py
# -----------------------------------------------------------------------
# LDLS -- the Linux Driver Layer. docs/plan/ldls.md.
#
# ldls/ko.c is freestanding and builds on both sides: here as part of a host
# tool, and later as part of the SchOS-side host. Same discipline as SchFS,
# 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 = $(BUILD)/tools/ldls-inspect$(EXE)
LDLS_REFMOD = $(BUILD)/ldls/refmod.o
$(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/inspect.c ldls/ko.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.
#
# -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is
# a thing the loader would have to build. A module is built -fno-pic for the
# same reason, so this matches what the real input looks like.
$(LDLS_REFMOD): ldls/test/refmod.c
@mkdir -p $(BUILD)/ldls
$(HOSTCC) -c -O1 -fno-pic -fno-asynchronous-unwind-tables -o $@ $<
ldls: $(LDLS_INSPECT)
# Load our own object and check the loader got it right. No network, no QEMU,
# no module from anywhere.
ldls-check: $(LDLS_INSPECT) $(LDLS_REFMOD)
$(PYTHON) tools/python/ldls_check.py $(LDLS_INSPECT) $(LDLS_REFMOD)
$(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I. -o $(MKFS) tools/mkfs.c

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`** | Not started. The plan exists; L0, the loader, is the first thing that would. |
| **`ldls.md`** | L0 done — the loader lays out and relocates 2,440 stock modules with nothing unhandled, and reports the API surface of each. L1 (the stubs), L2 (the hardware) and L3 (a packet) open. Nothing is executed yet. |
| **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

@ -144,7 +144,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. | |
| **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 and stops. `init_module` runs and dies at the first real call — which yields the *call order*, the second thing you need and the other thing you cannot guess. | |
| **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. | |
| **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. | |
@ -178,6 +178,72 @@ run.
---
## L0, and what it measured
`ldls/ko.c` is the loader: index the sections, place the allocatable ones,
resolve the undefined symbols against a table, apply the relocations. It is
freestanding — no libc, no allocation, no I/O — so the same source builds as a
host tool today and as the SchOS-side host later, which is the arrangement
`mkschfs` and the kernel already share.
```
$ ldls-inspect 8139too.ko
119288 bytes, 55 sections, 363 symbols
module 8139too
license GPL
describes RealTek RTL-8139 Fast Ethernet driver
loads to 33988 bytes
relocations: 1980
R_X86_64_64 363
R_X86_64_PC32 991
R_X86_64_PLT32 428
R_X86_64_32S 169
R_X86_64_PC64 29
undefined symbols: 94
...
stops at: unresolved: __fentry__
```
**The licence is printed first, before anything is loaded**, because it is the
one property that decides whether the object may be on this machine at all and
it is a plain string in `.modinfo`. Rule 1 is easier to keep when the answer is
on screen.
### What the measurement says
**94 symbols** is the API surface of `8139too` on Linux 6.18.46. Not an
estimate — the list, exactly, and it is L1's work queue. It is also smaller
than it looks, because a good fraction of it is not API at all: `__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.
**Five relocation types**, and a corpus run says that is the whole set: over
**2,440 real modules** from a stock 6.18.46 tree, every one lays out and
relocates to completion with nothing unhandled. That includes `nvidia-modeset`
at 7,069 sections and 74,305 relocations, which is what retired the loader's
fixed section table — the caller supplies it now, so there is no limit to run
into rather than a generous one.
Relocating against stubs is a separate question from listing what is missing,
and it is the one that says the loader is finished: with nothing resolved, the
first unknown name stops everything and no relocation type past it is ever
reached.
### Nothing foreign is in this tree
`make ldls-check` runs the loader against `ldls/test/refmod.c` — ours, compiled
with `-c`, written to contain one of each thing the loader has to get right:
undefined functions *and* undefined data, `.text` with calls, `.data` holding a
pointer into `.rodata`, a `.bss` that must arrive zeroed, and a `.modinfo`
shaped the way a module's is. The modules above were read from the host's own
`/nix/store` and never copied here; `.gitignore` refuses `*.ko` outright so
that cannot happen by accident.
---
## What it will not do
- **It is not a security boundary, and here that is worse than usual.** There
@ -208,6 +274,11 @@ run.
costs. A process has different rules, and where they differ the shim will be
wrong in ways that show up as rare races rather than as failures to build.
This is the deepest technical risk in the whole plan.
- **Loading is not running.** L0 proves 2,440 modules can be laid out and
relocated. That says the *format* is handled and says nothing whatever about
whether any of them would work: not one symbol is implemented, and the
distance between "relocates" and "transmits a packet" is the entire rest of
this plan.
- **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

230
ldls/host/inspect.c Normal file
View file

@ -0,0 +1,230 @@
// SPDX-License-Identifier: Apache-2.0
//
// ldls-inspect -- what is in a relocatable object, and what it wants.
//
// docs/plan/ldls.md milestone L0, run on the machine doing the building rather
// than inside SchOS. That is the same arrangement `mkschfs` has with the
// kernel, for the same reason: the interesting failures here are format
// failures, and a format failure found on the host costs a second instead of a
// boot.
//
// It loads and relocates for real -- into a buffer at a pretended address --
// so that "it parses" and "it can actually be laid out and relocated" are not
// confused with each other. Nothing is executed.
//
// ldls-inspect 8139too.ko
//
// **No module is committed to this tree**, and none ever will be; see
// docs/plan/ldls.md rule 1. `make ldls-check` runs this against an object
// built from ldls/test/refmod.c, which is ours.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../ko.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;
}
// At L0 nothing is known, so the resolver knows nothing -- and that is the
// point rather than a placeholder. Every name it refuses becomes a line in the
// work queue.
static int
resolve_nothing(void *ctx, const char *name, unsigned long long *out)
{
(void)ctx; (void)name; (void)out;
return -1;
}
// Resolve every name to one plausible address, so relocation runs to the end.
//
// This answers a different question from the list of undefined symbols, and it
// is the one that decides whether the loader is finished: does this module
// contain a relocation type the loader does not handle? With nothing resolved
// that can never be reached, because the first unknown name stops everything.
//
// The address is near the module rather than anywhere, because PC32 and PLT32
// have to reach it -- a fake symbol two gigabytes away would fail the range
// check and say something true about the fake rather than about the loader.
static int
resolve_to_stub(void *ctx, const char *name, unsigned long long *out)
{
(void)name;
*out = *(unsigned long long *)ctx;
return 0;
}
static const char *
flags_of(const struct ko_section *s)
{
static char b[8];
int n = 0;
b[n++] = (s->flags & KO_SHF_ALLOC) ? 'a' : '-';
b[n++] = (s->flags & KO_SHF_WRITE) ? 'w' : '-';
b[n++] = (s->flags & KO_SHF_EXECINSTR) ? 'x' : '-';
b[n] = 0;
return b;
}
int
main(int argc, char **argv)
{
struct ko k;
unsigned char *img, *mem;
unsigned long long len, span;
const char *lic;
int i, quiet = 0, stub = 0, nsec;
struct ko_section *sec;
if (argc < 2) {
fprintf(stderr, "usage: ldls-inspect [-q] [-r] <object.ko>\n");
return 2;
}
if (strcmp(argv[1], "-q") == 0) { quiet = 1; argv++; argc--; }
if (argc > 1 && strcmp(argv[1], "-r") == 0) { stub = 1; argv++; argc--; }
if (argc < 2) { fprintf(stderr, "usage: ldls-inspect [-q] <object>\n"); return 2; }
img = slurp(argv[1], &len);
if (!img) {
fprintf(stderr, "ldls-inspect: cannot read %s\n", argv[1]);
return 1;
}
// Exactly what this object needs, so there is no limit to run into.
nsec = ko_sections_needed(img, len);
if (nsec < 0) {
fprintf(stderr, "ldls-inspect: %s: not an ELF64 relocatable object\n",
argv[1]);
return 1;
}
sec = calloc((size_t)(nsec ? nsec : 1), sizeof(struct ko_section));
if (!sec) { fprintf(stderr, "ldls-inspect: out of memory\n"); return 1; }
if (ko_open(&k, img, len, sec, nsec) < 0) {
fprintf(stderr, "ldls-inspect: %s: %s\n", argv[1], ko_err(&k));
return 1;
}
printf("%s\n", argv[1]);
printf(" %llu bytes, %d sections, %d symbols\n",
(unsigned long long)len, k.nsh, k.nsym);
// The licence first, and deliberately so. It is the one property that
// decides whether this object may be on this machine at all, it is a string
// in the file, and nothing has to be loaded to read it.
lic = ko_modinfo(&k, "license");
printf(" module %s\n", ko_name(&k) ? ko_name(&k) : "(not a kernel module)");
printf(" license %s\n", lic ? lic : "(none declared)");
if (ko_modinfo(&k, "description"))
printf(" describes %s\n", ko_modinfo(&k, "description"));
span = ko_span(&k);
printf(" loads to %llu bytes\n", span);
if (!quiet) {
printf("\n sections that take space at run time\n");
for (i = 0; i < k.nsh; i++) {
const struct ko_section *s = &k.sec[i];
if (!s->alloc || s->size == 0)
continue;
printf(" %-24s %s %8llu align %llu%s\n",
s->name, flags_of(s), s->size, s->align,
s->type == KO_SHT_NOBITS ? " (zeroed)" : "");
}
}
// Lay it out and relocate at a plausible address, so that the report covers
// the parts that only fail once something has an address.
mem = malloc(span ? (size_t)span : 1);
if (!mem) { fprintf(stderr, "ldls-inspect: out of memory\n"); return 1; }
if (ko_layout(&k, mem, span, 0x40000000ULL) < 0) {
fprintf(stderr, "ldls-inspect: layout: %s\n", ko_err(&k));
return 1;
}
// What the loader is being asked to handle, exactly.
ko_reltypes(&k);
printf("\n relocations: %u\n", k.nrel);
{
static const struct { unsigned int t; const char *n; } known[] = {
{ KO_R_X86_64_64, "R_X86_64_64" },
{ KO_R_X86_64_PC32, "R_X86_64_PC32" },
{ KO_R_X86_64_PLT32, "R_X86_64_PLT32" },
{ KO_R_X86_64_32, "R_X86_64_32" },
{ KO_R_X86_64_32S, "R_X86_64_32S" },
{ KO_R_X86_64_PC64, "R_X86_64_PC64" },
{ KO_R_X86_64_NONE, "R_X86_64_NONE" },
};
unsigned int accounted = 0;
for (i = 0; i < (int)(sizeof(known) / sizeof(known[0])); i++) {
if (!k.reltype[known[i].t])
continue;
printf(" %-16s %u\n", known[i].n, k.reltype[known[i].t]);
accounted += k.reltype[known[i].t];
}
for (i = 0; i < 64; i++) {
unsigned int t = (unsigned int)i;
if (!k.reltype[i] || t == KO_R_X86_64_64 || t == KO_R_X86_64_PC32 ||
t == KO_R_X86_64_PLT32 || t == KO_R_X86_64_32 ||
t == KO_R_X86_64_32S || t == KO_R_X86_64_PC64 ||
t == KO_R_X86_64_NONE)
continue;
printf(" type %-11d %u <-- the loader does not handle this\n",
i, k.reltype[i]);
}
if (k.reltype_other)
printf(" type >= 64 %u <-- the loader does not handle this\n",
k.reltype_other);
(void)accounted;
}
printf("\n undefined symbols: %d%s\n", k.nundef,
k.undef_lost ? " (and more than the loader will hold)" : "");
for (i = 0; i < k.nundef; i++)
printf(" %s\n", k.undef[i]);
// With a resolver that knows nothing this must fail, and the first name it
// names is the first thing L1 has to provide. Reaching the end instead means
// the object needed nothing at all, which a real module never does.
if (stub) {
unsigned long long at = 0x40000000ULL + 0x200000ULL;
if (ko_relocate(&k, resolve_to_stub, &at) == 0)
printf("\n relocated in full against stubs -- every relocation type in\n"
" this object is one the loader handles\n");
else
printf("\n relocation failed: %s\n", ko_err(&k));
} else if (ko_relocate(&k, resolve_nothing, 0) == 0) {
printf("\n relocated with an empty symbol table -- it needs nothing\n");
} else {
printf("\n stops at: %s\n", ko_err(&k));
printf(" (try -r to resolve everything to a stub and reach the end)\n");
}
free(mem);
free(sec);
free(img);
return 0;
}

556
ldls/ko.c Normal file
View file

@ -0,0 +1,556 @@
// SPDX-License-Identifier: Apache-2.0
//
// Loading a relocatable object. See ldls/ko.h.
//
// Written from the ELF-64 gABI and the x86-64 psABI, both published
// specifications -- ../docs/licensing.md §5. Nothing in this file was taken
// from anyone's loader, and nothing in it needs to be: the format is fully
// described and the relocation arithmetic is three lines per type.
//
// Every read from the image is bounds-checked against the length the caller
// gave, and that is not defensive programming for its own sake. The input is a
// file the *user* supplied, from a kernel we did not build, and a loader that
// trusts its input is a loader that can be handed a crash.
#include "ko.h"
// --- small local helpers, so this file needs no libc on either side ---------
static void
ko_memcpy(unsigned char *d, const unsigned char *s, unsigned long long n)
{
while (n--)
*d++ = *s++;
}
static void
ko_memset(unsigned char *d, int c, unsigned long long n)
{
while (n--)
*d++ = (unsigned char)c;
}
static int
ko_streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == 0 && *b == 0;
}
static void
ko_fail(struct ko *k, const char *msg, const char *what)
{
int i = 0;
while (msg[i] && i < (int)sizeof(k->err) - 1) { k->err[i] = msg[i]; i++; }
if (what) {
int j = 0;
while (what[j] && i < (int)sizeof(k->err) - 1) k->err[i++] = what[j++];
}
k->err[i] = 0;
}
// A number, appended to the error. There is no formatter here and an error
// that says "relocation type" without saying which is not worth printing.
static void
ko_fail_num(struct ko *k, const char *msg, unsigned long long v)
{
char d[24];
int n = 0, i = 0;
while (msg[i] && i < (int)sizeof(k->err) - 1) { k->err[i] = msg[i]; i++; }
if (v == 0) d[n++] = '0';
while (v) { d[n++] = (char)('0' + v % 10); v /= 10; }
while (n-- > 0 && i < (int)sizeof(k->err) - 1)
k->err[i++] = d[n];
k->err[i] = 0;
}
// Is [off, off+n) inside the image? Every accessor goes through this.
static int
ko_fits(const struct ko *k, unsigned long long off, unsigned long long n)
{
if (off > k->len)
return 0;
if (n > k->len - off)
return 0;
return 1;
}
// A NUL-terminated string at `off` in a table of `size` bytes starting at
// `base` -- checked so that an unterminated string table cannot run away.
static const char *
ko_str_at(const struct ko *k, unsigned long long base, unsigned long long size,
unsigned int off)
{
unsigned long long i;
if (!ko_fits(k, base, size) || off >= size)
return 0;
for (i = base + off; i < base + size; i++)
if (k->img[i] == 0)
return (const char *)(k->img + base + off);
return 0; // runs to the end without a NUL
}
static const struct ko_shdr *
ko_sh(const struct ko *k, int i)
{
if (i < 0 || i >= k->nsh)
return 0;
return (const struct ko_shdr *)(k->shtab +
(unsigned long long)i * k->eh->e_shentsize);
}
// --- opening -----------------------------------------------------------------
int
ko_sections_needed(const void *image, unsigned long long len)
{
const struct ko_ehdr *eh = (const struct ko_ehdr *)image;
if (len < sizeof(struct ko_ehdr))
return -1;
if (eh->e_ident[0] != 0x7f || eh->e_ident[1] != 'E' ||
eh->e_ident[2] != 'L' || eh->e_ident[3] != 'F')
return -1;
if (eh->e_ident[4] != 2 || eh->e_type != KO_ET_REL)
return -1;
return eh->e_shnum;
}
int
ko_open(struct ko *k, const void *image, unsigned long long len,
struct ko_section *sec, int nsec)
{
const struct ko_ehdr *eh;
int i;
ko_memset((unsigned char *)k, 0, sizeof(*k));
k->img = (const unsigned char *)image;
k->len = len;
k->sec = sec;
if (!sec || nsec <= 0)
{ ko_fail(k, "no section table was provided", 0); return -1; }
if (len < sizeof(struct ko_ehdr))
{ ko_fail(k, "not an ELF object: too short", 0); return -1; }
eh = (const struct ko_ehdr *)image;
if (eh->e_ident[0] != 0x7f || eh->e_ident[1] != 'E' ||
eh->e_ident[2] != 'L' || eh->e_ident[3] != 'F')
{ ko_fail(k, "not an ELF object: bad magic", 0); return -1; }
if (eh->e_ident[4] != 2) // ELFCLASS64
{ ko_fail(k, "not a 64-bit object", 0); return -1; }
if (eh->e_ident[5] != 1) // ELFDATA2LSB
{ ko_fail(k, "not little-endian", 0); return -1; }
if (eh->e_type != KO_ET_REL)
{ ko_fail(k, "not a relocatable object -- a .ko is ET_REL, this is not", 0);
return -1; }
if (eh->e_machine != KO_EM_X86_64)
{ ko_fail_num(k, "wrong machine, e_machine=", eh->e_machine); return -1; }
if (eh->e_shentsize < sizeof(struct ko_shdr))
{ ko_fail(k, "section headers are smaller than ELF64 requires", 0);
return -1; }
k->eh = eh;
k->nsh = eh->e_shnum;
if (k->nsh <= 0)
{ ko_fail(k, "no section headers", 0); return -1; }
if (k->nsh > nsec)
{ ko_fail_num(k, "the caller's section table is too small; it needs ",
(unsigned long long)k->nsh); return -1; }
ko_memset((unsigned char *)sec, 0,
(unsigned long long)k->nsh * sizeof(struct ko_section));
if (!ko_fits(k, eh->e_shoff,
(unsigned long long)k->nsh * eh->e_shentsize))
{ ko_fail(k, "section headers run off the end of the file", 0); return -1; }
k->shtab = k->img + eh->e_shoff;
// Section names.
{
const struct ko_shdr *sh = ko_sh(k, eh->e_shstrndx);
if (!sh || sh->sh_type != KO_SHT_STRTAB)
{ ko_fail(k, "no section-name string table", 0); return -1; }
if (!ko_fits(k, sh->sh_offset, sh->sh_size))
{ ko_fail(k, "section names run off the end of the file", 0); return -1; }
k->shstr = (const char *)(k->img + sh->sh_offset);
}
// Index every section, and find the symbol table on the way past.
for (i = 0; i < k->nsh; i++) {
const struct ko_shdr *sh = ko_sh(k, i);
struct ko_section *s = &k->sec[i];
{
const struct ko_shdr *ss = ko_sh(k, k->eh->e_shstrndx);
const char *nm = ko_str_at(k, ss->sh_offset, ss->sh_size, sh->sh_name);
// An unreadable name is not fatal -- the section is still loadable and
// still relocatable. It only costs the report a label.
s->name = nm ? nm : "(unnamed)";
}
s->type = sh->sh_type;
s->flags = sh->sh_flags;
s->size = sh->sh_size;
s->align = sh->sh_addralign ? sh->sh_addralign : 1;
s->alloc = (sh->sh_flags & KO_SHF_ALLOC) != 0;
s->data = 0;
if (sh->sh_type != KO_SHT_NOBITS && sh->sh_size) {
if (!ko_fits(k, sh->sh_offset, sh->sh_size))
{ ko_fail(k, "section runs off the end of the file: ", s->name);
return -1; }
s->data = k->img + sh->sh_offset;
}
if (sh->sh_type == KO_SHT_SYMTAB) {
const struct ko_shdr *st;
if (sh->sh_entsize != sizeof(struct ko_sym))
{ ko_fail(k, "symbol entries are the wrong size", 0); return -1; }
if (!s->data)
{ ko_fail(k, "empty symbol table", 0); return -1; }
k->sym = (const struct ko_sym *)s->data;
k->nsym = (int)(sh->sh_size / sizeof(struct ko_sym));
st = ko_sh(k, sh->sh_link);
if (!st || st->sh_type != KO_SHT_STRTAB)
{ ko_fail(k, "symbol table has no string table", 0); return -1; }
if (!ko_fits(k, st->sh_offset, st->sh_size))
{ ko_fail(k, "symbol names run off the end of the file", 0);
return -1; }
k->str = (const char *)(k->img + st->sh_offset);
}
}
if (!k->sym)
{ ko_fail(k, "no symbol table -- nothing could be resolved", 0);
return -1; }
// The undefined symbols. This is L0's whole output and the reason the
// milestone exists: it is the API surface of this driver, measured rather
// than guessed at.
for (i = 0; i < k->nsym; i++) {
const char *nm;
if (k->sym[i].st_shndx != KO_SHN_UNDEF)
continue;
if (k->sym[i].st_name == 0)
continue; // the index-0 placeholder
nm = (const char *)(k->str + k->sym[i].st_name);
if (nm[0] == 0)
continue;
if (k->nundef < KO_MAXUNDEF)
k->undef[k->nundef++] = nm;
else
k->undef_lost = 1;
}
return 0;
}
const char *ko_err(const struct ko *k) { return k->err[0] ? k->err : "no error"; }
// --- layout ------------------------------------------------------------------
static unsigned long long
ko_round(unsigned long long v, unsigned long long a)
{
if (a <= 1)
return v;
return (v + a - 1) & ~(a - 1);
}
// Where each allocatable section lands, and how much that comes to.
//
// One contiguous block, sections in file order, each at its own alignment.
// Linux's own loader splits by permission so that text can be made read-only;
// that is a refinement worth having later and is not what L0 is measuring.
static unsigned long long
ko_plan(struct ko *k, unsigned long long base, void *mem)
{
unsigned long long off = 0;
int i;
for (i = 0; i < k->nsh; i++) {
struct ko_section *s = &k->sec[i];
if (!s->alloc || s->size == 0)
continue;
off = ko_round(off, s->align);
if (mem) {
s->addr = base + off;
s->where = (unsigned char *)mem + off;
}
off += s->size;
}
return off;
}
unsigned long long
ko_span(const struct ko *k)
{
return ko_plan((struct ko *)k, 0, 0);
}
int
ko_layout(struct ko *k, void *mem, unsigned long long cap,
unsigned long long base)
{
unsigned long long need = ko_plan(k, 0, 0);
int i;
if (need > cap)
{ ko_fail_num(k, "the object does not fit; it needs ", need); return -1; }
k->span = ko_plan(k, base, mem);
for (i = 0; i < k->nsh; i++) {
struct ko_section *s = &k->sec[i];
if (!s->alloc || s->size == 0)
continue;
if (s->type == KO_SHT_NOBITS)
ko_memset(s->where, 0, s->size); // .bss, and it must be zeroed
else
ko_memcpy(s->where, s->data, s->size);
}
k->laid_out = 1;
return 0;
}
// --- relocation --------------------------------------------------------------
// The address a symbol will have once loaded.
static int
ko_value(struct ko *k, unsigned int idx, ko_resolver r, void *ctx,
unsigned long long *out)
{
const struct ko_sym *sy;
const char *nm;
if ((int)idx >= k->nsym)
{ ko_fail_num(k, "relocation names symbol ", idx); return -1; }
sy = &k->sym[idx];
if (sy->st_shndx == KO_SHN_ABS) {
*out = sy->st_value;
return 0;
}
if (sy->st_shndx == KO_SHN_UNDEF) {
nm = (const char *)(k->str + sy->st_name);
if (!r || r(ctx, nm, out) != 0)
{ ko_fail(k, "unresolved: ", nm); return -1; }
return 0;
}
if (sy->st_shndx == KO_SHN_COMMON)
{ ko_fail(k, "common symbols are not supported: ",
(const char *)(k->str + sy->st_name)); return -1; }
if (sy->st_shndx >= k->nsh)
{ ko_fail_num(k, "symbol points at section ", sy->st_shndx); return -1; }
// A symbol defined in a section that was not laid out has no address. That
// happens for debug sections, and a relocation against one means the object
// is asking for something the loader deliberately did not load.
if (!k->sec[sy->st_shndx].alloc)
{ ko_fail(k, "relocation against a section that is not loaded: ",
k->sec[sy->st_shndx].name); return -1; }
*out = k->sec[sy->st_shndx].addr + sy->st_value;
return 0;
}
void
ko_reltypes(struct ko *k)
{
int i;
for (i = 0; i < 64; i++)
k->reltype[i] = 0;
k->reltype_other = 0;
k->nrel = 0;
for (i = 0; i < k->nsh; i++) {
const struct ko_shdr *sh = ko_sh(k, i);
unsigned long long n, j;
if (!sh || sh->sh_type != KO_SHT_RELA)
continue;
if (sh->sh_entsize != sizeof(struct ko_rela) || !k->sec[i].data)
continue;
if (sh->sh_info >= (unsigned int)k->nsh || !k->sec[sh->sh_info].alloc)
continue; // relocations for sections we do not load
n = sh->sh_size / sizeof(struct ko_rela);
for (j = 0; j < n; j++) {
const struct ko_rela *rl = (const struct ko_rela *)(k->sec[i].data) + j;
unsigned int t = KO_R_TYPE(rl->r_info);
k->nrel++;
if (t < 64)
k->reltype[t]++;
else
k->reltype_other++;
}
}
}
int
ko_relocate(struct ko *k, ko_resolver r, void *ctx)
{
int i;
if (!k->laid_out)
{ ko_fail(k, "relocate before layout", 0); return -1; }
for (i = 0; i < k->nsh; i++) {
const struct ko_shdr *sh = ko_sh(k, i);
struct ko_section *tgt;
unsigned long long n, j;
if (!sh || sh->sh_type != KO_SHT_RELA)
continue;
if (sh->sh_entsize != sizeof(struct ko_rela))
{ ko_fail(k, "relocation entries are the wrong size", 0); return -1; }
if (sh->sh_info >= (unsigned int)k->nsh)
{ ko_fail_num(k, "relocations target section ", sh->sh_info); return -1; }
tgt = &k->sec[sh->sh_info];
if (!tgt->alloc || tgt->size == 0)
continue; // relocations for .debug_* and friends
n = sh->sh_size / sizeof(struct ko_rela);
for (j = 0; j < n; j++) {
const struct ko_rela *rl =
(const struct ko_rela *)(k->sec[i].data) + j;
unsigned int type = KO_R_TYPE(rl->r_info);
unsigned long long S, P;
unsigned char *loc;
long long X;
if (rl->r_offset + 4 > tgt->size &&
!(type == KO_R_X86_64_64 || type == KO_R_X86_64_PC64))
{ ko_fail(k, "relocation points outside ", tgt->name); return -1; }
if ((type == KO_R_X86_64_64 || type == KO_R_X86_64_PC64) &&
rl->r_offset + 8 > tgt->size)
{ ko_fail(k, "relocation points outside ", tgt->name); return -1; }
if (type == KO_R_X86_64_NONE)
continue;
if (ko_value(k, KO_R_SYM(rl->r_info), r, ctx, &S) < 0)
return -1;
loc = tgt->where + rl->r_offset;
P = tgt->addr + rl->r_offset;
X = rl->r_addend;
switch (type) {
case KO_R_X86_64_64:
*(unsigned long long *)loc = S + (unsigned long long)X;
break;
case KO_R_X86_64_PC64:
*(unsigned long long *)loc = S + (unsigned long long)X - P;
break;
case KO_R_X86_64_PC32:
case KO_R_X86_64_PLT32: {
// PLT32 is treated as PC32. There is no procedure linkage table here:
// every call is resolved at load time, so the "call through the PLT"
// a compiler emitted is just a call to where the symbol ended up.
long long v = (long long)S + X - (long long)P;
if (v < -0x80000000LL || v > 0x7fffffffLL)
{ ko_fail(k, "a 32-bit PC-relative relocation does not reach; the "
"object was loaded too far away", 0); return -1; }
*(unsigned int *)loc = (unsigned int)(int)v;
break;
}
case KO_R_X86_64_32: {
unsigned long long v = S + (unsigned long long)X;
if (v > 0xffffffffULL)
{ ko_fail(k, "a 32-bit absolute relocation does not fit", 0);
return -1; }
*(unsigned int *)loc = (unsigned int)v;
break;
}
case KO_R_X86_64_32S: {
long long v = (long long)S + X;
if (v < -0x80000000LL || v > 0x7fffffffLL)
{ ko_fail(k, "a signed 32-bit relocation does not fit", 0);
return -1; }
*(unsigned int *)loc = (unsigned int)(int)v;
break;
}
default:
// Named, not ignored. A relocation quietly skipped is a wrong address
// discovered a long way from here.
ko_fail_num(k, "unhandled relocation type ", type);
return -1;
}
}
}
return 0;
}
// --- what the object says about itself ---------------------------------------
static const struct ko_section *
ko_find(const struct ko *k, const char *name)
{
int i;
for (i = 0; i < k->nsh; i++)
if (ko_streq(k->sec[i].name, name))
return &k->sec[i];
return 0;
}
// `.modinfo` is a run of NUL-terminated "key=value" strings. Reading it needs
// nothing loaded and no symbols resolved, which is why the licence a module
// declares can be printed before deciding whether to load it at all.
const char *
ko_modinfo(const struct ko *k, const char *key)
{
const struct ko_section *s = ko_find(k, ".modinfo");
unsigned long long i = 0;
if (!s || !s->data)
return 0;
while (i < s->size) {
const char *e = (const char *)(s->data + i);
unsigned long long n = 0;
const char *p = e;
const char *q = key;
while (i + n < s->size && e[n])
n++;
if (n == 0) { i++; continue; }
while (*q && *p == *q) { p++; q++; }
if (*q == 0 && *p == '=')
return p + 1;
i += n + 1;
}
return 0;
}
const char *
ko_name(const struct ko *k)
{
int i;
// The module's `struct module` lives in `.gnu.linkonce.this_module` and its
// name is at a fixed offset -- but that offset is a property of the kernel
// version, which is exactly the kind of thing docs/plan/ldls.md rule 4 says
// not to assume. The symbol table has the same information portably: the
// object defines `__this_module`, and `.modinfo` carries the rest.
for (i = 0; i < k->nsym; i++) {
const char *nm = (const char *)(k->str + k->sym[i].st_name);
if (ko_streq(nm, "__this_module"))
return ko_modinfo(k, "name") ? ko_modinfo(k, "name") : "(unnamed)";
}
return 0;
}

212
ldls/ko.h Normal file
View file

@ -0,0 +1,212 @@
#pragma once
// Loading a relocatable object. docs/plan/ldls.md milestone L0.
//
// A Linux `.ko` is an ordinary ELF64 relocatable object -- the same thing
// `gcc -c` produces -- and loading one is four steps with no mystery in any of
// them: index the sections, place the allocatable ones, resolve the undefined
// symbols against a table, and apply the relocations. There is no licence
// question anywhere in this file: ELF and the x86-64 relocation set are
// published specifications, which is the case [../docs/licensing.md] §5
// describes.
//
// **It is freestanding on purpose.** No libc, no allocation, no I/O: the
// caller hands over the whole image and a buffer, and everything happens in
// them. That is what lets the same source build as a host tool -- where a test
// takes a second -- and as the SchOS-side host, which is the discipline
// `mkschfs` and the kernel already share and the reason SchFS format bugs are
// found on the host.
//
// Types are spelled out rather than taken from a header, because the two sides
// have different ones and this file has to mean the same thing in both.
// The section table is the caller's, not ours.
//
// A fixed array needs a limit, and every limit that would fit in a struct is
// one a real object exceeds: measured over 300 modules the median is 50
// sections and the 95th percentile is 67, but an object built with
// -ffunction-sections runs to thousands -- nvidia-modeset has 7,069. Rather
// than pick a number that is generous until it is not, the caller says how
// many it is prepared for, and ko_sections_needed() says how many that is.
//
// A suggested size for a fixed table on the SchOS side, where allocating is
// less convenient. It is a caller's choice and not the loader's limit.
#define KO_SECHINT 128
// Undefined symbols the report will hold. Measured over the same corpus the
// median is 25 and the worst seen is 245, so this is room to spare -- and
// `undef_lost` says so rather than the list quietly ending.
#define KO_MAXUNDEF 512
// --- ELF64, the relocatable subset ------------------------------------------
//
// include/elf.h has `elfhdr` and `proghdr`, which are the ET_EXEC half. None of
// what a relocatable object needs is there, so it is here.
struct ko_ehdr {
unsigned char e_ident[16];
unsigned short e_type;
unsigned short e_machine;
unsigned int e_version;
unsigned long long e_entry;
unsigned long long e_phoff;
unsigned long long e_shoff;
unsigned int e_flags;
unsigned short e_ehsize;
unsigned short e_phentsize;
unsigned short e_phnum;
unsigned short e_shentsize;
unsigned short e_shnum;
unsigned short e_shstrndx;
};
struct ko_shdr {
unsigned int sh_name;
unsigned int sh_type;
unsigned long long sh_flags;
unsigned long long sh_addr;
unsigned long long sh_offset;
unsigned long long sh_size;
unsigned int sh_link;
unsigned int sh_info;
unsigned long long sh_addralign;
unsigned long long sh_entsize;
};
struct ko_sym {
unsigned int st_name;
unsigned char st_info;
unsigned char st_other;
unsigned short st_shndx;
unsigned long long st_value;
unsigned long long st_size;
};
struct ko_rela {
unsigned long long r_offset;
unsigned long long r_info;
long long r_addend;
};
#define KO_ET_REL 1
#define KO_EM_X86_64 62
#define KO_SHT_PROGBITS 1
#define KO_SHT_SYMTAB 2
#define KO_SHT_STRTAB 3
#define KO_SHT_RELA 4
#define KO_SHT_NOBITS 8
#define KO_SHF_WRITE 0x1
#define KO_SHF_ALLOC 0x2
#define KO_SHF_EXECINSTR 0x4
#define KO_SHN_UNDEF 0
#define KO_SHN_ABS 0xfff1
#define KO_SHN_COMMON 0xfff2
#define KO_R_SYM(i) ((unsigned int)((i) >> 32))
#define KO_R_TYPE(i) ((unsigned int)((i) & 0xffffffffu))
// The x86-64 relocations a compiled object actually contains. Anything else is
// refused by name rather than ignored -- a relocation silently skipped is a
// wrong address found much later.
#define KO_R_X86_64_NONE 0
#define KO_R_X86_64_64 1
#define KO_R_X86_64_PC32 2
#define KO_R_X86_64_PLT32 4
#define KO_R_X86_64_32 10
#define KO_R_X86_64_32S 11
#define KO_R_X86_64_PC64 24
// --- what the loader knows about a section ----------------------------------
struct ko_section {
const char *name;
unsigned int type;
unsigned long long flags;
unsigned long long size;
unsigned long long align;
const unsigned char *data; // 0 for NOBITS
int alloc; // takes space at run time
unsigned long long addr; // where it will run, once laid out
unsigned char *where; // where it is right now, once laid out
};
// Resolving a name to an address. Returns 0 and fills `out` on success, or
// non-zero to say it does not know it -- which is not an error at L0, it is
// the answer we are asking for.
typedef int (*ko_resolver)(void *ctx, const char *name,
unsigned long long *out);
struct ko {
const unsigned char *img;
unsigned long long len;
const struct ko_ehdr *eh;
const unsigned char *shtab; // raw, indexed with e_shentsize
int nsh;
const char *shstr; // section name strings
const struct ko_sym *sym;
int nsym;
const char *str; // symbol name strings
struct ko_section *sec; // the caller's, nsh entries used
const char *undef[KO_MAXUNDEF];
int nundef;
int undef_lost; // more than KO_MAXUNDEF of them
unsigned long long span; // bytes ko_layout needs
int laid_out;
// How many relocations of each type the object contains, counted by
// ko_reltypes(). The question "what does the loader actually have to
// handle" has an exact answer per module, and guessing at it is how a
// loader ends up refusing a driver on its last relocation.
unsigned int reltype[64];
unsigned int reltype_other;
unsigned int nrel;
char err[128];
};
// How many `struct ko_section` this object needs. -1 if it is not an ELF64
// object at all, which is worth knowing before allocating anything for it.
int ko_sections_needed(const void *image, unsigned long long len);
// Validate and index, using `sec` for the section table. Returns 0, or -1 with
// ko_err() set -- including when `nsec` is too small, which says how many were
// needed rather than just refusing.
int ko_open(struct ko *k, const void *image, unsigned long long len,
struct ko_section *sec, int nsec);
// Bytes ko_layout will need. Valid after ko_open.
unsigned long long ko_span(const struct ko *k);
// Place every allocatable section into `mem`, as if it will run at `base`.
// The two differ when the object is staged somewhere before being mapped;
// pass base == (address of mem) when they do not.
int ko_layout(struct ko *k, void *mem, unsigned long long cap,
unsigned long long base);
// Count the relocations by type, without applying any. Valid after ko_open.
void ko_reltypes(struct ko *k);
// Apply every relocation. Undefined symbols go to `r`; a name it does not know
// makes this fail, naming the first one.
int ko_relocate(struct ko *k, ko_resolver r, void *ctx);
// A value from the `.modinfo` section -- "license", "description", "author",
// "alias". Returns 0 if the object has no such key.
//
// Worth having at L0 rather than later: the first thing anyone should be able
// to ask about a module is what licence it declares, and that is a string in
// the file rather than anything that needs loading.
const char *ko_modinfo(const struct ko *k, const char *key);
// The module's own name, from `.gnu.linkonce.this_module`, or 0.
const char *ko_name(const struct ko *k);
const char *ko_err(const struct ko *k);

79
ldls/test/refmod.c Normal file
View file

@ -0,0 +1,79 @@
// 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;

107
tools/python/ldls_check.py Normal file
View file

@ -0,0 +1,107 @@
#!/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")
print("ldls-check: %d checks passed" % checks)
return 0
if __name__ == "__main__":
sys.exit(main())