Zyxxyz, the Firmest Booty of the Wares

What is BIOS / UEFI?

BIOS (Basic Input/Output System) is the first code that runs when you power on a computer. It lives in a small ROM chip on the motherboard and handles three jobs before anything else can happen:

  • Initialise every piece of hardware — CPU, RAM, storage controllers, USB, video.
  • Run the POST to verify hardware is healthy.
  • Hand control to the bootloader found on your storage device.

Modern systems replace the old BIOS with UEFI, which adds a graphical menu, network stack, driver model, and Secure Boot. Both serve the same fundamental purpose — just with very different implementations.

Tutorial — Building a Basic BIOS
This guide walks through the actual mechanics of a minimal x86 BIOS in assembly language. You do not need to compile or flash anything — use the emulator below to follow along interactively.
Step 1 — The Reset Vector

When power is applied, the CPU always begins execution at a fixed address called the reset vector. On classic x86 this is 0xFFFF:0x0000 (physical address 0xFFFF0), just 16 bytes before the top of the 1 MB address space. The BIOS ROM is mapped here so the very first instruction the CPU fetches is your code.

That 16-byte gap is too small for real code, so the reset vector is always a far jump into the main body of the BIOS:

; Assembled to load at 0xF0000 in the ROM image ; CPU starts in 16-bit Real Mode after reset BITS 16 ORG 0xFFFF0 ; reset vector location reset_vector: jmp 0xF000:bios_main ; far jump to BIOS start nop ; ── Main BIOS entry point ────────────────────────────────── ORG 0xF0000 bios_main: cli ; disable interrupts while we initialise cld ; clear direction flag (string ops go forward) ; zero all segment registers xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, 0x7C00 ; temporary stack below the boot sector
Real Mode — after reset the CPU is in Real Mode, a legacy compatibility state where only 1 MB of RAM is reachable and there is no memory protection. BIOS runs entirely in this mode; it hands Protected Mode off to the operating system bootloader later.
Step 2 — Initialise the PIT and PIC

Before any timed or interrupt-driven code can run, the BIOS must configure two essential chips:

  • The PIT — generates the system clock tick (IRQ 0) and drives timing.
  • The PIC — routes hardware IRQs from devices to the CPU.
init_pit: ; Channel 0, mode 3 (square wave), 18.2 Hz mov al, 0x36 ; control word: ch0, lo/hi byte, mode 3 out 0x43, al ; PIT command port mov ax, 0xFFFF ; divisor = 65535 → ~18.2 Hz out 0x40, al ; low byte → PIT channel 0 mov al, ah out 0x40, al ; high byte ret init_pic: ; ICW1 — cascade mode, edge-triggered, ICW4 needed mov al, 0x11 out 0x20, al ; master PIC command out 0xA0, al ; slave PIC command ; ICW2 — remap IRQ vectors (avoid conflict with CPU exceptions) mov al, 0x08 ; master IRQ 0-7 → INT 0x08-0x0F out 0x21, al mov al, 0x70 ; slave IRQ 8-15 → INT 0x70-0x77 out 0xA1, al ; ICW3, ICW4 (cascade wiring, 8086 mode) … ret
Step 3 — Memory Test (POST)

The POST memory test writes a pattern to each memory cell and reads it back. Bad cells get flagged and subtracted from the usable total. The BIOS also establishes the BDA (BIOS Data Area) at address 0x0400.

post_memory_test: mov cx, 0x0400 ; start checking from 1 KB (skip IVT+BDA) mov bx, 0x9FFF ; end of conventional RAM (640 KB mark) .loop: mov word [cx], 0xAA55 ; write test pattern cmp word [cx], 0xAA55 ; read it back jne .bad_cell add cx, 2 cmp cx, bx jbe .loop ret ; all cells passed .bad_cell: ; record faulty address in BDA, optionally beep jmp halt_system ; fatal error
Real BIOSes also initialise the IVT before testing RAM so that fault handlers can produce beep codes during the test. The order matters: IVT → PIC → PIT → RAM test → hardware detection → set services → boot.
Step 4 — The Interrupt Vector Table

The IVT at address 0x0000 maps interrupt numbers to handler routines. The BIOS populates it with its own service routines — for example:

  • INT 0x10 — Video services (print character, set mode)
  • INT 0x13 — Disk services (read/write sectors)
  • INT 0x16 — Keyboard services (read key, check buffer)
  • INT 0x1ARTC / time services
init_ivt: ; Each IVT entry = 4 bytes: offset (2) + segment (2) ; INT 0x10 lives at IVT[0x10] = address 0x0040 mov word [0x0040], int10_handler ; offset mov word [0x0042], 0xF000 ; segment (BIOS ROM segment) mov word [0x004C], int13_handler mov word [0x004E], 0xF000 mov word [0x0058], int16_handler mov word [0x005A], 0xF000 sti ; enable interrupts now IVT is ready ret ; ── Example: INT 0x10 AH=0x0E (teletype print) ────────────── int10_handler: cmp ah, 0x0E ; teletype output? jne .done out 0x3F8, al ; write char to first serial port (for debug) ; … real code writes to VGA framebuffer at 0xB8000 … .done: iret ; Interrupt RETurn — restores flags, CS, IP
Step 5 — The A20 Line

One of the quirkiest parts of x86 history: the A20 gate. Early PCs had only 20 address lines; addresses above 1 MB wrapped silently back to 0. When Intel added the 21st address line with the 286, this wrap-around behaviour was controlled by toggling a bit in the keyboard controller, of all places.

The BIOS must enable A20 before switching the CPU to Protected Mode, otherwise memory accesses above 1 MB silently fail.

enable_a20: ; Fast A20 method (port 0x92, works on most modern hardware) in al, 0x92 or al, 0x02 ; set bit 1 = enable A20 and al, 0xFE ; clear bit 0 (do not reset!) out 0x92, al ret
Step 6 — Boot Device Detection

After POST the BIOS checks each device in the configured boot order for a valid MBR or GPT partition table. Classic BIOS looks for the two-byte signature 0x55 0xAA at the end of the first sector:

find_boot_device: ; Try each drive in the BIOS boot order (stored in CMOS) mov dl, 0x80 ; first hard disk (0x80 = HDD, 0x00 = floppy) .try_disk: ; INT 13h, AH=02h — read sector mov ah, 0x02 ; function: read sectors mov al, 0x01 ; sector count: 1 (512 bytes) mov ch, 0x00 ; cylinder 0 mov cl, 0x01 ; sector 1 (1-indexed) mov dh, 0x00 ; head 0 mov bx, 0x7C00 ; load to 0x0000:0x7C00 int 0x13 ; call BIOS disk service jc .next_disk ; carry set = error, try next ; Verify MBR signature cmp word [0x7DFE], 0xAA55 jne .next_disk ; no signature → not bootable ; Jump to bootloader jmp 0x0000:0x7C00 .next_disk: inc dl ; try next drive number cmp dl, 0x82 ; checked 2 hard disks? jne .try_disk ; No bootable device found — print error and halt jmp halt_system
Step 7 — UEFI vs BIOS

UEFI takes a fundamentally different approach. Instead of a tiny assembly stub crammed into a ROM chip, UEFI is a full software ecosystem written in C with a defined API called Boot/Runtime Services.

  • UEFI reads a GPT partitioned disk and looks for an ESP (EFI System Partition) instead of an MBR.
  • Secure Boot validates every EFI binary with a cryptographic signature before running it.
  • Boot entries are stored in NVRAM variables, so the firmware remembers which OS to boot without scanning every disk.
  • ACPI tables expose hardware topology (CPU cores, power states, thermal sensors) to the operating system in a standardised way.

The result: UEFI boots faster, supports modern hardware, and is far more extensible — but is also significantly more complex to implement from scratch.

Want to explore UEFI development? The open-source EDK II (EFI Development Kit) from TianoCore is the reference implementation used by most motherboard vendors.
Virtual BIOS — Interactive Emulator

This emulator simulates a real BIOS setup utility. Click Power On to watch the POST sequence, then press DEL (or click the button) to enter the BIOS Setup menu. Use ↑↓ to navigate, Enter to edit, F10 to save and exit.

Click Power On to begin
── Power off. Press "Power On" to start. ──
UEFI BIOS Utility  |  Whymzykal Systems v1.0.0