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