diff --git a/Makefile b/Makefile index dd2c3ca..db47930 100644 --- a/Makefile +++ b/Makefile @@ -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 @mkdir -p $(BUILD)/boot - $(CC32) $(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 - $(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \ + $(CC) $(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 -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/bios/stage2main.c -o $(BUILD)/boot/stage2main.o + $(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 $(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) diff --git a/boot/bios/stage2.S b/boot/bios/stage2.S index 44de461..e486b07 100644 --- a/boot/bios/stage2.S +++ b/boot/bios/stage2.S @@ -499,11 +499,100 @@ start32: movw %ax, %gs 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: 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 gdt: SEG_NULLASM diff --git a/boot/bios/stage2main.c b/boot/bios/stage2main.c index c189f59..5d68827 100644 --- a/boot/bios/stage2main.c +++ b/boot/bios/stage2main.c @@ -569,6 +569,20 @@ splash(void) 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 stage2main(void) { @@ -643,21 +657,31 @@ stage2main(void) if (g_have_fb) progress_label("Starting SchOS..."); - // Jump to kernel entry. NOT a normal C call: kernel/arch/x86_64/ - // entry.S's mboot_entry reads its "argument" (the framebuffer - // physical address) out of EAX directly, not off the stack — a - // multiboot-style raw-register handoff, not cdecl. A plain - // `((void(*)(uint32))entry)(fbaddr)` pushes fbaddr on the stack - // per cdecl and leaves EAX as whatever happened to be lying around; - // it only ever "worked" because simpler bootmain.c code coincidentally - // left the right value in EAX as a side effect of its own codegen. - // Adding the boot-menu/progress-bar code changed that codegen and - // broke it silently (kernel received garbage instead of the real - // LFB address). Pin it down explicitly instead of hoping. + // Enter through the kernel's 64-bit entry, not the multiboot one. + // + // The multiboot header still says where the kernel's segments go -- + // that part is unchanged -- but stage2 is in long mode now, and the + // entry named in that header is 32-bit code whose whole job is to + // climb into it. Using it would mean dropping back to 32 bits to redo + // a climb already done, and a mode switch is not a thing to do twice + // for no reason. + // + // So the same SCH64 header the UEFI loader looks for is found here, + // 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 entry = hdr->entry_addr; - asm volatile("jmp *%0" : : "r"(entry), "a"(fbaddr)); + uint32 fbaddr = *(uint32*)(uint64)(SCHBOOT_VBE_LFB); + uint64 entry64 = find_sch64_entry((const uchar*)x, 16384); + + // 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)); } } }