boot: separate BIOS from UEFI
boot/bios/ now holds the MBR, the El Torito stub and the real-mode stage 2; boot/uefi/ is empty and documented. Separate directories rather than #ifdefs, because the two share almost nothing. A BIOS loader lives in 16-bit real mode, reaches the firmware through software interrupts, and walks itself up into long mode. A UEFI loader is a 64-bit PE that the firmware has already placed in long mode with paging on, calling services through a table of function pointers. What they do share is the hand-off contract, which boot/README.md now writes down: ELF segments placed, long mode, a 24-byte-entry memory map at E820_MAP_ADDR, framebuffer geometry at VBE_INFO_ADDR. That contract is the point of documenting it now. UEFI's GetMemoryMap returns EFI_MEMORY_DESCRIPTORs, and the right move is to translate them into the entries kernel/arch/x86_64/e820.c already parses rather than teach the kernel a second format. Build output paths under $(BUILD)/boot are unchanged; only sources moved.
This commit is contained in:
parent
8a4ac03ea9
commit
08a1a9033a
19 changed files with 125 additions and 33 deletions
101
boot/bios/bootasm.S
Normal file
101
boot/bios/bootasm.S
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# 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
|
||||
48
boot/bios/cdboot.S
Normal file
48
boot/bios/cdboot.S
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# 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
|
||||
#define BOOT_MEDIA_FLAG_SEG 0x3000
|
||||
#define BOOT_MEDIA_FLAG_OFF 0x101
|
||||
#define BOOT_MEDIA_CD 1
|
||||
|
||||
.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 $BOOT_MEDIA_FLAG_SEG, %ax
|
||||
movw %ax, %es
|
||||
movw $BOOT_MEDIA_FLAG_OFF, %di
|
||||
movb $BOOT_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
|
||||
129
boot/bios/stage2.S
Normal file
129
boot/bios/stage2.S
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# 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).
|
||||
movw $0x3000, %ax
|
||||
movw %ax, %es
|
||||
xorw %di, %di # ES:DI = 3000:0000 = flat 0x30000
|
||||
movw $0x0143, %cx
|
||||
movw $0x4F01, %ax
|
||||
int $0x10
|
||||
|
||||
cmpw $0x004F, %ax
|
||||
jne e820
|
||||
movw $0x4143, %bx
|
||||
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 via BIOS teletype (real mode only).
|
||||
# Clobbers AX, BX, SI.
|
||||
print16:
|
||||
.Lp16loop:
|
||||
lodsb
|
||||
testb %al, %al
|
||||
jz .Lp16done
|
||||
movb $0x0E, %ah
|
||||
movw $0x0007, %bx
|
||||
int $0x10
|
||||
jmp .Lp16loop
|
||||
.Lp16done:
|
||||
ret
|
||||
|
||||
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
|
||||
call stage2main
|
||||
|
||||
spin:
|
||||
jmp spin
|
||||
|
||||
.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
|
||||
878
boot/bios/stage2main.c
Normal file
878
boot/bios/stage2main.c
Normal file
|
|
@ -0,0 +1,878 @@
|
|||
#include "../include/types.h"
|
||||
#include "../include/x86.h"
|
||||
#include "../include/memlayout.h"
|
||||
#include "../include/font.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 "logo.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).
|
||||
#define VBE_INFO_ADDR 0x30000
|
||||
|
||||
// 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.
|
||||
#define BOOT_VERBOSE_FLAG_ADDR (VBE_INFO_ADDR + 0x100)
|
||||
|
||||
// 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 VBE_INFO_ADDR.
|
||||
#define BOOT_MEDIA_FLAG_ADDR (VBE_INFO_ADDR + 0x101)
|
||||
#define BOOT_MEDIA_CD 1
|
||||
|
||||
// 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_COUNT_ADDR (VBE_INFO_ADDR + 0x200)
|
||||
#define E820_MAP_ADDR (VBE_INFO_ADDR + 0x208)
|
||||
#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*)E820_COUNT_ADDR;
|
||||
struct e820entry *e = (struct e820entry*)E820_MAP_ADDR;
|
||||
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);
|
||||
static void draw_centered(int y, const char *s, uint32 color);
|
||||
static void draw_memory_line(int y, uint32 color);
|
||||
|
||||
// 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*)(VBE_INFO_ADDR + 16);
|
||||
g_width = *(ushort*)(VBE_INFO_ADDR + 18);
|
||||
g_height = *(ushort*)(VBE_INFO_ADDR + 20);
|
||||
uchar bpp = *(uchar*)(VBE_INFO_ADDR + 25);
|
||||
uint32 fbaddr = *(uint32*)(VBE_INFO_ADDR + 40);
|
||||
g_fb = (uint8*)fbaddr;
|
||||
g_have_fb = (bpp == 32 && fbaddr && g_width && g_height);
|
||||
}
|
||||
|
||||
static void
|
||||
draw_pixel(int x, int y, uint32 color)
|
||||
{
|
||||
*(uint32*)(g_fb + y * g_stride + x * 4) = color;
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
static void
|
||||
draw_logo(int x0, int y0)
|
||||
{
|
||||
if (!g_have_fb)
|
||||
return;
|
||||
for (int dy = 0; dy < LOGO_H; dy++) {
|
||||
int y = y0 + dy;
|
||||
if (y < 0 || y >= g_height)
|
||||
continue;
|
||||
uint32 *row = (uint32*)(g_fb + y * g_stride);
|
||||
for (int dx = 0; dx < LOGO_W; dx++) {
|
||||
int x = x0 + dx;
|
||||
if (x < 0 || x >= g_width)
|
||||
continue;
|
||||
unsigned a = LOGO_INK(dy * LOGO_W + dx);
|
||||
// Skip empty pixels rather than painting them in the background colour.
|
||||
// The mask is square but the mark is a circle, so most of it is empty;
|
||||
// filling that would stamp a visible box onto the splash.
|
||||
if (a == 0)
|
||||
continue;
|
||||
if (a >= 255) {
|
||||
row[x] = SCH_FG;
|
||||
continue;
|
||||
}
|
||||
// Blend towards SCH_BG per channel. Integer only -- there is no FPU set
|
||||
// up at this point in boot, and no libgcc to fake one with.
|
||||
uint32 r = (((SCH_FG >> 16) & 0xFF) * a + ((SCH_BG >> 16) & 0xFF) * (255 - a)) / 255;
|
||||
uint32 g = (((SCH_FG >> 8) & 0xFF) * a + ((SCH_BG >> 8) & 0xFF) * (255 - a)) / 255;
|
||||
uint32 b = (((SCH_FG ) & 0xFF) * a + ((SCH_BG ) & 0xFF) * (255 - a)) / 255;
|
||||
row[x] = (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
fill_rect(int x0, int y0, int w, int h, uint32 color)
|
||||
{
|
||||
// Compute each row's base pointer once instead of re-deriving it (via a
|
||||
// multiply + add) for every single pixel through draw_pixel() — this is
|
||||
// the full-screen splash fill, 480,000 pixels at 800x600, done at least
|
||||
// twice per boot.
|
||||
for (int y = y0; y < y0 + h; y++) {
|
||||
uint32 *row = (uint32*)(g_fb + y * g_stride) + x0;
|
||||
for (int x = 0; x < w; x++)
|
||||
row[x] = color;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
draw_char(int x, int y, uchar c, uint32 color, uint32 bg)
|
||||
{
|
||||
if (c > 127) return;
|
||||
for (int row = 0; row < 16; row++) {
|
||||
uchar bits = font_data[c][row];
|
||||
for (int col = 0; col < 8; col++)
|
||||
draw_pixel(x + col, y + row, (bits & (0x80 >> col)) ? color : bg);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
draw_string(int x, int y, const char *s, uint32 color, uint32 bg)
|
||||
{
|
||||
while (*s) {
|
||||
draw_char(x, y, *s, color, bg);
|
||||
x += 8;
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
// Pads with trailing spaces (drawn in bg) so re-drawing a shorter string
|
||||
// over a longer one doesn't leave stale glyphs behind.
|
||||
static void
|
||||
draw_string_padded(int x, int y, const char *s, uint32 color, uint32 bg, int cols)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) { draw_char(x + n * 8, y, s[n], color, bg); n++; }
|
||||
while (n < cols) { draw_char(x + n * 8, y, ' ', color, bg); n++; }
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
|
||||
static void
|
||||
progress_label(const char *label)
|
||||
{
|
||||
draw_string_padded(g_bar_x, g_bar_y - 22, label, SCH_ACCENT, SCH_BG, BAR_WIDTH / 8);
|
||||
}
|
||||
|
||||
// Border never changes once drawn — separated out so it's only ever drawn
|
||||
// once per bar instead of being redrawn on every single tick.
|
||||
static void
|
||||
progress_border(void)
|
||||
{
|
||||
for (int x = g_bar_x; x < g_bar_x + BAR_WIDTH; x++) {
|
||||
draw_pixel(x, g_bar_y, SCH_FG);
|
||||
draw_pixel(x, g_bar_y + BAR_HEIGHT, SCH_FG);
|
||||
}
|
||||
for (int y = g_bar_y; y <= g_bar_y + BAR_HEIGHT; y++) {
|
||||
draw_pixel(g_bar_x, y, SCH_FG);
|
||||
draw_pixel(g_bar_x + BAR_WIDTH, y, SCH_FG);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
progress_fill(int pct)
|
||||
{
|
||||
if (pct > 100) pct = 100;
|
||||
int fillw = (BAR_WIDTH - 4) * pct / 100;
|
||||
for (int y = g_bar_y + 2; y < g_bar_y + BAR_HEIGHT - 1; y++)
|
||||
for (int x = g_bar_x + 2; x < g_bar_x + BAR_WIDTH - 2; x++)
|
||||
draw_pixel(x, y, (x - g_bar_x - 2 < fillw) ? SCH_ACCENT : SCH_BG);
|
||||
}
|
||||
|
||||
static void
|
||||
progress_start(const char *label, uint32 total)
|
||||
{
|
||||
if (!g_have_fb) return;
|
||||
g_bar_x = (g_width - BAR_WIDTH) / 2;
|
||||
g_bar_y = SPLASH_BAR_Y;
|
||||
g_load_total = total;
|
||||
g_load_done = 0;
|
||||
g_last_pct = -1;
|
||||
g_progress_active = 1;
|
||||
progress_label(label);
|
||||
progress_border();
|
||||
progress_fill(0);
|
||||
g_last_pct = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
progress_tick(uint32 n)
|
||||
{
|
||||
if (!g_have_fb || !g_progress_active) return;
|
||||
g_load_done += n;
|
||||
// Deliberately all 32-bit. The uint64 cast this used to carry was there to
|
||||
// stop `done * 100` overflowing, but a 64-bit divide in 32-bit code calls
|
||||
// libgcc's __udivdi3, which a freestanding bootloader does not link -- it
|
||||
// only ever built because uint64 was accidentally 32 bits wide here.
|
||||
//
|
||||
// Scaling both sides to KiB first keeps the multiply inside 32 bits for any
|
||||
// image up to 4 GiB, which is comfortably more than a kernel will ever be.
|
||||
uint32 done_k = g_load_done >> 10;
|
||||
uint32 total_k = g_load_total >> 10;
|
||||
uint32 pct = total_k ? (done_k * 100) / total_k : 0;
|
||||
if (pct > 100) pct = 100;
|
||||
// A full-bar redraw is a few thousand pixel writes; without this, a
|
||||
// ~330KB kernel image redraws it ~650 times (once per 512-byte sector)
|
||||
// instead of at most 100 (once per percentage point) — the single
|
||||
// biggest chunk of the bootloader's own wall-clock time.
|
||||
if ((int)pct == g_last_pct) return;
|
||||
g_last_pct = (int)pct;
|
||||
progress_fill((int)pct);
|
||||
}
|
||||
|
||||
// --- Boot menu -------------------------------------------------------
|
||||
|
||||
static void
|
||||
reboot_to_firmware(void)
|
||||
{
|
||||
if (g_have_fb) {
|
||||
fill_rect(0, 0, g_width, g_height, SCH_BG);
|
||||
draw_string((g_width - 26 * 8) / 2, g_height / 2 - 8,
|
||||
"Rebooting to firmware...", SCH_ACCENT, SCH_BG);
|
||||
}
|
||||
outb(0x64, 0xFE); // pulse the keyboard controller's reset line
|
||||
for (;;)
|
||||
;
|
||||
}
|
||||
|
||||
static void
|
||||
show_drive_line(int row, const char *label, int present)
|
||||
{
|
||||
draw_string(g_width / 2 - 160, 340 + row * 20, label, SCH_FG, SCH_BG);
|
||||
draw_string(g_width / 2 + 20, 340 + row * 20,
|
||||
present ? "present" : "not present",
|
||||
present ? SCH_ACCENT : SCH_DIM, SCH_BG);
|
||||
}
|
||||
|
||||
// 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 (;;) {
|
||||
fill_rect(0, 0, g_width, g_height, SCH_BG);
|
||||
draw_centered(60, "SchOS Boot Manager", SCH_ACCENT);
|
||||
draw_memory_line(84, SCH_DIM);
|
||||
|
||||
for (int i = 0; i < nitems; i++) {
|
||||
uint32 fg = (i == selected) ? SCH_ACCENT : SCH_FG;
|
||||
draw_string(g_width / 2 - 140, 140 + i * 24, (i == selected) ? "> " : " ", fg, SCH_BG);
|
||||
draw_string(g_width / 2 - 124, 140 + i * 24, items[i], fg, SCH_BG);
|
||||
}
|
||||
|
||||
draw_string(g_width / 2 - 160, 300, "Detected drives:", SCH_FG, SCH_BG);
|
||||
show_drive_line(0, "Primary Master (hda)", hda);
|
||||
show_drive_line(1, "Primary Slave (hdb)", hdb);
|
||||
|
||||
draw_string(g_width / 2 - 168, g_height - 40,
|
||||
"Up/Down to choose, Enter to select", SCH_DIM, SCH_BG);
|
||||
|
||||
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).
|
||||
static void
|
||||
draw_centered(int y, const char *s, uint32 color)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
draw_string((g_width - n * 8) / 2, y, s, color, SCH_BG);
|
||||
}
|
||||
|
||||
// "Memory: NNNN MB" — assembled by hand, no printf in the bootloader.
|
||||
static void
|
||||
draw_memory_line(int y, uint32 color)
|
||||
{
|
||||
char line[32];
|
||||
char num[12];
|
||||
int i = 0;
|
||||
const char *pfx = "Memory: ";
|
||||
while (*pfx) line[i++] = *pfx++;
|
||||
utoa10(e820_usable_mb(), num);
|
||||
for (int j = 0; num[j]; j++) line[i++] = num[j];
|
||||
const char *sfx = " MB";
|
||||
while (*sfx) line[i++] = *sfx++;
|
||||
line[i] = 0;
|
||||
draw_centered(y, line, color);
|
||||
}
|
||||
|
||||
static void
|
||||
splash(void)
|
||||
{
|
||||
if (!g_have_fb) return;
|
||||
|
||||
fill_rect(0, 0, g_width, g_height, SCH_BG);
|
||||
|
||||
// The product, not the loader.
|
||||
//
|
||||
// This used to read "SchOS Boot Manager / version 1.0 / Memory: 1023 MB /
|
||||
// Press F8 for boot menu...", centred, in gold on purple. Every part of that
|
||||
// is something a system shows its developer rather than its user: the name
|
||||
// of the component doing the work, its version, and a memory figure nobody
|
||||
// asked for. No shipping OS names its bootloader on the splash.
|
||||
//
|
||||
// What is left is the name, a rule in the accent colour, and the one hint
|
||||
// that is actually actionable -- moved to the bottom of the screen, where a
|
||||
// hint belongs, rather than sitting under the title as though it were part
|
||||
// of the identity.
|
||||
// The mark, drawn here in the loader rather than by the kernel for a timing
|
||||
// reason: the kernel does not reach vga_init() until the last few hundred
|
||||
// milliseconds of a ~2.8s boot, so a kernel-drawn logo appeared for about one
|
||||
// frame before console_takeover() wiped it. Here it is up for the whole boot.
|
||||
draw_logo((g_width - LOGO_W) / 2, SPLASH_MARK_Y);
|
||||
|
||||
draw_centered(SPLASH_TEXT_Y, "S C H U N I X", SCH_FG);
|
||||
|
||||
// A 120x2 rule, centred, in the accent -- slightly wider than the 104px
|
||||
// wordmark so it reads as an underline rather than a strikethrough. Two
|
||||
// pixels rather than one: a single-pixel line looks like a rendering
|
||||
// artefact at this size.
|
||||
fill_rect((g_width - 120) / 2, SPLASH_RULE_Y, 120, SPLASH_RULE_H, SCH_ACCENT);
|
||||
|
||||
draw_centered(g_height - 40, "Press F8 for boot options", SCH_DIM);
|
||||
}
|
||||
|
||||
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*)BOOT_MEDIA_FLAG_ADDR == BOOT_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*)BOOT_VERBOSE_FLAG_ADDR = verbose;
|
||||
|
||||
if (g_have_fb) {
|
||||
fill_rect(0, 0, g_width, g_height, SCH_BG);
|
||||
draw_string((g_width - 7 * 8) / 2, g_height / 2 - 50, "SCHOS", SCH_ACCENT, SCH_BG);
|
||||
}
|
||||
|
||||
// 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*)BOOT_MEDIA_FLAG_ADDR == BOOT_MEDIA_CD && !g_from_cd) {
|
||||
draw_centered(g_height / 2, "No readable boot disc found.", SCH_ACCENT);
|
||||
draw_centered(g_height / 2 + 20, "The drive did not answer ATAPI.", SCH_DIM);
|
||||
for (;;)
|
||||
asm volatile("hlt");
|
||||
}
|
||||
|
||||
if (g_have_fb)
|
||||
progress_start("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)
|
||||
progress_start("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)
|
||||
progress_label("Starting SchOS...");
|
||||
|
||||
// Jump to kernel entry. NOT a normal C call: kernel/arch/x86_64/
|
||||
// entry.S's mboot_entry reads its "argument" (the framebuffer
|
||||
// physical address) out of EAX directly, not off the stack — a
|
||||
// multiboot-style raw-register handoff, not cdecl. A plain
|
||||
// `((void(*)(uint32))entry)(fbaddr)` pushes fbaddr on the stack
|
||||
// per cdecl and leaves EAX as whatever happened to be lying around;
|
||||
// it only ever "worked" because simpler bootmain.c code coincidentally
|
||||
// left the right value in EAX as a side effect of its own codegen.
|
||||
// Adding the boot-menu/progress-bar code changed that codegen and
|
||||
// broke it silently (kernel received garbage instead of the real
|
||||
// LFB address). Pin it down explicitly instead of hoping.
|
||||
{
|
||||
uint32 fbaddr = *(uint32*)(VBE_INFO_ADDR + 40);
|
||||
uint32 entry = hdr->entry_addr;
|
||||
asm volatile("jmp *%0" : : "r"(entry), "a"(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);
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in a new issue