boot: stage2 climbs to long mode, and both loaders use one kernel entry

The BIOS loader handed the kernel a 32-bit entry that climbed into long
mode itself; the UEFI loader arrived already there and used a separate
64-bit entry. Two entries meant two copies of the same preconditions, and
that is not a hypothetical cost -- it is exactly where EFER.SCE went
missing for months, set on one path and not the other, invisible until a
program tried to return from a syscall.

stage2 does the climb now. It builds the page tables where the UEFI
loader builds them and meaning the same thing -- 4 GB identity-mapped
with 1 GB pages, and the same 4 GB again at 0xffff800000000000, because
entry64low's first instruction is a movabs to a high address and identity
alone triple faults on it -- sets PAE, LME and SCE, enables paging, and
far jumps through a GDT with an L-bit code descriptor, which a 32-bit
descriptor cannot express.

stage2main.c is compiled for x86_64 and runs in long mode. It still reads
the multiboot header to find where the kernel's segments go, but enters
through the SCH64 header rather than the entry named there: that entry is
32-bit code whose only job is the climb, and dropping back to 32 bits to
redo work already done is a mode switch for no reason. If the SCH64
header is absent it halts, because there is no longer anything to fall
back to.

So both loaders now hand over identically -- long mode, interrupts off,
CR3 mapping the low 4 GB twice, framebuffer in RBX -- and there is one
set of preconditions instead of two that can drift.

Verified on every path that exists: BIOS to schinit; BIOS with -smp 2,
which brings up cpu1 through the AP trampoline's own separate climb; the
resolution menu still selecting 1024x768 over serial; the ISO, which
loads through ATAPI and a ramdisk root; UEFI unchanged at 1018 MB in 7
regions; bootcheck 108 [ok]. stage2 is 21988 of 24576 bytes.

The 32-bit mboot_entry stays in entry.S. Nothing of ours uses it now, but
it is what makes the kernel loadable by a multiboot loader that is not
ours, and removing it is a separate decision from this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-10 22:09:09 +02:00
commit 879db62379
3 changed files with 131 additions and 18 deletions

View file

@ -569,9 +569,9 @@ $(BUILD)/boot/cdboot: boot/bios/cdboot.S
$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h $(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h
@mkdir -p $(BUILD)/boot @mkdir -p $(BUILD)/boot
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/stage2.S -o $(BUILD)/boot/stage2asm.o $(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/stage2.S -o $(BUILD)/boot/stage2asm.o
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o $(CC) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \ $(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \
-o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o -o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o
$(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2 $(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2
./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES) ./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES)

View file

@ -499,11 +499,100 @@ start32:
movw %ax, %gs movw %ax, %gs
movl $0x7c00, %esp movl $0x7c00, %esp
call stage2main
/* ---------------------------------------------------------------------
* Climb into long mode here, rather than leaving it to the kernel.
*
* The kernel has always had a 32-bit multiboot entry that does this climb
* itself, and the UEFI loader arrives already in long mode and uses a
* separate 64-bit entry. Two entries meant two versions of the setup, and
* that is exactly where EFER.SCE went missing on one of them for months:
* the 32-bit path set it, the 64-bit path did not, and nothing noticed
* until a program tried to return from a syscall.
*
* Climbing here means both loaders hand over in the same state, through the
* same kernel entry, and there is one copy of these preconditions instead of
* two that can drift.
*
* The tables are built exactly where the UEFI loader builds them, and mean
* the same thing: 4 GB identity-mapped with 1 GB pages, and the same 4 GB
* again at 0xffff800000000000, because entry64low's first instruction is a
* movabs to a high address and identity alone triple faults on it.
* ------------------------------------------------------------------- */
/* Zero the two table pages at 0x1000-0x3000. */
movl $0x1000, %edi
movl $0x2000, %ecx
xorl %eax, %eax
rep stosb
/* pml4[0] and pml4[256] both -> the one PDPT at 0x2000. Entry 256 is
* 0xffff800000000000 >> 39, and its slot is at 0x1000 + 256*8 = 0x1800. */
movl $(0x2000 | PTE_P | PTE_W), 0x1000
movl $(0x2000 | PTE_P | PTE_W), 0x1800
/* pdpt[0..3]: four 1 GB pages covering the low 4 GB. */
movl $(0x00000000 | PTE_P | PTE_W | PTE_PS), 0x2000
movl $(0x40000000 | PTE_P | PTE_W | PTE_PS), 0x2008
movl $(0x80000000 | PTE_P | PTE_W | PTE_PS), 0x2010
movl $(0xC0000000 | PTE_P | PTE_W | PTE_PS), 0x2018
/* PAE. Long mode has no other paging format. */
movl %cr4, %eax
bts $5, %eax
movl %eax, %cr4
movl $0x1000, %eax
movl %eax, %cr3
/* EFER: LME to enter long mode, SCE so SYSCALL works. SCE belongs here and
* not in the kernel, so that whichever loader ran, the kernel starts able
* to return from a system call. */
movl $MSR_EFER, %ecx
rdmsr
bts $0, %eax /* SCE */
bts $8, %eax /* LME */
wrmsr
/* Paging on: this is the instruction that actually enters long mode. */
movl %cr0, %eax
orl $CR0_PG, %eax
movl %eax, %cr0
lgdt gdt64desc
ljmp $0x08, $start64
spin: spin:
jmp spin jmp spin
.code64
start64:
movw $0x10, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %ax, %ax
movw %ax, %fs
movw %ax, %gs
movq $0x7c00, %rsp
call stage2main
spin64:
jmp spin64
/* A 64-bit GDT. The 32-bit one above got us into protected mode; long mode
* needs a code descriptor with the L bit, which a 32-bit descriptor cannot
* express. Selector 0x08 is code, 0x10 is data. */
.p2align 3
gdt64:
.quad 0
.quad 0x00209A0000000000 /* code: present, DPL0, executable, L=1 */
.quad 0x0000920000000000 /* data: present, DPL0, writable */
gdt64desc:
.word (gdt64desc - gdt64 - 1)
.quad gdt64
.p2align 2 .p2align 2
gdt: gdt:
SEG_NULLASM SEG_NULLASM

View file

@ -569,6 +569,20 @@ splash(void)
draw_centered(g_height - 40, "Press F8 for boot options", SCH_DIM); draw_centered(g_height - 40, "Press F8 for boot options", SCH_DIM);
} }
// Find the kernel's 64-bit entry: the SCH64 header, 8-byte aligned in the first
// 16 KB, exactly as include/schboot.h describes it. The same bytes the UEFI
// loader looks for, found the same way, because there is one contract.
static uint64
find_sch64_entry(const uchar *buf, unsigned len)
{
for (unsigned off = 0; off + sizeof(struct sch64_header) <= len; off += 8) {
const struct sch64_header *h = (const struct sch64_header *)(buf + off);
if (h->magic0 == SCH64_MAGIC0 && h->magic1 == SCH64_MAGIC1)
return h->entry;
}
return 0;
}
void void
stage2main(void) stage2main(void)
{ {
@ -643,21 +657,31 @@ stage2main(void)
if (g_have_fb) if (g_have_fb)
progress_label("Starting SchOS..."); progress_label("Starting SchOS...");
// Jump to kernel entry. NOT a normal C call: kernel/arch/x86_64/ // Enter through the kernel's 64-bit entry, not the multiboot one.
// entry.S's mboot_entry reads its "argument" (the framebuffer //
// physical address) out of EAX directly, not off the stack — a // The multiboot header still says where the kernel's segments go --
// multiboot-style raw-register handoff, not cdecl. A plain // that part is unchanged -- but stage2 is in long mode now, and the
// `((void(*)(uint32))entry)(fbaddr)` pushes fbaddr on the stack // entry named in that header is 32-bit code whose whole job is to
// per cdecl and leaves EAX as whatever happened to be lying around; // climb into it. Using it would mean dropping back to 32 bits to redo
// it only ever "worked" because simpler bootmain.c code coincidentally // a climb already done, and a mode switch is not a thing to do twice
// left the right value in EAX as a side effect of its own codegen. // for no reason.
// Adding the boot-menu/progress-bar code changed that codegen and //
// broke it silently (kernel received garbage instead of the real // So the same SCH64 header the UEFI loader looks for is found here,
// LFB address). Pin it down explicitly instead of hoping. // and both loaders hand over identically: long mode, interrupts off,
// CR3 mapping the low 4 GB twice, framebuffer in RBX. One entry, one
// set of preconditions, nothing left to drift between two copies.
{ {
uint32 fbaddr = *(uint32*)(SCHBOOT_VBE_INFO + 40); uint32 fbaddr = *(uint32*)(uint64)(SCHBOOT_VBE_LFB);
uint32 entry = hdr->entry_addr; uint64 entry64 = find_sch64_entry((const uchar*)x, 16384);
asm volatile("jmp *%0" : : "r"(entry), "a"(fbaddr));
// Nothing to fall back to: the 32-bit entry cannot be reached from
// long mode. Halt rather than jump somewhere arbitrary.
if (!entry64)
for (;;) asm volatile("hlt");
// RBX, because that is what entry64uefi reads. Not a C call -- the
// kernel entry is not a function and has no prologue to match.
asm volatile("jmp *%0" : : "r"(entry64), "b"((uint64)fbaddr));
} }
} }
} }