-
Notifications
You must be signed in to change notification settings - Fork 0
disk loading int13
How the 512-byte bootloader reaches past its own size limit and pulls a full kernel off disk — CHS addressing, sector counts, and the carry-flag error path.
A boot sector is only 510 usable bytes — far too small for a C
kernel. So from stage 2 onward, the boot
sector becomes a bootloader whose central job is to read the rest of the
kernel from disk into memory before switching to
protected mode. It does this with the BIOS disk service,
int 0x13, function AH = 0x02 (read sectors). This page dissects exactly how
MyOS-Simple's disk_load routine works.
The bootloader sets up three things and calls disk_load:
; Load kernel from disk
mov bx, KERNEL_OFFSET
mov dh, 16 ; Load 16 sectors (kernel is ~3.6KB, needs >2)
mov dl, [BOOT_DRIVE]
call disk_loadhelloworld-os-c/boot.asm:34-37
-
BX = KERNEL_OFFSET(0x1000) — the destination offset in memory. -
DH = 16— the number of sectors to read (this value is the only thing that changes between stages). -
DL = [BOOT_DRIVE]— the drive number the BIOS gave us inDLat boot, which the bootloader saved on its very first instruction (helloworld-os-c/boot.asm:19).
KERNEL_OFFSET is defined as 0x1000 at the top of the file
(helloworld-os-c/boot.asm:15) — the same address the
linker script links the kernel to, and the same address the
bootloader later calls to enter the kernel.
disk_load:
pusha
push dx
mov ah, 0x02 ; BIOS read sectors
mov al, dh ; Number of sectors
mov cl, 0x02 ; Start from sector 2
mov ch, 0x00 ; Cylinder 0
mov dh, 0x00 ; Head 0
int 0x13
jc disk_error
pop dx
popa
rethelloworld-os-c/boot.asm:66-81
Walking the registers that int 0x13 / AH = 0x02 expects:
| Register | Value | Meaning |
|---|---|---|
AH |
0x02 |
Function: read sectors |
AL |
DH from caller (e.g. 16) |
Number of sectors to read |
CL |
0x02 |
Starting sector number (1-based) |
CH |
0x00 |
Cylinder number |
DH |
0x00 |
Head number |
DL |
boot drive | Which drive to read from |
ES:BX |
0x0000:0x1000 |
Destination address in memory |
Note the mov al, dh on entry: the caller passes the sector count in DH,
and the routine copies it into AL where int 0x13 wants it. Then DH is
immediately reused as the head number (mov dh, 0x00). The push dx/pop dx
around the call preserves the caller's drive number in DL across the BIOS call.
ES was set to 0 back in the segment setup (boot.asm:23), and BX holds
0x1000, so the destination ES:BX resolves to physical 0x1000.
The BIOS read uses CHS geometry — Cylinder, Head, Sector — the three-coordinate scheme inherited from physical spinning disks. The single most important detail:
💡 Tidbit: CHS sectors are 1-based, not 0-based. Sector 1 is the very first sector on the track — and that is the boot sector itself. The kernel therefore begins at sector 2, which is exactly why MyOS-Simple's loader sets
CL = 0x02. (Cylinders and heads, confusingly, are 0-based — henceCH = 0andDH = 0.)
So the read says: "starting at cylinder 0, head 0, sector 2, read AL sectors
into ES:BX." Because the kernel is laid out contiguously right after the boot
sector, sectors 2, 3, 4, … hold the kernel image in order, and a single int 0x13 call slurps them all into memory at 0x1000.
The sector count (DH at the call site) is the one number that changes from
stage to stage, because the kernel grows:
| Stage | Project directory | Sectors read | Source |
|---|---|---|---|
| 2 | helloworld-os-c |
16 | helloworld-os-c/boot.asm:35 |
| 3 | os-c-with-shell |
15 | os-c-with-shell/boot.asm:34 |
| 4 | helloworld-os-c-v2 |
39 | helloworld-os-c-v2/boot.asm:35 |
| 5 | helloworld-os-c-v3 |
39 | helloworld-os-c-v3/boot.asm:35 |
The guiding principle is "read more than the minimum". Each comment in the
source spells out the reasoning — e.g. stage 2's ; Load 16 sectors (kernel is ~3.6KB, needs >2). A 3.6 KB kernel only strictly needs 8 sectors
(3600 ÷ 512 ≈ 7.0, rounded up), but reading 16 leaves comfortable headroom so a
slightly larger build is not silently truncated.
⚠️ Caveat: If the loader reads too few sectors, the tail of the kernel never makes it into memory. The code that was cut off is simply absent — the kernel will jump into whatever garbage happens to sit at those addresses and crash or hang, often far from the real cause. When a kernel grows past its sector budget, bumping the count inboot.asmis the fix. This is exactly why the count jumps to 39 once the stage 4/5 kernels add the RTC clock and other features.
⚠️ Caveat: This scheme assumes the kernel occupies contiguous sectors starting at sector 2. MyOS-Simple has no filesystem — the build process simply concatenates the boot sector and the kernel binary into one flat disk image, so the kernel really is the bytes immediately following the boot sector. There is no directory to consult; "where is the kernel?" is answered by raw geometry.
If the BIOS cannot complete the read, it sets the carry flag. The routine checks it immediately:
int 0x13
jc disk_errorhelloworld-os-c/boot.asm:76-77
disk_error:
mov si, DISK_ERROR_MSG
call print_16
jmp $helloworld-os-c/boot.asm:83-86
On failure the loader prints "Disk read error!" (via the same
teletype int 0x10 AH=0x0E loop the rest of the boot code
uses) and then jmp $ — an infinite self-loop that hangs the machine. There is
no retry and no recovery: at this point in the boot there is nothing else the
code can do.
💡 Tidbit:
jc("jump if carry") is the idiomatic BIOS error check. Almost everyint 0x13function reports failure by setting the carry flag (and an error code inAH), soint 0x13followed byjc <error>is a pattern you will see in every bootloader ever written.
The source also defines a sectors_error handler and a "Incorrect sectors read!" message (helloworld-os-c/boot.asm:88-91, 138), intended for verifying
that AL (the BIOS's count of sectors actually read) matches the requested
count. In MyOS-Simple's loader that check is not wired into the read path — only
the carry-flag disk_error branch is reached — but the scaffolding is present
for anyone extending the loader to verify the returned count.
Once the kernel is safely at 0x1000, the bootloader continues:
cli, lgdt, set CR0.PE, far-jump into
protected mode, reload the data selectors, set the stack to
0x90000, and call 0x1000 to enter the kernel. That hand-off is the subject of
the boot process spine and the
Global Descriptor Table page.
- The boot process — the full boot timeline this read sits in
- The boot sector — why the kernel can't fit in sector 1
-
BIOS services —
int 0x13alongsideint 0x10andint 0x16 -
Real mode — the environment in which
int 0x13is callable -
Linker scripts — why the kernel is linked at
0x1000 - Protected mode — where the boot hands off after the load
-
Memory map —
0x1000kernel load address and more - Stage 2: C in protected mode · Stage 4: clock, processes, calc
- Home
Stages
- 1 · Assembly boot
- 2 · C protected mode
- 3 · Interactive shell
- 4 · Clock / processes / calc
- 5 · Stabilized release
Concepts — boot
Concepts — protected mode
Concepts — hardware
Concepts — OS services
Reference
- Memory map
- I/O ports
- GDT descriptor format
- Scancode tables
- Command reference
- Toolchain & build
- Glossary
Guides