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:
parent
5263150d3d
commit
1be5f3a704
9 changed files with 1296 additions and 2 deletions
212
ldls/ko.h
Normal file
212
ldls/ko.h
Normal 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);
|
||||
Loading…
Reference in a new issue