An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
878 lines
31 KiB
C
878 lines
31 KiB
C
#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/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 Schunix",
|
|
"Boot Schunix (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, "Schunix 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 "Schunix 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, "SCHUNIX", 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 Schunix...");
|
|
|
|
// 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);
|
|
}
|