Read

BIOS and UEFI: why a browser VM still boots like it is 1981

The interrupt-call API IBM shipped in 8 KB of ROM, the thirty years it survived, the small operating system that replaced it, and why the machines here still use the old one.

When a machine in this site's catalog reads its first sector, it does it by putting 0x02 in AH, a drive number in DL, a buffer address in ES:BX and executing INT 13h — the same call, with the same registers, that IBM documented for the 5150 in August 1981. Nothing about that is nostalgia. It is that the contract was never broken.

Eight kilobytes of ROM, and a table of entry points

The original IBM PC BIOS was 8 KB of read-only memory mapped at 0xFE000–0xFFFFF. It had two jobs. The first was to test the machine and find something to boot, which is covered in what actually happens when a computer boots. The second, and the one that mattered historically, was to stay resident afterwards as a library of device routines that any program could call.

There was no linker and no shared library format, so the calling convention was the software interrupt. The BIOS filled slots in the interrupt vector table with pointers into its own ROM, and from then on INT 10h meant video, INT 13h disk, INT 16h keyboard, INT 1Ah the clock, INT 11h the equipment list. Arguments went in registers, AH chose the function, the carry flag reported failure. That is the whole API.

What a call looks like

Here is the disk read, in both forms it has ever taken — the 1981 original and the extension added when disks outgrew it.

; The 1981 call: read sectors by cylinder / head / sector.
        mov     ah, 0x02        ; function 02h: read sectors
        mov     al, 8           ; how many
        mov     ch, 0           ; cylinder, low 8 bits
        mov     cl, 2           ; sector 1..63  (sectors are numbered from 1)
        mov     dh, 0           ; head
        mov     dl, 0x80        ; drive: 0x00 = floppy A, 0x80 = first hard disk
        mov     bx, 0x8000      ; ES:BX = where to put the data
        int     0x13
        jc      disk_error      ; carry set = it failed, AH = error code

; The replacement: INT 13h extensions, LBA in a packet in DS:SI.
        mov     ah, 0x42        ; function 42h: extended read
        mov     dl, [boot_drive]
        mov     si, dap
        int     0x13
        jc      disk_error

dap:    db      0x10            ; size of this packet, in bytes
        db      0               ; reserved
        dw      8               ; sectors to read
        dw      0x8000          ; destination offset
        dw      0x0000          ; destination segment
        dq      1               ; starting LBA, 64 bits

Two things are worth noticing. Sectors are numbered from one while heads and cylinders start at zero, an off-by-one that has cost more debugging hours than it should. And CL carries a six-bit sector number with its top two bits stolen for the cylinder — the packing you do when every byte of ROM is expensive.

Why it lasted thirty years

Because it was small enough to reimplement. Compaq had engineers write a clean-room BIOS in 1982 so it could ship a portable that ran IBM's software, and Phoenix Technologies licensed a compatible one to anybody from 1984. That is the whole clone industry in two sentences: the hardware was off the shelf, and the only proprietary part was 8 KB of ROM that could be legally rewritten from its documented behaviour.

It also survived because operating systems stopped depending on it without ever being able to abandon it. DOS was built directly on those interrupts. Windows 95 and every Linux since replace them with their own protected-mode drivers within a second of starting. But the code that gets you to those drivers — the boot sector, the second stage, the loader that reads the kernel — has no drivers of its own. It runs before anything is set up, so it uses the only interface that exists.

Where it ran out

The constraints that made it easy to copy are the same ones that eventually made it untenable.

  • 16-bit real mode, one megabyte. Every BIOS service runs in the processor's 1978 mode, with segment arithmetic and a 20-bit address bus. A 64-bit system that wants to call one must drop back down and climb out again.
  • 640 KB of usable memory, because everything above is reserved for video, option ROMs and the BIOS. Every DOS memory manager ever written exists to work around that boundary.
  • A one-sector bootstrap. 512 bytes, of which 446 are code once the partition table and signature are subtracted. Anything real needs a second stage, which needs somewhere to hide — hence the unpartitioned gap on every disk.
  • Disk size limits, repeatedly. Cylinder/head/sector addressing capped out at 504 MiB, then 8.4 GB with translation tricks. The extensions fixed the interface, but the MBR still records a partition's start and length as 32-bit sector counts: 2 TiB at 512 bytes a sector, and no further.
  • No way to extend it. A card could add a service only by placing a 16-bit option ROM in the fixed window between 0xC0000 and 0xEFFFF, hooking an interrupt vector and hoping nothing else wanted the same one.
  • No notion of trust. The firmware jumps to whatever code sits in the first sector, provided two bytes at offset 510 are 0x55 0xAA. There is nothing that could be verified.

UEFI: firmware as a small operating system

Intel began work on EFI in the late 1990s for Itanium, a processor with no PC BIOS to inherit and no real mode to run one in. The specification passed to a multi-vendor body, the UEFI Forum, in 2005, and by the early 2010s it was what shipped on ordinary PCs. The idea underneath it is that firmware should be a small operating system rather than a table of entry points: it has a driver model, a memory allocator, a filesystem, a shell and an event loop, and hands all of that to the program it boots.

Because it is a small operating system, it needs an executable format, and it borrowed one: PE32+, the same COFF-derived format Windows uses. A UEFI bootloader is a normal 64-bit program with a main that receives a handle and a pointer to a table of services. It runs with paging on, memory identity-mapped and the whole address space available. Nothing about it looks like a boot sector.

A partition full of programs

Where BIOS had a sector, UEFI has a filesystem: the EFI System Partition, formatted FAT32 and marked with the type GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B. Bootloaders are files on it, usually under a vendor directory such as \EFI\ubuntu\grubx64.efi. Removable media use a fallback path the firmware tries without being told: \EFI\BOOT\BOOTX64.EFI on 64-bit x86, BOOTIA32.EFI on 32-bit. That fallback is the only part of UEFI booting that resembles the old convention.

The boot order lives in the firmware

A BIOS boot order is a short list of device classes. A UEFI boot order is a list of named entries in the board's non-volatile memory: variables Boot0000 through BootFFFF, each holding a description, a device path and optional arguments, with BootOrder giving the sequence. Installing an operating system writes one; efibootmgr on Linux and bcdedit on Windows edit them from a running system. A machine's boot configuration is now software state, and it can be lost.

GPT instead of MBR

UEFI brought its own partition table. GPT puts a protective MBR at LBA 0 — one fake partition of type 0xEE covering the disk, so old tools see something they will not touch — a header at LBA 1, and 128 entries of 128 bytes from LBA 2. Addresses are 64-bit, partitions have GUIDs and names, header and table are CRC-checked, and a full backup sits at the end of the disk. At 512-byte sectors the theoretical limit is 8 ZiB.

Secure Boot, and what it does not do

Once the firmware loads programs from a filesystem, it can check them first. Secure Boot verifies the signature on every image before executing it, against key databases in NVRAM: PK at the root, KEK to authorise updates, db listing what is allowed, dbx what is revoked. Most machines ship with Microsoft's keys in db, which is why Linux distributions boot through shim — a small loader signed by Microsoft that then verifies the distribution's own bootloader.

What it guarantees is narrow: that the code the firmware executed carried a signature the firmware trusts. It does not record what ran — that is measured boot, a separate mechanism using TPM registers. It does not protect anything after the kernel starts. And it does not help if the signed code is itself exploitable: BootHole, in 2020, was a buffer overflow in GRUB2's config parser, reachable through a loader everyone's firmware trusted, and later bootkits shipped an old, still-signed Microsoft loader with a known flaw. Revocation exists, but pushing entries into dbx has repeatedly bricked dual-boot machines. It raises the cost of a bootkit. It is not a wall.

The Compatibility Support Module

UEFI firmware could not simply stop booting the world's existing disks, so most of it shipped a CSM: a module that reconstructs the legacy environment — interrupt vector table, INT 10h, INT 13h, the 1 MB memory map — and then does the old thing, loading a 512-byte sector to 0x7C00 and jumping. It is a BIOS emulator inside the firmware, and it works only because the processor still has real mode. Intel announced in 2017 that it intended to drop legacy support by 2020, and boards from then on increasingly have no CSM at all. Windows 11 requires UEFI and Secure Boot capability outright. A legacy-bootable image is now, in practice, something you run in a virtual machine.

The two, side by side

PropertyBIOSUEFI
Mode at handoff16-bit real mode, 1 MB addressable32- or 64-bit, paging on, memory identity-mapped
Partition schemeMBR: four primaries, 32-bit sector fieldsGPT: 128 entries, 64-bit LBAs, CRC-checked, backup table
Bootloader locationFirst 512-byte sector, plus a hidden gap for stage twoA PE32+ file on a FAT32 partition: \EFI\BOOT\BOOTX64.EFI
Maximum disk2 TiB (2^32 sectors of 512 bytes)8 ZiB at the same sector size
Extensibility16-bit option ROMs in a fixed window, hooking interrupt vectorsDrivers and applications as loadable modules, services found at runtime
VerificationTwo bytes at offset 510Signature checked against db and dbx, if Secure Boot is on
Boot configurationA list of device classes in CMOSNamed Boot#### variables in NVRAM, sequenced by BootOrder

Why this site runs SeaBIOS

The emulator underneath TempMV is v86, and the firmware it loads is SeaBIOS — an open-source implementation of a 16-bit x86 BIOS, the same one QEMU boots by default. It is a genuine BIOS, not a shim: it runs POST, builds the vector table, provides INT 10h, INT 13h and the rest, and prints Booting from Hard Disk... before it jumps. Its ROM is well under a megabyte.

There is no UEFI option, and the reason is one level down. v86 emulates a 32-bit x86 machine with no long mode, and the practical open UEFI implementation — TianoCore's OVMF — is built and tested for 64-bit, is several megabytes of firmware, and expects a richer device model than v86 provides. Support has been asked for; nobody has done the work. How x86 emulation works in the browser explains why the 32-bit limit is not a small thing to lift.

What that means for what you can boot

Everything here has to be legacy-bootable and 32-bit or smaller. A current Ubuntu or Fedora image fails on both counts, and there is no workaround. What works is anything that still ships an MBR boot sector or an El Torito ISO with a BIOS-mode loader: FreeDOS and MS-DOS 4 from floppy images, the small Linux distributions through SYSLINUX or GRUB, KolibriOS from a 1.44 MB floppy written in assembly, MINIX 3, 9front, and 32-bit builds of Haiku and ReactOS. Buildroot skips firmware entirely: the emulator loads its bzImage directly.