ldls: the stubs, and the driver's own voice
docs/plan/ldls.md milestone L1. Every undefined symbol resolves to a
trampoline -- twenty generated bytes that load the symbol's index and jump to
a shared thunk, which saves the argument registers and reports the call. Then
init_module is simply called.
$ ldls-run 8139too.ko
module 8139too
license GPL
wants 94 symbols
calling init_module at 0x416fc020
1 _printk(0x416fb6c0, 0, 0)
| 8139too: 8139too Fast Ethernet driver 0.9.28
2 __pci_register_driver(0x416fb400, 0x416fff80, 0x416fbdbb)
init_module returned 0 after 2 stub call(s)
That second line is the driver's own banner, printed through a _printk that
is ours, from a string in the module's own .rodata that a relocation put
there. There is no stronger evidence that a load worked than the loaded thing
speaking.
Stubs return zero rather than stopping, which departs from what the milestone
originally said and does so deliberately: stopping at the first call tells you
one name, carrying on tells you the sequence, and the sequence is the thing a
symbol table cannot give you. A driver that then dereferences the zero faults,
and that is a result too -- it says how far it got before it needed the kernel
to have really done something.
The first run failed on neither of those. It failed on a relocation:
ldls-run: relocate: a signed 32-bit relocation does not fit
A module is compiled -mcmodel=kernel, so the compiler emits R_X86_64_32S for
anything it believes lives in the top 2 GB, and an ordinary mmap at 0x7f...
has nowhere to put it. Linux gives modules a region within 2 GB of the kernel
text; the harness uses MAP_32BIT to reach the same window from the other end.
On SchOS this costs nothing -- a process's mmap area starts at 0x40000000 and
is already inside it -- but it is a real constraint on where the host may load
a module, and a loader that did not check its own arithmetic would have found
it much later and much more confusingly.
Swept over 500 stock modules: 449 ran init to completion, 22 faulted on a
stub's zero, 29 have no init_module, and zero hit a loader error.
Recorded honestly in "what it will not do": running init is not working. Every
symbol is a stub, so what has been shown is that drivers ask for things in a
readable order, not that anything answers. Also recorded: every undefined
symbol in a module is NOTYPE, so the stubs cannot tell jiffies from
pci_iomap and both get a trampoline -- BTF distinguishes VAR from FUNC and
that is where the fix belongs rather than in a guess.
make ldls-check is 23 checks now, five of them L1's, all against
ldls/test/refmod.c which is ours and now has an init_module of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
901ae03a69
commit
7a7c7611e0
8 changed files with 636 additions and 10 deletions
136
ldls/stub.c
Normal file
136
ldls/stub.c
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Stubs. See ldls/stub.h.
|
||||
//
|
||||
// Two pieces of x86-64 that have to be exactly right, and both are short
|
||||
// enough to read:
|
||||
//
|
||||
// the trampoline 20 bytes generated per symbol, which loads the symbol's
|
||||
// index and jumps to the thunk
|
||||
// the thunk one shared asm block that saves the argument registers,
|
||||
// calls ldls_stub(), and returns whatever it said
|
||||
//
|
||||
// Written from the x86-64 psABI and the instruction encodings in the Intel
|
||||
// manual -- both published, both specifications, and neither one anybody's
|
||||
// source code.
|
||||
|
||||
#include "stub.h"
|
||||
|
||||
// --- the thunk ---------------------------------------------------------------
|
||||
//
|
||||
// Reached by `jmp` from a trampoline, with %r11 holding the symbol index and
|
||||
// every argument register still holding what the driver put there.
|
||||
//
|
||||
// The stack arithmetic is the part worth checking. A `call` from the driver
|
||||
// leaves %rsp ≡ 8 (mod 16). Six pushes move it by 48, which is 0 (mod 16), so
|
||||
// it is still ≡ 8 -- and the ABI wants it ≡ 0 immediately before a `call`.
|
||||
// Hence the extra `sub $8`: without it every stub call would enter C with a
|
||||
// misaligned stack, which is survivable right up until something uses SSE and
|
||||
// then is not.
|
||||
__asm__(
|
||||
".text\n"
|
||||
".globl ldls_thunk\n"
|
||||
".hidden ldls_thunk\n"
|
||||
"ldls_thunk:\n"
|
||||
" pushq %r9\n"
|
||||
" pushq %r8\n"
|
||||
" pushq %rcx\n"
|
||||
" pushq %rdx\n"
|
||||
" pushq %rsi\n"
|
||||
" pushq %rdi\n"
|
||||
" movq %rsp, %rsi\n" /* args[0..5] = rdi, rsi, rdx, rcx, r8, r9 */
|
||||
" movq %r11, %rdi\n" /* the index the trampoline put there */
|
||||
" subq $8, %rsp\n"
|
||||
" call ldls_stub\n"
|
||||
" addq $8, %rsp\n"
|
||||
" addq $48, %rsp\n"
|
||||
" ret\n"
|
||||
".size ldls_thunk, .-ldls_thunk\n"
|
||||
);
|
||||
|
||||
extern void ldls_thunk(void);
|
||||
|
||||
// --- the trampolines ---------------------------------------------------------
|
||||
|
||||
static void
|
||||
put64(unsigned char *p, unsigned long long v)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 8; i++)
|
||||
p[i] = (unsigned char)(v >> (i * 8));
|
||||
}
|
||||
|
||||
static void
|
||||
put32(unsigned char *p, unsigned int v)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < 4; i++)
|
||||
p[i] = (unsigned char)(v >> (i * 8));
|
||||
}
|
||||
|
||||
static int
|
||||
streq(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *a == *b) { a++; b++; }
|
||||
return *a == 0 && *b == 0;
|
||||
}
|
||||
|
||||
int
|
||||
stub_is_bare(const char *name)
|
||||
{
|
||||
return streq(name, "__fentry__") ||
|
||||
streq(name, "__x86_return_thunk") ||
|
||||
streq(name, "__x86_indirect_thunk_rax");
|
||||
}
|
||||
|
||||
void
|
||||
stub_build(struct stubs *s, unsigned char *code, int n)
|
||||
{
|
||||
int i;
|
||||
|
||||
s->code = code;
|
||||
s->n = n;
|
||||
|
||||
for (i = 0; i < n; i++) {
|
||||
unsigned char *p = code + (unsigned long long)i * STUB_SIZE;
|
||||
int j;
|
||||
|
||||
for (j = 0; j < STUB_SIZE; j++)
|
||||
p[j] = 0xCC; // int3, so a mistake stops here and not later
|
||||
|
||||
// endbr64. Harmless without CET and required with it -- an indirect jump
|
||||
// to a target that does not begin with this faults on a machine that
|
||||
// enforces control-flow integrity, and a driver's calls through function
|
||||
// pointers are exactly that.
|
||||
p[0] = 0xF3; p[1] = 0x0F; p[2] = 0x1E; p[3] = 0xFA;
|
||||
|
||||
// movl $i, %r11d -- 41 bb imm32. The upper half of %r11 is zeroed by the
|
||||
// 32-bit form, which is what we want for a small index.
|
||||
p[4] = 0x41; p[5] = 0xBB;
|
||||
put32(p + 6, (unsigned int)i);
|
||||
|
||||
// movabsq $ldls_thunk, %r10 -- 49 ba imm64
|
||||
p[10] = 0x49; p[11] = 0xBA;
|
||||
put64(p + 12, (unsigned long long)(void *)ldls_thunk);
|
||||
|
||||
// jmpq *%r10 -- 41 ff e2
|
||||
p[20] = 0x41; p[21] = 0xFF; p[22] = 0xE2;
|
||||
}
|
||||
}
|
||||
|
||||
// A bare `ret`, for the names in stub_is_bare().
|
||||
void
|
||||
stub_make_bare(unsigned char *p)
|
||||
{
|
||||
int j;
|
||||
for (j = 0; j < STUB_SIZE; j++)
|
||||
p[j] = 0xCC;
|
||||
p[0] = 0xF3; p[1] = 0x0F; p[2] = 0x1E; p[3] = 0xFA; // endbr64
|
||||
p[4] = 0xC3; // ret
|
||||
}
|
||||
|
||||
unsigned long long
|
||||
stub_addr(const struct stubs *s, int i)
|
||||
{
|
||||
return (unsigned long long)(s->code + (unsigned long long)i * STUB_SIZE);
|
||||
}
|
||||
Loading…
Reference in a new issue