Skip to content

Commit 4d4328f

Browse files
UnbreakableMJclaude
andcommitted
debug(bios): instrument kmain + disk; isolates hang to trampoline
M1-16 progress — the BIOS boot chain is now diagnosed all the way to the real culprit. Observed serial output (`qemu-system-x86_64 -serial file:…`): ZAMAK Stage 1 DL=80 Loading Stage 2... ZPMD[]!01 Decoding the checkpoints (each byte is one `outb 0x3F8`): Z = zamak-bios real-mode entry reached P = 16→32-bit protected-mode switch succeeded M = kmain entered D = Disk::new returned [ = call_bios_int dispatched ] = call_bios_int returned ! = BIOS signaled non-zero AH 01 = hex(AH) = 0x01 ("invalid function") Same `INT 13h AH=0x42` (Extended Read) succeeds when issued from stage1 directly in real mode, so the 32→real→32 trampoline in `zamak-bios/src/entry.rs::call_bios_int` is leaving BIOS state corrupted (IVT/A20/PIC/GDT/segment-descriptor-cache restore is likely the culprit). Debugging the ~40-instruction trampoline is non-trivial; see TODO.md for the two proposed paths forward. Changes in this commit: - `zamak-bios/src/main.rs`: added `mark(b: u8)` helper that `outb`s a single byte to COM1 — a lightweight breadcrumb that works in both real and protected mode without allocator or panic-path involvement. Sprinkled markers through `kmain`: entry (`M`), post-`Disk::new` (`D`), post-MBR-read (`m`), post-partition-scan (`P`), pre-FS-probe (`F`), post-mount (`f`), post-config-parse (`C`). - `zamak-bios/src/main.rs`: kmain now reads sector 0, parses the MBR partition table (types 0x01/04/06/0B/0C/83), and passes the partition's starting LBA to `Fat32::parse` / `Ext2::mount` instead of hard-coding LBA 0. The disk image built by `build-images.sh` puts the FAT32 partition at LBA 4096 behind the sfdisk-written partition entry, so this fixes "wrong LBA" as a confounding variable. - `zamak-bios/src/disk.rs`: `read_sectors_internal` emits `[` / `]` around the `call_bios_int`, `.` on success, `!` + two hex digits on BIOS error. Only visible during manual M1-16 bring-up (the test harness doesn't consume them). - `TODO.md`: M1-16 note updated with the current symptom and the two paths forward (debug trampoline vs. do all BIOS I/O before leaving real mode, Limine-style). Not affected: UEFI boot-smoke + asm-verification stay `[PASS]` locally; clippy + fmt + the full workspace build remain clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 653eacd commit 4d4328f

3 files changed

Lines changed: 80 additions & 9 deletions

File tree

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ SPDX-FileCopyrightText: 2026 Mohamed Hammad
115115
| M1-13 | `[✓]` | KASLR: RDSEED → RDRAND → RDTSC fallback chain in `X86KaslrRng` with CPUID detection (§FR-MM-003) |
116116
| M1-14 | `[✓]` | BLAKE2B hash implementation in `zamak-core::blake2b` (pure `no_std`, RFC 7693) for `#hash` URI suffix (§FR-CFG-003) |
117117
| M1-15 | `[✓]` | ISO 9660 filesystem driver (`zamak-core::iso9660`) — read-only, supports path traversal, ECMA-119 |
118-
| M1-16 | `[~]` | End-to-end BIOS Limine-Protocol kernel boot under QEMU. UEFI case `[✓]`. BIOS chain runs through `_start → protected mode → kmain entry` under QEMU: (a) `build-images.sh` builds real MBR + stage2 + FAT32-partition disk at `target/zamak-bios.img`; (b) `zamak-stage1` writes progress to COM1 (0x3F8) directly (BIOS teletype doesn't route to serial in headless QEMU) and does extended-INT-13h in 64-sector chunks so `zamak-bios` > 32 KiB doesn't blow the 64 KiB segment boundary; (c) `zamak-bios` prints `Z` on real-mode entry and `P` after the 16→32 mode switch; (d) `smp`/`trampoline` modules feature-gated off (AP trampoline needs position-independent rewrite — separate scope). Current hang is inside `kmain()` — likely in the `call_bios_int` 32→real→32 round-trip or the FAT32/partition-table reads. Next step for whoever picks it up: add serial `outb 0x3F8` after every major kmain step (E820 fetch, disk init, partition probe, FAT32 mount, config read, kernel load, long-mode handoff) and iterate. `bios-boot-smoke` remains removed from the `boot-smoke` suite until kmain reaches kernel handoff. |
118+
| M1-16 | `[~]` | End-to-end BIOS Limine-Protocol kernel boot under QEMU. UEFI case `[✓]`. BIOS chain diagnosed all the way to the `call_bios_int` 32→real→32 trampoline. Observed serial: `ZAMAK Stage 1 / DL=80 / Loading Stage 2... / ZPMD[]!01`. Meaning: stage1 + stage2 + mode switch + `kmain` entry + `Disk::new` all work; the first `INT 13h AH=0x42` dispatched through the protected-mode-round-trip trampoline returns `AH=0x01` ("invalid function"). Same call issued **directly in real mode from stage1** succeeds, so the trampoline is leaving BIOS state wrong (likely IVT/A20/PIC/GDT not fully restored after the 32→real switch). Two paths forward for whoever picks this up: (A) debug the existing `zamak-bios/src/entry.rs` `call_bios_int` asm (non-trivial — ~40-instruction mode-transition block with CR0/segment/stack gymnastics), or (B) re-architect `zamak-bios` to do all BIOS I/O while still in real mode before the `ljmp 0x08, init_32` — simpler and matches how Limine's own stage3 does it. Build tooling (`build-images.sh`), stage1 multi-chunk INT 13h, MBR partition-table scan, and serial instrumentation are all in place. `bios-boot-smoke` remains removed from the suite until `call_bios_int` works or is refactored away. |
119119

120120
---
121121

zamak-bios/src/disk.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ impl Disk {
9393
regs.edx = self.drive_id as u32;
9494
regs.esi = DAP_ADDR; // DS:SI -> DAP.
9595

96+
// Serial breadcrumbs: '[' before the 32→real→32 mode round-trip,
97+
// ']' after. If we see '[' but not ']' during M1-16 bring-up the
98+
// `call_bios_int` trampoline is the culprit.
99+
crate::mark(b'[');
96100
// SAFETY:
97101
// Preconditions: DAP at 0x6000 is valid; drive_id is the BIOS boot drive
98102
// Postconditions: sectors read to buffer_addr; regs updated with status
@@ -101,11 +105,19 @@ impl Disk {
101105
unsafe {
102106
call_bios_int(0x13, &mut regs);
103107
}
104-
105-
if (regs.eax >> 8) & 0xFF != 0 {
106-
return Err((regs.eax >> 8) as u8);
108+
crate::mark(b']');
109+
let ah = ((regs.eax >> 8) & 0xFF) as u8;
110+
if ah != 0 {
111+
// Emit two hex digits of AH to serial so the bring-up log
112+
// shows which BIOS error fired instead of a silent panic.
113+
let hi = ah >> 4;
114+
let lo = ah & 0x0F;
115+
crate::mark(b'!');
116+
crate::mark(if hi < 10 { b'0' + hi } else { b'A' + (hi - 10) });
117+
crate::mark(if lo < 10 { b'0' + lo } else { b'A' + (lo - 10) });
118+
return Err(ah);
107119
}
108-
120+
crate::mark(b'.'); // successful read
109121
Ok(())
110122
}
111123
}

zamak-bios/src/main.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,69 @@ fn fulfill_requests(
187187
use zamak_core::arch::x86 as arch;
188188
use zamak_core::rng::{KaslrRng, X86KaslrRng};
189189

190+
/// Writes a single ASCII byte to COM1 (0x3F8). Used as a boot-progress
191+
/// checkpoint marker — the test harness doesn't consume these, they're
192+
/// for humans reading QEMU serial logs during M1-16 bring-up.
193+
#[inline(always)]
194+
fn mark(b: u8) {
195+
// SAFETY:
196+
// Preconditions: COM1 (0x3F8) exists in every QEMU PC machine.
197+
// Postconditions: byte appears on -serial stdio output.
198+
// Clobbers: DX, AL (temps).
199+
// Worst-case: spurious byte appears on serial.
200+
unsafe {
201+
core::arch::asm!(
202+
"mov dx, 0x3F8",
203+
"out dx, al",
204+
in("al") b,
205+
out("dx") _,
206+
options(nostack, nomem, preserves_flags),
207+
);
208+
}
209+
}
210+
190211
#[no_mangle]
191212
pub extern "C" fn kmain(drive_id: u8) -> ! {
213+
mark(b'M'); // kmain entry
214+
192215
// 2. Initialize Disk
193216
let mut disk = Disk::new(drive_id);
194217
let mut disk_ext2 = disk.clone();
218+
mark(b'D'); // Disk::new returned
219+
220+
// 3. Read the MBR partition table to find a FAT32 / EXT2 partition.
221+
// An unpartitioned disk would have the filesystem at LBA 0, but
222+
// `zamak-test/build-images.sh` stamps a proper MBR partition
223+
// table via `sfdisk`, so we follow that pointer.
224+
use zamak_core::fs::BlockDevice;
225+
let mut mbr_buf = [0u8; 512];
226+
BlockDevice::read_sectors(&disk, 0, 1, &mut mbr_buf).expect("Failed to read MBR sector");
227+
mark(b'm'); // MBR read
228+
let mut part_lba: u64 = 0;
229+
for i in 0..4 {
230+
let base = 446 + i * 16;
231+
let type_byte = mbr_buf[base + 4];
232+
// 0x01=FAT12, 0x04=FAT16<32MB, 0x06=FAT16, 0x0B=FAT32/CHS,
233+
// 0x0C=FAT32/LBA, 0x83=Linux (EXT*).
234+
if matches!(type_byte, 0x01 | 0x04 | 0x06 | 0x0B | 0x0C | 0x83) {
235+
let lba = u32::from_le_bytes([
236+
mbr_buf[base + 8],
237+
mbr_buf[base + 9],
238+
mbr_buf[base + 10],
239+
mbr_buf[base + 11],
240+
]);
241+
if lba != 0 {
242+
part_lba = lba as u64;
243+
break;
244+
}
245+
}
246+
}
247+
if part_lba == 0 {
248+
panic!("No FAT32/EXT2 partition entry in MBR");
249+
}
250+
mark(b'P'); // Partition LBA resolved
195251

196-
// 3. Mount Filesystem
252+
// 4. Mount Filesystem
197253
// We try FAT32 first, then EXT2
198254
use crate::fat32::Fat32;
199255
use zamak_core::ext2::Ext2;
@@ -202,16 +258,18 @@ pub extern "C" fn kmain(drive_id: u8) -> ! {
202258
let mut fs_fat: Option<Fat32> = None;
203259
let mut fs_ext2: Option<Ext2> = None;
204260

205-
// Probe FAT32
206-
if let Ok(f) = Fat32::parse(&mut disk, 0) {
261+
mark(b'F'); // About to probe filesystems
262+
// Probe FAT32
263+
if let Ok(f) = Fat32::parse(&mut disk, part_lba) {
207264
fs_fat = Some(f);
208265
}
209266
// If not FAT32, probe EXT2
210-
else if let Ok(f) = Ext2::mount(&mut disk_ext2, 0) {
267+
else if let Ok(f) = Ext2::mount(&mut disk_ext2, part_lba) {
211268
fs_ext2 = Some(f);
212269
} else {
213270
panic!("No supported filesystem found on boot partition");
214271
}
272+
mark(b'f'); // Filesystem mounted
215273

216274
let fs: &dyn FileSystem = if let Some(ref f) = fs_fat {
217275
f
@@ -229,6 +287,7 @@ pub extern "C" fn kmain(drive_id: u8) -> ! {
229287
// Simple parser
230288
let config_str = core::str::from_utf8(&config_file_buf[..config_size]).unwrap_or("");
231289
let config = zamak_core::config::parse(config_str);
290+
mark(b'C'); // Config parsed
232291

233292
// 4. Initialize Graphics (VBE) for TUI
234293
let mut fb_opt = vbe::find_and_set_vbe_mode(1024, 768, 32);

0 commit comments

Comments
 (0)