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
230
ldls/host/inspect.c
Normal file
230
ldls/host/inspect.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue