diff --git a/Makefile b/Makefile index 7c5585d..b0a3d17 100644 --- a/Makefile +++ b/Makefile @@ -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 \ @@ -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) diff --git a/docs/boot/boot.md b/docs/boot/boot.md index 9e9ca22..0b59e39 100644 --- a/docs/boot/boot.md +++ b/docs/boot/boot.md @@ -9,7 +9,7 @@ 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: @@ -17,8 +17,20 @@ First we store the boot drive in a defined place in memory. BIOS puts the boot d - 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 diff --git a/src/boot/boot_sect.asm b/src/boot/boot_sect.asm index cc04946..43ff72b 100644 --- a/src/boot/boot_sect.asm +++ b/src/boot/boot_sect.asm @@ -1,11 +1,48 @@ [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" @@ -13,14 +50,184 @@ begin: [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: @@ -28,10 +235,12 @@ begin_pm: 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 \ No newline at end of file +dw 0xaa55 ;magic boot sector number