diff --git a/boot/uefi/main.c b/boot/uefi/main.c index dea1987..a5eec96 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -1164,13 +1164,29 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) // RBX carries the framebuffer address into kmain, and must never be 1 -- // that value marks an application processor in entry64high. A physical // MMIO address or zero, so it cannot collide. + // + // "rbx" is in the clobber list because this writes it, and leaving it out + // is not a tidiness problem -- it is a jump to the wrong address. Without + // it the compiler may allocate an *input* into rbx, and it did: clang 21 + // put `entry` there, so the second instruction overwrote the jump target + // with the framebuffer address and the third jumped into video memory. The + // machine then executed pixels until KVM gave up. + // + // movq %rax, %cr3 <- %0, the page tables + // movq %rcx, %rbx <- %1, the framebuffer, into rbx + // jmpq *%rbx <- %2 was rbx, and now holds the framebuffer + // + // It had worked for as long as the compiler happened to choose differently. + // That is the same failure as the BIOS loader's EAX hand-off in + // docs/history/kernel.md section 4: a register the code depends on, that + // nothing in the source actually asks for. __asm__ volatile( "mov %0, %%cr3\n\t" "mov %1, %%rbx\n\t" "jmp *%2\n\t" : : "r"((UINT64)PML4_ADDR), "r"((UINT64)fb), "r"(entry) - : "memory"); + : "rbx", "memory"); for (;;) __asm__ volatile("hlt"); diff --git a/docs/history/kernel.md b/docs/history/kernel.md index 3d5c06a..1540288 100644 --- a/docs/history/kernel.md +++ b/docs/history/kernel.md @@ -15,6 +15,7 @@ only how it got that way. | [5](#5-at-syscalls-ignoredrejected-real-dirfds) | `*at()` ignored real dirfds, and `ls -l` hung | | [6](#6-boot-speed) | Boot speed, in the loader | | [7](#7-at-syscalls-real-signal-delivery-and-mksh-job-control) | Real signal delivery, and mksh's job control | +| [7b](#7b-the-loader-jumped-into-the-framebuffer-and-executed-pixels) | The loader jumped into the framebuffer and executed pixels | | [8](#8-two-abi-assumptions-that-only-gcc-was-keeping-true) | Two ABI assumptions only GCC was keeping true | | [9](#9-the-boot-looked-like-a-hobby-os-and-two-things-were-why) | The boot looked like a hobby OS | | [10](#10-a-full-screen-program-typed-the-whole-session-into-the-shell) | A full-screen program typed the session into the shell | @@ -444,6 +445,69 @@ signal-delivery mechanism, which is now confirmed solid. --- +## 7b. The loader jumped into the framebuffer and executed pixels + +The same bug as section 4, in the other loader, found five years later by a +compiler upgrade. Worth its own entry because it is the *third* time a register +the code depends on but never asks for has broken this project. + +**Symptom.** Boot died immediately after `schos: exiting boot services` with a +KVM internal error and a register dump: + + RIP=00000000801a5dc5 RBX=0000000080000000 + Code=13 0e 0c 00 13 0e 0c 00 ... dc d5 d1 00 dc d1 00 + +`0x80000000` is the framebuffer. `0x0C0E13` is `SCH_BG` and `0xD1D5DC` is a grey +from the splash's metal gradient. So `RIP` was *inside video memory* and the +bytes being executed were the boot logo. The machine was executing the picture +of itself starting up. + +**Cause.** `boot/uefi/main.c`'s hand-off to the kernel: + + "mov %0, %%cr3\n\t" + "mov %1, %%rbx\n\t" <- writes rbx + "jmp *%2\n\t" + : : "r"(PML4_ADDR), "r"(fb), "r"(entry) + : "memory"); <- rbx NOT declared clobbered + +Nothing told the compiler rbx was written, so it was free to allocate an +*input* there. clang 21 allocated `entry`: + + movq %rax, %cr3 + movq %rcx, %rbx <- the framebuffer, into rbx + jmpq *%rbx <- %2 was rbx; it now holds the framebuffer + +The fix is `"rbx"` in the clobber list, which forbids the allocation. Confirmed +by reading the generated assembly before and after -- `jmpq *%rbx` became +`jmpq *%r14`. + +**Why it appeared now.** It did not. It had been latent since the loader was +written and worked only because every compiler that had built it happened to +choose a different register. It surfaced the first time the binary was relinked +with a different clang. + +That is the whole lesson, and it is identical to section 4's: **a register the +code depends on, that nothing in the source actually asks for, is not a +dependency -- it is a coincidence with a long half-life.** Section 4 was the +BIOS loader passing the framebuffer in EAX by luck; section 8 was `%fs:0` used +as scratch when the ABI reserves it; this is an asm block writing a register it +did not declare. + +Reading the *generated* assembly is what settles all three, and it takes a +minute: + + clang $(UEFI_CFLAGS) -S -o - boot/uefi/main.c | grep -B3 'jmpq.*\*' + +**A second failure sat underneath it and is worth its own note.** The loader had +been stale for half a day -- the Makefile rule for `BOOTX64.EFI` did not list +`logo.h`, `theme.h` or `font.h`, all of which `boot/common/ui.c` includes, so a +new logo never rebuilt it. The splash kept drawing an old mask while `make` +reported nothing to do. Fixing that dependency is what caused the relink, which +is what exposed the register bug. A stale artifact hides the bug that the fresh +one reveals. + +--- + ## 8. Two ABI assumptions that only GCC was keeping true Found when the build moved from GCC-in-WSL to clang-on-Windows. Neither is a