boot: the BIOS loader is gone

1449 lines of boot/bios/, tools/sign.pl and tools/padstage2.pl, plus the
build rules, the disk image and the ISO entry that existed to carry them.

Everything since roughly 2012 boots UEFI, and CSM -- the firmware-side
BIOS emulation that lets a legacy loader run on such a machine at all --
has been going away since Intel dropped support for it in 2020. So this
is not a path being retired early; it is one whose remaining hardware is
already the exception.

The cost was never the code, it was the tax on everything near it. Stage
2 had a 65-sector ceiling it overflowed silently -- 25440 bytes against
24576 -- and because the kernel is written at sector 1 + STAGE2_SECTORS,
an overflowing loader was overwritten by the very kernel it was loading.
The resolution list could not be shared with UEFI either: VBE mode
setting is INT 10h, so matching it needed unreal mode, which is the
riskiest code in the tree for a feature that already worked elsewhere.

What replaces schos.img is the staged ESP, handed to QEMU as a virtual
FAT volume. That is a development arrangement and not an installable
disk; a real GPT image with a real FAT partition is the installer's job,
and until it exists nothing here writes a disk a machine boots unaided.
Secure Boot is untouched too -- the loader is unsigned.

The ISO keeps one El Torito entry, the FAT image holding loader and
kernel. It never booted from a USB stick via BIOS anyway: the firmware
presents the stick as a hard disk while stage2main.c read the kernel back
over ATAPI.

The 32-bit multiboot entry in entry.S now has no caller and is left in
place. Deleting kernel entry code is its own change with its own way of
failing silently, and it should not ride along with this one.

Ported with it: run.ps1, the six screenshot tools, and schcom_test.sh,
each with its own OVMF varstore copy so no run inherits another's UEFI
variables.

Two things this shook out. OVMF now belongs in `all`: nothing boots
without firmware, `make clean` wipes build/ including the copy fetched
from the host, and a clean followed by a build left every run script
throwing. And it cannot be listed as a prerequisite at the top of the
file -- make expands prerequisites when it reads the line, and
$(OVMF_CODE) is defined further down, so naming it there appends nothing
and says nothing.

Verified from a clean tree: 110 [ok] and no failures on the full suite,
the ISO boots with no hard disk attached, schfs-check passes, schcom's
end-to-end pixel test passes, and fbshot captures a real 1280x800 frame.
This commit is contained in:
Nexoniarz 2026-08-11 10:20:28 +02:00
commit 651899220c
23 changed files with 317 additions and 1826 deletions

155
Makefile
View file

@ -186,7 +186,6 @@ ifdef USE_CLANG
# object is silently the wrong format and the first thing to complain is the # object is silently the wrong format and the first thing to complain is the
# linker. # linker.
CC = clang --target=x86_64-elf CC = clang --target=x86_64-elf
CC32 = clang --target=i386-elf
LD = ld.lld LD = ld.lld
OBJCOPY = llvm-objcopy OBJCOPY = llvm-objcopy
OBJDUMP = llvm-objdump OBJDUMP = llvm-objdump
@ -221,7 +220,6 @@ else
TOOLPREFIX ?= TOOLPREFIX ?=
endif endif
CC = $(TOOLPREFIX)gcc CC = $(TOOLPREFIX)gcc
CC32 = $(CC) -m32
LD = $(TOOLPREFIX)ld LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump OBJDUMP = $(TOOLPREFIX)objdump
@ -363,16 +361,24 @@ $(shell mkdir -p $(BUILD); \
# usbstick.img is built here rather than on demand because bootcheck -Usb # 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 # 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. # 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 # schos.img is gone with the BIOS loader that gave it its shape. It was a raw
dd if=/dev/zero of=schos.img count=10000 # disk with the boot sector at LBA 0, stage2 next and the kernel at a fixed
dd if=$(BUILD)/boot/bootblock of=schos.img conv=notrunc # offset -- a layout only a loader that seeks by sector number can use. UEFI
dd if=$(BUILD)/boot/stage2 of=schos.img seek=1 conv=notrunc # firmware wants a filesystem, so what boots now is the staged ESP, offered to
# seek is 1 + STAGE2_SECTORS, not a literal. It was 25 against a 24-sector # QEMU through its virtual FAT.
# stage2; raising STAGE2_SECTORS for the boot logo without touching it wrote #
# the kernel straight over stage2's own tail. # That is a development arrangement, not an installable disk. A real GPT image
dd if=$(BUILD)/kernel/kernel of=schos.img seek=$$(( 1 + $(STAGE2_SECTORS) )) conv=notrunc # with a real FAT partition is the installer's job.
kernel: $(BUILD)/kernel/kernel kernel: $(BUILD)/kernel/kernel
@ -451,6 +457,11 @@ $(OVMF_VARS):
cp "$(OVMF_VARS_SRC)" $@ cp "$(OVMF_VARS_SRC)" $@
chmod u+w $@ 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-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) $(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 rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1 schos.iso $(MAKE) RAMROOT=1 schos.iso
# The UEFI loader is a dependency now: the disc carries a second El Torito # One El Torito entry, pointing at a FAT image holding the loader and kernel.
# entry pointing at a FAT image that holds it, so one disc boots both firmwares.
# sysroot-install because the disc also carries the tree as a real hierarchy. # sysroot-install because the disc also carries the tree as a real hierarchy.
schos.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel \ schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install
$(BUILD)/uefi/BOOTX64.EFI sysroot-install SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
STAGE2_SECTORS=$(STAGE2_SECTORS) SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
# Boot the ISO the way a machine booting off a disc would: no hard disk at # 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 # 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 \ -cdrom schos.iso -boot d \
-m 1G -smp 2 -machine pc-i440fx-9.2 -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 # Kernel binary
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@ -912,9 +856,10 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
@echo "Creating SchOS File System Image..." @echo "Creating SchOS File System Image..."
@# A running QEMU holds this file open, and on Windows that makes rm fail @# 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 @# with "Device or resource busy" -- which stops the build *after* the
@# kernel has already relinked, leaving schos.img stale. The next test run @# kernel has already relinked, leaving the staged ESP holding a kernel
@# then boots the previous kernel and the change appears to have done @# from the previous build. The next test run then boots that one and the
@# nothing, which has cost several debugging cycles. Say so plainly. @# change appears to have done nothing, which has cost several debugging
@# cycles. Say so plainly.
@rm -f $@ || { \ @rm -f $@ || { \
echo ""; \ echo ""; \
echo " $@ is in use -- a QEMU is still holding it."; \ 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 # 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 # value: the compositor saying "I composited it" proves nothing, and a
# screenshot nobody inspects proves less. Needs `make projects` first. # 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 sh tools/posix/schcom_test.sh
qemu-kvm: schos.img $(BUILD)/schfs.img # The firmware and the disks, in one place. Every run target below boots the
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu host \ # same way, because there is only one way to boot now: OVMF, the staged ESP as
-hda schos.img -hdb $(BUILD)/schfs.img \ # the first disk, the SchFS volume as the second. Keeping the argument list in
-m 1G -smp 2 -machine pc-i440fx-9.2 # 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 \ $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \ $(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2
-m 1G -smp 2 -machine pc-i440fx-9.2
# The root filesystem. Built from the staged sysroot; the kernel mounts it as # The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret(). # / 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. # the live v1 root and the SchFS probe correctly finds nothing there.
# #
# Once up: cat /proc/schfs # 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 \ $(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-hda schos.img -hdb $(BUILD)/schfs.img \ $(OVMF_DRIVES) -m 1G -smp 2 -machine pc-i440fx-9.2
-m 1G -smp 2 -machine pc-i440fx-9.2
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# Misc # Misc
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# schos.img is deliberately NOT here. It is a real file with real # Nothing that is a real file with real prerequisites belongs here. schos.img
# prerequisites; listing it as phony made make skip the timestamp check and # used to be listed, and being phony made make skip the timestamp check and
# re-run the four dd commands -- including zeroing five megabytes -- on every # re-run its four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed. # 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 # 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 # 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. # 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 \ .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-test schfs-objtest schfs-image-test schfs-check mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ 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 .PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives, # 'clean' removes generated output only. dist/ (upstream tarballs) survives,
# so a rebuild never needs the network. # so a rebuild never needs the network.
clean: 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 distclean: clean
rm -rf dist rm -rf dist

View file

@ -72,7 +72,7 @@ report.
## Quick start ## Quick start
```sh ```sh
make # kernel + bootloader + root filesystem -> schos.img make # kernel + UEFI loader + root filesystem
make qemu # build it and boot it make qemu # build it and boot it
make iso # a bootable ISO for VMware or real hardware 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/ kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/
fs/schfs/ the filesystem -- same source the host tools compile fs/schfs/ the filesystem -- same source the host tools compile
include/ kernel and userland headers 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 lib/ cmd/ first-party userland runtime and programs
ports/ third-party software built from source ports/ third-party software built from source
tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl

View file

@ -1,21 +1,30 @@
<!-- SPDX-License-Identifier: Apache-2.0 --> <!-- SPDX-License-Identifier: Apache-2.0 -->
# Booting SchOS # Booting SchOS
Two firmware paths, kept apart on purpose: One firmware path:
``` ```
boot/ boot/
bios/ legacy BIOS: MBR, El Torito CD stub, real-mode stage 2 uefi/ the loader: a 64-bit PE application
uefi/ UEFI application (not written yet) common/ the user interface, shared with nothing else now
``` ```
They are separate directories rather than `#ifdef`s in one because they share There were two. `boot/bios/` held an MBR, an El Torito CD stub and a real-mode
almost nothing. A BIOS loader lives in 16-bit real mode, talks to the firmware stage 2, and it was a separate directory rather than `#ifdef`s because the two
through software interrupts, and has to walk itself up into long mode. A UEFI shared almost nothing: a BIOS loader lives in 16-bit real mode and talks to the
loader is a 64-bit PE executable that the firmware has *already* placed in long firmware through software interrupts, while UEFI hands a loader long mode with
mode with paging on, and it asks for services by calling through a table of paging already on and a table of function pointers to call.
function pointers. The only things genuinely in common are the logo bitmap, the
menu's appearance, and the hand-off contract below. 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 ## 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 | | Framebuffer | `VBE_INFO_ADDR` | address, pitch, width, height, depth |
| Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu | | Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu |
The addresses are defined once, in `include/memlayout.h` and The addresses are defined once, in `include/memlayout.h` and `include/schboot.h`.
`boot/bios/stage2main.c`. A UEFI loader must fill the same structures — the The kernel does not care which firmware produced them.
kernel does not care which firmware produced them, and that is what keeps the
split cheap.
## Two entry points, because the two firmwares hand over differently ## The entry point
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.
UEFI hands a loader long mode with paging already on. Dropping back to 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 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`, `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 — 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. found by scanning. `include/schboot.h` holds the magic so loader and kernel
`include/schboot.h` holds the magic so loader and kernel cannot disagree. 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: 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 translate it into the same 24-byte entries rather than inventing a second
format for the kernel to learn. 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 `make esp` stages `EFI/BOOT/BOOTX64.EFI` and `schos/kernel`, and QEMU is
`tools/sign.pl`, loads stage 2 from LBA 1 and jumps. pointed at OVMF with that directory as a virtual FAT volume — `make qemu`.
2. **`bios/cdboot.S`** — the same job for optical media, as an El Torito `make iso` builds a disc with a single El Torito entry pointing at a FAT image
no-emulation image. Separate because the loader never uses INT 13h and so holding the same two files.
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.
`STAGE2_SECTORS` is defined **once**, in the Makefile, and passed to both the Neither is an installable disk. A real GPT image with a real FAT partition is
assembler and the `dd` that writes the image. It used to be written out by hand the installer's job, and until it exists there is nothing here that writes to a
in five places and silently truncated the loader when they disagreed. disk a machine could boot from unaided.
## Why UEFI is worth doing Secure Boot is not addressed either: the loader is unsigned, so a machine
enforcing it will refuse to run this.
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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -1,8 +1,9 @@
<!-- SPDX-License-Identifier: Apache-2.0 --> <!-- SPDX-License-Identifier: Apache-2.0 -->
# UEFI loader # UEFI loader
Not written yet. This directory exists so the BIOS path could be moved into The loader. This directory once existed only so the BIOS path could be moved
`boot/bios/` without pretending the split was hypothetical. 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 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 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. parses. Not a second format for the kernel to learn.
4. `ExitBootServices` before touching the memory it described. 4. `ExitBootServices` before touching the memory it described.
5. Graphics Output Protocol for the framebuffer, filling the same 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 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.

View file

@ -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. change that only works on one of them is a bug.
```sh ```sh
make # kernel + bootloader + root filesystem -> schos.img make # kernel + UEFI loader + root filesystem
make qemu # build and boot it make qemu # build and boot it
``` ```

View file

@ -52,7 +52,7 @@ schos/
``` ```
build/ build/
├─ boot/ bootblock, stage2 ├─ boot/ uefi/ loader, common/ UI
├─ kernel/ object files, the linked kernel, kernel.asm ├─ kernel/ object files, the linked kernel, kernel.asm
├─ lib/ libc glue objects, initcode blob ├─ lib/ libc glue objects, initcode blob
├─ cmd/ linked first-party programs ├─ 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 `ramroot` build reuses stale objects, so run `rm -rf build/kernel build/lib
build/cmd` first (or just `make clean`). build/cmd` first (or just `make clean`).
The image costs boot time: stage2 loads the kernel by PIO one sector at a time, The image costs boot time: the loader reads the kernel through the firmware's
so a 6 MB ramroot kernel is visibly slower to start than the 350 KB default. 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 **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 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 blob's build location and those symbol names change; update the Makefile to
match or the kernel fails to link. match or the kernel fails to link.
**`STAGE2_SECTORS` now lives in exactly one place: the root `Makefile`.** It used **`STAGE2_SECTORS` is gone, with the BIOS loader it sized.** It is recorded here
to be written out by hand in five — `boot/bios/bootasm.S`, `boot/bios/stage2main.c`'s because the shape of the bug outlived it: one number that four files each
`readseg()`, the `dd ... seek=` offset, and twice more inside `tools/posix/mkiso.sh` believed independently — `boot/bios/bootasm.S`, `stage2main.c`'s `readseg()`,
(its stage2 size assertion and `-boot-load-size`). This note used to warn you to the `dd ... seek=` offset, and twice inside `mkiso.sh`. Raising it from 24 to 48
keep three of them in step and did not know about the other two. 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 The fix was to derive all five from one Makefile variable. The lesson worth
seek=25` offset stayed behind, so the kernel was written directly over stage2's keeping is the other half: `tools/padstage2.pl` *failed the build* when stage 2
own tail. Now the Makefile passes `-DSTAGE2_SECTORS` to both boot sources, overflowed its budget rather than truncating it, which is why the overflow to
derives the padding budget and the `seek=` offset from the same variable, and 25440 bytes against a 24576 limit was found at all. A layout constant that
exports it to `mkiso.sh`. The `#define`s that remain in the two boot sources are several artifacts must agree on wants a check that stops the build, not a
`#ifndef`-guarded fallbacks so those files still read sensibly alone; the comment asking people to remember.
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`.

View file

@ -82,7 +82,7 @@ else needs it: `make`, `make schfs-check` and `make qemu` all work without.
## 4. Build and check ## 4. Build and check
```sh ```sh
make # kernel + root filesystem -> schos.img make # kernel + root filesystem
make schfs-check # B-tree, object and image suites -- all host-native make schfs-check # B-tree, object and image suites -- all host-native
make qemu # boot it make qemu # boot it
``` ```

21
run.ps1
View file

@ -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 # The two images QEMU needs. Keep in sync with the 'qemu' target in the
# Makefile. # 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 the root filesystem, mounted as / by forkret()
# #
# hdb was build/fs.img until SchFS became the root. Nothing builds fs.img on # 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 # 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 # panicked with SCHFS_ROOT. That is the failure this script is supposed to
# prevent, so it is worth naming. # prevent, so it is worth naming.
$hda = Join-Path $root 'schos.img' $esp = Join-Path $root 'build/esp'
$hdb = Join-Path $root 'build/schfs.img' $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) { if (-not $NoBuild) {
Write-Host '==> building (native, no WSL)' -ForegroundColor Cyan Write-Host '==> building (native, no WSL)' -ForegroundColor Cyan
@ -88,11 +90,16 @@ if (-not $NoBuild) {
if ($LASTEXITCODE -ne 0) { throw "build failed (exit $LASTEXITCODE)" } 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)) { 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 # 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 # 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 = @( $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" '-drive', "file=$hdb,format=raw,index=1,media=disk"
'-m', '1G' '-m', '1G'
'-smp', '2' '-smp', '2'

View file

@ -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 <file> <size-in-bytes>\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);

View file

@ -3,62 +3,37 @@
# #
# tools/posix/mkiso.sh [output.iso] # 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 a FAT image holding the loader and the kernel
# /boot/efiboot.img UEFI: a FAT image holding EFI/BOOT/BOOTX64.EFI
# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc # /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc
# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy # /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 # There was a second, for BIOS: cdboot and stage2 concatenated with the kernel
# how SchOS actually loads itself. # 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 # What UEFI wants is not that mechanism at all. Firmware does not run
# alone is bigger than that. # no-emulation boot code; it looks for a FAT filesystem containing
# hard disk emulation presents the image through the BIOS's INT 13h, and # EFI/BOOT/BOOTX64.EFI and hands that volume to the application it finds. So
# SchOS's loader does not use INT 13h -- it drives # the entry is a FAT image built with mtools, and it carries the kernel as well
# the IDE controller's registers directly, which # as the loader -- BOOTX64.EFI opens \schos\kernel on the volume it was itself
# cannot reach a BIOS-emulated drive. # loaded from, and for a UEFI boot that volume is this FAT image, not the ISO
# no emulation the BIOS loads a chosen run of sectors at 0x7C00 # 9660 tree around it. Without the kernel in here the loader comes up and halts
# and gets out of the way. That is this. # with EFI_NOT_FOUND, which is what it did.
# #
# So the layout below is deliberate: -boot-load-size is 1 + STAGE2_SECTORS, # The loader is also copied into the ISO tree at /EFI/BOOT/BOOTX64.EFI, because
# 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
# some firmware looks there directly when booting from a USB stick. # some firmware looks there directly when booting from a USB stick.
# #
# --- 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 # -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 # partition, so writing this .iso to a stick with dd produces something UEFI
# will boot. # 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
# That covers UEFI only, and deliberately. The classic BIOS isohybrid trick # the kernel back over ATAPI, so it never booted this way even when it existed.
# 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.
# #
# --- Rock Ridge and Joliet -------------------------------------------------- # --- Rock Ridge and Joliet --------------------------------------------------
# #
@ -76,34 +51,29 @@ set -e
BUILD=${BUILD:-build} BUILD=${BUILD:-build}
OUT=${1:-schos.iso} OUT=${1:-schos.iso}
STAGE2_SECTORS=${STAGE2_SECTORS:-48}
STAGE2_BYTES=$((STAGE2_SECTORS * 512))
LOAD_SIZE=$((1 + STAGE2_SECTORS))
STAGING="$BUILD/iso" STAGING="$BUILD/iso"
SYSROOT=${SYSROOT:-sysroot} SYSROOT=${SYSROOT:-sysroot}
CDBOOT="$BUILD/boot/cdboot"
STAGE2="$BUILD/boot/stage2"
KERNEL="$BUILD/kernel/kernel" KERNEL="$BUILD/kernel/kernel"
EFIAPP="$BUILD/uefi/BOOTX64.EFI" EFIAPP="$BUILD/uefi/BOOTX64.EFI"
for f in "$CDBOOT" "$STAGE2" "$KERNEL"; do [ -f "$KERNEL" ] || { echo "mkiso: missing $KERNEL -- run make first" >&2; exit 1; }
[ -f "$f" ] || { echo "mkiso: missing $f -- run make first" >&2; exit 1; }
done
[ -f "$EFIAPP" ] || { echo "mkiso: missing $EFIAPP -- run 'make uefi' 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 # mtools is not optional any more. It used to be: without it the disc fell back
# readseg() finds the kernel at sector (1 + STAGE2_SECTORS) of the boot image, # to being BIOS-only, which was a degraded disc rather than no disc. There is no
# so a short stage2 would silently shift the kernel out from under it. # other entry to fall back to now, so a missing mformat means the ISO would not
cdboot_sz=$(wc -c < "$CDBOOT") # boot anything, and failing here says so instead of shipping it.
stage2_sz=$(wc -c < "$STAGE2") if ! command -v mformat >/dev/null 2>&1 || ! command -v mcopy >/dev/null 2>&1; then
[ "$cdboot_sz" -eq 512 ] || { echo "mkiso: cdboot is $cdboot_sz bytes, expected 512" >&2; exit 1; } echo "mkiso: need mtools (mformat, mcopy) to build the EFI system partition" >&2
[ "$stage2_sz" -eq "$STAGE2_BYTES" ] || { echo "mkiso: stage2 is $stage2_sz bytes, expected $STAGE2_BYTES (STAGE2_SECTORS=$STAGE2_SECTORS)" >&2; exit 1; } echo " Fedora: sudo dnf install mtools" >&2
echo " Debian/Ubuntu: apt install mtools" >&2
exit 1
fi
rm -rf "$STAGING" rm -rf "$STAGING"
mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT" mkdir -p "$STAGING/boot" "$STAGING/EFI/BOOT"
cat "$CDBOOT" "$STAGE2" "$KERNEL" > "$STAGING/boot/schos.bin"
cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI" cp "$EFIAPP" "$STAGING/EFI/BOOT/BOOTX64.EFI"
# The FAT image for the UEFI El Torito entry. # 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 # 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. # 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 # The ISO 9660 tree still carries the sysroot for a running system to read; the
# for the BIOS entry, once here for the UEFI one. The alternative is teaching # loader's own two files live in here because that is the volume it is handed.
# 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.
# #
# Sized from what actually goes in plus slack, rather than fixed: a hardcoded # 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. # 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=$(( ( (payload_kb + 512 + 31) / 32 ) * 32 ))
[ "$img_kb" -lt 512 ] && img_kb=512 [ "$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. # Create the file first and format it with an explicit geometry.
# #
# Not `mformat -f`: that takes one of the standard floppy capacities # 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 mmd -i "$EFI_IMG" ::/EFI ::/EFI/BOOT ::/schos
mcopy -i "$EFI_IMG" "$EFIAPP" ::/EFI/BOOT/BOOTX64.EFI mcopy -i "$EFI_IMG" "$EFIAPP" ::/EFI/BOOT/BOOTX64.EFI
mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel 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 fi
# The sysroot, as an ordinary hierarchy. Copied rather than linked so that # The sysroot, as an ordinary hierarchy. Copied rather than linked so that
@ -175,12 +137,6 @@ else
exit 1 exit 1
fi 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 # shellcheck disable=SC2086 # EFI_ARGS is a deliberate word list
$MKISOFS \ $MKISOFS \
-quiet \ -quiet \
@ -188,18 +144,12 @@ $MKISOFS \
-A "SchOS" \ -A "SchOS" \
-R -J -joliet-long \ -R -J -joliet-long \
-cache-inodes \ -cache-inodes \
-b boot/schos.bin \ -e boot/efiboot.img \
-no-emul-boot \ -no-emul-boot \
-boot-load-size "$LOAD_SIZE" \ $HYBRID \
$EFI_ARGS \
-o "$OUT" \ -o "$OUT" \
"$STAGING" "$STAGING"
echo "==> $OUT ($(wc -c < "$OUT") bytes)" echo "==> $OUT ($(wc -c < "$OUT") bytes)"
if [ "$HAVE_EFI" = 1 ]; then echo " UEFI: qemu-system-x86_64 -bios OVMF_CODE.fd -cdrom $OUT"
echo " BIOS: qemu-system-x86_64 -cdrom $OUT -boot d" echo " USB: dd if=$OUT of=/dev/sdX bs=4M"
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

View file

@ -17,6 +17,10 @@ LOG=$BUILD/schcom-test.log
PPM=${1:-$BUILD/schcom.ppm} PPM=${1:-$BUILD/schcom.ppm}
rm -f "$LOG" "$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' PORT=$(python3 - <<'EOF'
import socket import socket
s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close() s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1]); s.close()
@ -32,7 +36,11 @@ EOF
sleep 40 sleep 40
) | timeout -k 5 120 qemu-system-x86_64 \ ) | timeout -k 5 120 qemu-system-x86_64 \
-machine pc-i440fx-9.2 -cpu qemu64,+rdtscp \ -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 \ -display none -vga std \
-monitor tcp:127.0.0.1:$PORT,server,nowait \ -monitor tcp:127.0.0.1:$PORT,server,nowait \
-chardev stdio,id=c0,logfile="$LOG" -serial chardev:c0 \ -chardev stdio,id=c0,logfile="$LOG" -serial chardev:c0 \

View file

@ -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;

View file

@ -26,11 +26,8 @@ param(
# attached, which is the only way to prove the ISO is self-contained. # attached, which is the only way to prove the ISO is self-contained.
# Needs `make iso` first. # Needs `make iso` first.
[switch]$Iso, [switch]$Iso,
# Boot through OVMF instead of the BIOS loader. The disk layout is # Accepted and ignored. It used to select OVMF over the BIOS loader; there
# deliberately the same -- the staged ESP lands on index 0 and the SchFS # is only the one path now, so it names a choice that no longer exists.
# 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.
[switch]$Uefi, [switch]$Uefi,
# Terminal behaviour: pipes, isatty, SIGPIPE, and Ctrl-C. None of these # 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 # 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' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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(). # choice -- a kernel handed anything else on device 1 panics in forkret().
$hdb = Join-Path $root 'build/schfs.img' $hdb = Join-Path $root 'build/schfs.img'
@ -249,73 +245,56 @@ $commands += 'echo MARKER-DONE'
$isoPath = Join-Path $root 'schos.iso' $isoPath = Join-Path $root 'schos.iso'
if ($Iso -and -not (Test-Path $isoPath)) { throw "no $isoPath -- run 'make iso' first" } if ($Iso -and -not (Test-Path $isoPath)) { throw "no $isoPath -- run 'make iso' first" }
$drives = if ($Uefi) { # OVMF is not optional any more: the BIOS loader is gone, so every run boots
# The varstore is written by the firmware, so each run gets its own copy. # through firmware. -Uefi is still accepted and does nothing, because it named
# Sharing one would let a run that changed a UEFI variable -- boot order, # a choice that no longer exists.
# console mode -- decide how the next run behaves, which is exactly the $code = Join-Path $root 'build/OVMF_CODE.fd'
# kind of dependency between runs that makes a suite stop being evidence. $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' $esp = Join-Path $root 'build/esp'
if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) { if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) {
throw "no staged ESP at $esp -- run 'make esp' first" throw "no staged ESP at $esp -- run 'make esp' first"
} }
$code = Join-Path $root 'build/OVMF_CODE.fd' $firmware + @("-drive file=fat:rw:`"$esp`",index=0,media=disk"
$vars = Join-Path $root 'build/OVMF_VARS.fd' "-drive file=`"$hdb`",format=raw,index=1,media=disk")
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")
} }
$psi = New-Object System.Diagnostics.ProcessStartInfo $psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$usbDevices = if ($Usb) { $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 qemu-xhci,id=xhci'
'-device usb-kbd,bus=xhci.0' '-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-hub,id=hub,bus=xhci.0,port=2'
'-device usb-mouse,bus=xhci.0,port=2.1' '-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" "-drive id=usbstick,file=`"$(Join-Path $root 'build/usbstick.img')`",format=raw,if=none"
'-device usb-storage,bus=xhci.0,drive=usbstick') '-device usb-storage,bus=xhci.0,drive=usbstick')
} else { @() } } else { @() }
# The USB run needs the QEMU monitor, because `sendkey` is the only way to # A free loopback port, found by binding one and letting go. Hardcoding it made
# press a key on an emulated USB keyboard -- typing on stdin goes to the # two runs on one machine collide.
# 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.
$monPort = if ($Usb -or $Evdev) { $monPort = if ($Usb -or $Evdev) {
$l = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) $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 } } else { 0 }
$monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() } $monitor = if ($Usb -or $Evdev) { @("-monitor tcp:127.0.0.1:$monPort,server,nowait") } else { @() }
$psi.Arguments = ($drives + $usbDevices + $monitor + @( $psi.Arguments = ($drives + $usbDevices + $monitor + @(
@ -327,9 +306,7 @@ $psi.Arguments = ($drives + $usbDevices + $monitor + @(
$psi.RedirectStandardInput = $true $psi.RedirectStandardInput = $true
$psi.UseShellExecute = $false $psi.UseShellExecute = $false
$what = if ($Uefi) { "OVMF, hdb = $(Split-Path $hdb -Leaf)" } $what = if ($Iso) { 'schos.iso, no hard disk' } else { "hdb = $(Split-Path $hdb -Leaf)" }
elseif ($Iso) { 'schos.iso, no hard disk' }
else { "hdb = $(Split-Path $hdb -Leaf)" }
Write-Host "==> booting ($what)" -ForegroundColor Cyan Write-Host "==> booting ($what)" -ForegroundColor Cyan
$p = [System.Diagnostics.Process]::Start($psi) $p = [System.Diagnostics.Process]::Start($psi)
# Recorded immediately, so that a run killed one instruction later is still # Recorded immediately, so that a run killed one instruction later is still

View file

@ -45,7 +45,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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 # 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 # 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$psi.Arguments = @( $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" "-drive file=`"$hdb`",format=raw,index=1,media=disk"
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none' '-display none'

View file

@ -50,7 +50,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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 # 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 # 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$psi.Arguments = @( $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" "-drive file=`"$hdb`",format=raw,index=1,media=disk"
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none' '-display none'

View file

@ -44,7 +44,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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 # 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 # 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$psi.Arguments = @( $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" "-drive file=`"$hdb`",format=raw,index=1,media=disk"
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none' '-display none'

View file

@ -55,7 +55,20 @@ Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $dir | Out-Null New-Item -ItemType Directory -Force $dir | Out-Null
Remove-Item -Force $log -ErrorAction SilentlyContinue 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 # 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 # 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 # 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 # 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 # every run. -snapshot keeps writes in a temporary overlay, so the originals
# are untouched either way. # 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" "-drive file=`"$hdb`",format=raw,index=1,media=disk,file.locking=off"
'-snapshot' '-snapshot'
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'

View file

@ -50,7 +50,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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 # 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 # 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$psi.Arguments = @( $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" "-drive file=`"$hdb`",format=raw,index=1,media=disk"
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none' '-display none'

View file

@ -43,7 +43,20 @@ if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/. # Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $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 # 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 # 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 # 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 = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu $psi.FileName = $Qemu
$psi.Arguments = @( $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" "-drive file=`"$hdb`",format=raw,index=1,media=disk"
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2' '-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none' '-display none'