storage: AHCI, GPT/MBR partitions, and a disk a PC can boot

q35 stopped with "no root filesystem" because its disk is SATA, like
every PC since 2008, and SchOS only had IDE. And SchFS could only sit
on a whole raw device, so nothing could boot from a disk that also
carried an ESP.

- include/blkdev.h, kernel/dev/blkdev.c: a registry of block devices
  found at probe time, numbered from 4. A disk's GPT (or MBR) is read
  when it registers, and each partition becomes a device whose block 0
  is the partition's first sector. SchFS needs no format change to live
  in a partition; a Linux SchFS driver will see the same bytes on
  /dev/sdX2. SchFS has its own GPT type:
  47138FA9-B79C-47A6-9F7D-AC1F7F572D5E.
- AHCI driver, written from the AHCI 1.3.1 spec: BIOS handoff,
  COMRESET when the link is down, IDENTIFY, 48-bit DMA read/write,
  FLUSH. Completion is polled, so the root disk does not depend on
  interrupt routing.
- USB sticks also register as usb0, so their partitions are found.
- The root probe tries registered devices between USB and IDE,
  SchFS-typed partitions first.
- make disk / tools/posix/mkdisk.sh: a GPT image with a 64 MB ESP and
  a SchFS partition, ready to dd to a stick or a disk.

Tested in QEMU q35: the old two-disk setup roots on sata1; disk.img as
the only SATA disk roots on sata0p2; disk.img as a USB stick (firmware
boots from it) roots on usb0p2. Writes survive sync in each case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HH7xukt4aaiGj1nQEAoQf1
This commit is contained in:
Nexoniarz 2026-10-22 10:57:41 +02:00
commit d7352c0db0
10 changed files with 1027 additions and 17 deletions

View file

@ -24,7 +24,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
panic.o uaccess.o qr.o time.o ksym.o klog.o sbuf.o kmem.o) panic.o uaccess.o qr.o time.o ksym.o klog.o sbuf.o kmem.o)
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \
pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
dbg.o netuser.o audio.o) dbg.o netuser.o audio.o)
@ -47,6 +47,7 @@ KERN_DRIVERS += $(BUILD)/kernel/drivers/net/r8169/r8169.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/ahci/ahci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
@ -736,6 +737,13 @@ iso:
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install
SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@ SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
# A partitioned disk -- GPT, an ESP and a SchFS partition -- which is what a
# real machine boots from: dd it to a USB stick or a disk. See
# tools/posix/mkdisk.sh. `make qemu-disk` boots it on q35 as the only disk.
disk: $(BUILD)/disk.img
$(BUILD)/disk.img: esp $(BUILD)/schfs.img tools/posix/mkdisk.sh
sh tools/posix/mkdisk.sh $(BUILD)/esp $(BUILD)/schfs.img $@
# Boot the ISO the way a machine booting off a disc would: no hard disk at # Boot the ISO the way a machine booting off a disc would: no hard disk at
# all, so a kernel that still secretly depends on one fails here rather than # all, so a kernel that still secretly depends on one fails here rather than
# in someone's optical drive. # in someone's optical drive.
@ -1505,7 +1513,7 @@ qrtest: $(QRTEST)
qrcheck: $(QRTEST) qrcheck: $(QRTEST)
$(PYTHON) tools/python/qrcheck.py $(PYTHON) tools/python/qrcheck.py
.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ .PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \ schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids ovmf esp uefi logo qrtest qrcheck crashqr usb-ids ovmf esp uefi

View file

@ -447,12 +447,28 @@ what makes a loop over candidates legal at all.
|---|---|---| |---|---|---|
| 1 | `RAMDISK_DEV` | only in a `RAMDISK_ROOT` build, where it is an explicit choice | | 1 | `RAMDISK_DEV` | only in a `RAMDISK_ROOT` build, where it is an explicit choice |
| 2 | `USBDISK_DEV` | a stick that is plugged in was plugged in on purpose | | 2 | `USBDISK_DEV` | a stick that is plugged in was plugged in on purpose |
| 3 | IDE 1 | what the old constant named | | 3 | registered partitions typed SchFS | labelled that way on purpose |
| 4 | IDE 0 | usually the ESP, so usually not SchFS | | 4 | every other registered device | whole disks, untyped and MBR partitions |
| 5 | IDE 1 | what the old constant named |
| 6 | IDE 0 | usually the ESP, so usually not SchFS |
The **registered** devices are the ones found at probe time rather than known
at build time, numbered from 4 (`include/blkdev.h`): a SATA disk behind AHCI
(`sata0`), the USB stick seen a second time (`usb0`), and every partition on
either (`sata0p2`, `usb0p2`). When a disk registers, its GPT is read, or its MBR
if it has no GPT. Each partition then becomes a device whose block 0 is the
partition's first sector, so SchFS in a partition and SchFS on a whole disk are
the same bytes. The ESP is skipped because it is FAT.
SchFS's GPT partition type is `47138FA9-B79C-47A6-9F7D-AC1F7F572D5E`.
`make disk` builds `build/disk.img` (`tools/posix/mkdisk.sh`), a GPT disk with a
64 MB ESP and a SchFS partition of that type. `dd` it to a stick or a disk, or
give it to QEMU's q35 as the only disk. Both boot, the root comes up as
`sata0p2` or `usb0p2`, and the q35 run is the same path a PC takes.
`iderw()` *panics* on an absent drive rather than failing, so each candidate is `iderw()` *panics* on an absent drive rather than failing, so each candidate is
checked with `ide_disk_present()` / `usb_storage_present()` first. That is the checked with `ide_disk_present()` / `usb_storage_present()` /
whole reason those predicates exist. `blkdev_present()` first. That is the whole reason those predicates exist.
When nothing matches, the panic names what it looked at, because "no root When nothing matches, the panic names what it looked at, because "no root
filesystem" with no list is the least useful thing a kernel can say at the one filesystem" with no list is the least useful thing a kernel can say at the one
@ -590,9 +606,10 @@ in the one process that runs first.
stall, which is the wrong order for finding out. stall, which is the wrong order for finding out.
- **`/proc/bootmode` is read once, at start.** A program that wants to be - **`/proc/bootmode` is read once, at start.** A program that wants to be
quieter or louder later has nowhere to say so, and nothing re-reads it. quieter or louder later has nowhere to say so, and nothing re-reads it.
- **The root device is compiled in.** Booting from a USB disk needs the root - **One USB disk, found at boot.** The mass-storage driver drives the first
device to be selectable, which it is not; the USB mass-storage driver works stick and no other, and a stick plugged in later is not seen. See
and a superblock reads back through `bread()`, but nothing can mount it. [usb.md](usb.md).
- **No NVMe yet.** SATA (AHCI) and IDE are the only fixed disks.
- **`make ramroot` is the only way to boot with no data disk.** It links a - **`make ramroot` is the only way to boot with no data disk.** It links a
filesystem image into the kernel binary, which costs boot time and carries a filesystem image into the kernel binary, which costs boot time and carries a
licensing constraint — see [../licensing.md](../licensing.md) §4. licensing constraint — see [../licensing.md](../licensing.md) §4.

49
include/blkdev.h Normal file
View file

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Block devices that are found rather than assumed.
//
// The first four device numbers are fixed and always were: IDE 0 and 1, the
// ramdisk, the first USB stick. Everything since -- a SATA disk behind AHCI,
// an NVMe namespace, and every partition on any of them -- registers here at
// probe time and is handed the next free number from BLKDEV_FIRST.
//
// A partition is a block device in its own right, with block 0 at its first
// sector. That is the whole of partition support from a filesystem's side:
// SchFS on /dev/sata0p2 sees a volume that starts at zero, exactly as it would
// on a whole disk, and needs to know nothing about GPT -- which is also what
// a Linux driver for SchFS will see when it is handed /dev/sda2.
#define BLKDEV_FIRST 4
#define NBLKDEV 16 // so device numbers stay below SCHFS_DEV (64)
// The GPT partition type of a SchFS volume, as it is written and printed:
// 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E. sgdisk and fdisk accept it as is.
// Generated for SchFS on 2026-10-22; nothing else uses it.
#define SCHFS_GPT_TYPE "47138FA9-B79C-47A6-9F7D-AC1F7F572D5E"
// What a driver supplies. `rw` moves `count` of the device's own sectors at
// `lba` to or from `buf`, a kernel address; it is never asked for more than
// BSIZE bytes at once. Returns 0, or -1 if the device refused.
struct blkdev_ops {
int (*rw)(void *priv, uint64 lba, uint32 count, void *buf, int write);
int (*flush)(void *priv); // 0 when there is nothing to flush
};
// Register a whole disk. Its partition table, if it has one, is read here and
// every partition registered after it. Returns the disk's device number, or
// -1 if the table is full.
int blkdev_register(const char *name, const struct blkdev_ops *ops,
void *priv, uint64 nsectors, uint32 secsize);
int blkdev_present(uint dev);
void blkdev_rw(struct buf *b);
int blkdev_flush(uint dev);
// For the root probe: the registered devices in order, partitions after the
// disk they are on. Returns the device number of the i'th, or -1 past the end.
int blkdev_nth(int i);
const char *blkdev_name(uint dev);
int blkdev_is_schfs(uint dev); // a partition typed SCHFS_GPT_TYPE
int blkdev_info(char *buf, int cap); // /proc/partitions

View file

@ -27,6 +27,7 @@
#include "buf.h" #include "buf.h"
#include "x86.h" #include "x86.h"
#include "fsstat.h" #include "fsstat.h"
#include "blkdev.h"
struct fsstat fsstat; struct fsstat fsstat;
@ -86,6 +87,12 @@ bdevrw_inner(struct buf *b)
usb_storage_rw(b); usb_storage_rw(b);
return; return;
} }
// SATA, NVMe and every partition: registered at probe time rather than
// known at build time. See include/blkdev.h.
if(b->dev >= BLKDEV_FIRST){
blkdev_rw(b);
return;
}
iderw(b); iderw(b);
} }
@ -99,6 +106,8 @@ bdevflush(uint dev)
return 0; // memory has no volatile stage below it return 0; // memory has no volatile stage below it
if(dev == USBDISK_DEV) if(dev == USBDISK_DEV)
return usb_storage_flush(); return usb_storage_flush();
if(dev >= BLKDEV_FIRST)
return blkdev_flush(dev);
return ideflush(dev); return ideflush(dev);
} }

334
kernel/dev/blkdev.c Normal file
View file

@ -0,0 +1,334 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// The block device registry, and the partition tables on what is in it.
// See include/blkdev.h.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "blkdev.h"
struct blkdev {
int used;
char name[16];
const struct blkdev_ops *ops;
void *priv;
uint64 start; // first sector, in the disk's own sectors
uint64 nsectors;
uint32 secsize;
int parent; // the disk's slot for a partition, -1 for a disk
int schfs; // GPT says this is a SchFS volume
};
static struct blkdev devs[NBLKDEV];
static struct spinlock blklock;
static int blkinit;
static struct blkdev *
lookup(uint dev)
{
if (dev < BLKDEV_FIRST || dev >= BLKDEV_FIRST + NBLKDEV)
return 0;
struct blkdev *d = &devs[dev - BLKDEV_FIRST];
return d->used ? d : 0;
}
static int
add(const char *name, const struct blkdev_ops *ops, void *priv,
uint64 start, uint64 nsectors, uint32 secsize, int parent, int schfs)
{
if (!blkinit) {
initlock(&blklock, "blkdev");
blkinit = 1;
}
acquire(&blklock);
for (int i = 0; i < NBLKDEV; i++) {
if (devs[i].used)
continue;
struct blkdev *d = &devs[i];
safestrcpy(d->name, name, sizeof(d->name));
d->ops = ops;
d->priv = priv;
d->start = start;
d->nsectors = nsectors;
d->secsize = secsize;
d->parent = parent;
d->schfs = schfs;
__sync_synchronize();
d->used = 1;
release(&blklock);
return BLKDEV_FIRST + i;
}
release(&blklock);
cprintf("blkdev: no slot for %s; it will not be usable\n", name);
return -1;
}
// --- partition tables -------------------------------------------------------
static uint32 le32(const uchar *p) { return p[0] | p[1] << 8 | p[2] << 16 | (uint32)p[3] << 24; }
static uint64 le64(const uchar *p) { return le32(p) | (uint64)le32(p + 4) << 32; }
// A GUID as GPT stores it: the first three fields little-endian, the last two
// as bytes. Compared against the printed form, so the constant in blkdev.h is
// the one a person types into sgdisk.
static int
guid_is(const uchar *g, const char *text)
{
static const int order[16] = { 3,2,1,0, 5,4, 7,6, 8,9, 10,11,12,13,14,15 };
uchar want[16];
int n = 0;
for (const char *p = text; *p && n < 32; p++) {
int v;
if (*p == '-')
continue;
if (*p >= '0' && *p <= '9') v = *p - '0';
else if (*p >= 'a' && *p <= 'f') v = *p - 'a' + 10;
else if (*p >= 'A' && *p <= 'F') v = *p - 'A' + 10;
else return 0;
if (n & 1) want[n / 2] |= v; else want[n / 2] = v << 4;
n++;
}
for (int i = 0; i < 16; i++)
if (g[order[i]] != want[i])
return 0;
return 1;
}
static int
guid_zero(const uchar *g)
{
for (int i = 0; i < 16; i++)
if (g[i])
return 0;
return 1;
}
static void
partname(char *out, const char *disk, int n)
{
int i = 0;
for (; disk[i] && i < 12; i++)
out[i] = disk[i];
out[i++] = 'p';
if (n >= 10)
out[i++] = '0' + n / 10;
out[i++] = '0' + n % 10;
out[i] = 0;
}
static int
readsec(struct blkdev *d, uint64 lba, uchar *buf)
{
return d->ops->rw(d->priv, lba, 1, buf, 0);
}
#define ESP_TYPE "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
// The GUID partition table: the header in sector 1, and its entries wherever
// it says. Only the primary copy is read -- a disk whose primary is damaged
// gets reported as having no partitions rather than half of some.
//
// The EFI system partition is skipped. It is FAT, nothing here can mount it,
// and registering it would only give the root probe one more thing to try.
static int
scan_gpt(int disk, uchar *sec)
{
struct blkdev *d = &devs[disk];
int found = 0;
if (readsec(d, 1, sec) < 0 || memcmp(sec, "EFI PART", 8) != 0)
return -1;
uint64 elba = le64(sec + 72);
uint32 count = le32(sec + 80);
uint32 esize = le32(sec + 84);
if (esize < 128 || esize > d->secsize || (esize & (esize - 1)) ||
count > 256 || elba < 2 || elba >= d->nsectors) {
cprintf("blkdev: %s: GPT header makes no sense; ignoring it\n", d->name);
return -1;
}
uint32 per = d->secsize / esize;
for (uint32 i = 0; i < count; i++) {
if (i % per == 0 && readsec(d, elba + i / per, sec) < 0)
break;
uchar *e = sec + (i % per) * esize;
if (guid_zero(e) || guid_is(e, ESP_TYPE))
continue;
uint64 first = le64(e + 32), last = le64(e + 40);
if (first == 0 || last < first || last >= d->nsectors)
continue;
char name[16];
partname(name, d->name, i + 1);
int schfs = guid_is(e, SCHFS_GPT_TYPE);
if (add(name, d->ops, d->priv, first, last - first + 1, d->secsize,
disk, schfs) >= 0) {
kinfo("blkdev: %s: %d MB%s\n", name,
(int)(((last - first + 1) * d->secsize) >> 20),
schfs ? ", SchFS" : "");
found++;
}
}
return found;
}
// The PC partition table, for disks that have one instead. Primary entries
// only; a protective entry (0xEE) means the disk is GPT and was read above.
static void
scan_mbr(int disk, uchar *sec)
{
struct blkdev *d = &devs[disk];
if (readsec(d, 0, sec) < 0 || sec[510] != 0x55 || sec[511] != 0xAA)
return;
for (int i = 0; i < 4; i++) {
uchar *e = sec + 446 + i * 16;
uint32 type = e[4], first = le32(e + 8), n = le32(e + 12);
// Empty, protective, extended (which would need its own chain read), and
// FAT/ESP types: none of them can be SchFS.
if (type == 0 || type == 0xEE || type == 0x05 || type == 0x0F ||
type == 0xEF || n == 0 || first + (uint64)n > d->nsectors)
continue;
char name[16];
partname(name, d->name, i + 1);
if (add(name, d->ops, d->priv, first, n, d->secsize, disk, 0) >= 0)
kinfo("blkdev: %s: %d MB, MBR type %x\n", name,
(int)(((uint64)n * d->secsize) >> 20), type);
}
}
int
blkdev_register(const char *name, const struct blkdev_ops *ops, void *priv,
uint64 nsectors, uint32 secsize)
{
if (secsize == 0 || secsize > BSIZE || BSIZE % secsize)
return -1;
int dev = add(name, ops, priv, 0, nsectors, secsize, -1, 0);
if (dev < 0)
return -1;
uchar *sec = (uchar *)kalloc();
if (sec) {
if (scan_gpt(dev - BLKDEV_FIRST, sec) < 0)
scan_mbr(dev - BLKDEV_FIRST, sec);
kfree((char *)sec);
}
return dev;
}
// --- I/O ---------------------------------------------------------------------
int
blkdev_present(uint dev)
{
return lookup(dev) != 0;
}
// Same contract as iderw(): fill b->data and set B_VALID on a read, write it
// out and clear B_DIRTY on a write. A failed transfer panics, for the reason
// usb_storage_rw() gives: bread() has no way to report one, and a buffer that
// silently holds the wrong bytes is how a filesystem is corrupted.
void
blkdev_rw(struct buf *b)
{
struct blkdev *d = lookup(b->dev);
int write = (b->flags & B_DIRTY) != 0;
if (!holdingsleep(&b->lock))
panic("blkdev_rw: buf not locked");
if (!d)
panic("blkdev_rw: no such device");
uint32 per = BSIZE / d->secsize;
uint64 lba = b->blockno * per;
if (lba + per > d->nsectors)
panic("blkdev_rw: block out of range");
if (d->ops->rw(d->priv, d->start + lba, per, b->data, write) < 0) {
cprintf("blkdev: %s: %s of block %d failed\n", d->name,
write ? "write" : "read", (int)b->blockno);
panic("blkdev_rw: transfer failed");
}
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
}
int
blkdev_flush(uint dev)
{
struct blkdev *d = lookup(dev);
if (!d || !d->ops->flush)
return 0;
return d->ops->flush(d->priv);
}
int
blkdev_nth(int i)
{
for (int s = 0; s < NBLKDEV; s++)
if (devs[s].used && i-- == 0)
return BLKDEV_FIRST + s;
return -1;
}
const char *
blkdev_name(uint dev)
{
struct blkdev *d = lookup(dev);
return d ? d->name : "?";
}
int
blkdev_is_schfs(uint dev)
{
struct blkdev *d = lookup(dev);
return d ? d->schfs : 0;
}
// --- /proc/partitions --------------------------------------------------------
static int
put(char *buf, int cap, int off, const char *s)
{
while (*s && off < cap)
buf[off++] = *s++;
return off;
}
static int
putu(char *buf, int cap, int off, uint64 v)
{
char t[24];
int n = 0;
do { t[n++] = '0' + v % 10; v /= 10; } while (v);
while (n && off < cap)
buf[off++] = t[--n];
return off;
}
int
blkdev_info(char *buf, int cap)
{
int off = put(buf, cap, 0, "dev name MB type\n");
for (int s = 0; s < NBLKDEV; s++) {
struct blkdev *d = &devs[s];
if (!d->used)
continue;
off = putu(buf, cap, off, BLKDEV_FIRST + s);
off = put(buf, cap, off, " ");
off = put(buf, cap, off, d->name);
for (int pad = 12 - strlen(d->name); pad > 0; pad--)
off = put(buf, cap, off, " ");
off = putu(buf, cap, off, (d->nsectors * d->secsize) >> 20);
off = put(buf, cap, off, d->parent < 0 ? " disk" :
d->schfs ? " schfs" : " part");
off = put(buf, cap, off, "\n");
}
return off;
}

View file

@ -35,6 +35,7 @@ extern const struct pci_driver e1000_driver;
extern const struct pci_driver ac97_driver; extern const struct pci_driver ac97_driver;
extern const struct pci_driver r8169_driver; extern const struct pci_driver r8169_driver;
extern const struct pci_driver rtl8139_driver; extern const struct pci_driver rtl8139_driver;
extern const struct pci_driver ahci_driver;
// Order matters here in one respect: virtio-gpu is offered the bus before the // Order matters here in one respect: virtio-gpu is offered the bus before the
// VBE framebuffer driver, so that on a machine with both, the device with real // VBE framebuffer driver, so that on a machine with both, the device with real
@ -48,6 +49,7 @@ static const struct pci_driver *drivers[] = {
&r8169_driver, &r8169_driver,
&rtl8139_driver, &rtl8139_driver,
&xhci_driver, &xhci_driver,
&ahci_driver,
&ac97_driver, &ac97_driver,
}; };

View file

@ -0,0 +1,468 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// AHCI: SATA disks, written from the Serial ATA AHCI 1.3.1 specification.
//
// This is how every PC since about 2008 attaches its internal disk, and the
// IDE driver beside it is how almost none of them do. Under QEMU's q35 the
// boot disk is here, which is why that machine used to stop with "no root
// filesystem" while the very same image booted on i440fx.
//
// The model is small. The HBA has up to 32 ports; each has a command list of
// up to 32 slots, each slot points at a command table, and the table holds
// the ATA command as a register FIS plus a scatter list of where the data
// goes. Issue a command by setting its bit in PxCI; the HBA clears the bit
// when the command is done.
//
// One slot per port and **completion by polling**, deliberately. A disk is
// on the path to a shell, and polling PxCI is the one way of finishing a
// command that does not depend on interrupt routing -- which on real hardware
// without an AML interpreter is the part that cannot be trusted (see
// kernel/dev/irq.c). A SATA command completes in tens of microseconds to a
// few milliseconds, so the cost is that, spent spinning, and it is yielded
// once a wait goes long.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "proc.h"
#include "pci.h"
#include "dma.h"
#include "blkdev.h"
// --- HBA registers (section 3.1) ---------------------------------------------
#define HBA_CAP 0x00
#define CAP_S64A (1u << 31) // 64-bit DMA addresses
#define CAP_SSS (1u << 27) // staggered spin-up: ports start spun down
#define HBA_GHC 0x04
#define GHC_AE (1u << 31) // AHCI enable
#define GHC_HR (1u << 0) // HBA reset
#define HBA_PI 0x0C // ports implemented
#define HBA_VS 0x10
#define HBA_CAP2 0x24
#define CAP2_BOH (1u << 0) // BIOS/OS handoff supported
#define HBA_BOHC 0x28
#define BOHC_BOS (1u << 0) // the BIOS owns the HBA
#define BOHC_OOS (1u << 1) // the OS asks for it
#define BOHC_BB (1u << 4) // BIOS busy
// --- port registers (section 3.3), at 0x100 + port * 0x80 --------------------
#define PX_CLB 0x00
#define PX_CLBU 0x04
#define PX_FB 0x08
#define PX_FBU 0x0C
#define PX_IS 0x10
#define PX_IE 0x14
#define PX_CMD 0x18
#define CMD_ST (1u << 0) // start processing the command list
#define CMD_SUD (1u << 1) // spin up
#define CMD_POD (1u << 2) // power on
#define CMD_FRE (1u << 4) // FIS receive enable
#define CMD_FR (1u << 14) // FIS receive running
#define CMD_CR (1u << 15) // command list running
#define PX_TFD 0x20
#define TFD_ERR 0x01
#define TFD_DRQ 0x08
#define TFD_BSY 0x80
#define PX_SIG 0x24
#define SIG_ATA 0x00000101
#define SIG_ATAPI 0xEB140101
#define PX_SSTS 0x28
#define PX_SCTL 0x2C
#define PX_SERR 0x30
#define PX_CI 0x38
#define IS_TFES (1u << 30) // task file error
// --- ATA ---------------------------------------------------------------------
#define ATA_IDENTIFY 0xEC
#define ATA_READ_DMA_EXT 0x25
#define ATA_WRITE_DMA_EXT 0x35
#define ATA_FLUSH_EXT 0xEA
#define FIS_H2D 0x27
// One command header (section 4.2.2).
struct cmdhdr {
uint16 flags; // CFL in 4:0, A 5, W 6, P 7, R 8, B 9, C 10
uint16 prdtl; // scatter entries in the table
uint32 prdbc; // bytes transferred, written by the HBA
uint32 ctba, ctbau; // the command table, 128-byte aligned
uint32 rsv[4];
};
struct prd {
uint32 dba, dbau;
uint32 rsv;
uint32 dbc; // byte count - 1 in 21:0, interrupt-on-completion 31
};
// The command table (section 4.2.3): the FIS, an ATAPI command we never send,
// and one scatter entry -- every transfer here is one bounce buffer.
struct cmdtbl {
uint8 cfis[64];
uint8 acmd[16];
uint8 rsv[48];
struct prd prdt[1];
};
#define AHCI_MAX_PORTS 32
#define AHCI_MAX_DISKS 8
struct ahci_port {
volatile uint8 *r; // this port's registers
struct cmdhdr *cl; // command list, 1 KB
uint8 *fis; // received FIS area, 256 bytes
struct cmdtbl *ct; // the one command table
uint8 *xfer; // bounce buffer, BSIZE
uint64 nsectors;
uint32 secsize;
struct sleeplock lock; // one command at a time on this port
char name[8];
int dev;
};
static struct ahci_port disks[AHCI_MAX_DISKS];
static int ndisks;
static int nhba;
static inline uint32 rd(volatile uint8 *b, int o) { return *(volatile uint32 *)(b + o); }
static inline void wr(volatile uint8 *b, int o, uint32 v) { *(volatile uint32 *)(b + o) = v; }
// Spin until (reg & mask) == want, for at most `ms`. Returns 0 or -1.
static int
waitreg(volatile uint8 *b, int o, uint32 mask, uint32 want, int ms)
{
for (int i = 0; i < ms * 100; i++) {
if ((rd(b, o) & mask) == want)
return 0;
microdelay(10);
}
return (rd(b, o) & mask) == want ? 0 : -1;
}
// Stop the port's engines, which is the precondition for touching CLB and FB.
// Section 10.1.2: clear ST and wait for CR, clear FRE and wait for FR.
static int
port_stop(volatile uint8 *r)
{
wr(r, PX_CMD, rd(r, PX_CMD) & ~CMD_ST);
if (waitreg(r, PX_CMD, CMD_CR, 0, 500) < 0)
return -1;
wr(r, PX_CMD, rd(r, PX_CMD) & ~CMD_FRE);
return waitreg(r, PX_CMD, CMD_FR, 0, 500);
}
// The link, if any. DET = 3 is "device present and communication established"
// and IPM = 1 is "active". Firmware normally leaves a link up; when it has not,
// a COMRESET (section 10.4.2) brings it up, and a port with nothing on it
// simply never answers.
static int
port_link(volatile uint8 *r)
{
uint32 ssts = rd(r, PX_SSTS);
if ((ssts & 0xF) == 3)
return 1;
wr(r, PX_SCTL, (rd(r, PX_SCTL) & ~0xFu) | 1); // DET = 1: COMRESET
microdelay(2000); // at least 1 ms
wr(r, PX_SCTL, rd(r, PX_SCTL) & ~0xFu);
for (int i = 0; i < 100; i++) { // up to 1 s for the link
if ((rd(r, PX_SSTS) & 0xF) == 3)
return 1;
microdelay(10000);
}
return 0;
}
// Issue the command in slot 0 and wait for it. `buf` is BSIZE bytes of the
// port's bounce buffer, which is where the data goes or comes from.
static int
port_exec(struct ahci_port *p, uint8 cmd, uint64 lba, uint32 count,
uint32 bytes, int write)
{
volatile uint8 *r = p->r;
struct cmdtbl *t = p->ct;
// Wait out anything still busy from before. A port that stays busy is one
// the drive has wedged; failing is better than issuing into it.
if (waitreg(r, PX_TFD, TFD_BSY | TFD_DRQ, 0, 1000) < 0)
return -1;
memset(t, 0, sizeof(*t));
uint8 *f = t->cfis;
f[0] = FIS_H2D;
f[1] = 0x80; // C: this is a command, not a control
f[2] = cmd;
f[4] = lba; f[5] = lba >> 8; f[6] = lba >> 16;
f[7] = 0x40; // LBA mode
f[8] = lba >> 24; f[9] = lba >> 32; f[10] = lba >> 40;
f[12] = count; f[13] = count >> 8;
struct cmdhdr *h = &p->cl[0];
h->flags = 5 | (write ? (1 << 6) : 0); // a register FIS is five dwords
h->prdbc = 0;
if (bytes) {
uint64 pa = dma_phys(p->xfer);
t->prdt[0].dba = (uint32)pa;
t->prdt[0].dbau = (uint32)(pa >> 32);
t->prdt[0].dbc = bytes - 1;
h->prdtl = 1;
} else {
h->prdtl = 0;
}
wr(r, PX_IS, 0xFFFFFFFF);
__sync_synchronize();
wr(r, PX_CI, 1);
// Spin for the first few milliseconds, which is when nearly every command
// finishes; after that give the processor away between looks. yield() only
// from a process -- the root probe and IDENTIFY run before there is one.
uint64 start = rdtsc(), limit = tsc_hz * 10; // ten seconds
while (rd(r, PX_CI) & 1) {
if (rd(r, PX_IS) & IS_TFES)
break;
uint64 now = rdtsc() - start;
if (now > limit) {
cprintf("ahci: %s: command %x timed out\n", p->name, cmd);
return -1;
}
if (now > tsc_hz / 200 && myproc())
yield();
else
asm volatile("pause");
}
if ((rd(r, PX_IS) & IS_TFES) || (rd(r, PX_TFD) & TFD_ERR)) {
cprintf("ahci: %s: command %x failed, tfd %x\n", p->name, cmd,
rd(r, PX_TFD));
return -1;
}
return 0;
}
static int
ahci_rw(void *priv, uint64 lba, uint32 count, void *buf, int write)
{
struct ahci_port *p = priv;
uint32 bytes = count * p->secsize;
int rc;
if (bytes > BSIZE)
return -1;
acquiresleep(&p->lock);
if (write)
memmove(p->xfer, buf, bytes);
rc = port_exec(p, write ? ATA_WRITE_DMA_EXT : ATA_READ_DMA_EXT,
lba, count, bytes, write);
if (rc == 0 && !write)
memmove(buf, p->xfer, bytes);
releasesleep(&p->lock);
return rc;
}
static int
ahci_flush(void *priv)
{
struct ahci_port *p = priv;
acquiresleep(&p->lock);
int rc = port_exec(p, ATA_FLUSH_EXT, 0, 0, 0, 0);
releasesleep(&p->lock);
return rc;
}
static const struct blkdev_ops ahci_ops = {
.rw = ahci_rw,
.flush = ahci_flush,
};
// IDENTIFY DEVICE's model string: forty characters, byte-swapped in pairs,
// padded with spaces.
static void
model_of(const uint16 *id, char *out, int cap)
{
int n = 0;
for (int w = 27; w < 47 && n + 2 < cap; w++) {
out[n++] = id[w] >> 8;
out[n++] = id[w] & 0xFF;
}
while (n > 0 && out[n - 1] == ' ')
n--;
out[n] = 0;
}
// Bring one port up and, if a SATA disk answers on it, register it.
static void
port_init(volatile uint8 *hba, int port, uint32 cap)
{
volatile uint8 *r = hba + 0x100 + port * 0x80;
if (ndisks >= AHCI_MAX_DISKS)
return;
if (port_stop(r) < 0) {
cprintf("ahci: port %d will not stop; skipping it\n", port);
return;
}
// Power and spin up first: with staggered spin-up the drive is not even
// turning until SUD is set, and its link never comes up until it is.
if (cap & CAP_SSS)
wr(r, PX_CMD, rd(r, PX_CMD) | CMD_POD | CMD_SUD);
if (!port_link(r))
return; // nothing plugged in
struct ahci_port *p = &disks[ndisks];
memset(p, 0, sizeof(*p));
p->r = r;
p->cl = dma_alloc(1024, 1024);
p->fis = dma_alloc(256, 256);
p->ct = dma_alloc(sizeof(struct cmdtbl), 128);
p->xfer = dma_alloc(BSIZE, 4096);
if (!p->cl || !p->fis || !p->ct || !p->xfer) {
cprintf("ahci: no DMA memory for port %d\n", port);
return;
}
// Without S64A the HBA cannot see above 4 GB. The DMA arena is below it
// (see include/dma.h), so this holds, and says so if that ever changes.
if (!(cap & CAP_S64A) && dma_phys(p->xfer) + BSIZE > 0xFFFFFFFFULL) {
cprintf("ahci: DMA memory above 4 GB and the HBA is 32-bit\n");
return;
}
memset(p->cl, 0, 1024);
memset(p->fis, 0, 256);
uint64 cl = dma_phys(p->cl), fb = dma_phys(p->fis), ct = dma_phys(p->ct);
p->cl[0].ctba = (uint32)ct;
p->cl[0].ctbau = (uint32)(ct >> 32);
wr(r, PX_CLB, (uint32)cl);
wr(r, PX_CLBU, (uint32)(cl >> 32));
wr(r, PX_FB, (uint32)fb);
wr(r, PX_FBU, (uint32)(fb >> 32));
wr(r, PX_SERR, 0xFFFFFFFF); // write-1-to-clear
wr(r, PX_IS, 0xFFFFFFFF);
wr(r, PX_IE, 0); // polled; see the top of the file
wr(r, PX_CMD, rd(r, PX_CMD) | CMD_FRE);
// The drive's first D2H FIS sets the signature; until then it reads all
// ones. ATAPI (an optical drive) and port multipliers are not disks here.
if (waitreg(r, PX_TFD, TFD_BSY | TFD_DRQ, 0, 3000) < 0) {
cprintf("ahci: port %d: the drive stays busy\n", port);
return;
}
uint32 sig = rd(r, PX_SIG);
if (sig != SIG_ATA) {
if (sig == SIG_ATAPI)
kinfo("ahci: port %d: ATAPI device, not used\n", port);
return;
}
wr(r, PX_CMD, rd(r, PX_CMD) | CMD_ST);
p->name[0] = 's'; p->name[1] = 'a'; p->name[2] = 't'; p->name[3] = 'a';
p->name[4] = '0' + ndisks; p->name[5] = 0;
initsleeplock(&p->lock, "ahci");
p->secsize = 512;
if (port_exec(p, ATA_IDENTIFY, 0, 0, 512, 0) < 0) {
cprintf("ahci: port %d: IDENTIFY failed\n", port);
return;
}
uint16 *id = (uint16 *)p->xfer;
if (!(id[83] & (1 << 10))) {
cprintf("ahci: port %d: no 48-bit LBA; not supported\n", port);
return;
}
p->nsectors = (uint64)id[100] | (uint64)id[101] << 16 |
(uint64)id[102] << 32 | (uint64)id[103] << 48;
// A logical sector larger than 512 (word 106 bit 12, size in 117-118 as
// words). Rare, and handled: the block layer works in BSIZE and asks for
// BSIZE / secsize of them.
if ((id[106] & 0xC000) == 0x4000 && (id[106] & (1 << 12)))
p->secsize = 2 * ((uint32)id[117] | (uint32)id[118] << 16);
if (p->secsize < 512 || p->secsize > BSIZE || BSIZE % p->secsize) {
cprintf("ahci: port %d: sector size %d not supported\n", port, p->secsize);
return;
}
char model[44];
model_of(id, model, sizeof(model));
ndisks++;
kinfo("ahci: %s: %s, %d MB, port %d\n", p->name, model,
(int)((p->nsectors * p->secsize) >> 20), port);
p->dev = blkdev_register(p->name, &ahci_ops, p, p->nsectors, p->secsize);
}
// Take the HBA from the firmware if it is holding it (section 10.6). Most
// firmware lets go at ExitBootServices; the specification says to ask, and
// some does not let go until asked.
static void
bios_handoff(volatile uint8 *hba)
{
if (!(rd(hba, HBA_CAP2) & CAP2_BOH))
return;
wr(hba, HBA_BOHC, rd(hba, HBA_BOHC) | BOHC_OOS);
if (waitreg(hba, HBA_BOHC, BOHC_BOS, 0, 25) < 0 ||
(rd(hba, HBA_BOHC) & BOHC_BB)) {
// BIOS busy: it is allowed two seconds to finish what it is doing.
waitreg(hba, HBA_BOHC, BOHC_BB | BOHC_BOS, 0, 2000);
}
}
static int
ahci_probe(struct pci_dev *d)
{
if (d->prog_if != 0x01)
return -1; // a RAID or IDE-mode controller
if (!d->bar[5].base || d->bar[5].is_io) {
cprintf("ahci: no ABAR; not driving this controller\n");
return -1;
}
pci_enable(d, PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
volatile uint8 *hba = ioremap(d->bar[5].base,
d->bar[5].size ? d->bar[5].size : 0x1100);
if (!hba)
return -1;
bios_handoff(hba);
// AHCI mode, with interrupts off at the HBA. No HBA reset: firmware has
// already brought the links up, and resetting throws that away for a
// second or more of COMRESETs per port to get it back.
wr(hba, HBA_GHC, (rd(hba, HBA_GHC) | GHC_AE) & ~(1u << 1)); // IE off
uint32 cap = rd(hba, HBA_CAP), pi = rd(hba, HBA_PI);
uint32 vs = rd(hba, HBA_VS);
kinfo("ahci: controller %x:%x, AHCI %d.%d, %d ports\n",
d->vendor, d->device, vs >> 16, (vs >> 8) & 0xFF, (cap & 0x1F) + 1);
for (int port = 0; port < AHCI_MAX_PORTS; port++)
if (pi & (1u << port))
port_init(hba, port, cap);
nhba++;
return 0;
}
static const struct pci_match ahci_match[] = {
{ PCI_ANY, PCI_ANY, 0x01, 0x06 }, // mass storage, SATA
{ 0, 0, 0, 0 },
};
const struct pci_driver ahci_driver = {
.name = "ahci",
.devname = "sata",
.match = ahci_match,
.probe = ahci_probe,
};

View file

@ -25,6 +25,7 @@
#include "buf.h" #include "buf.h"
#include "dma.h" #include "dma.h"
#include "usb.h" #include "usb.h"
#include "blkdev.h"
#define CBW_SIGNATURE 0x43425355 // "USBC" #define CBW_SIGNATURE 0x43425355 // "USBC"
#define CSW_SIGNATURE 0x53425355 // "USBS" #define CSW_SIGNATURE 0x53425355 // "USBS"
@ -332,6 +333,40 @@ usb_storage_dump(void)
brelse(b); brelse(b);
} }
// The same disk, as the block device registry sees it: whole, as "usb0", and
// with its partitions read and registered behind it. A stick written with
// tools/posix/mkdisk.sh -- or by any installer -- keeps SchFS in a partition
// next to its ESP, and the fixed USBDISK_DEV, which is the whole medium, can
// never see it there.
static int
ums_blk_rw(void *priv, uint64 lba, uint32 count, void *buf, int write)
{
int n;
(void)priv;
if(count * SECTOR_SIZE > BSIZE || lba + count > ums.nsectors)
return -1;
acquiresleep(&ums.lock);
if(write)
memmove(ums.xfer, buf, count * SECTOR_SIZE);
n = scsi_rw((uint32)lba, count, ums.xfer, write);
if(n == (int)(count * SECTOR_SIZE) && !write)
memmove(buf, ums.xfer, count * SECTOR_SIZE);
releasesleep(&ums.lock);
return n == (int)(count * SECTOR_SIZE) ? 0 : -1;
}
static int
ums_blk_flush(void *priv)
{
(void)priv;
return usb_storage_flush();
}
static const struct blkdev_ops ums_blk_ops = {
.rw = ums_blk_rw,
.flush = ums_blk_flush,
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static int static int
@ -407,6 +442,7 @@ storage_probe(struct usb_dev *dev)
cprintf("usb-storage: %s %s, %d sectors (%d MB)\n", cprintf("usb-storage: %s %s, %d sectors (%d MB)\n",
vendor, product, ums.nsectors, vendor, product, ums.nsectors,
(int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024))); (int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024)));
blkdev_register("usb0", &ums_blk_ops, 0, ums.nsectors, SECTOR_SIZE);
return 0; return 0;
fail: fail:

View file

@ -14,6 +14,7 @@
#include "file.h" #include "file.h"
#include "posix.h" #include "posix.h"
#include "errno.h" #include "errno.h"
#include "blkdev.h"
// Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S. // Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S.
_Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18, _Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18,
@ -884,6 +885,8 @@ rootdev_usable(uint dev)
return ramdisk_present(); return ramdisk_present();
if (dev == USBDISK_DEV) if (dev == USBDISK_DEV)
return usb_storage_present(); return usb_storage_present();
if (dev >= BLKDEV_FIRST)
return blkdev_present(dev);
// iderw() panics on an absent drive rather than failing, so a probe has to // iderw() panics on an absent drive rather than failing, so a probe has to
// ask first. That is the whole reason ide_disk_present() exists. // ask first. That is the whole reason ide_disk_present() exists.
return ide_disk_present((int)dev); return ide_disk_present((int)dev);
@ -892,16 +895,27 @@ rootdev_usable(uint dev)
static void static void
rootdev_mount(void) rootdev_mount(void)
{ {
static const struct { uint dev; const char *what; } cand[] = { // The fixed devices, with the registered ones -- SATA, NVMe, and every
// partition on them -- between USB and IDE. A partition GPT types as SchFS
// goes ahead of the rest of the registry, because it was labelled that way
// on purpose; the others are probed too, so a volume in an untyped or MBR
// partition, or on a whole unpartitioned disk, still boots.
struct { uint dev; const char *what; } cand[4 + 2 * NBLKDEV];
int ncand = 0;
#ifdef RAMDISK_ROOT #ifdef RAMDISK_ROOT
{ RAMDISK_DEV, "ramdisk" }, cand[ncand].dev = RAMDISK_DEV; cand[ncand++].what = "ramdisk";
#endif #endif
{ USBDISK_DEV, "USB disk" }, cand[ncand].dev = USBDISK_DEV; cand[ncand++].what = "USB disk";
{ 1, "IDE disk 1" }, for (int pass = 0; pass < 2; pass++)
{ 0, "IDE disk 0" }, for (int i = 0, d; (d = blkdev_nth(i)) >= 0; i++)
}; if (blkdev_is_schfs(d) == !pass) {
cand[ncand].dev = d;
cand[ncand++].what = blkdev_name(d);
}
cand[ncand].dev = 1; cand[ncand++].what = "IDE disk 1";
cand[ncand].dev = 0; cand[ncand++].what = "IDE disk 0";
for (int i = 0; i < (int)NELEM(cand); i++) { for (int i = 0; i < ncand; i++) {
if (!rootdev_usable(cand[i].dev)) if (!rootdev_usable(cand[i].dev))
continue; continue;
schfs_kmount(cand[i].dev); schfs_kmount(cand[i].dev);
@ -922,7 +936,7 @@ rootdev_mount(void)
// which is how the first version of this message came out as // which is how the first version of this message came out as
// `%-11s device 1596400`. See print_int() in kernel/core/console.c. // `%-11s device 1596400`. See print_int() in kernel/core/console.c.
cprintf("root: no SchFS volume on any candidate device. Looked at:\n"); cprintf("root: no SchFS volume on any candidate device. Looked at:\n");
for (int i = 0; i < (int)NELEM(cand); i++) for (int i = 0; i < ncand; i++)
cprintf(" device %d, %s: %s\n", cand[i].dev, cand[i].what, cprintf(" device %d, %s: %s\n", cand[i].dev, cand[i].what,
rootdev_usable(cand[i].dev) ? "present, but no SchFS on it" rootdev_usable(cand[i].dev) ? "present, but no SchFS on it"
: "not present"); : "not present");

73
tools/posix/mkdisk.sh Normal file
View file

@ -0,0 +1,73 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Build a partitioned SchOS disk: the image a real machine boots from.
#
# tools/posix/mkdisk.sh <esp-dir> <schfs.img> <output.img>
#
# A GUID partition table with two entries:
#
# 1 EFI system partition FAT, holding <esp-dir> (the loader and kernel)
# 2 SchFS <schfs.img>, byte for byte, typed
# 47138FA9-B79C-47A6-9F7D-AC1F7F572D5E
#
# This is the layout every UEFI machine expects: the firmware finds the ESP by
# its type and runs EFI/BOOT/BOOTX64.EFI from it, and the kernel finds the root
# by scanning the same table -- the SchFS type first, anything else after (see
# rootdev_mount() in kernel/proc/proc.c). Write it to a USB stick or a disk
# with dd and it boots; attach it to QEMU's q35 as its only disk and it boots
# the same way.
#
# The SchFS partition holds the volume unchanged. A filesystem addresses
# blocks from the start of whatever it is on, and the kernel hands a partition
# to it as a device whose block 0 is the partition's first sector -- so the
# image mkschfs writes for a whole disk is already the right content for a
# partition, and so is anything a Linux SchFS driver is given as /dev/sdX2.
#
# Needs sfdisk (util-linux) and mtools. Nothing here needs root or loop
# devices: the FAT is built in a file and copied in at its offset.
set -eu
esp=$1
vol=$2
out=$3
SCHFS_TYPE=47138FA9-B79C-47A6-9F7D-AC1F7F572D5E
ESP_TYPE=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
for t in sfdisk mformat mcopy; do
command -v $t >/dev/null || { echo "mkdisk: needs $t" >&2; exit 1; }
done
# Sizes in 512-byte sectors. The ESP is 64 MB, which leaves room for the
# kernel to grow by an order of magnitude; partitions start on 1 MB
# boundaries, which is what every partitioning tool does and what flash
# media erase in.
esp_mb=64
esp_start=2048
esp_sectors=$((esp_mb * 2048))
vol_bytes=$(wc -c < "$vol")
vol_start=$((esp_start + esp_sectors))
vol_sectors=$(( (vol_bytes + 511) / 512 ))
# 1 MB at the end for the backup GPT, rounded the same way.
total=$((vol_start + vol_sectors + 2048))
rm -f "$out" "$out.esp"
dd if=/dev/zero of="$out" bs=512 count=0 seek=$total 2>/dev/null
sfdisk --quiet --label gpt "$out" <<EOF
start=$esp_start, size=$esp_sectors, type=$ESP_TYPE, name="EFI system"
start=$vol_start, size=$vol_sectors, type=$SCHFS_TYPE, name="SchOS"
EOF
# The ESP, built apart and copied in.
dd if=/dev/zero of="$out.esp" bs=1M count=$esp_mb 2>/dev/null
mformat -i "$out.esp" -F -v SCHOS ::
mcopy -i "$out.esp" -s "$esp"/* ::
dd if="$out.esp" of="$out" bs=512 seek=$esp_start conv=notrunc 2>/dev/null
rm -f "$out.esp"
dd if="$vol" of="$out" bs=512 seek=$vol_start conv=notrunc 2>/dev/null
echo "mkdisk: $out -- ESP ${esp_mb} MB, SchFS $((vol_bytes >> 20)) MB"