I’m writing a simple bootloader that uses BIOS interrupt int 0x13 to read a second-stage loader from disk and then jump to it.
; boot\boot.asm
[bits 16]
[org 0x7C00]
jmp strict short start
nop
OEM_LABEL db "DAYS0.1 "
; BIOS Parameter Block (BPB)
bpb_bytes_per_sector dw 512 ; 512 bytes per sector
bpb_sector_per_cluster db 1 ; 1 sector per cluster
bpb_reserved_sectors dw 1 ; 1 reserved sector (boot sector)
bpb_fat_count db 2 ; 2 FAT tables (1 default, 1 reserved)
bpb_root_entries_count dw 512 ; Number of entries in root directory
bpb_total_sectors_small dw 32768 ; 16 * 1024*1024 (16MB)/512 = 32768
; sectors
bpb_media_descriptor dw 0xf8 ; 0xF8 = fixed disk(hard disk)
bpb_sectors_per_fat dw 128 ; 1 FAT-table = 128 sectors
bpb_sectors_per_track dw 32 ; dummy geometry for virtual disk
bpb_head_count dw 16 ; dummy geometry for virtual disk
bpb_hidden_sectors dd 0 ; no hidden sectors before this volume
bpb_total_sectors_large dd 0 ; 0 = use bpb_total_sectors_small
; Extended Boot Record (FAT16)
ebr_drive_number db 0 ; BIOS drive number: 0x80 = hard disk
; Set by bootloader using DL register
ebr_reserved db 0 ; Reserved byte, must be zero
ebr_boot_signature db 0x29 ; Extended boot signature (must be 0x29)
; Indicates that the following fields are
; valid:
; ebr_volume_id, ebr_volume_label,
; ebr_filesystem_type
ebr_volume_id dd 0x7E73B1BE ; Volume serial number
; (any 32-bit value, usually random)
; Used by the OS
ebr_volume_label db "DAYS_OS " ; Volume label (must be 11 bytes,
; padded with spaces)
ebr_filesystem_type db "FAT16 " ; File system type label (8 bytes, padded
; with spaces)
; Calculated constants from BPB
bpb_root_dir_sectors equ (512 * 32) / 512
bpb_first_root_sector equ 1 + (2 * 128)
bpb_first_data_sector equ bpb_first_root_sector + bpb_root_dir_sectors
; Bootloader start
start:
cmp dl, 0x80 ; Compare BIOS-loaded boot drive number (in DL) with 0x80
je main ; Jump to main code if dl = 0x80 (correct drive device)
; Error: Booted from unexpected device
mov si, wrong_drive_error_msg
call puts
jmp halt
main:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax ; Set Stack Segment (SS) to 0x0000
mov sp, 0x7c00 ; Set Stack Pointer (SP) to 0x7c00
mov ax, cs ; Load current Code Segment (CS) into AX
mov si, no_error_msg
call puts
call load_second_stage ; Load second stage boot sector and jump to in
load_second_stage:
; CH = cylinder number
mov ch, 0x00
; CL = sector number
mov cl, 0x02
; DH = head number
mov dh, 0x09
; DL already contain the boot drive number (0x80 for HDD),
; so no need to modify it unless you're switching drives.
mov ax, 0x0800 ; Set ES:BX as destination memory address for sector
mov es, ax ; Physical address = ES * 16 + BX =
xor bx, bx ; 0x0800 * 16 + 0x0000 = 0x8000
mov ah, 0x02 ; AH = 0x02 -> BIOS function: Read sector
mov al, 0x01 ; AL = number of sectors to read (1 sector)
int 0x13 ; BIOS disk interrupt - reads sector into ES:BX
; If Carry Flag (CF) set, jump to .disk_read_error label
; (BIOS sets flag (CF) if reading with "int 0x13" failed
jc .disk_read_error
; Success: jump to the entry point of the second loading stage
jmp 0x0800:0x0000
.disk_read_error:
mov si, disk_error_msg
call puts
jmp halt
; Function puts - prints a null-terminated string
puts:
push si
push ax
.loop:
lodsb
or al, al
jz .done
mov ah, 0x0e
mov bh, 0
int 0x10
jmp .loop
.done:
pop ax
pop si
ret
halt:
hlt
jmp halt
; data
wrong_drive_error_msg db "error: Booted from unexpected device", 0
disk_error_msg db "error: Disk read error", 0
no_error_msg db "Booted coorectly", 0
new_line db 0x0d, 0x0a, 0
times 510 - ($ - $$) db 0
dw 0xAA55
; loader\loader.asm
[org 0x0000]
[bits 16]
start:
mov ax, 0x0800
mov ds, ax
mov es, ax
; stack
mov ax, 0x0900
mov ss, ax
mov sp, 0x9200
; print 'S'
mov ah, 0x0E
mov al, 'S'
int 0x10
.loop
jmp .loop
times 512 - ($ - $$) db 0
; Makefile
BOOT = build/boot.bin
KERNEL = build/kernel.bin
LOADER = build/loader.bin
IMAGE = os.img
NASM = nasm
DD = dd
MTOOLS = mcopy
all: $(IMAGE)
# creating build directory if it's not exists
build:
mkdir -p build
# building boot.asm
$(BOOT): boot/boot.asm | build
$(NASM) -f bin boot/boot.asm -o $(BOOT)
# building kernel.asm
#$(KERNEL): kernel/kernel.asm | build
# $(NASM) -f bin kernel/kernel.asm -o $(KERNEL)
#building loader.asm
$(LOADER): loader/loader.asm | build
$(NASM) -f bin loader/loader.asm -o $(LOADER)
# creating FAT16-image with boot-loader and file
$(IMAGE): $(BOOT) $(LOADER)
# 1. creating empty file with 16MB size
$(DD) if=/dev/zero of=$(IMAGE) bs=1M count=16
# 2. formating image like FAT16 not touching first sector
mkfs.fat -F 16 -n DAYS_OS $(IMAGE)
# 3. Loads boot-loader sector
$(DD) if=$(BOOT) of=$(IMAGE) bs=512 count=1 conv=notrunc
# 4. Copying loader into the image
$(DD) if=$(LOADER) of=$(IMAGE) bs=512 seek=289 count=1 conv=notrunc
run:
qemu-system-x86_64 -drive format=raw,file=$(IMAGE)
clean:
rm -rf build $(IMAGE)
; hexdump os.img
00000000 eb 3d 90 44 41 59 53 30 2e 31 20 00 02 01 01 00 |.=.DAYS0.1 .....|
00000010 02 00 02 00 80 f8 00 80 00 20 00 10 00 00 00 00 |......... ......|
00000020 00 00 00 00 00 00 00 29 be b1 73 7e 44 41 59 53 |.......)..s~DAYS|
00000030 5f 4f 53 20 20 20 20 46 41 54 31 36 20 20 20 80 |_OS FAT16 .|
00000040 fa 80 74 08 be 9a 7c e8 3b 00 eb 4b fa 31 c0 8e |..t...|.;..K.1..|
00000050 d8 8e c0 8e d0 bc 00 7c 8c c8 be d6 7c e8 25 00 |.......|....|.%.|
00000060 e8 00 00 b5 00 b1 02 b6 09 b8 00 08 8e c0 31 db |..............1.|
00000070 b4 02 b0 01 cd 13 72 05 ea 00 00 00 08 be bf 7c |......r........||
00000080 e8 02 00 eb 12 56 50 ac 08 c0 74 08 b4 0e b7 00 |.....VP...t.....|
00000090 cd 10 eb f3 58 5e c3 f4 eb fd 65 72 72 6f 72 3a |....X^....error:|
000000a0 20 42 6f 6f 74 65 64 20 66 72 6f 6d 20 75 6e 65 | Booted from une|
000000b0 78 70 65 63 74 65 64 20 64 65 76 69 63 65 00 65 |xpected device.e|
000000c0 72 72 6f 72 3a 20 44 69 73 6b 20 72 65 61 64 20 |rror: Disk read |
000000d0 65 72 72 6f 72 00 42 6f 6f 74 65 64 20 63 6f 6f |error.Booted coo|
000000e0 72 65 63 74 6c 79 00 0d 0a 00 00 00 00 00 00 00 |rectly..........|
000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|
00000200 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000800 f8 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00004800 f8 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00004810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00008800 44 41 59 53 5f 4f 53 20 20 20 20 08 00 00 ea 82 |DAYS_OS .....|
00008810 04 5b 04 5b 00 00 ea 82 04 5b 00 00 00 00 00 00 |.[.[.....[......|
00008820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00024200 b4 0e b0 53 cd 10 b8 00 08 8e d8 8e c0 b8 00 09 |...S............|
00024210 8e d0 bc 00 92 b4 0e b0 53 cd 10 fa f4 eb fe 00 |........S.......|
00024220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
01000000
The first stage bootloader loads successfully from sector 1, and I confirmed it works — reading the sector with int 0x13 succeeds and I can output a character via int 0x10.
The second stage loader should be loaded from sector 289 (calculated using disk geometry: 16 heads, 32 sectors per track, cylinder = 0, head = 9, sector = 2).
I set segment register ES = 0x0800 and BX = 0, call int 0x13 to read one sector. The Carry Flag (CF) is clear, so no disk read error.
I check the first byte loaded at ES:[BX] — it differs from what I expect, but if I read sector 1 instead, the byte matches correctly.
After reading the second stage, I do a far jump jmp 0x0800:0x0000 to the loaded code, which was assembled with [org 0x0000].
In the second stage, I properly initialize segment registers (DS, ES).
I also placed code at the start of the second stage to output a character using int 0x10 to verify it runs, but no output appears and control does not seem to transfer.
I verified that sector 289 in my disk image (disk.img) contains the expected second-stage code at the offset 289 * 512 = 0x24200.
Thanks to @Nassau for pointing out the correct CHS values using sfdisk -g
. I had been using BPB values for calculating CHS, which led me to load the wrong sector.
Correcting the seek=
in dd
to match C:0 H:4 S:38
(i.e., sector 289) fixed the problem.