The Bootsector

The Bootsector

Robbert Haarman

2010-12-11


Introduction

The operating system starts at the bootloader. The machine reads the first 512 bytes of the disk (called the boot sector) into memory and executes it. In a very simplistic scenario, the boot sector could contain actually contain the whole OS. Usually, however, the boot sector contains code that either loads the kernel, or a so-called second-stage boot loader, which subsequently loads the OS.


Simple Bootsector

Let's look at a very simple boot sector:

Compile the boot sector with nasm as follows:

nasm simple_bootsect.asm -o simple_bootsect.bin

Next, create a floppy disk image:

dd if=/dev/zero of=bootdisk.bin bs=512 count=2880

Write the boot sector to the disk image:

dd if=simple_bootsect.bin of=bootdisk.bin conv=notrunc

You can now boot the image with qemu by issuing

qemu -fda bootdisk.bin -boot a

As an alternative to creating a disk image, you could write the boot sector to a real diskette by saying

dd if=simple_bootsect.bin of=/dev/fd0

after which you can insert the diskette in a PC and boot it up.

What it does

There is quite a bit of magic in this little program. I'll analyze it block by block.


push word 0b800h
pop es
xor di, di
mov [es:di], word 441h

This piece of code puts a red A in the top-left corner of the display. The first line pushes the segment of the display on the stack. This is the address at which the display starts. The next line takes this value from the stack and puts it in es, the Extra Segment register. xor di, di takes the exclusive OR of di (Destination Index) with itself, effectively making di equal to zero. The last line writes the word 441h at the address specified by the segment es and the offset di (the top-left corner of the screen). 41h is the ASCII code for A, and 4 indicates a red foreground color.

The x86 Memory Model

The x86 architecture uses a segmented memory model. Memory addresses are determined by a segment and an offset within that segment. In real mode, which is what x86 machines start up in, the memory address is calculated by multiplying the segment by 16 and adding the offset. Since segment registers are 16 bits, this allows a total of 1 megabyte (216 * 16 bytes) to be addressed. The area between 640 KB and 1 MB is reserved for the BIOS, so that roughly 640 KB is available to program code.


jmp $

This instruction executes an unconditional branch to its own address. This causes an infinite loop; the instruction is read and executed over and over again.


times 510 - ($ - $$) db 0
db 55h
db 0aah

The boot sector for x86 machines is required to contain a magic value or signature at the last two bytes. This code inserts bytes with value zero until the output contains 510 bytes, then writes the magic value. Note that this will only work if the code coming before takes 510 or fewer bytes.


Minimal Usable Bootsector

Now, let's see a usable (albeit still very simplified) boot sector.

Analysis

We first define a couple of constants that are used later on in the code. Then, we load the kernel using a BIOScall:


mov ax, 200h + KERNEL_SIZE
push word KERNEL_SEGMENT
pop es
xor bx, bx
mov cx, KERNEL_START + 1
mov dx, 0
int 13h
jnc ok
jmp $
ok:

The actual read is performed by the instruction that reads int 13h. This interrupt call invokes the BIOS to perform the read. It expects the following register values:

AH = 2	(opcode to read sectors)
AL = number of sectors to read
ES:BX = segment and offset to write data to
CH = cylinder to read from
CL (lower 6 bits) = sector to start reading
CL (higher 2 bits) = high-order bits of cylinder
DH = head
DL = drive number

AH and AL are the high-order and low-order byte of AX, so that mov ax, 200h + KERNEL_START sets AH to 2, and AL to KERNEL_START. If anything goes wrong during the read, the BIOSsets the carry flag. The instruction jnc (Jump if Not Carry) tests the carry flag, and jumps to the specified label if the carry flag is clear. This skips over the infinite loop. Now, what are all those cylinders, sectors, heads, and drives about? The drive number is simple. It is a number assigned (by the BIOS) to every drive in the system. In this case, we are booting from the first floppy drive, which is 0. The second floppy drive would be 1, the first had drive 80h. The cylinders and such are explained below.

Drive Geometry

PC floppy drives (and also hard drives, especially older ones) have a so-called drive geometry, which is expressed as cylinders, heads and sectors (CHS). The drive consists of one or more platters, which are disks that spin around.

Cylinders are obtained by slicing the platter at regular intervals; e.g. the outer edge becomes cylinder 0, the slice just a little to the inside becomes cylinder 1, a bit more to the inside is cylinder 2, and so on.

The heads float above the platters and detect the magnetic fields on the platter, thus reading the data stored on the platter. They can be moved around to float above different cylinders, thus accessing different parts of the disk.

Sectors are best imagined as pie or pizza slices. They denote a certain section on each cylinder.

Together, the cylinders, heads and sectors define the total capacity of a disk drive. For example, a 1.44 megabyte diskette has 80 cylinders, 2 heads (one on either side of the disc), and 18 sectors per track. This makes for a total of 80 * 2 * 18 = 2880 sectors. Each sector being 512 bytes, or half a kilobyte, this equals 1440 kilobytes.

If the read is successful, we jump into the kernel by specifying the segment and offset at which it is located. Now, of course, we haven't written the kernel yet. That will come in the next part: A Simple Kernel.

Valid XHTML 1.1! Valid CSS! Viewable with Any Browser