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

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);