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

View file

@ -1,21 +1,30 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Booting SchOS
Two firmware paths, kept apart on purpose:
One firmware path:
```
boot/
bios/ legacy BIOS: MBR, El Torito CD stub, real-mode stage 2
uefi/ UEFI application (not written yet)
uefi/ the loader: a 64-bit PE application
common/ the user interface, shared with nothing else now
```
They are separate directories rather than `#ifdef`s in one because they share
almost nothing. A BIOS loader lives in 16-bit real mode, talks to the firmware
through software interrupts, and has to walk itself up into long mode. A UEFI
loader is a 64-bit PE executable that the firmware has *already* placed in long
mode with paging on, and it asks for services by calling through a table of
function pointers. The only things genuinely in common are the logo bitmap, the
menu's appearance, and the hand-off contract below.
There were two. `boot/bios/` held an MBR, an El Torito CD stub and a real-mode
stage 2, and it was a separate directory rather than `#ifdef`s because the two
shared almost nothing: a BIOS loader lives in 16-bit real mode and talks to the
firmware through software interrupts, while UEFI hands a loader long mode with
paging already on and a table of function pointers to call.
That asymmetry is what removed it. Everything since roughly 2012 boots UEFI,
CSM — the firmware-side BIOS emulation that let legacy loaders run at all — has
been going away since Intel dropped support for it in 2020, and the cost of the
second path was not the code but the tax on everything that touched it: stage 2
had a 65-sector ceiling that it silently overflowed, and the resolution menu
could not be shared because VBE mode setting is `INT 10h` and needs real mode.
The contract below survives it. It was written to let a second loader exist, and
it is worth keeping in that shape: it is what the kernel expects, stated once,
rather than a description of whoever happens to be calling.
## What the loader must hand the kernel
@ -30,24 +39,24 @@ a second loader is correct exactly when it satisfies it.
| Framebuffer | `VBE_INFO_ADDR` | address, pitch, width, height, depth |
| Verbose flag | `BOOT_VERBOSE_FLAG_ADDR` | set by the F8 menu |
The addresses are defined once, in `include/memlayout.h` and
`boot/bios/stage2main.c`. A UEFI loader must fill the same structures — the
kernel does not care which firmware produced them, and that is what keeps the
split cheap.
The addresses are defined once, in `include/memlayout.h` and `include/schboot.h`.
The kernel does not care which firmware produced them.
## Two entry points, because the two firmwares hand over differently
The BIOS path enters the kernel in **32-bit protected mode** at the multiboot
entry, with EAX holding the framebuffer address; `mboot_entry` then builds page
tables and climbs into long mode itself.
## The entry point
UEFI hands a loader long mode with paging already on. Dropping back to
protected mode only to climb out again is a mode switch whose failure mode is a
triple fault with no console, so the kernel publishes a second entry instead.
triple fault with no console, so the kernel publishes an entry that takes the
machine as it already is.
`kernel/arch/x86_64/entry.S` carries a `sch64_header` — magic `SCH64BOO`,
8-byte aligned within the first 16 KB, followed by a 64-bit physical address —
found by scanning, exactly as the BIOS loader scans for the multiboot magic.
`include/schboot.h` holds the magic so loader and kernel cannot disagree.
found by scanning. `include/schboot.h` holds the magic so loader and kernel
cannot disagree.
The 32-bit multiboot entry is still in `entry.S` and now has no caller: the
BIOS loader was the only thing that used it. It is left in place rather than
removed in the same change that removed the loader, because deleting kernel
entry code is its own piece of work with its own way of failing silently.
What that entry expects:
@ -69,25 +78,16 @@ surfaced as `unexpected trap 13 ... cs=38`, a long way from the cause.
translate it into the same 24-byte entries rather than inventing a second
format for the kernel to learn.
## The BIOS path today
## What booting looks like now
1. **`bios/bootasm.S`** — the literal MBR sector. Signed to 512 bytes by
`tools/sign.pl`, loads stage 2 from LBA 1 and jumps.
2. **`bios/cdboot.S`** — the same job for optical media, as an El Torito
no-emulation image. Separate because the loader never uses INT 13h and so
needs a real ATAPI read.
3. **`bios/stage2.S`** — collects the E820 map, sets a VBE mode, enters
protected mode.
4. **`bios/stage2main.c`** — the boot menu and splash, then loads the kernel
and enters long mode.
`make esp` stages `EFI/BOOT/BOOTX64.EFI` and `schos/kernel`, and QEMU is
pointed at OVMF with that directory as a virtual FAT volume — `make qemu`.
`make iso` builds a disc with a single El Torito entry pointing at a FAT image
holding the same two files.
`STAGE2_SECTORS` is defined **once**, in the Makefile, and passed to both the
assembler and the `dd` that writes the image. It used to be written out by hand
in five places and silently truncated the loader when they disagreed.
Neither is an installable disk. A real GPT image with a real FAT partition is
the installer's job, and until it exists there is nothing here that writes to a
disk a machine could boot from unaided.
## Why UEFI is worth doing
Everything since roughly 2012 boots UEFI by default, and Secure Boot machines
will not run the BIOS path at all without firmware settings most people never
touch. The BIOS loader stays regardless: it is what QEMU's default machine and
older hardware use, and it costs nothing to keep once the two are separated.
Secure Boot is not addressed either: the loader is unsigned, so a machine
enforcing it will refuse to run this.

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 -->
# UEFI loader
Not written yet. This directory exists so the BIOS path could be moved into
`boot/bios/` without pretending the split was hypothetical.
The loader. This directory once existed only so the BIOS path could be moved
into `boot/bios/` without pretending the split was hypothetical; it is now the
only path there is.
See `../README.md` for the hand-off contract a loader has to satisfy. The
short version: the kernel wants long mode with paging on, its ELF segments
@ -22,7 +23,7 @@ What this will need, in rough order:
parses. Not a second format for the kernel to learn.
4. `ExitBootServices` before touching the memory it described.
5. Graphics Output Protocol for the framebuffer, filling the same
`VBE_INFO_ADDR` structure the BIOS path fills.
`VBE_INFO_ADDR` structure the kernel reads.
The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the
ISO gains an El Torito EFI entry alongside the BIOS one it already has.
ISO carries a single El Torito EFI entry.