Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ ASM_OS_ENTRY_SOURCE := ./src/boot/os_entry.asm
BOOT_OBJ := boot.o
OS_BIN := mOS.bin

# The total number of 512-byte sectors for the size of the OS binary.
# WARNING: This MUST be equal to the identically named constant in the
# file 'src/boot/boot_sect.asm'.
OS_BIN_TOTAL_SECTORS := 42
OS_BIN_SIZE_BYTES := $$((512*$(OS_BIN_TOTAL_SECTORS)))

C_FILES = $(shell find ./ -name '*.[ch]')

OBJ_NAMES := src/os/main.o src/os/test.o os_entry.o src/os/paging.o \
Expand All @@ -62,6 +68,12 @@ $(OS_BIN): $(OBJ_NAMES) $(BOOT_OBJ)
$(LD) $(LFLAGS) -T link.ld $(OBJ_NAMES) -o mOS.elf
$(OBJCOPY) -O binary mOS.elf intermediate.bin
cat $(BOOT_OBJ) intermediate.bin > $(OS_BIN)
@if [ $$(du -b mOS.bin | grep -o "[0-9]*") -gt $(OS_BIN_SIZE_BYTES) ]; \
then \
echo "ERROR: mOS.bin too large!"; \
false; \
fi
truncate -s ">$(OS_BIN_SIZE_BYTES)" $(OS_BIN)

$(BOOT_OBJ): $(ASM_BOOT_SECT_SOURCE)
nasm $^ -f bin -o $@ $(DEBUG_NASM_FLAGS)
Expand Down
18 changes: 15 additions & 3 deletions docs/boot/boot.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,28 @@ Finally, we get to the OS, BIOS will load the first sector (512-bytes) of the di

## Boot Sector

The boot sector (see [boot_sect.asm](../../src/boot/boot_sect.asm)) is the very first code that we write that gets executed. It is important to keep in mind at this point we only have 512-bytes of code/data loaded, which is not much. In addition we also need the "magic word" 0xaa55 as the last word in the sector to signify that this is a bootable sector. `times 509 - ($ - $$) db 0` is a neat assembly trick that gets our binary to exactly 512-bytes.
The boot sector (see [boot_sect.asm](../../src/boot/boot_sect.asm)) is the very first code that we write that gets executed. It is important to keep in mind at this point we only have 512-bytes of code/data loaded, which is not much. In addition we also need the "magic word" 0xaa55 as the last word in the sector to signify that this is a bootable sector. `times 506 - ($ - $$) db 0` is a neat assembly trick that gets our binary to exactly 512-bytes.
You may have noticed `[bits 16]` in the assembly, this is because BIOS starts us out in "real mode" which is fancy for 16-bit. Real mode uses segmentation heavily, but discussing segmentation is outside of the scope of this document since we don't have to deal with it.
First we store the boot drive in a defined place in memory. BIOS puts the boot drive in `dl` on boot. Next we set the stack to be at 0x9000 (`bp` and `sp` are the 16-bit stack registers). Our next step is to load the rest of the kernel. BIOS uses Cylinder Head Sector (CHS) addressing for disk access, here are some important details:

- A sector is a 512-byte section that is indexed from 1.
- A cylinder is a ring on a platter that is indexed from 0.
- A head is the physical reader that is also indexed from 0.

Since we want the second sector (first is the boot sector) onwards, we only need the very first cylinder and head. Now we want to tell the BIOS to execute a read operation.
We move `2` into `ah` to signify we want to read. Then `42` into `al` to signify we want to read 42 sectors (512 * 42 = 21504-bytes). Then `cl` gets 2 for the sector, `ch` and `dh` get 0 for cylinder and head respectively. Then `OS_OFFSET` (0x1000) is put in `bx`, this tells BIOS where we want the result of our read to be stored in memory. Finally, we do `int 0x13` which is the disk interrupt for BIOS.
CHS addressing is not super convenient to use; it would be better if we could use Logical Block Addresses (LBA) instead, which would just give us a simple linear address space for our drive.
Thus, to aid in loading the rest of the binary for our OS, we define an assembly routine called `read_drive` which takes an LBA in register `al`, converts the LBA to a CHS address, reads from the drive, and returns the status of the read in the registers `cf`, `ah`, and `al` (see [boot_sect.asm](../../src/boot/boot_sect.asm)). LBAs are converted to CHS addresses using an algorithm described on OSDev (see [https://wiki.osdev.org/Disk_access_using_the_BIOS_(INT_13h)]).

The `read_drive` routine is implemented on top of the `int 0x13` BIOS interrupt, which provides functionality for manipulating drives (see https://en.wikipedia.org/wiki/INT_13H). In our case we call `int 0x13` with the following register arguments:
- `ah=2` -- 2 tells it to do a read.
- `al=1` -- Read just one sector.
- `cl=sector` -- Where `sector` is computed from the LBA.
- `ch=cylinder` -- Where `cylinder` is computed from the LBA.
- `dh=head` -- Where `head` is computed from the LBA.
- `dl=drive` -- Where `drive` is the drive number to read from.
- `es=0:bx=offset` -- Where `offset` is computed from the LBA (that was passed in to `read_drive`) such that `offset=((LBA-1)*512)+0x1000`, which has the effect of mapping LBA 1 onward onto the memory region starting at 0x1000.

The `read_drive` routine is used inside of a loop that reads the rest of the OS binary one sector at a time. If a read for any given sector fails, we attempt to read that that sector at most two more times before giving up on booting entirely.

Now that we have the OS loaded, we need to get into 32-bit mode (also known as protected mode, long mode is 64-bit). Now we switch over to [enter_pm.asm](../../src/boot/enter_pm.asm) and [gdt.asm](../../src/boot/gdt.asm).

### Protected Mode
Expand Down
227 changes: 218 additions & 9 deletions src/boot/boot_sect.asm
Original file line number Diff line number Diff line change
@@ -1,37 +1,246 @@
[org 0x7c00]
OS_OFFSET equ 0x1000
MAX_READ_TRIES equ 3


;;; Total number of sectors to read from the boot drive, including the boot
;;; sector.
;;; WARNING: This MUST be equal to the identically named constant in the
;;; Makefile.
OS_BIN_TOTAL_SECTORS equ 42
;;; The number of sectors to read from the boot drive in addition to the boot
;;; sector.
ADDITIONAL_SECTORS_TO_READ equ OS_BIN_TOTAL_SECTORS-1

[bits 16]
begin:
mov [BOOT_DRIVE], dl
mov ax, 0
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov bp, 0x9000
mov sp, bp

;;; Get Drive Parameters
mov ah, 8
mov dl, [BOOT_DRIVE]
mov di, 0
clc
int 0x13
jnc get_params_no_error
;;; Print and stop if there is an error reading the drive parameters
call print_error
mov al, ah
call print_byte
jmp $

get_params_no_error:
;;; Save some of the drive parameters
add dh, 1
mov [DRIVE_N_HEADS], dh
and cl, 0x3f
mov [DRIVE_SECTS_PER_TRACK], cl
jmp load_kernel

%include "src/boot/gdt.asm"
%include "src/boot/enter_pm.asm"

[bits 16]
load_kernel:
mov ah, 2 ;read BIOS chs
mov al, 42 ;sectors to read
mov cl, 0x02 ;start at sector 2
mov ch, 0x00 ;cylinder
mov dh, 0x00 ;head

;;; Read drive one sector at a time
mov bl, 1
read:
mov al, bl ; current sector to read
call read_drive
add bl, 1
cmp bl, ADDITIONAL_SECTORS_TO_READ
jle read
call enter_pm

;;; function read_drive:
;;; Reads one sector from the drive at the given LBA into the kernel memory
;;; space at the appropriate offset (assuming 512 byte sectors).
;;; Input:
;;; [al] = LBA of sector to read
;;; Output:
;;; [cf] = Set on error
;;; [ah] = status, as returned by INT 0x13/AH=0x02
;;; [al] = sectors transferred, as returned by INT 0x13/AH=0x02
read_drive:
push cx
push bx
push ax

mov byte [READ_TRY_COUNT], 0
read_drive_retry:
add byte [READ_TRY_COUNT], 1
mov cx, 0
mov es, cx ; Ensure segment register [es] is zero for interrupt
;;; Calculate Offset for Kernel
mov cl, al
sub cl, 1
shl cx, 9
mov bx, OS_OFFSET
add bx, cx

;;; Calculate C,H,S using algorithm from OSDev:
;;; https://wiki.osdev.org/Disk_access_using_the_BIOS_(INT_13h)
mov ah, 0 ; Ensure top half of [ax] is zero so that [al] behaves as dividend
div byte [DRIVE_SECTS_PER_TRACK]
add ah, 1
mov cl, ah
mov ah, 0
div byte [DRIVE_N_HEADS]
mov dh, ah
mov ch, al
mov al, 1
mov ah, 2
mov dl, [BOOT_DRIVE]
clc
int 0x13 ;do read
call enter_pm
jnc read_drive_no_error

;;; If an error occurred during the read, print out the following, separated by
;;; commas:
;;; - [al] : The actual number of sectors read.
;;; - [ah] : The return code from interrupt 0x13.
;;; - READ_TRY_COUNT : The amount of times a read has been tried so far.
;;; - Original value of [al] when read_drive was called which is the sector
;;; at which the read was attempted.
;;; After printing this info, try reading again if the number of retries has not
;;; exceeded MAX_READ_TRIES, otherwise stop.
call print_error
call print_byte
call print_comma
mov al, ah
call print_byte
call print_comma
mov bl, [READ_TRY_COUNT]
mov al, bl
call print_byte
call print_comma
mov ax, [esp] ; Restore [al] (LBA of sector to read from)
call print_byte
call print_newline
cmp bl, MAX_READ_TRIES
jl read_drive_retry
jmp $

read_drive_no_error:
pop bx ; Pop old value of ax which is no longer needed
pop bx
pop cx
ret

;;; function print_char:
;;; Prints the character in register [al].
print_char:
push ax
push bx
mov ah, 0x0e
mov bl, 1
mov bh, 0
int 0x10
pop bx
pop ax
ret

;;; function print_half_byte:
;;; Print the 4 least significant bits in the register [al].
print_half_byte:
pushfd
and al, 0x0F
add al, '0'
cmp al, '9'
jle skip_hex
add al, 7
skip_hex:
call print_char
popfd
ret

;;; function print_byte:
;;; Print the byte in register [al].
print_byte:
push ax

shr al, 4
call print_half_byte
mov ax, [esp]
mov ah, 0
call print_half_byte

pop ax
ret

;;; function print_word:
;;; Print a 16-bit word in the register [ax].
print_word:
push ax
mov al, ah
call print_byte
mov ax, [esp]
call print_byte
pop ax
ret

;;; function print_newline:
;;; Print a newline character. Takes no arguments.
print_newline:
push ax
mov al, `\n`
call print_char
mov al, `\r`
call print_char
pop ax
ret

;;; function print_comma:
;;; Print a comma character. Takes no arguments.
print_comma:
push ax
mov al, ','
call print_char
pop ax
ret

;;; function print_error:
;;; Print the string "ERROR: ". Takes no arguments.
print_error:
push ax
mov al, 'E'
call print_char
mov al, 'R'
call print_char
mov al, 'R'
call print_char
mov al, 'O'
call print_char
mov al, 'R'
call print_char
mov al, ':'
call print_char
mov al, ' '
call print_char
pop ax
ret

[bits 32]
begin_pm:
call OS_OFFSET
hlt


times 509 - ($ - $$) db 0 ;padding

times 506 - ($ - $$) db 0 ;padding

READ_TRY_COUNT db 0
DRIVE_N_HEADS db 0
DRIVE_SECTS_PER_TRACK db 0
BOOT_DRIVE db 0 ;0x7dfd
;above is data that can always be found at 0x7dfd - n during boot process

dw 0xaa55 ;magic boot sector number
dw 0xaa55 ;magic boot sector number