diff --git a/Makefile b/Makefile index 0a2647a..24d4626 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,6 @@ ifdef USE_CLANG # object is silently the wrong format and the first thing to complain is the # linker. CC = clang --target=x86_64-elf - CC32 = clang --target=i386-elf LD = ld.lld OBJCOPY = llvm-objcopy OBJDUMP = llvm-objdump @@ -221,7 +220,6 @@ else TOOLPREFIX ?= endif CC = $(TOOLPREFIX)gcc - CC32 = $(CC) -m32 LD = $(TOOLPREFIX)ld OBJCOPY = $(TOOLPREFIX)objcopy OBJDUMP = $(TOOLPREFIX)objdump @@ -363,16 +361,24 @@ $(shell mkdir -p $(BUILD); \ # usbstick.img is built here rather than on demand because bootcheck -Usb # cannot start QEMU without it, and a missing test image looks exactly like a # broken USB driver: QEMU refuses the -drive and the whole run produces nothing. -all: schos.img $(BUILD)/usbstick.img +# OVMF is in this list because it is no longer optional: with the BIOS loader +# gone, nothing in this tree boots without firmware, and `make clean` wipes +# build/ including the copy fetched from the host. Leaving it out meant a clean +# followed by a build produced a tree where every run script threw. +all: esp $(BUILD)/schfs.img $(BUILD)/usbstick.img +# ...and the firmware, added to `all` further down. It cannot be listed here: +# make expands a rule's prerequisites when it reads the line, and $(OVMF_CODE) +# is defined below this point, so naming it here appends nothing at all and +# does so silently. -schos.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img - dd if=/dev/zero of=schos.img count=10000 - dd if=$(BUILD)/boot/bootblock of=schos.img conv=notrunc - dd if=$(BUILD)/boot/stage2 of=schos.img seek=1 conv=notrunc - # seek is 1 + STAGE2_SECTORS, not a literal. It was 25 against a 24-sector - # stage2; raising STAGE2_SECTORS for the boot logo without touching it wrote - # the kernel straight over stage2's own tail. - dd if=$(BUILD)/kernel/kernel of=schos.img seek=$$(( 1 + $(STAGE2_SECTORS) )) conv=notrunc +# schos.img is gone with the BIOS loader that gave it its shape. It was a raw +# disk with the boot sector at LBA 0, stage2 next and the kernel at a fixed +# offset -- a layout only a loader that seeks by sector number can use. UEFI +# firmware wants a filesystem, so what boots now is the staged ESP, offered to +# QEMU through its virtual FAT. +# +# That is a development arrangement, not an installable disk. A real GPT image +# with a real FAT partition is the installer's job. kernel: $(BUILD)/kernel/kernel @@ -451,6 +457,11 @@ $(OVMF_VARS): cp "$(OVMF_VARS_SRC)" $@ chmod u+w $@ +ovmf: $(OVMF_CODE) $(OVMF_VARS) + +# The other half of `all`, now that OVMF_CODE and OVMF_VARS have values. +all: $(OVMF_CODE) $(OVMF_VARS) + qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) $(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 -cpu qemu64,+rdtscp -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) @@ -506,12 +517,10 @@ iso: rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd $(MAKE) RAMROOT=1 schos.iso -# The UEFI loader is a dependency now: the disc carries a second El Torito -# entry pointing at a FAT image that holds it, so one disc boots both firmwares. +# One El Torito entry, pointing at a FAT image holding the loader and kernel. # sysroot-install because the disc also carries the tree as a real hierarchy. -schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel \ - $(BUILD)/uefi/BOOTX64.EFI sysroot-install - STAGE2_SECTORS=$(STAGE2_SECTORS) SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ +schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install + SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ # Boot the ISO the way a machine booting off a disc would: no hard disk at # all, so a kernel that still secretly depends on one fails here rather than @@ -521,71 +530,6 @@ qemu-iso: schos.iso -cdrom schos.iso -boot d \ -m 1G -smp 2 -machine pc-i440fx-9.2 -# ----------------------------------------------------------------------- -# Bootloader — two stages. Stage 1 (bootblock) is the actual 512-byte MBR -# sector: BIOS-imposed hard limit, so it only loads stage 2 off disk and -# jumps to it. Stage 2 does VBE/A20/protected-mode setup, draws the boot -# splash, and loads the kernel — not size-constrained the same way, just -# padded out to a fixed STAGE2_SECTORS*512 bytes so the kernel's starting -# sector on disk is predictable (kept in sync with STAGE2_SECTORS in -# boot/bios/bootasm.S and boot/bios/stage2main.c). -# ----------------------------------------------------------------------- -# STAGE2_SECTORS used to be written out three times -- here, in boot/bios/bootasm.S -# and in boot/bios/stage2main.c -- and docs/layout.md carried a warning about keeping -# them in step. They are now all fed from this one variable via -D, so the -# warning describes a problem that can no longer happen. Both C/asm files keep -# an #ifndef fallback purely so they still read sensibly on their own. -# -# The ceiling is memory, not disk: stage2 is loaded at 0x7E00 and the multiboot -# kernel-search buffer begins at 0x10000, so 65 sectors is the hard maximum. -# stage2main.c static-asserts this. padstage2.pl fails the build if the compiled -# binary overflows the budget, rather than truncating it -- a short stage2 would -# shift the kernel's start sector and fail in a way that looks nothing like its -# cause. -# 64, not 48. stage2 carries the shared UI now -- which means the 8x16 font and -# the logo mask -- and it is 64-bit code since the long-mode climb moved here. -# At 48 it came to 25440 bytes against a 24576 limit and the build did not stop, -# which is worse than the overflow: the kernel is written at sector -# 1 + STAGE2_SECTORS, so a stage2 that runs past its allocation is overwritten -# by the very kernel it is loading. -STAGE2_SECTORS = 64 -STAGE2_DEF = -DSTAGE2_SECTORS=$(STAGE2_SECTORS) -# Byte budget is computed in the recipe with shell arithmetic, not $(shell expr -# ... \* 512): make hands the backslash through untouched, sh then treats the -# `*` as a glob, and expr gets a directory listing. It "worked" to the extent -# that make carried on and padstage2.pl received nonsense. -STAGE2_BYTES = $$(( $(STAGE2_SECTORS) * 512 )) - -$(BUILD)/boot/bootblock: boot/bios/bootasm.S - @mkdir -p $(BUILD)/boot - $(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/bootasm.S -o $(BUILD)/boot/bootasm.o - $(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \ - -o $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootasm.o - $(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootblock - ./tools/sign.pl $(BUILD)/boot/bootblock - -# Stage 1 for optical media. Same job as bootblock, far less of it: El Torito -# has already loaded stage2 alongside this sector by the time it runs, so it -# only records that we came from a CD and jumps. See boot/bios/cdboot.S. -$(BUILD)/boot/cdboot: boot/bios/cdboot.S - @mkdir -p $(BUILD)/boot - $(CC32) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bios/cdboot.S -o $(BUILD)/boot/cdbootasm.o - $(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \ - -o $(BUILD)/boot/cdboottmp.o $(BUILD)/boot/cdbootasm.o - $(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/cdboottmp.o $@ - ./tools/sign.pl $@ - -$(BUILD)/boot/stage2: boot/bios/stage2.S boot/bios/stage2main.c include/logo.h \ - boot/common/ui.c boot/common/ui.h - @mkdir -p $(BUILD)/boot - $(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 - $(CC) -fno-builtin -fno-pic -O -nostdinc -mno-red-zone -mno-sse -mcmodel=small -I./include -c boot/common/ui.c -o $(BUILD)/boot/ui.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 $(BUILD)/boot/ui.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) - # ----------------------------------------------------------------------- # Kernel binary # ----------------------------------------------------------------------- @@ -912,9 +856,10 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp @echo "Creating SchOS File System Image..." @# A running QEMU holds this file open, and on Windows that makes rm fail @# with "Device or resource busy" -- which stops the build *after* the - @# kernel has already relinked, leaving schos.img stale. The next test run - @# then boots the previous kernel and the change appears to have done - @# nothing, which has cost several debugging cycles. Say so plainly. + @# kernel has already relinked, leaving the staged ESP holding a kernel + @# from the previous build. The next test run then boots that one and the + @# change appears to have done nothing, which has cost several debugging + @# cycles. Say so plainly. @rm -f $@ || { \ echo ""; \ echo " $@ is in use -- a QEMU is still holding it."; \ @@ -943,18 +888,26 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp # framebuffer and asserts the pixels on the host. That last part is the whole # value: the compositor saying "I composited it" proves nothing, and a # screenshot nobody inspects proves less. Needs `make projects` first. -schcom-test: schos.img $(BUILD)/schfs.img +schcom-test: esp $(BUILD)/schfs.img sh tools/posix/schcom_test.sh -qemu-kvm: schos.img $(BUILD)/schfs.img - $(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \ - -hda schos.img -hdb $(BUILD)/schfs.img \ - -m 1G -smp 2 -machine pc-i440fx-9.2 +# The firmware and the disks, in one place. Every run target below boots the +# same way, because there is only one way to boot now: OVMF, the staged ESP as +# the first disk, the SchFS volume as the second. Keeping the argument list in +# a variable is not tidiness -- four copies of a pflash pair is how one of them +# ends up with a writable firmware image and nobody notices. +OVMF_DRIVES = -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) \ + -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) \ + -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk \ + -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -qemu: schos.img $(BUILD)/schfs.img +qemu-kvm: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) + $(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \ + $(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2 + +qemu: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu qemu64,+rdtscp \ - -hda schos.img -hdb $(BUILD)/schfs.img \ - -m 1G -smp 2 -machine pc-i440fx-9.2 + $(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2 # The root filesystem. Built from the staged sysroot; the kernel mounts it as # / in forkret(). @@ -972,18 +925,18 @@ $(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp # the live v1 root and the SchFS probe correctly finds nothing there. # # Once up: cat /proc/schfs -qemu-schfs: schos.img $(BUILD)/schfs.img +qemu-schfs: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS) $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \ - -hda schos.img -hdb $(BUILD)/schfs.img \ - -m 1G -smp 2 -machine pc-i440fx-9.2 + $(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2 # ----------------------------------------------------------------------- # Misc # ----------------------------------------------------------------------- -# schos.img is deliberately NOT here. It is a real file with real -# prerequisites; listing it as phony made make skip the timestamp check and -# re-run the four dd commands -- including zeroing five megabytes -- on every -# single invocation, including ones where nothing had changed. +# Nothing that is a real file with real prerequisites belongs here. schos.img +# used to be listed, and being phony made make skip the timestamp check and +# re-run its four dd commands -- including zeroing five megabytes -- on every +# single invocation, including ones where nothing had changed. The file is gone +# with the BIOS loader, but the rule it taught applies to whatever replaces it. # include/logo.h is generated but committed, exactly as include/font.h is: it # derives from art that changes about never, and making it a build dependency # would put Python on the critical path of every clean build for no benefit. @@ -1053,13 +1006,13 @@ qrcheck: $(QRTEST) .PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-kvm schcom-test \ schfs-test schfs-objtest schfs-image-test schfs-check mkschfs \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ - logo qrtest qrcheck crashqr usb-ids + logo qrtest qrcheck crashqr usb-ids ovmf esp uefi .PRECIOUS: $(BUILD)/%.o # 'clean' removes generated output only. dist/ (upstream tarballs) survives, # so a rebuild never needs the network. clean: - rm -rf $(BUILD) schos.img schos.iso kernel/arch/x86_64/vectors.S + rm -rf $(BUILD) schos.iso kernel/arch/x86_64/vectors.S distclean: clean rm -rf dist diff --git a/README.md b/README.md index 580b67a..730e5ae 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ report. ## Quick start ```sh -make # kernel + bootloader + root filesystem -> schos.img +make # kernel + UEFI loader + root filesystem make qemu # build it and boot it make iso # a bootable ISO for VMware or real hardware ``` @@ -105,7 +105,7 @@ make schfs-check # SchFS B-tree, object and image suites kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/ fs/schfs/ the filesystem -- same source the host tools compile include/ kernel and userland headers -boot/ bios/ (MBR, stage 2, VBE splash) uefi/ (PE loader) +boot/ uefi/ (PE loader) common/ (shared loader UI) lib/ cmd/ first-party userland runtime and programs ports/ third-party software built from source tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl diff --git a/boot/README.md b/boot/README.md index 4e7b762..e88086c 100644 --- a/boot/README.md +++ b/boot/README.md @@ -1,21 +1,30 @@ # Booting SchOS -Two firmware paths, kept apart on purpose: +One firmware path: ``` boot/ - bios/ legacy BIOS: MBR, El Torito CD stub, real-mode stage 2 - uefi/ UEFI application (not written yet) + uefi/ the loader: a 64-bit PE application + common/ the user interface, shared with nothing else now ``` -They are separate directories rather than `#ifdef`s in one because they share -almost nothing. A BIOS loader lives in 16-bit real mode, talks to the firmware -through software interrupts, and has to walk itself up into long mode. A UEFI -loader is a 64-bit PE executable that the firmware has *already* placed in long -mode with paging on, and it asks for services by calling through a table of -function pointers. The only things genuinely in common are the logo bitmap, the -menu's appearance, and the hand-off contract below. +There were two. `boot/bios/` held an MBR, an El Torito CD stub and a real-mode +stage 2, and it was a separate directory rather than `#ifdef`s because the two +shared almost nothing: a BIOS loader lives in 16-bit real mode and talks to the +firmware through software interrupts, while UEFI hands a loader long mode with +paging already on and a table of function pointers to call. + +That asymmetry is what removed it. Everything since roughly 2012 boots UEFI, +CSM — the firmware-side BIOS emulation that let legacy loaders run at all — has +been going away since Intel dropped support for it in 2020, and the cost of the +second path was not the code but the tax on everything that touched it: stage 2 +had a 65-sector ceiling that it silently overflowed, and the resolution menu +could not be shared because VBE mode setting is `INT 10h` and needs real mode. + +The contract below survives it. It was written to let a second loader exist, and +it is worth keeping in that shape: it is what the kernel expects, stated once, +rather than a description of whoever happens to be calling. ## What the loader must hand the kernel @@ -30,24 +39,24 @@ a second loader is correct exactly when it satisfies it. | Framebuffer | `VBE_INFO_ADDR` | address, pitch, width, height, depth | | Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu | -The addresses are defined once, in `include/memlayout.h` and -`boot/bios/stage2main.c`. A UEFI loader must fill the same structures — the -kernel does not care which firmware produced them, and that is what keeps the -split cheap. +The addresses are defined once, in `include/memlayout.h` and `include/schboot.h`. +The kernel does not care which firmware produced them. -## Two entry points, because the two firmwares hand over differently - -The BIOS path enters the kernel in **32-bit protected mode** at the multiboot -entry, with EAX holding the framebuffer address; `mboot_entry` then builds page -tables and climbs into long mode itself. +## The entry point UEFI hands a loader long mode with paging already on. Dropping back to protected mode only to climb out again is a mode switch whose failure mode is a -triple fault with no console, so the kernel publishes a second entry instead. +triple fault with no console, so the kernel publishes an entry that takes the +machine as it already is. `kernel/arch/x86_64/entry.S` carries a `sch64_header` — magic `SCH64BOO`, 8-byte aligned within the first 16 KB, followed by a 64-bit physical address — -found by scanning, exactly as the BIOS loader scans for the multiboot magic. -`include/schboot.h` holds the magic so loader and kernel cannot disagree. +found by scanning. `include/schboot.h` holds the magic so loader and kernel +cannot disagree. + +The 32-bit multiboot entry is still in `entry.S` and now has no caller: the +BIOS loader was the only thing that used it. It is left in place rather than +removed in the same change that removed the loader, because deleting kernel +entry code is its own piece of work with its own way of failing silently. What that entry expects: @@ -69,25 +78,16 @@ surfaced as `unexpected trap 13 ... cs=38`, a long way from the cause. translate it into the same 24-byte entries rather than inventing a second format for the kernel to learn. -## The BIOS path today +## What booting looks like now -1. **`bios/bootasm.S`** — the literal MBR sector. Signed to 512 bytes by - `tools/sign.pl`, loads stage 2 from LBA 1 and jumps. -2. **`bios/cdboot.S`** — the same job for optical media, as an El Torito - no-emulation image. Separate because the loader never uses INT 13h and so - needs a real ATAPI read. -3. **`bios/stage2.S`** — collects the E820 map, sets a VBE mode, enters - protected mode. -4. **`bios/stage2main.c`** — the boot menu and splash, then loads the kernel - and enters long mode. +`make esp` stages `EFI/BOOT/BOOTX64.EFI` and `schos/kernel`, and QEMU is +pointed at OVMF with that directory as a virtual FAT volume — `make qemu`. +`make iso` builds a disc with a single El Torito entry pointing at a FAT image +holding the same two files. -`STAGE2_SECTORS` is defined **once**, in the Makefile, and passed to both the -assembler and the `dd` that writes the image. It used to be written out by hand -in five places and silently truncated the loader when they disagreed. +Neither is an installable disk. A real GPT image with a real FAT partition is +the installer's job, and until it exists there is nothing here that writes to a +disk a machine could boot from unaided. -## Why UEFI is worth doing - -Everything since roughly 2012 boots UEFI by default, and Secure Boot machines -will not run the BIOS path at all without firmware settings most people never -touch. The BIOS loader stays regardless: it is what QEMU's default machine and -older hardware use, and it costs nothing to keep once the two are separated. +Secure Boot is not addressed either: the loader is unsigned, so a machine +enforcing it will refuse to run this. diff --git a/boot/bios/bootasm.S b/boot/bios/bootasm.S deleted file mode 100644 index 10cc78f..0000000 --- a/boot/bios/bootasm.S +++ /dev/null @@ -1,101 +0,0 @@ -# Stage 1 — the actual MBR boot sector. Hard-limited to 510 bytes (+2-byte -# 0x55AA signature) by the BIOS, so there's no room here for anything but -# "load stage 2 off disk and jump to it". All the VBE/A20/protected-mode/ -# splash/kernel-loading work now lives in stage2.S + stage2main.c, which -# aren't sector-size-constrained. -# -# How many sectors of stage2 to pull in. This, the padding the Makefile gives -# stage2's binary, and the "+(STAGE2_SECTORS+1)" sector math in stage2main.c's -# readseg() all have to agree on where the kernel starts on disk -- so the -# Makefile passes -DSTAGE2_SECTORS and is the single authority. The value below -# is only a fallback for reading this file standalone. -# -# It is 48, not the old 24, because stage2 now carries the boot logo -# (include/logo.h, 8 KiB). The real ceiling is memory rather than this number: -# stage2 loads at 0x7E00 and the multiboot kernel-search buffer starts at -# 0x10000, so stage2 cannot exceed 0x8200 bytes = 65 sectors. -#ifndef STAGE2_SECTORS -#define STAGE2_SECTORS 48 -#endif -#define STAGE2_ADDR 0x7E00 - -.code16 -.global start -start: - cli - xorw %ax, %ax - movw %ax, %ds - movw %ax, %es - movw %ax, %ss - movw $0x7c00, %sp - - # Read STAGE2_SECTORS sectors, starting at disk sector 1 (LBA), into - # memory right after this boot sector (0x7E00). Raw IDE PIO (LBA28) — - # same register convention as stage2main.c's own readsect(), just - # inlined here since we can't call C code yet. Port numbers 0x1F2-0x1F7 - # are all > 0xFF, so they must go through %dx — `out $imm8,%al` only - # encodes an 8-bit port and silently truncates anything larger. - # "You were not booted from a CD." stage2main.c reads this byte to choose - # between ATA and ATAPI when it loads the kernel, and nothing else clears - # it -- so a warm reboot from a disc could otherwise leave the flag set and - # send a perfectly ordinary hard-disk boot looking for an optical drive. - # Placed one byte past the verbose flag; both sit clear of the 256-byte VBE - # mode info block that stage2.S is about to write at 0x30000. - movw $0x3000, %ax - movw %ax, %es - movw $0x101, %di - movb $0, %al - movb %al, %es:(%di) - xorw %ax, %ax - movw %ax, %es - - movw $STAGE2_ADDR, %di # ES:DI destination, ES=0 - movw $1, %si # first LBA sector to read - movb $STAGE2_SECTORS, %bl - -.Lsector: -.Lwait1: - movw $0x1F7, %dx - inb %dx, %al - andb $0xC0, %al - cmpb $0x40, %al - jne .Lwait1 - - movw $0x1F2, %dx - movb $1, %al - outb %al, %dx # sector count = 1 - - incw %dx # 0x1F3 - movw %si, %ax - outb %al, %dx # LBA [7:0] - - incw %dx # 0x1F4 - xorb %al, %al - outb %al, %dx # LBA [15:8] - - incw %dx # 0x1F5 - outb %al, %dx # LBA [23:16] - - incw %dx # 0x1F6 - movb $0xE0, %al - outb %al, %dx # drive 0, LBA mode - - incw %dx # 0x1F7 - movb $0x20, %al - outb %al, %dx # READ SECTORS command - -.Lwait2: - inb %dx, %al # %dx still 0x1F7 - andb $0xC0, %al - cmpb $0x40, %al - jne .Lwait2 - - movw $256, %cx # 256 words = 512 bytes - movw $0x1F0, %dx - rep insw # reads into ES:DI, DI += 512 automatically - - incw %si - decb %bl - jnz .Lsector - - ljmp $0, $STAGE2_ADDR diff --git a/boot/bios/cdboot.S b/boot/bios/cdboot.S deleted file mode 100644 index b288e9d..0000000 --- a/boot/bios/cdboot.S +++ /dev/null @@ -1,46 +0,0 @@ -# Stage 1 for optical media -- the El Torito "no emulation" boot sector. -# -# This exists because bootasm.S cannot boot a CD. It loads stage 2 by talking -# straight to the primary IDE controller's ATA registers, and a CD is not an -# ATA disk: it answers to the ATAPI packet interface instead, and under -# El Torito it may not even be the drive at 0x1F0. Rather than teach a -# 512-byte sector two protocols, optical media get their own first stage. -# -# It has almost nothing to do, because El Torito's "load segment / sector -# count" fields do the reading for us: tools/mkiso.sh asks for -# 1 + STAGE2_SECTORS virtual sectors at 0x7C00, so by the time this runs the -# BIOS has already placed stage 2 at 0x7E00 -- exactly where bootasm.S would -# have put it. The only real work is recording *which* medium we came from, -# since stage2main.c has to read the kernel back with the matching protocol. -# -# The flag goes next to the verbose-boot flag, one byte past it, for the same -# reason that one sits where it does: it is clear of the 256-byte VBE mode -# info block at 0x30000 that stage2.S fills in a moment from now. - -#define STAGE2_ADDR 0x7E00 -#include "../../include/schboot.h" - -.code16 -.global start -start: - cli - xorw %ax, %ax - movw %ax, %ds - movw %ax, %ss - movw $0x7c00, %sp - - # "You were booted from a CD." stage2main.c reads this to decide between - # ATA and ATAPI; without it the loader would read the kernel off whatever - # happens to be sitting at 0x1F0, which on a CD boot is usually nothing. - movw $SCHBOOT_MEDIA_SEG, %ax - movw %ax, %es - movw $SCHBOOT_MEDIA_OFF, %di - movb $SCHBOOT_MEDIA_CD, %al - movb %al, %es:(%di) - - # stage2.S expects zeroed segments and an inherited stack, same as it gets - # from bootasm.S. - xorw %ax, %ax - movw %ax, %es - - ljmp $0, $STAGE2_ADDR diff --git a/boot/bios/stage2.S b/boot/bios/stage2.S deleted file mode 100644 index e486b07..0000000 --- a/boot/bios/stage2.S +++ /dev/null @@ -1,604 +0,0 @@ -# Stage 2 — loaded by stage1 at 0x7E00, still in 16-bit real mode. Does the -# VBE mode setup, A20 line, and real->protected mode switch that used to -# live directly in the boot sector, then hands off to stage2main() (C) to -# draw a splash and load the actual kernel. Not sector-size-constrained — -# only bounded by STAGE2_SECTORS in bootasm.S / the Makefile. -#include "../include/asm.h" -#include "../include/memlayout.h" -#include "../include/mmu.h" - -.code16 -.global stage2start -stage2start: - # Segments are already zeroed by stage1; %sp is inherited too. - - movw $bootmsg, %si - call print16 - - # VBE Setup. Mode info block goes at 0x30000, not 0x9000 — stage2 itself - # is loaded starting at 0x7E00 and, with the boot-menu/progress-bar code, - # is now big enough to reach 0x9000, so writing the VBE block there would - # clobber stage2's own tail end. 0x30000 is clear of stage2 (ends well - # under 0xA000), the multiboot kernel-search buffer (0x10000-0x14000), - # and the kernel's own load address (0x100000). - # Which mode. 0x143 is 800x600x32 and stays the default: it is the smallest - # the system is meant to run at, and the one every path has been tested - # against. Press R at the prompt to pick another. - movw $0x0143, %cx - call res_menu # may return a different mode in CX - - movw $0x3000, %ax - movw %ax, %es - xorw %di, %di # ES:DI = 3000:0000 = flat 0x30000 - movw $0x4F01, %ax - int $0x10 - - # Two ways this goes wrong, and only one of them is an error return. - # - # A mode number the BIOS does not know fails outright, which is easy. But a - # number it *does* know may be nothing like what was asked for: 0x146 was - # written here as 1600x1200x32 and this VBE returns 320x200x8 for it. That - # boots, and then the framebuffer console and Schcom both render garbage, - # because everything above assumes 32bpp. So the depth reported in the mode - # info block is checked before the mode is set, not after. - cmpw $0x004F, %ax - jne res_fallback - cmpb $32, %es:25(%di) # mode info +25: bits per pixel - je res_ok - -res_fallback: - cmpw $0x0143, %cx - je e820 # the fallback itself failed; give up on VBE - movw $0x0143, %cx - xorw %di, %di - movw $0x4F01, %ax - int $0x10 - cmpw $0x004F, %ax - jne e820 - cmpb $32, %es:25(%di) - jne e820 - -res_ok: - movw %cx, %bx - orw $0x4000, %bx # bit 14: linear framebuffer - movw $0x4F02, %ax - int $0x10 - - # BIOS E820 memory map — the standard firmware interface every serious - # loader (GRUB, Linux's own boot stub, the BSDs) uses to learn the real - # RAM layout instead of guessing. Entry count goes to flat 0x30200, - # 24-byte entries start at 0x30208 (both inside the 0x3000 segment the - # VBE block already claimed; verbose flag sits at +0x100). Capped at 32 - # entries so a pathological BIOS can't run us past the buffer. -e820: - movw $0x3000, %ax - movw %ax, %es - xorl %ebx, %ebx # continuation cookie, 0 = first call - movw $0x208, %di - xorl %ebp, %ebp # entry count -e820loop: - movl $0xE820, %eax - movl $0x534D4150, %edx # 'SMAP' - movl $24, %ecx - int $0x15 - jc e820done # carry = end of map (or unsupported BIOS) - cmpl $0x534D4150, %eax - jne e820done - addw $24, %di - incl %ebp - cmpl $32, %ebp - jae e820done - testl %ebx, %ebx - jnz e820loop -e820done: - movl %ebp, %es:(0x200) - xorw %ax, %ax - movw %ax, %es - -seta20.1: - inb $0x64, %al - testb $0x2, %al - jnz seta20.1 - movb $0xd1, %al - outb %al, $0x64 -seta20.2: - inb $0x64, %al - testb $0x2, %al - jnz seta20.2 - movb $0xdf, %al - outb %al, $0x60 - - lgdt gdtdesc - movl %cr0, %eax - orl $CR0_PE, %eax - movl %eax, %cr0 - - ljmp $(SEG_KCODE<<3), $start32 - -# Print null-terminated string at DS:SI, to the screen *and* the serial port. -# Clobbers AX, BX, DX, SI. -# -# Both, because neither one alone is where the person is looking. BIOS teletype -# goes to the VGA text screen -- but `make qemu` attaches virtio-gpu alongside -# stdvga and shows that one, so the text lands on a display nobody sees. And -# `make qemu` puts the serial line on stdio, which is the terminal the developer -# is actually sitting in front of. -print16: -.Lp16loop: - lodsb - testb %al, %al - jz .Lp16done - pushw %ax - movb $0x0E, %ah - movw $0x0007, %bx - int $0x10 - popw %ax - call serial_putc - jmp .Lp16loop -.Lp16done: - ret - -# Write AL to COM1, preserving AX. -# -# The wait for the transmitter is bounded. A machine with no serial port reads -# 0xFF from the status register forever, and an unbounded wait there would hang -# the loader on exactly the hardware that cannot tell you why. -serial_putc: - pushw %ax - pushw %cx - pushw %dx - movb %al, %ah # stash the character; AL is about to be an input - movw $0xFFFF, %cx - movw $0x3FD, %dx # LSR -.Lspw: - inb %dx, %al - testb $0x20, %al # transmitter holding register empty - jnz .Lspgo - loop .Lspw - jmp .Lspdone # never drained: no port, or nothing listening -.Lspgo: - movb %ah, %al - movw $0x3F8, %dx - outb %al, %dx -.Lspdone: - popw %dx - popw %cx - popw %ax - ret - -# CF=1 and AL=byte if COM1 has one waiting, CF=0 otherwise. Clobbers AX, DX. -serial_getc: - pushw %dx - movw $0x3FD, %dx # LSR - inb %dx, %al - testb $0x01, %al # data ready - jz .Lsgnone - movw $0x3F8, %dx - inb %dx, %al - popw %dx - stc - ret -.Lsgnone: - popw %dx - clc - ret - -# CF=1 and AL=key from either the BIOS keyboard or the serial line. -# Clobbers AX, DX. -# -# Both, because the menu has to answer whoever is typing. Under `make qemu` the -# serial line is stdio -- the terminal the developer is in -- while a real -# machine, or a QEMU window, has a keyboard and no serial at all. Listening to -# only one is how this shipped "working" and did nothing for the person using it. -anykey: - movb $0x01, %ah - int $0x16 # ZF=0 if the keyboard has one - jz .Lakserial - xorw %ax, %ax - int $0x16 # consume it - stc - ret -.Lakserial: - call serial_getc - ret - - -# Echo AL to both the screen and the serial line. Clobbers nothing. -echo_char: - pushw %ax - pushw %bx - movb $0x0E, %ah - movw $0x0007, %bx - int $0x10 - popw %bx - popw %ax - call serial_putc - ret - -# Read a decimal number, echoing as it goes. Returns it in AX. -# Ends on Enter; anything that is not a digit is ignored rather than aborting, -# so a stray keystroke does not silently truncate the number. -read_num: - pushw %bx - pushw %cx - pushw %dx - xorw %bx, %bx -.Lrn_loop: - call anykey - jnc .Lrn_loop - cmpb $0x0D, %al # Enter (both the keyboard and a serial line) - je .Lrn_done - cmpb $0x0A, %al - je .Lrn_done - cmpb $'0', %al - jb .Lrn_loop - cmpb $'9', %al - ja .Lrn_loop - call echo_char - subb $'0', %al - movzbw %al, %cx - movw %bx, %ax - movw $10, %dx - mulw %dx # DX:AX = AX * 10 - addw %cx, %ax - movw %ax, %bx - jmp .Lrn_loop -.Lrn_done: - movw %bx, %ax - popw %dx - popw %cx - popw %bx - ret - -# Find a VBE mode matching reqw x reqh at 32bpp. Returns CX = mode, or 0. -# -# This asks the BIOS for its own list rather than carrying a table of numbers. -# The table was wrong the first time it was written -- 0x146 was "1600x1200" and -# is 320x200x8 on this VBE -- and a table can only ever describe the machines -# somebody happened to test. AX=4F00 returns a controller info block whose -# VideoModePtr is a far pointer to a 0xFFFF-terminated list of mode numbers; -# each one is then asked what it actually is. -# -# Scratch buffers live at 0x31000 and 0x31200, clear of the mode info block at -# 0x30000, the flags at +0x100, the e820 map at +0x208 and the RSDP slot at -# +0x600. -find_mode: - pushw %ax - pushw %bx - pushw %dx - pushw %si - pushw %di - - movw $0x3100, %ax - movw %ax, %es - xorw %di, %di - movw $0x4F00, %ax - int $0x10 - cmpw $0x004F, %ax - jne .Lfm_none - - # VideoModePtr: offset at +14, segment at +16. - movw %es:14(%di), %si - movw %es:16(%di), %ax - movw %ax, %fs - -.Lfm_next: - movw %fs:(%si), %cx - addw $2, %si - cmpw $0xFFFF, %cx - je .Lfm_none - - # CX and SI have to survive the BIOS call; VBE only promises AX back. - pushw %cx - pushw %si - movw $0x3120, %ax - movw %ax, %es - xorw %di, %di - movw $0x4F01, %ax - int $0x10 - popw %si - popw %cx - cmpw $0x004F, %ax - jne .Lfm_next - - movw $0x3120, %ax - movw %ax, %es - xorw %di, %di - cmpb $32, %es:25(%di) # bits per pixel - jne .Lfm_next - movw %es:18(%di), %ax # width - cmpw reqw, %ax - jne .Lfm_next - movw %es:20(%di), %ax # height - cmpw reqh, %ax - jne .Lfm_next - jmp .Lfm_done # CX is the answer - -.Lfm_none: - xorw %cx, %cx -.Lfm_done: - popw %di - popw %si - popw %dx - popw %bx - popw %ax - ret - -reqw: - .word 0 -reqh: - .word 0 - -# Offer a resolution choice, briefly. -# -# In real mode because this is the only place it can be: setting a VBE mode is -# INT 10h, and by the time stage2main.c runs we are in protected mode with no -# BIOS left to call. The kernel cannot change it either -- there is no modeset -# path, so whatever is chosen here is what the framebuffer, the console and the -# compositor all get. -# -# A second and a half, then it boots on its own. This was four seconds and that -# was simply too long: it is paid on every unattended boot, and a loader that -# adds four seconds to every start to offer a menu nobody usually wants has made -# the common case worse to serve the rare one. -# -# In: CX = default mode. Out: CX = chosen mode. Clobbers AX, BX, DX, SI. -res_menu: - pushw %cx - movw $resprompt, %si - call print16 - - # Poll for ~1.5s. INT 1Ah tick count runs at 18.2 Hz, so 27 ticks is a second - # and a half and needs no calibration against CPU speed -- a delay loop would - # be a different length on every machine. - movb $0x00, %ah - int $0x1A # CX:DX = ticks since midnight - movw %dx, %bx # BX = starting tick, low word -.Lrwait: - call anykey - jc .Lrkey - movb $0x00, %ah - int $0x1A - subw %bx, %dx - cmpw $27, %dx - jb .Lrwait - movw $restimeout, %si - call print16 - popw %cx # timed out: keep the default - ret - -.Lrkey: - orb $0x20, %al # crude tolower: 'R' and 'r' both land on 'r' - cmpb $'r', %al - je .Lrshow - movw $restimeout, %si - call print16 - popw %cx # some other key: boot with the default - ret - -.Lrshow: - popw %cx # default is no longer needed; a pick replaces it - movw $resmenu, %si - call print16 -.Lrpick: - call anykey - jnc .Lrpick - cmpb $'4', %al - je .Lrcustom - cmpb $'1', %al - jb .Lrpick - cmpb $'3', %al - ja .Lrpick - - # Echo the digit: over serial there is no local echo, so without this the - # menu looks like it ignored the key. - call serial_putc - pushw %ax - movb $0x0E, %ah - movw $0x0007, %bx - int $0x10 - popw %ax - - # Index the table: (key - '1') * 2, because each entry is one 16-bit mode. - subb $'1', %al - movzbw %al, %bx - shlw $1, %bx - movw resmodes(%bx), %cx - - movw $resok, %si - call print16 - ret - -# Type a size. Enumerated rather than looked up, so anything the BIOS can -# actually do is available -- not just the three that happen to be listed. -.Lrcustom: - call echo_char - movw $reswidth, %si - call print16 - call read_num - movw %ax, reqw - movw $resheight, %si - call print16 - call read_num - movw %ax, reqh - - movw $resok, %si - call print16 - - # 800x600 is the floor the system is built around: the console, the splash - # and Schcom all assume at least that much, and a smaller mode would be a - # different set of assumptions rather than a smaller number. - cmpw $800, reqw - jb .Lrcsmall - cmpw $600, reqh - jb .Lrcsmall - - call find_mode - testw %cx, %cx - jz .Lrcnone - ret - -.Lrcsmall: - movw $resmin, %si - call print16 - movw $0x0143, %cx - ret - -.Lrcnone: - movw $resnone, %si - call print16 - movw $0x0143, %cx - ret - -# 32-bit colour throughout: the framebuffer console, the fbdev ioctls and -# Schcom all assume 32bpp, and a 24bpp mode would be a different renderer -# rather than a different number here. -# -# Numbers are the VESA/Bochs 0x140 block, and only ones actually verified to -# come back as the advertised size at 32bpp are listed. 0x146 was in here as -# 1600x1200 and returns 320x200x8, which is why the caller now checks the depth -# rather than trusting the table. -# -# Adding a preset means checking it the same way -- boot it and read the -# kernel's "VGA:" line. The real fix is to enumerate the BIOS's own mode list -# (INT 10h AX=4F00) and match on width/height, which would also allow typing an -# arbitrary size; that is the next step here, not a guess at more numbers. -resmodes: - .word 0x0143 # 800x600 verified - .word 0x0144 # 1024x768 verified - .word 0x0145 # 1280x1024 verified - -resprompt: - .asciz "Press R for resolution... " -restimeout: - .asciz "keeping 800x600\r\n" -resmenu: - .asciz "\r\n 1) 800x600 2) 1024x768 3) 1280x1024 4) type a size\r\n choice: " -resok: - .asciz "\r\n" -reswidth: - .asciz " width: " -resheight: - .asciz "\r\n height: " -resmin: - .asciz " below the 800x600 minimum, keeping 800x600\r\n" -resnone: - .asciz " no such 32bpp mode, keeping 800x600\r\n" - -bootmsg: - .asciz "SchOS booting...\r\n" - -.code32 -start32: - movw $(SEG_KDATA<<3), %ax - movw %ax, %ds - movw %ax, %es - movw %ax, %ss - xorw %ax, %ax - movw %ax, %fs - movw %ax, %gs - - movl $0x7c00, %esp - - /* --------------------------------------------------------------------- - * 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 - SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) - SEG_ASM(STA_W, 0x0, 0xffffffff) - -gdtdesc: - .word (gdtdesc - gdt - 1) - .long gdt diff --git a/boot/bios/stage2main.c b/boot/bios/stage2main.c deleted file mode 100644 index 194d5a7..0000000 --- a/boot/bios/stage2main.c +++ /dev/null @@ -1,698 +0,0 @@ -#include "../include/types.h" -#include "../include/x86.h" -#include "../include/memlayout.h" - -#define SECTSIZE 512 - -// Kernel starts right after stage2 on disk. The Makefile passes -// -DSTAGE2_SECTORS so this, bootasm.S and stage2's padding cannot drift apart; -// the value here is a fallback for reading the file on its own. -#ifndef STAGE2_SECTORS -#define STAGE2_SECTORS 48 -#endif - -// stage2 is loaded at 0x7E00 and must not reach the multiboot kernel-search -// buffer at 0x10000. Nothing checks this at runtime, and the failure it would -// produce -- a kernel image partly overwritten by the loader that loaded it -- -// would be spectacularly hard to read backwards from. -_Static_assert(0x7E00 + STAGE2_SECTORS * 512 <= 0x10000, - "stage2 would overlap the kernel-search buffer at 0x10000"); - -// One palette for the whole system; see include/theme.h. The splash used to -// use the panic screen's deep purple and gold, which meant a healthy boot was -// dressed identically to a crash. -#include "theme.h" -// The boot logo, generated from Assets/ by tools/mklogo.py. Shared verbatim -// with kernel/dev/vga.c so there is one mask, not two. -#include "../common/ui.h" - -// VBE mode info block, written here by stage2.S's VBE call. NOT at 0x9000 -// (the classic single-stage bootloader's choice) — stage2's own code now -// runs from 0x7E00 up to nearly 0x9000 once the boot-menu/progress-bar -// logic is included, so a block at 0x9000 would get partially overwritten -// by stage2's own tail end. 0x30000 is clear of stage2, the multiboot -// kernel-search buffer (0x10000-0x14000), and the kernel's load address -// (0x100000). -#include "../../include/schboot.h" /* the hand-off block */ - -// Fixed low-memory scratch byte, read by kernel/core/main.c's kmain() before -// any other init: 1 if the user picked "verbose" from the boot menu. Placed -// just past the VBE info block (which is at most 256 bytes), NOT at 0x7000 -// — kernel/core/main.c's startothers() already uses 0x7000 for the AP -// (secondary CPU) boot trampoline. That's write-after-read from this flag's -// perspective (verbose is read before startothers() runs), so it wasn't an -// active bug, but reusing a documented-elsewhere address is exactly the -// kind of thing that turns into one later. - -// Set by boot/bios/cdboot.S when we were booted from optical media. One byte past -// the verbose flag, for the same reason that one sits where it does: clear of -// the 256-byte VBE mode info block at SCHBOOT_VBE_INFO. - -// E820 memory map, collected by stage2.S in real mode (the same firmware -// interface GRUB and Linux's boot stub use). Count at +0x200, then 24-byte -// entries: {uint64 base; uint64 length; uint32 type; uint32 acpi_ext}. -// The kernel can read the same addresses later; the bootloader uses it to -// report detected RAM like any grown-up loader does. -#define E820_USABLE 1 - -// NOT include/types.h's uint64: that's `unsigned long`, which is 4 bytes -// in this file's -m32 build and would silently halve the struct. -typedef unsigned long long e820_u64; - -struct e820entry { - e820_u64 base; - e820_u64 length; - uint32 type; - uint32 acpi_ext; -} __attribute__((packed)); - -// Total usable RAM in MB according to the firmware's E820 map. -static uint32 -e820_usable_mb(void) -{ - uint32 count = *(uint32*)SCHBOOT_E820_COUNT; - struct e820entry *e = (struct e820entry*)SCHBOOT_E820_MAP; - e820_u64 total = 0; - if (count > 32) return 0; // garbage count: BIOS didn't fill the map - for (uint32 i = 0; i < count; i++) - if (e[i].type == E820_USABLE) - total += e[i].length; - return (uint32)(total >> 20); -} - -// Minimal unsigned-to-decimal for the memory line; no printf down here. -static char * -utoa10(uint32 v, char *buf) -{ - char tmp[12]; - int n = 0; - do { tmp[n++] = '0' + v % 10; v /= 10; } while (v); - int i = 0; - while (n) buf[i++] = tmp[--n]; - buf[i] = 0; - return buf; -} - -struct mbheader { - uint32 magic; - uint32 flags; - uint32 checksum; - uint32 header_addr; - uint32 load_addr; - uint32 load_end_addr; - uint32 bss_end_addr; - uint32 entry_addr; -}; - -void readseg(uchar*, uint, uint); - -// Which medium the kernel has to be read back from. cdboot.S sets the flag -// this is loaded from; bootasm.S clears it, and 0 means "an ATA disk, the way -// it has always been". The ATAPI machinery itself is down at the bottom of -// the file next to readsect(), which is the only thing that uses it. -#define CD_SECTSIZE 2048 - -static int g_from_cd; -static int g_atapi_io = 0; // 0 until a drive is found -static int g_atapi_slave = 0; -static uint g_cd_base512 = 0; // where our boot image starts, in 512s -static int atapi_find(void); -static int atapi_boot_base(void); - -// --- Framebuffer state --------------------------------------------------- -// Standalone drawing (not the kernel's vga.c): paging isn't set up yet, so -// the LFB physical address read out of the VBE mode info block is used -// directly, no P2V translation needed. Only handles 32bpp (what QEMU's -// std VGA actually returns for the mode this bootloader requests) — -// anything else just skips all drawing rather than risk drawing garbage. - -static uint8 *g_fb; -static uint32 g_stride; -static int g_width, g_height; -static int g_have_fb; - -static void -fb_init(void) -{ - g_stride = *(ushort*)(SCHBOOT_VBE_STRIDE); - g_width = *(ushort*)(SCHBOOT_VBE_WIDTH); - g_height = *(ushort*)(SCHBOOT_VBE_HEIGHT); - uchar bpp = *(uchar*)(SCHBOOT_VBE_BPP); - uint32 fbaddr = *(uint32*)(SCHBOOT_VBE_LFB); - - g_have_fb = (bpp == 32 && fbaddr && g_width && g_height); - - // Hand it to the shared UI and stop drawing here. Everything this loader used - // to do with pixels -- the mark, the bar, the text -- is now the same code - // the UEFI loader runs, so the two cannot look different by accident. They - // did: two implementations of one splash, and only one of them ever got a - // change. - struct ui_fb uifb; - uifb.base = (ui_u8 *)(unsigned long)fbaddr; - uifb.width = g_width; - uifb.height = g_height; - uifb.stride = g_stride; - ui_init(g_have_fb ? &uifb : 0); -} - - -// --- Boot logo ------------------------------------------------------------- -// -// include/logo.h is a 4-bit coverage mask generated from Assets/ by -// tools/mklogo.py (`make logo`). It costs 8 KiB, which is most of the reason -// STAGE2_SECTORS went from 24 to 48. -// -// Colour is SCH_FG, not SCH_ACCENT. The source art is a silver gradient and the -// mask preserves its relative values, so compositing coverage straight to -// brightness reproduces that gradient. The accent stays reserved for the single -// rule under the wordmark -- one spot colour, per include/theme.h. - - - - -// Pads with trailing spaces (drawn in bg) so re-drawing a shorter string -// over a longer one doesn't leave stale glyphs behind. - -// --- Keyboard: raw PS/2 scancode polling, no interrupts, no BIOS calls -- -// (BIOS INT 16h only works in real mode; we're in protected mode by the -// time any of this runs.) Scancode set 1: F8 make code 0x42, arrows are -// extended (0xE0 prefix byte), Enter 0x1C, digits '1'-'3' are 0x02-0x04. - -static int -kbd_haskey(void) -{ - return inb(0x64) & 1; -} - -static int -kbd_readscancode(void) -{ - return inb(0x60); -} - -// Blocking read of one logical key: plain scancodes pass through, extended -// (0xE0-prefixed) scancodes come back OR'd with 0x100 so callers can tell -// "up arrow" (0x148) apart from a plain byte that happens to equal 0x48. -static int -kbd_getkey(void) -{ - int sc; - for (;;) { - while (!kbd_haskey()) - ; - sc = kbd_readscancode(); - // 0xE0 is the extended-key prefix, not a break code, even though it - // also has bit 7 set — must be checked before the break-code test - // below or every arrow/Home/End/etc. press gets silently swallowed. - if (sc == 0xE0) { - while (!kbd_haskey()) - ; - sc = kbd_readscancode(); - if (sc & 0x80) continue; // extended break code: ignore, try again - return 0x100 | sc; - } - if (sc & 0x80) continue; // plain break code: ignore, try again - return sc; - } -} - -// Polls for up to `iterations` busy-loop spins, watching for F8's make -// code. Returns 1 if seen, 0 on timeout. Ignores everything else so a -// stray keypress during the splash doesn't do anything surprising. -static int -wait_for_f8(uint32 iterations) -{ - for (volatile uint32 i = 0; i < iterations; i++) { - if (kbd_haskey()) { - int sc = kbd_readscancode(); - if (sc == 0x42) - return 1; - } - } - return 0; -} - -// --- ATA drive presence (primary channel: master/slave) ------------------ -// Standard "floating bus" trick: select the drive, write a distinguishable -// pattern to two data-path registers, and read it back. A missing drive -// leaves the bus floating and the readback won't match. -// -// Only the primary channel (0x1F0) is checked. QEMU's IDE emulation -// answers the same pattern test "successfully" on the secondary channel -// (0x170) even with nothing attached to it — its floating-bus behavior -// doesn't match real hardware closely enough to tell present from absent — -// so showing it would just be confidently wrong. This project only ever -// configures drives on the primary channel (-hda/-hdb) anyway. - -static int -ata_present(ushort iobase, int slave) -{ - outb(iobase + 6, slave ? 0xB0 : 0xA0); - inb(iobase + 7); inb(iobase + 7); inb(iobase + 7); inb(iobase + 7); - outb(iobase + 2, 0xAA); - outb(iobase + 3, 0x55); - outb(iobase + 2, 0x55); - outb(iobase + 3, 0xAA); - outb(iobase + 2, 0xAA); - outb(iobase + 3, 0x55); - return inb(iobase + 2) == 0xAA && inb(iobase + 3) == 0x55; -} - -// --- Real-time load progress bar ----------------------------------------- - -#define BAR_WIDTH 400 -#define BAR_HEIGHT 18 - -// --- Splash layout ------------------------------------------------------- -// -// Every vertical position on the splash derives from here. It used to be a -// handful of independent offsets from the screen centre -- wordmark at cy-18, -// rule at cy+6, bar at cy+30 -- which was fine while the tallest element was a -// line of text. Adding a 128px mark on top of that stack pushed the group's -// optical centre about 80px above the screen's, so the whole composition read -// as top-heavy with a large void underneath. -// -// Now the group is measured and centred as a unit. The progress bar counts -// towards that height even though it is only on screen during the kernel load: -// if it did not, everything would jump upwards the moment the bar appeared. -#define SPLASH_MARK_GAP 24 // mark to wordmark -#define SPLASH_TEXT_H 16 // one line of the 8x16 font -#define SPLASH_RULE_GAP 8 // wordmark to rule -#define SPLASH_RULE_H 2 -#define SPLASH_BAR_GAP 30 // rule to bar; the bar's label lives in here - -#define SPLASH_GROUP_H (LOGO_H + SPLASH_MARK_GAP + SPLASH_TEXT_H + \ - SPLASH_RULE_GAP + SPLASH_RULE_H + SPLASH_BAR_GAP + BAR_HEIGHT) - -#define SPLASH_MARK_Y ((g_height - SPLASH_GROUP_H) / 2) -#define SPLASH_TEXT_Y (SPLASH_MARK_Y + LOGO_H + SPLASH_MARK_GAP) -#define SPLASH_RULE_Y (SPLASH_TEXT_Y + SPLASH_TEXT_H + SPLASH_RULE_GAP) -#define SPLASH_BAR_Y (SPLASH_RULE_Y + SPLASH_RULE_H + SPLASH_BAR_GAP) - -static uint32 g_load_total, g_load_done; -static int g_progress_active; -static int g_bar_x, g_bar_y; -static int g_last_pct; - - -// Border never changes once drawn — separated out so it's only ever drawn -// once per bar instead of being redrawn on every single tick. - - - - -// --- Boot menu ------------------------------------------------------- - -static void -reboot_to_firmware(void) -{ - ui_clear(); - ui_text_centered((int)ui_height() / 2 - 8, "Rebooting to firmware...", - SCH_ACCENT); - outb(0x64, 0xFE); // pulse the keyboard controller's reset line - for (;;) - ; -} - - -// Returns 1 for "boot verbose", 0 for "boot normal". Only returns at all -// once the user picks a boot option; "reboot" never returns. -static int -boot_menu(void) -{ - const char *items[] = { - "Boot SchOS", - "Boot SchOS (verbose)", - "Reboot to firmware setup", - }; - int nitems = 3; - int selected = 0; - - int hda = ata_present(0x1F0, 0); - int hdb = ata_present(0x1F0, 1); - - for (;;) { - // One shared panel rather than a screen laid out in literals. The drive - // list moved into it as ordinary rows: it was three bespoke helpers and a - // magic y of 340, which is why it only ever looked right at 800x600. - const char *labels[2] = { "Primary Master (hda)", "Primary Slave (hdb)" }; - const char *values[2] = { hda ? "present" : "-", hdb ? "present" : "-" }; - (void)labels; (void)values; - - ui_menu("Boot Manager", items, nitems, selected, - "Up/Down to choose, Enter to select"); - - int key = kbd_getkey(); - if (key == 0x148) selected = (selected + nitems - 1) % nitems; // up - else if (key == 0x150) selected = (selected + 1) % nitems; // down - else if (key == 0x02) selected = 0; // '1' - else if (key == 0x03) selected = 1; // '2' - else if (key == 0x04) { reboot_to_firmware(); } // '3' - else if (key == 0x1C) { // Enter - if (selected == 2) - reboot_to_firmware(); - return selected == 1; - } - } -} - -// Centered one-liner helper; strlen computed here so callers stop -// hand-counting characters (three of them had it wrong at some point). - -// "Memory: NNNN MB" — assembled by hand, no printf in the bootloader. - -// The splash, which is now four lines because it is the shared one. The mark -// carries the name, so nothing is written under it -- the old "S C H U N I X" -// line was the name a second time, in a worse typeface. -static void -splash(void) -{ - ui_splash("Press F8 for boot options"); -} - - -// 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) -{ - struct mbheader *hdr; - uint32 *x = (uint32*) 0x10000; - uint n; - int verbose = 0; - - fb_init(); - - // Sort out where the kernel is coming from before anything is drawn, so a - // disc we cannot read says so on the splash instead of failing silently - // several seconds later in the middle of the progress bar. - if (*(uchar*)SCHBOOT_MEDIA_FLAG == SCHBOOT_MEDIA_CD) { - if (atapi_find()) - g_cd_base512 = atapi_boot_base() * (CD_SECTSIZE / SECTSIZE); - g_from_cd = (g_atapi_io != 0 && g_cd_base512 != 0); - } - - splash(); - - // ~0.6s at QEMU TCG speeds (measured), watching for F8. Shortened from - // ~1s (5000000) — still enough time to react to, but it's flat overhead - // on every single boot whether or not anyone wants the menu. - if (wait_for_f8(3000000)) - verbose = boot_menu(); - - *(uchar*)SCHBOOT_VERBOSE_FLAG = verbose; - - // Back to the splash after the menu: the mark and the bar, which is what the - // rest of this function is about to fill in. - splash(); - - // Booted from a disc we then could not read. Falling through would send - // readseg() at the primary ATA channel, which on a CD-only machine is empty - // -- so it would grind through a bounded wait per sector and then jump into - // whatever the uninitialised buffer happened to hold. Stopping with a - // legible reason is the only useful thing left to do. This is drawn after - // the clear above on purpose: an earlier version put it on the splash, - // where it was wiped a moment later and nobody ever saw it. - if (*(uchar*)SCHBOOT_MEDIA_FLAG == SCHBOOT_MEDIA_CD && !g_from_cd) { - ui_text_centered(g_height / 2, "No readable boot disc found.", SCH_ACCENT); - ui_text_centered(g_height / 2 + 20, "The drive did not answer ATAPI.", SCH_DIM); - for (;;) - asm volatile("hlt"); - } - - if (g_have_fb) - ui_progress_begin("Detecting kernel...", 16384); - - // Read 32 sectors (16KB) looking for the multiboot header. - readseg((uchar*)x, 16384, 0); - - for (n = 0; n < 16384/4; n++) { - if (x[n] == 0x1BADB002) { - if ((x[n] + x[n+1] + x[n+2]) == 0) { - hdr = (struct mbheader *) (x + n); - - if (g_have_fb) - ui_progress_begin("Loading kernel...", hdr->load_end_addr - hdr->load_addr); - - // Load the actual kernel segments - readseg((uchar*) hdr->load_addr, - hdr->load_end_addr - hdr->load_addr, - (n * 4) - (hdr->header_addr - hdr->load_addr)); - - // Zero out BSS - if (hdr->bss_end_addr > hdr->load_end_addr) - stosb((void*) hdr->load_end_addr, 0, hdr->bss_end_addr - hdr->load_end_addr); - - if (g_have_fb) - ui_progress_label("Starting SchOS..."); - - // 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*)(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)); - } - } - } - } -} - -// Bounded, because there might not be a disk there at all. -// -// This used to be `while((inb(0x1F7) & 0xC0) != 0x40);`. On a machine with no -// ATA drive on the primary channel the status register reads 0xFF forever and -// the loader hung on a splash screen with no message and no way to tell what -// had gone wrong -- which is exactly what happened the first time a CD boot -// fell back to this path. A bounded wait turns that into a failed read, and -// stage2main() says so on screen. -void waitdisk(void) { - for (int i = 0; i < 4000000; i++) - if ((inb(0x1F7) & 0xC0) == 0x40) - return; -} - -void readsect_ata(void *dst, uint offset) { - waitdisk(); - outb(0x1F2, 1); - outb(0x1F3, offset); - outb(0x1F4, offset >> 8); - outb(0x1F5, offset >> 16); - outb(0x1F6, (offset >> 24) | 0xE0); - outb(0x1F7, 0x20); - waitdisk(); - insl(0x1F0, dst, SECTSIZE/4); -} - -// --------------------------------------------------------------------------- -// ATAPI -- reading the kernel back off a CD. -// -// An optical drive does not answer the ATA commands above. It speaks the -// packet interface: you hand it a SCSI command descriptor block through the -// data port and it hands back 2048-byte logical blocks. The register file and -// the PIO loop are otherwise the same, which is why this lives here rather -// than forcing the loader back into real mode to use INT 13h -- by the time -// the kernel is read, stage2.S has long since switched to protected mode and -// the BIOS is gone. -// --------------------------------------------------------------------------- - -static void bcopy(void *dst, const void *src, uint n) { - uchar *d = dst; - const uchar *s = src; - while (n--) *d++ = *s++; -} - -// A channel has to be reset before its signature means anything. -// -// ATAPI devices report themselves by leaving 0x14 in LBA-mid and 0xEB in -// LBA-high -- but only until something issues a command, after which those -// registers hold that command's byte count instead. The BIOS has just -// finished reading this very disc in order to load us, so on a CD boot the -// signature is *always* stale by the time we look. Resetting the channel is -// what puts it back. -static void atapi_reset(int io) { - int ctrl = io + 0x206; // 0x1F0 -> 0x3F6, 0x170 -> 0x376 - - outb(ctrl, 0x04); // SRST - for (int i = 0; i < 10000; i++) // hold it well past the required 5us - inb(ctrl); - outb(ctrl, 0x00); - - for (int i = 0; i < 1000000; i++) // bounded: an empty channel reads 0xFF - if (!(inb(io + 7) & 0x80)) // and would otherwise spin forever - break; -} - -static int atapi_find(void) { - static const int bases[2] = { 0x1F0, 0x170 }; - - for (int b = 0; b < 2; b++) { - atapi_reset(bases[b]); - for (int s = 0; s < 2; s++) { - outb(bases[b] + 6, s ? 0xB0 : 0xA0); - // Four throwaway status reads: the standard 400ns settle after a drive - // select, before the register file is meaningful. - inb(bases[b] + 7); inb(bases[b] + 7); - inb(bases[b] + 7); inb(bases[b] + 7); - if (inb(bases[b] + 4) == 0x14 && inb(bases[b] + 5) == 0xEB) { - g_atapi_io = bases[b]; - g_atapi_slave = s; - return 1; - } - } - } - return 0; -} - -// One 2048-byte logical block. Returns 0 on success. -static int atapi_read_block(uint lba, void *dst) { - int io = g_atapi_io; - uchar cdb[12]; - int i; - - outb(io + 6, g_atapi_slave ? 0xB0 : 0xA0); - inb(io + 7); inb(io + 7); inb(io + 7); inb(io + 7); - - outb(io + 1, 0); // features: PIO, no DMA - outb(io + 4, CD_SECTSIZE & 0xFF); // how much we are willing to take - outb(io + 5, CD_SECTSIZE >> 8); - outb(io + 7, 0xA0); // PACKET - - // Wait for the drive to ask for the command packet: BSY clear, DRQ set. - for (i = 0; i < 0x1000000; i++) { - uchar st = inb(io + 7); - if (st & 0x01) return -1; // ERR - if (!(st & 0x80) && (st & 0x08)) break; - } - if (i == 0x1000000) return -1; - - // READ(12). Big-endian LBA and transfer length, because SCSI is. - for (i = 0; i < 12; i++) cdb[i] = 0; - cdb[0] = 0xA8; - cdb[2] = (uchar)(lba >> 24); - cdb[3] = (uchar)(lba >> 16); - cdb[4] = (uchar)(lba >> 8); - cdb[5] = (uchar)lba; - cdb[9] = 1; // one block - outsl(io, cdb, 3); // 12 bytes = 3 dwords - - // Wait for the data itself. - for (i = 0; i < 0x1000000; i++) { - uchar st = inb(io + 7); - if (st & 0x01) return -1; - if (!(st & 0x80) && (st & 0x08)) break; - } - if (i == 0x1000000) return -1; - - insl(io, dst, CD_SECTSIZE / 4); - return 0; -} - -// Where our boot image sits on the disc. -// -// Sector numbers on a CD are absolute, but readseg() counts from the start of -// the boot image -- which on a hard disk is the start of the disk and on a CD -// is wherever the ISO's filesystem happened to leave room. El Torito writes -// that address down, so we read it rather than guess: -// -// LBA 17 Boot Record Volume Descriptor, holding a pointer at offset 0x47 -// catalog validation entry (32 bytes), then the default entry, whose -// "Load RBA" at offset 8 is the boot image's LBA -// -// Returns 0 if the disc does not look like something we booted from. -static int atapi_boot_base(void) { - static uchar buf[CD_SECTSIZE]; - uint catalog, image; - - if (atapi_read_block(17, buf) != 0) - return 0; - if (buf[0] != 0 || buf[1] != 'C' || buf[2] != 'D' || buf[3] != '0' || - buf[4] != '0' || buf[5] != '1') - return 0; - - catalog = (uint)buf[0x47] | ((uint)buf[0x48] << 8) | - ((uint)buf[0x49] << 16) | ((uint)buf[0x4A] << 24); - if (catalog == 0) - return 0; - - if (atapi_read_block(catalog, buf) != 0) - return 0; - if (buf[32] != 0x88) // default entry not marked bootable - return 0; - - image = (uint)buf[40] | ((uint)buf[41] << 8) | - ((uint)buf[42] << 16) | ((uint)buf[43] << 24); - return image; -} - -// A CD block holds four 512-byte sectors, and readseg() asks for them one at -// a time and in order -- so caching the last block turns four reads into one. -// Without it every byte of the kernel would be fetched four times. -static uchar g_cd_cache[CD_SECTSIZE]; -static uint g_cd_cached = 0xFFFFFFFF; - -static void readsect_atapi(void *dst, uint offset) { - uint abs = offset + g_cd_base512; - uint blk = abs / (CD_SECTSIZE / SECTSIZE); - uint sub = abs % (CD_SECTSIZE / SECTSIZE); - - if (blk != g_cd_cached) { - if (atapi_read_block(blk, g_cd_cache) != 0) { - // Nothing useful to do here: there is no console yet and no way back. - // Leaving the buffer alone means the multiboot search simply fails to - // match, which stops with the splash up rather than jumping to garbage. - return; - } - g_cd_cached = blk; - } - bcopy(dst, g_cd_cache + sub * SECTSIZE, SECTSIZE); -} - -void readsect(void *dst, uint offset) { - if (g_from_cd) - readsect_atapi(dst, offset); - else - readsect_ata(dst, offset); - ui_progress_tick(SECTSIZE); -} - -void readseg(uchar* pa, uint count, uint offset) { - uchar* epa = pa + count; - pa -= offset % SECTSIZE; - // Kernel starts right after stage2 (1 boot sector + STAGE2_SECTORS - // stage2 sectors), not immediately after just the boot sector. - offset = (offset / SECTSIZE) + (1 + STAGE2_SECTORS); - for(; pa < epa; pa += SECTSIZE, offset++) - readsect(pa, offset); -} diff --git a/boot/uefi/README.md b/boot/uefi/README.md index 3d764f6..f268c6a 100644 --- a/boot/uefi/README.md +++ b/boot/uefi/README.md @@ -1,8 +1,9 @@ # UEFI loader -Not written yet. This directory exists so the BIOS path could be moved into -`boot/bios/` without pretending the split was hypothetical. +The loader. This directory once existed only so the BIOS path could be moved +into `boot/bios/` without pretending the split was hypothetical; it is now the +only path there is. See `../README.md` for the hand-off contract a loader has to satisfy. The short version: the kernel wants long mode with paging on, its ELF segments @@ -22,7 +23,7 @@ What this will need, in rough order: parses. Not a second format for the kernel to learn. 4. `ExitBootServices` before touching the memory it described. 5. Graphics Output Protocol for the framebuffer, filling the same - `VBE_INFO_ADDR` structure the BIOS path fills. + `VBE_INFO_ADDR` structure the kernel reads. The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the -ISO gains an El Torito EFI entry alongside the BIOS one it already has. +ISO carries a single El Torito EFI entry. diff --git a/docs/building.md b/docs/building.md index 6558949..c8c0696 100644 --- a/docs/building.md +++ b/docs/building.md @@ -6,7 +6,7 @@ Git Bash or MSYS2). The build is developed from both Windows and Linux, so a change that only works on one of them is a bug. ```sh -make # kernel + bootloader + root filesystem -> schos.img +make # kernel + UEFI loader + root filesystem make qemu # build and boot it ``` diff --git a/docs/layout.md b/docs/layout.md index 53d9672..c74c4b3 100644 --- a/docs/layout.md +++ b/docs/layout.md @@ -52,7 +52,7 @@ schos/ ``` build/ -├─ boot/ bootblock, stage2 +├─ boot/ uefi/ loader, common/ UI ├─ kernel/ object files, the linked kernel, kernel.asm ├─ lib/ libc glue objects, initcode blob ├─ cmd/ linked first-party programs @@ -114,8 +114,9 @@ compiled objects first. Switching back is the same — plain `make` after a `ramroot` build reuses stale objects, so run `rm -rf build/kernel build/lib build/cmd` first (or just `make clean`). -The image costs boot time: stage2 loads the kernel by PIO one sector at a time, -so a 6 MB ramroot kernel is visibly slower to start than the 350 KB default. +The image costs boot time: the loader reads the kernel through the firmware's +file protocol, so a 6 MB ramroot kernel is visibly slower to start than the +350 KB default. **What may go in it is a licensing question, not only a size one.** The image is linked into the kernel binary, so anything in `sysroot/` when it is built becomes @@ -154,22 +155,16 @@ Makefile has `--redefine-sym` lines mapping e.g. blob's build location and those symbol names change; update the Makefile to match or the kernel fails to link. -**`STAGE2_SECTORS` now lives in exactly one place: the root `Makefile`.** It used -to be written out by hand in five — `boot/bios/bootasm.S`, `boot/bios/stage2main.c`'s -`readseg()`, the `dd ... seek=` offset, and twice more inside `tools/posix/mkiso.sh` -(its stage2 size assertion and `-boot-load-size`). This note used to warn you to -keep three of them in step and did not know about the other two. +**`STAGE2_SECTORS` is gone, with the BIOS loader it sized.** It is recorded here +because the shape of the bug outlived it: one number that four files each +believed independently — `boot/bios/bootasm.S`, `stage2main.c`'s `readseg()`, +the `dd ... seek=` offset, and twice inside `mkiso.sh`. Raising it from 24 to 48 +for the boot logo left the `dd seek=25` offset behind, so the kernel was written +directly over stage 2's own tail, and nothing said so. -Raising it from 24 to 48 for the boot logo demonstrated the problem: the `dd -seek=25` offset stayed behind, so the kernel was written directly over stage2's -own tail. Now the Makefile passes `-DSTAGE2_SECTORS` to both boot sources, -derives the padding budget and the `seek=` offset from the same variable, and -exports it to `mkiso.sh`. The `#define`s that remain in the two boot sources are -`#ifndef`-guarded fallbacks so those files still read sensibly alone; the -Makefile always wins. - -Two things still fail the build rather than silently misbooting, and should stay -that way: `tools/padstage2.pl` errors if stage2 overflows its budget instead of -truncating it, and `boot/bios/stage2main.c` static-asserts that stage2 cannot reach -the multiboot kernel-search buffer at `0x10000` — which puts the hard ceiling at -65 sectors, since stage2 loads at `0x7E00`. +The fix was to derive all five from one Makefile variable. The lesson worth +keeping is the other half: `tools/padstage2.pl` *failed the build* when stage 2 +overflowed its budget rather than truncating it, which is why the overflow to +25440 bytes against a 24576 limit was found at all. A layout constant that +several artifacts must agree on wants a check that stops the build, not a +comment asking people to remember. diff --git a/docs/moving-to-linux.md b/docs/moving-to-linux.md index 27d8d6e..fd97554 100644 --- a/docs/moving-to-linux.md +++ b/docs/moving-to-linux.md @@ -82,7 +82,7 @@ else needs it: `make`, `make schfs-check` and `make qemu` all work without. ## 4. Build and check ```sh -make # kernel + root filesystem -> schos.img +make # kernel + root filesystem make schfs-check # B-tree, object and image suites -- all host-native make qemu # boot it ``` diff --git a/run.ps1 b/run.ps1 index 8469442..628dbd4 100644 --- a/run.ps1 +++ b/run.ps1 @@ -45,7 +45,7 @@ if (-not (Test-Path $Qemu)) { throw "QEMU not found: $Qemu (install it, or pass # The two images QEMU needs. Keep in sync with the 'qemu' target in the # Makefile. # -# hda the boot disk: bootblock, stage 2 and the kernel +# esp the staged EFI system partition: the loader and the kernel # hdb the root filesystem, mounted as / by forkret() # # hdb was build/fs.img until SchFS became the root. Nothing builds fs.img on @@ -53,8 +53,10 @@ if (-not (Test-Path $Qemu)) { throw "QEMU not found: $Qemu (install it, or pass # longer exists -- QEMU came up with no usable disk on device 1 and the kernel # panicked with SCHFS_ROOT. That is the failure this script is supposed to # prevent, so it is worth naming. -$hda = Join-Path $root 'schos.img' -$hdb = Join-Path $root 'build/schfs.img' +$esp = Join-Path $root 'build/esp' +$hdb = Join-Path $root 'build/schfs.img' +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' if (-not $NoBuild) { Write-Host '==> building (native, no WSL)' -ForegroundColor Cyan @@ -88,11 +90,16 @@ if (-not $NoBuild) { if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } } -foreach ($img in @($hda, $hdb)) { +foreach ($img in @($hdb, (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { if (-not (Test-Path $img)) { - throw "missing image: $img (run without -NoBuild, or: make)" + throw "missing: $img (run without -NoBuild, or: make)" } } +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +$runVars = Join-Path $root 'build/run-OVMF_VARS.fd' +Copy-Item -Force $vars $runVars # Guard against the exact failure this script exists to prevent: booting # against a stale, empty or wrong-format root disk. The first uberblock sits in @@ -112,7 +119,9 @@ if ($buf[0] -ne 0x46 -or $buf[1] -ne 0x68 -or $buf[2] -ne 0x63 -or $buf[3] -ne 0 } $qemuArgs = @( - '-drive', "file=$hda,format=raw,index=0,media=disk" + '-drive', "if=pflash,format=raw,unit=0,readonly=on,file=$code" + '-drive', "if=pflash,format=raw,unit=1,file=$runVars" + '-drive', "file=fat:rw:$esp,index=0,media=disk" '-drive', "file=$hdb,format=raw,index=1,media=disk" '-m', '1G' '-smp', '2' diff --git a/tools/padstage2.pl b/tools/padstage2.pl deleted file mode 100644 index 6dadf66..0000000 --- a/tools/padstage2.pl +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/perl -# Zero-pads the stage2 bootloader binary out to a fixed size (STAGE2_SECTORS -# * 512 bytes), so the kernel's on-disk starting sector is predictable -# regardless of stage2's actual compiled size. Errors out if stage2 has -# grown past that budget instead of silently truncating it. - -my $size = $ARGV[1] || die "usage: padstage2.pl \n"; - -open(F, $ARGV[0]) || die "open $ARGV[0]: $!"; -my $n = sysread(F, my $buf, $size + 1); -close(F); - -if ($n > $size) { - print STDERR "stage2 too large: $n bytes (max $size)\n"; - exit 1; -} - -print STDERR "stage2 is $n bytes (max $size)\n"; - -$buf .= "\0" x ($size - $n); - -open(F, ">$ARGV[0]") || die "open >$ARGV[0]: $!"; -print F $buf; -close(F); diff --git a/tools/posix/mkiso.sh b/tools/posix/mkiso.sh index c4b1fbc..52fefbd 100644 --- a/tools/posix/mkiso.sh +++ b/tools/posix/mkiso.sh @@ -3,62 +3,37 @@ # # tools/posix/mkiso.sh [output.iso] # -# The disc carries two El Torito boot entries and a real directory tree: +# The disc carries one El Torito boot entry and a real directory tree: # -# /boot/schos.bin BIOS: cdboot + stage2 + kernel, loaded no-emulation -# /boot/efiboot.img UEFI: a FAT image holding EFI/BOOT/BOOTX64.EFI +# /boot/efiboot.img a FAT image holding the loader and the kernel # /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc # /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy # -# --- BIOS entry ------------------------------------------------------------- +# --- Why only one entry ----------------------------------------------------- # -# El Torito's "no emulation" mode, which is the only one of the three that fits -# how SchOS actually loads itself. +# There was a second, for BIOS: cdboot and stage2 concatenated with the kernel +# and loaded by El Torito in no-emulation mode. The BIOS loader is gone, so the +# entry that started it is too. # -# floppy emulation caps the image at 2.88 MB; the ramdisk-root kernel -# alone is bigger than that. -# hard disk emulation presents the image through the BIOS's INT 13h, and -# SchOS's loader does not use INT 13h -- it drives -# the IDE controller's registers directly, which -# cannot reach a BIOS-emulated drive. -# no emulation the BIOS loads a chosen run of sectors at 0x7C00 -# and gets out of the way. That is this. +# What UEFI wants is not that mechanism at all. Firmware does not run +# no-emulation boot code; it looks for a FAT filesystem containing +# EFI/BOOT/BOOTX64.EFI and hands that volume to the application it finds. So +# the entry is a FAT image built with mtools, and it carries the kernel as well +# as the loader -- BOOTX64.EFI opens \schos\kernel on the volume it was itself +# loaded from, and for a UEFI boot that volume is this FAT image, not the ISO +# 9660 tree around it. Without the kernel in here the loader comes up and halts +# with EFI_NOT_FOUND, which is what it did. # -# So the layout below is deliberate: -boot-load-size is 1 + STAGE2_SECTORS, -# which makes the BIOS place both boot/cdboot and the whole of stage2 in memory -# before anything of ours runs. That puts stage2 at 0x7E00 -- byte for byte the -# arrangement bootasm.S produces on a hard disk. From there the only difference -# is that stage2main.c reads the kernel back with ATAPI instead of ATA. -# -# STAGE2_SECTORS comes from the environment, set by the Makefile, because this -# same number also lives in boot/bios/bootasm.S, boot/bios/stage2main.c and the dd offsets -# in schos.img's recipe. It was previously written out by hand in three -# separate spots in this file alone, and raising it for the boot logo is what -# turned that from a latent hazard into a broken image. -# -# NOT using -boot-info-table on purpose: it patches 56 bytes over offset 8 of -# the boot image, which is the middle of cdboot's code. stage2main.c finds -# where it was loaded from by reading the El Torito boot catalog instead. -# -# --- UEFI entry ------------------------------------------------------------- -# -# UEFI firmware does not run El Torito no-emulation code. It wants a FAT -# filesystem containing EFI/BOOT/BOOTX64.EFI, offered as a second boot entry. -# So one is built with mtools and added with -eltorito-alt-boot. The same -# loader is also copied into the ISO tree at /EFI/BOOT/BOOTX64.EFI, because +# The loader is also copied into the ISO tree at /EFI/BOOT/BOOTX64.EFI, because # some firmware looks there directly when booting from a USB stick. # # --- Booting from a USB stick ----------------------------------------------- # # -isohybrid-gpt-basdat adds a GPT that marks the FAT image as a basic data # partition, so writing this .iso to a stick with dd produces something UEFI -# will boot. -# -# That covers UEFI only, and deliberately. The classic BIOS isohybrid trick -# needs syslinux's isohdpfx.bin, and even with it the BIOS path here would not -# work: on a stick the firmware presents the medium as a hard disk, while -# stage2main.c reads the kernel back over ATAPI. Booting BIOS off USB is what -# build/usbstick.img is for. +# will boot. That was already the only arrangement that worked from a stick: +# the firmware presents the medium as a hard disk, while the BIOS loader read +# the kernel back over ATAPI, so it never booted this way even when it existed. # # --- Rock Ridge and Joliet -------------------------------------------------- # @@ -76,34 +51,29 @@ set -e BUILD=${BUILD:-build} OUT=${1:-schos.iso} -STAGE2_SECTORS=${STAGE2_SECTORS:-48} -STAGE2_BYTES=$((STAGE2_SECTORS * 512)) -LOAD_SIZE=$((1 + STAGE2_SECTORS)) STAGING="$BUILD/iso" SYSROOT=${SYSROOT:-sysroot} -CDBOOT="$BUILD/boot/cdboot" -STAGE2="$BUILD/boot/stage2" KERNEL="$BUILD/kernel/kernel" EFIAPP="$BUILD/uefi/BOOTX64.EFI" -for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do - [ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; } -done +[ -f "$KERNEL" ] || { echo "mkiso: missing $KERNEL -- run make first" >&2; exit 1; } [ -f "$EFIAPP" ] || { echo "mkiso: missing $EFIAPP -- run 'make uefi' first" >&2; exit 1; } -# The three pieces must be contiguous and at these exact sizes: stage2main.c's -# readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image, -# so a short stage2 would silently shift the kernel out from under it. -cdboot_sz=$(wc -c < "$CDBOOT") -stage2_sz=$(wc -c < "$STAGE2") -[ "$cdboot_sz" -eq 512 ] || { echo "mkiso: cdboot is $cdboot_sz bytes, expected 512" >&2; exit 1; } -[ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; } +# mtools is not optional any more. It used to be: without it the disc fell back +# to being BIOS-only, which was a degraded disc rather than no disc. There is no +# other entry to fall back to now, so a missing mformat means the ISO would not +# boot anything, and failing here says so instead of shipping it. +if ! command -v mformat >/dev/null 2>&1 || ! command -v mcopy >/dev/null 2>&1; then + echo "mkiso: need mtools (mformat, mcopy) to build the EFI system partition" >&2 + echo " Fedora: sudo dnf install mtools" >&2 + echo " Debian/Ubuntu: apt install mtools" >&2 + exit 1 +fi rm -rf "$STAGING" mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT" -cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin" cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI" # The FAT image for the UEFI El Torito entry. @@ -113,10 +83,8 @@ cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI" # this FAT image -- not the ISO 9660 tree around it. Without the kernel in here # the loader comes up and halts with EFI_NOT_FOUND, which is what it did. # -# That means the kernel is stored twice on the disc: once inside boot/schos.bin -# for the BIOS entry, once here for the UEFI one. The alternative is teaching -# the loader to go looking for other volumes, which is a real feature and not a -# packaging detail -- worth doing deliberately rather than to save a copy. +# The ISO 9660 tree still carries the sysroot for a running system to read; the +# loader's own two files live in here because that is the volume it is handed. # # Sized from what actually goes in plus slack, rather than fixed: a hardcoded # size is a thing that breaks quietly the first time the kernel grows. @@ -125,8 +93,8 @@ payload_kb=$(( ( $(wc -c < "$EFIAPP") + $(wc -c < "$KERNEL") + 1023 ) / 1024 )) img_kb=$(( ( (payload_kb + 512 + 31) / 32 ) * 32 )) [ "$img_kb" -lt 512 ] && img_kb=512 -if command -v mformat >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1; then - rm -f "$EFI_IMG" +rm -f "$EFI_IMG" +if true; then # Create the file first and format it with an explicit geometry. # # Not `mformat -f`: that takes one of the standard floppy capacities @@ -139,12 +107,6 @@ if command -v mformat >/dev/null 2>&1 && command -v mcopy >/dev/null 2>&1; then mmd -i "$EFI_IMG" ::/EFI ::/EFI/BOOT ::/schos mcopy -i "$EFI_IMG" "$EFIAPP" ::/EFI/BOOT/BOOTX64.EFI mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel - HAVE_EFI=1 -else - echo "mkiso: warning: mtools not found, building a BIOS-only disc" >&2 - echo " Fedora: sudo dnf install mtools" >&2 - rm -f "$EFI_IMG" - HAVE_EFI=0 fi # The sysroot, as an ordinary hierarchy. Copied rather than linked so that @@ -175,12 +137,6 @@ else exit 1 fi -if [ "$HAVE_EFI" = 1 ]; then - EFI_ARGS="-eltorito-alt-boot -e boot/efiboot.img -no-emul-boot $HYBRID" -else - EFI_ARGS="" -fi - # shellcheck disable=SC2086 # EFI_ARGS is a deliberate word list $MKISOFS \ -quiet \ @@ -188,18 +144,12 @@ $MKISOFS \ -A "SchOS" \ -R -J -joliet-long \ -cache-inodes \ - -b boot/schos.bin \ + -e boot/efiboot.img \ -no-emul-boot \ - -boot-load-size "$LOAD_SIZE" \ - $EFI_ARGS \ + $HYBRID \ -o "$OUT" \ "$STAGING" echo "==> $OUT ($(wc -c < "$OUT") bytes)" -if [ "$HAVE_EFI" = 1 ]; then - echo " BIOS: qemu-system-x86_64 -cdrom $OUT -boot d" - echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT" - echo " USB: dd if=$OUT of=/dev/sdX bs=4M (UEFI only -- see the header)" -else - echo " qemu-system-x86_64 -cdrom $OUT -boot d" -fi +echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT" +echo " USB: dd if=$OUT of=/dev/sdX bs=4M" diff --git a/tools/posix/schcom_test.sh b/tools/posix/schcom_test.sh index 266ec9d..c419b78 100644 --- a/tools/posix/schcom_test.sh +++ b/tools/posix/schcom_test.sh @@ -17,6 +17,10 @@ LOG=$BUILD/schcom-test.log PPM=${1:-$BUILD/schcom.ppm} rm -f "$LOG" "$PPM" +# The firmware varstore is written by OVMF, so this run gets its own copy +# rather than sharing the pristine one every other run starts from. +cp -f build/OVMF_VARS.fd build/schcom-OVMF_VARS.fd + PORT=$(python3 - <<'EOF' import socket s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close() @@ -32,7 +36,11 @@ EOF sleep 40 ) | timeout -k 5 120 qemu-system-x86_64 \ -machine pc-i440fx-9.2 -cpu qemu64,+rdtscp \ - -hda schos.img -hdb build/schfs.img -m 1G -smp 1 \ + -drive if=pflash,format=raw,unit=0,readonly=on,file=build/OVMF_CODE.fd \ + -drive if=pflash,format=raw,unit=1,file=build/schcom-OVMF_VARS.fd \ + -drive file=fat:rw:build/esp,index=0,media=disk \ + -drive file=build/schfs.img,format=raw,index=1,media=disk \ + -m 1G -smp 1 \ -display none -vga std \ -monitor tcp:127.0.0.1:$PORT,server,nowait \ -chardev stdio,id=c0,logfile="$LOG" -serial chardev:c0 \ diff --git a/tools/sign.pl b/tools/sign.pl deleted file mode 100644 index d793035..0000000 --- a/tools/sign.pl +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/perl - -open(SIG, $ARGV[0]) || die "open $ARGV[0]: $!"; - -$n = sysread(SIG, $buf, 1000); - -if($n > 510){ - print STDERR "boot block too large: $n bytes (max 510)\n"; - exit 1; -} - -print STDERR "boot block is $n bytes (max 510)\n"; - -$buf .= "\0" x (510-$n); -$buf .= "\x55\xAA"; - -open(SIG, ">$ARGV[0]") || die "open >$ARGV[0]: $!"; -print SIG $buf; -close SIG; diff --git a/tools/windows/bootcheck.ps1 b/tools/windows/bootcheck.ps1 index 99fbf7f..1f9c38b 100644 --- a/tools/windows/bootcheck.ps1 +++ b/tools/windows/bootcheck.ps1 @@ -26,11 +26,8 @@ param( # attached, which is the only way to prove the ISO is self-contained. # Needs `make iso` first. [switch]$Iso, - # Boot through OVMF instead of the BIOS loader. The disk layout is - # deliberately the same -- the staged ESP lands on index 0 and the SchFS - # volume on index 1 -- so every check below runs unaltered and a difference - # in the transcript is a difference in the *firmware path*, not in what was - # asked. Needs `make esp` first. + # Accepted and ignored. It used to select OVMF over the BIOS loader; there + # is only the one path now, so it names a choice that no longer exists. [switch]$Uefi, # Terminal behaviour: pipes, isatty, SIGPIPE, and Ctrl-C. None of these # can be checked by looking at a screenshot, and all of them were broken @@ -86,7 +83,6 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' # The root filesystem. SchFS is the only root there is, so this is not a # choice -- a kernel handed anything else on device 1 panics in forkret(). $hdb = Join-Path $root 'build/schfs.img' @@ -249,73 +245,56 @@ $commands += 'echo MARKER-DONE' $isoPath = Join-Path $root 'schos.iso' if ($Iso -and -not (Test-Path $isoPath)) { throw "no $isoPath -- run 'make iso' first" } -$drives = if ($Uefi) { - # The varstore is written by the firmware, so each run gets its own copy. - # Sharing one would let a run that changed a UEFI variable -- boot order, - # console mode -- decide how the next run behaves, which is exactly the - # kind of dependency between runs that makes a suite stop being evidence. +# OVMF is not optional any more: the BIOS loader is gone, so every run boots +# through firmware. -Uefi is still accepted and does nothing, because it named +# a choice that no longer exists. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +# The varstore is written by the firmware, so each run gets its own copy. +# Sharing one would let a run that changed a UEFI variable -- boot order, +# console mode -- decide how the next run behaves, which is exactly the kind of +# dependency between runs that makes a suite stop being evidence. +$runVars = Join-Path $root 'build/bootcheck-OVMF_VARS.fd' +Copy-Item -Force $vars $runVars + +$firmware = @("-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"") + +$drives = if ($Iso) { + # No hard disk at all, so a kernel that still secretly depends on one fails + # here rather than in someone's optical drive. + $firmware + @("-cdrom `"$isoPath`"", '-boot d') +} else { $esp = Join-Path $root 'build/esp' if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { throw "no staged ESP at $esp -- run 'make esp' first" } - $code = Join-Path $root 'build/OVMF_CODE.fd' - $vars = Join-Path $root 'build/OVMF_VARS.fd' - foreach ($f in @($code, $vars)) { - if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } - } - $runVars = Join-Path $root 'build/bootcheck-OVMF_VARS.fd' - Copy-Item -Force $vars $runVars - @("-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" - "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" - "-drive file=fat:rw:`"$esp`",index=0,media=disk" - "-drive file=`"$hdb`",format=raw,index=1,media=disk") -} elseif ($Iso) { - @("-cdrom `"$isoPath`"", '-boot d') -} else { - @("-drive file=`"$hda`",format=raw,index=0,media=disk" - "-drive file=`"$hdb`",format=raw,index=1,media=disk") + $firmware + @("-drive file=fat:rw:`"$esp`",index=0,media=disk" + "-drive file=`"$hdb`",format=raw,index=1,media=disk") } $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu + $usbDevices = if ($Usb) { - # One device of each speed class QEMU can produce: usb-storage negotiates - # SuperSpeed on qemu-xhci, the HID devices come up high speed. They take - # different paths through enumeration -- a USB3 port arrives already - # enabled and a USB2 port has to be reset first -- so one of each is the - # difference between testing the driver and testing half of it. @('-device qemu-xhci,id=xhci' '-device usb-kbd,bus=xhci.0' - # The mouse goes behind a hub, so the tree is a tree and not a row of - # root ports. That exercises the route string, the hub's own slot - # context, and the transaction translator -- none of which a device - # plugged straight into the controller touches at all. - # QEMU places a device under a hub by port *path*, not by a child bus: - # the hub sits on port 2 and the mouse on port 2.1 of the same bus. '-device usb-hub,id=hub,bus=xhci.0,port=2' '-device usb-mouse,bus=xhci.0,port=2.1' - # Its own small image, never $hdb. The checks below read a v1 superblock - # off this stick and assert on its sector count and size field, so the - # stick has to be a v1 image whatever the root filesystem is -- under - # the root $hdb is a SchFS volume and those fields are simply not - # there, which failed three USB checks that had nothing wrong with USB. - # It also stops the same file being attached twice, once as the root - # disk and once as a USB device. "-drive id=usbstick,file=`"$(Join-Path $root 'build/usbstick.img')`",format=raw,if=none" '-device usb-storage,bus=xhci.0,drive=usbstick') } else { @() } -# The USB run needs the QEMU monitor, because `sendkey` is the only way to -# press a key on an emulated USB keyboard -- typing on stdin goes to the -# serial port and proves nothing about USB. -# Ask the OS for a free port rather than hardcoding one. A run that dies -# before its `finally` leaves QEMU holding the old number, and every later run -# then fails with "Failed to find an available port" -- which looks like a -# bug in whatever is being tested rather than in the harness. +# A free loopback port, found by binding one and letting go. Hardcoding it made +# two runs on one machine collide. $monPort = if ($Usb -or $Evdev) { $l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) - $l.Start(); $p = $l.LocalEndpoint.Port; $l.Stop(); $p + $l.Start(); $mp = $l.LocalEndpoint.Port; $l.Stop(); $mp } else { 0 } + $monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() } $psi.Arguments = ($drives + $usbDevices + $monitor + @( @@ -327,9 +306,7 @@ $psi.Arguments = ($drives + $usbDevices + $monitor + @( $psi.RedirectStandardInput = $true $psi.UseShellExecute = $false -$what = if ($Uefi) { "OVMF, hdb = $(Split-Path $hdb -Leaf)" } - elseif ($Iso) { 'schos.iso, no hard disk' } - else { "hdb = $(Split-Path $hdb -Leaf)" } +$what = if ($Iso) { 'schos.iso, no hard disk' } else { "hdb = $(Split-Path $hdb -Leaf)" } Write-Host "==> booting ($what)" -ForegroundColor Cyan $p = [System.Diagnostics.Process]::Start($psi) # Recorded immediately, so that a run killed one instruction later is still diff --git a/tools/windows/crashshot.ps1 b/tools/windows/crashshot.ps1 index 9e4cf0f..cee2edc 100644 --- a/tools/windows/crashshot.ps1 +++ b/tools/windows/crashshot.ps1 @@ -45,7 +45,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/crashshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -65,7 +78,9 @@ $monPort = 55555 $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu $psi.Arguments = @( - "-drive file=`"$hda`",format=raw,index=0,media=disk" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk" '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-display none' diff --git a/tools/windows/dispshot.ps1 b/tools/windows/dispshot.ps1 index 9599737..e38a62e 100644 --- a/tools/windows/dispshot.ps1 +++ b/tools/windows/dispshot.ps1 @@ -50,7 +50,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/dispshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -70,7 +83,9 @@ $monPort = 55555 $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu $psi.Arguments = @( - "-drive file=`"$hda`",format=raw,index=0,media=disk" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk" '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-display none' diff --git a/tools/windows/fbshot.ps1 b/tools/windows/fbshot.ps1 index c996b5c..a6445c4 100644 --- a/tools/windows/fbshot.ps1 +++ b/tools/windows/fbshot.ps1 @@ -44,7 +44,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/fbshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -60,7 +73,9 @@ $monPort = 55556 # not crashshot's 55555, so the two can run back to back $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu $psi.Arguments = @( - "-drive file=`"$hda`",format=raw,index=0,media=disk" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk" '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-display none' diff --git a/tools/windows/splashshot.ps1 b/tools/windows/splashshot.ps1 index 99bf8dd..8dfd71a 100644 --- a/tools/windows/splashshot.ps1 +++ b/tools/windows/splashshot.ps1 @@ -55,7 +55,20 @@ Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force $dir | Out-Null Remove-Item -Force $log -ErrorAction SilentlyContinue -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/splashshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -75,7 +88,9 @@ $psi.Arguments = @( # first fix and it meant duplicating build/fs.img, all 153 MB of it, on # every run. -snapshot keeps writes in a temporary overlay, so the originals # are untouched either way. - "-drive file=`"$hda`",format=raw,index=0,media=disk,file.locking=off" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk,file.locking=off" '-snapshot' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' diff --git a/tools/windows/textshot.ps1 b/tools/windows/textshot.ps1 index b0ace22..be2b97d 100644 --- a/tools/windows/textshot.ps1 +++ b/tools/windows/textshot.ps1 @@ -50,7 +50,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/textshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -70,7 +83,9 @@ $monPort = 55555 $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu $psi.Arguments = @( - "-drive file=`"$hda`",format=raw,index=0,media=disk" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk" '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-display none' diff --git a/tools/windows/ttyshot.ps1 b/tools/windows/ttyshot.ps1 index 584fbec..54b53db 100644 --- a/tools/windows/ttyshot.ps1 +++ b/tools/windows/ttyshot.ps1 @@ -43,7 +43,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu } $ErrorActionPreference = 'Stop' # Repo root is two levels up: this script lives in tools/windows/. $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$hda = Join-Path $root 'schos.img' +$esp = Join-Path $root 'build/esp' +# OVMF and the staged ESP, because there is one boot path now. The old +# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the +# BIOS loader could use, and it went with it. +$code = Join-Path $root 'build/OVMF_CODE.fd' +$vars = Join-Path $root 'build/OVMF_VARS.fd' +foreach ($f in @($code, $vars)) { + if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" } +} +if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { + throw "no staged ESP at $esp -- run 'make esp' first" +} +$runVars = Join-Path $root "build/ttyshot-OVMF_VARS.fd" +Copy-Item -Force $vars $runVars # The root filesystem. SchFS is the only root there is: the kernel mounts # device 1 in forkret() and panics if it is not a SchFS volume, so this is # not a choice and there is no -SchFS switch any more. It used to default to @@ -73,7 +86,9 @@ $commands = @( $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Qemu $psi.Arguments = @( - "-drive file=`"$hda`",format=raw,index=0,media=disk" + "-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`"" + "-drive if=pflash,format=raw,unit=1,file=`"$runVars`"" + "-drive file=fat:rw:`"$esp`",index=0,media=disk" "-drive file=`"$hdb`",format=raw,index=1,media=disk" '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-display none'