Read

Disk images explained: ISO, IMG, floppies and kernels

What the file you are about to boot actually is, why raw and qcow2 are not the same kind of thing, and how to work out which slot it belongs in.

A disk image has no structure of its own. It is a file whose byte 0 is the disk's byte 0, whose byte 512 begins the second sector, and which carries on that way to the end. Nothing in it marks the partitions, records that it came from a CD rather than a floppy, or states how large the sectors were. All of that is inferred, and most trouble with images comes from inferring it wrong.

Raw, and everything that is not raw

The raw family is the one just described: .img, .raw, .bin, .hdd, .dsk, and often no extension at all. They are interchangeable. disk.img renamed to disk.raw is the same object, because the name was never load-bearing — the bytes are the disk.

The other family wraps those sectors in a container. QEMU's qcow2 — the file opens with the magic QFI\xfb — stores the guest's sectors in clusters and maps a guest offset to a position in the host file through two levels of table: an L1 table pointing at L2 tables, each L2 table exactly one cluster in size. Clusters never written are simply absent, so a 40 GB image of a system with 2 GB installed occupies about 2 GB. It also understands a backing_file: reads fall through to a named parent for any cluster the child has not got, writes land in the child. That is how ten virtual machines share one base install and cost the size of their differences.

The rest differ in details: VMware's VMDK (sometimes a text descriptor plus separate extent files), VirtualBox's VDI, Microsoft's VHD with its ceiling of 2,040 GB, and VHDX, which raised that to 64 TB and added a metadata log so a host crash cannot corrupt the image's bookkeeping.

v86, the emulator behind the machines here, has no image-format parsers at all. Attach a hard disk and it serves sectors straight out of an ArrayBuffer: sector *n* is at offset *n* × 512, and that is the entire implementation. A qcow2 in that slot is not rejected — its header is read as a boot sector, no 55 AA is found, and the BIOS moves on. Convert first.

ExtensionWhat it isBootable howWorks here
.img .raw .bin .hddRaw sectors — floppy or hard disk; the extension does not say whichMBR at offset 0, or a FAT boot sector on a floppyYes
.isoISO 9660 optical image, 2,048-byte logical sectorsEl Torito boot catalog, if one was writtenYes, CD-ROM slot
.qcow2QEMU container: sparse clusters, copy-on-write, backing filesOnly once a hypervisor unwraps it into sectorsConvert to raw
.vmdk .vdiVMware and VirtualBox containersThrough their own hypervisorConvert to raw
.vhd .vhdxMicrosoft containers; VHD caps at 2,040 GB, VHDX at 64 TBThrough Hyper-VConvert to raw
bzImage vmlinuzA compressed Linux kernel. No sectors, no partition tableLoaded into memory and jumped to, no bootloaderYes, kernel slot
.cpio.gz (initrd)An archive the kernel unpacks into RAMNot alone; it is the kernel's first root filesystemYes, with a kernel

A floppy is a geometry, not a size

The 1.44 MB floppy is 80 cylinders × 2 heads × 18 sectors × 512 bytes = 1,474,560 bytes, exactly. It is not 1.44 of anything: divided by 1,048,576 that is 1.40625. The marketing figure came from dividing by 1,024 once and by 1,000 once, which is the only place that unit has ever been used.

The number matters because the floppy controller is not addressed by offset. You give it a cylinder, a head and a sector, and it seeks. An image therefore needs a geometry, and since a raw file carries none, the emulator infers it from the file's length against a table of standard formats: 360 KB, 720 KB, 1,200 KB, 1,440 KB, 2,880 KB. A file matching none of them gets a guess, and the guess fails quietly — the first sector runs, then a read walks off the end of a track that is not the length the controller believes, and you have a cursor and no message.

The image usually holds a second opinion about its own shape. A FAT floppy's boot sector carries a BIOS Parameter Block, where offset 0x15 is the media descriptor (0xF0 for 1.44 MB), 0x18 is sectors per track and 0x1A is heads. When those disagree with the controller, DOS and the BIOS read the same disk two different ways. The fields are laid out in what a filesystem actually is.

Hard disks: CHS, LBA and the first 512 bytes

A hard disk image opens with a Master Boot Record: one sector divided three ways — 446 bytes of code, four 16-byte partition entries starting at offset 446 (0x1BE), and the signature 55 AA at offset 510. Modern MBRs spend only 440 bytes on code, then four on a disk signature and two on zeros, because Windows NT wanted somewhere to write a unique disk ID.

Each entry is a status byte (0x80 means active), three bytes of starting CHS, a type byte (0x83 Linux, 0x0C FAT32 with LBA, 0xEF an EFI System Partition), three bytes of ending CHS, then a 32-bit starting LBA and a 32-bit sector count. Those last two are why MBR stops at 2 TiB: 2³² sectors of 512 bytes and no more.

CHS addressing died because it ran out: the old INT 13h call gave 10 bits of cylinder, 8 of head and 6 of sector, which stops at 8.4 GB. The CHS fields are still written, usually saturated to FE FF FF, meaning *past what I can express, use the LBA*. The two calling conventions are set out in BIOS and UEFI; the handoff they belong to is in what actually happens when a computer boots.

ISO 9660 and El Torito

An ISO is a filesystem for a medium that cannot be rewritten, with 2,048-byte logical sectors rather than 512. Its first 32 KB — sixteen sectors — is the System Area, which the standard declares none of its business. The Primary Volume Descriptor sits at sector 16, byte offset 32,768: a type byte 0x01, the five ASCII characters CD001, a version byte 0x01.

Bootability is bolted on separately by El Torito, published by Phoenix and IBM in January 1995. A Boot Record Volume Descriptor goes at sector 17, carrying the identifier EL TORITO SPECIFICATION and a pointer to a boot catalog elsewhere on the disc. The catalog entry names an emulation type and a count of 512-byte virtual sectors. Almost everything modern uses no-emulation mode: the firmware reads that run — commonly four sectors, one 2,048-byte CD block — to 0x7C00 and jumps, exactly as for a floppy. The other modes make the BIOS pretend the disc is drive 0x00 or 0x80 and serve an image embedded inside it.

So an ISO with no El Torito record is a perfectly valid ISO 9660 filesystem and completely unbootable. Data discs, archived collections and anything built by mkisofs without -b behave this way: mount them and every file is there; boot them and the firmware finds nothing and moves on. The image is not corrupt. It was never built to start a computer.

Kernels and initrds are not disk images

A bzImage has no sectors and no filesystem. It is a Linux kernel with a small real-mode setup header at the front — the magic HdrS at offset 0x202 — followed by a compressed payload and the decompressor that unpacks it. The bz is *big zImage*, nothing to do with bzip2: the format exists because the older zImage had to fit below 640 KB and kernels stopped fitting.

Normally a bootloader reads that header, fills in the command line pointer and the initrd address, and jumps in. v86 does the bootloader's job itself: give it a kernel and a command line and it places the image in memory and starts it, skipping firmware, boot sector and GRUB entirely. That is why a Buildroot machine reaches a shell in seconds while an ISO of a full distribution takes a minute or more.

An initrd is an archive, not an image: cpio in the newc format, every member preceded by a 110-byte header of ASCII hex beginning 070701, normally gzipped. The kernel unpacks it into a tmpfs and runs /init. No superblock, no partition table, no boot sector, so it will never boot on its own.

Working out what you have

Five commands settle almost every case. The last two want cdrkit and util-linux installed.

$ file *
alpine.iso:  ISO 9660 CD-ROM filesystem data 'alpine-standard' (bootable)
boot.img:    DOS/MBR boot sector, code offset 0x3c+2, OEM-ID 'mkfs.fat',
             sectors/cluster 1, root entries 224, sectors 2880 (volumes <=32 MB),
             FAT (12 bit), sectors/FAT 9, sectors/track 18, heads 2
disk.qcow2:  QEMU QCOW2 Image (v3), 42949672960 bytes
bzImage:     Linux kernel x86 boot executable bzImage, version 6.6.32, RO-rootFS

$ stat -c %s boot.img
1474560                 # 80 * 2 * 18 * 512 exactly: a 1.44 MB floppy

$ xxd -s 510 -l 2 hda.img
000001fe: 55aa                                     U.
                                                   ^ all the BIOS checks

$ xxd -l 32 hda.img
00000000: eb63 9010 8ed0 bc00 b0b8 0000 8ed8 8ec0  .c..............
00000010: fbbe 007c bf00 06b9 0002 f3a4 ea21 0600  ...|.........!..

$ fdisk -l hda.img
Disk hda.img: 512 MiB, 536870912 bytes, 1048576 sectors
Units: sectors of 1 * 512 = 512 bytes
Disklabel type: dos
Disk identifier: 0x1b8f2e04

Device     Boot Start     End Sectors  Size Id Type
hda.img1   *     2048 1048575 1046528  511M 83 Linux

$ isoinfo -d -i alpine.iso | head -6
CD-ROM is in ISO 9660 format
System id: LINUX
Volume id: alpine-standard
Logical block size is: 2048
Volume size is: 96256
El Torito VD version 1 found, boot catalog is in sector 20

The last line answers the question people actually have: no El Torito line means no boot catalog, which means the disc will not boot, whatever else is on it.

Converting, and starting from nothing

# Any container to raw. Works for qcow2, vmdk, vdi, vhd and vhdx alike.
qemu-img convert -f qcow2 -O raw disk.qcow2 disk.img

# What am I holding, and does it have a parent?
qemu-img info disk.qcow2

# A blank 512 MB hard disk. Sparse: it costs almost nothing until written.
qemu-img create -f raw blank.img 512M
truncate -s 512M blank.img          # identical result, no qemu needed

# A blank, formatted 1.44 MB floppy, the right size to the byte.
dd if=/dev/zero of=floppy.img bs=512 count=2880
mkfs.fat -F 12 -n TEMPMV floppy.img

# Lift the first sector off a device, or write one back.
dd if=/dev/sdb of=mbr.bin bs=512 count=1

Expect one surprise: a raw image is as large as the disk claimed to be, not as large as the data in it. A 40 GB qcow2 holding 2 GB of files becomes a 40 GB file. It may still occupy 2 GB on a host filesystem that supports holes, but it is 40 GB to a browser reading it into memory. Shrink the filesystem and the partition first, then mount a machine.

What happens when you pick the wrong slot

Nothing dramatic, which is the problem. An ISO attached as hda is not refused: the emulated IDE controller serves its bytes in 512-byte sectors, the BIOS reads sector 0 and finds the System Area, which is zeros. No signature, no boot, next device. Unless the ISO is a hybrid image — isohybrid, which most current install ISOs are — in which case a real MBR has been grafted into that System Area precisely so the file can be written to a USB stick with dd. Those boot from the hard disk slot quite happily, which is the whole explanation for why some ISOs work there and others do nothing.

A bzImage in the floppy slot is stranger, because it half works. The setup header still keeps the word AA55 at offset 0x1FE, a leftover from when you could write a kernel straight onto a floppy, so the BIOS loads and runs the first sector. Kernels up to 6.4 answered with a stub that printed Use a boot loader. and stopped. Linux 6.5 removed the stub to make room in the header, so newer kernels do not even say that — the machine executes whatever those bytes happen to mean and wanders off. Put the kernel in the kernel slot.

The reverse mistake fails cleanly: a floppy image in the CD-ROM slot is served as 2,048-byte sectors, the firmware looks at sector 17 — byte 34,816 — for CD001 and does not find it.

Images that live somewhere else

A machine can start before its image has finished downloading. Given a URL, a declared size and async: true, v86 fetches only the sectors the guest actually reads, using HTTP Range: bytes= requests. A 700 MB ISO that reaches a login prompt after touching 30 MB of itself costs 30 MB of transfer.

The conditions are unforgiving. The server must answer a ranged request with 206 Partial Content rather than the whole file, and advertise Accept-Ranges: bytes. If it is on a different origin from the page — which it always is, for someone else's mirror — it must also send an Access-Control-Allow-Origin header covering that page. Plenty of hosts do the first and not the second. When the console shows a CORS error, nothing is wrong with the image and nothing can be done from inside the browser: that restriction is the point of the same-origin policy. Download the file and attach it locally instead. Anything attached that way is read in the page and uploaded nowhere, because there is nowhere to upload it to.