; Copyright (c) 2019, k4m1 <k4m1@protonmail.com>
; All rights reserved. See /LICENSE for full license agreement.
;
; This code is responsible of loading the kernel from boot device, and
; then relocating it to 0x100000.
;
%include "src/consoles.asm"
%include "src/bioscall.asm"
USED_SECTORS equ (SECTOR_CNT + 1)
TARGET_ADDRESS equ 0x00100000
kernel_sectors_left:
dd 0
kernel_entry_offset:
dw 0
current_target_addr:
dd TARGET_ADDRESS
current_sector:
db 0
loader_main:
push bp
mov bp, sp
; sector offset on disk is set to be amount of sectors this bootloader
; uses, so that we don't waste time looking for kernel header from
; within the bootloader.
mov byte [current_sector], USED_SECTORS
; normal disk read is used to first find the kernel header, only
; then after that we'll make use of extended disk read.
;
xor ecx, ecx
mov cl, 10
.kernel_load_loop:
call load_sector
call parse_kernel_header
jc .kernel_found
add byte [current_sector], 1
loop .kernel_load_loop
; kernel was not found, notify user and halt
mov si, msg_no_kernel
call panic
.kernel_found:
; load kernel expects 3 values to be set, all these are
; set by parse_kernel_header.
; - kernel_sectors_left: size of kernel
; - kernel_entry_offset: offset to kernel entry incase there's
; junk between bootloader & kernel
; - current_target_addr: address where to read disk to, this
; starts at 0x100000
;
pusha
mov eax, dword [kernel_sectors_left]
call write_serial_hex
shr eax, 16
call write_serial_hex
popa
call load_kernel
; prepare kernel entry address to EBX & setup 32-bit protected
; mode with simple GDT & disabled interrupts
cli
lgdt [gdt32]
mov eax, cr0
or al, 1
mov cr0, eax
jmp 0x08:.protected_mode_entry
.protected_mode_entry:
mov ax, 0x10
mov es, ax
mov fs, ax
mov ds, ax
mov gs, ax
mov ss, ax
[bits 32]
mov ebx, TARGET_ADDRESS
add ebx, 8
jmp ebx
[bits 16]
; =================================================================== ;
; End of main "logic", rest is helper functions 'n stuff ;
; =================================================================== ;
; This function loads single sector from disk to memory, sector to read
; is choosen by [current_sector]
;
load_sector:
push bp
mov bp, sp
pusha
; do disk read (int 0x13, ax = 0x0210), target = 0x2000
mov bx, 0x2000
xor cx, cx
mov cl, byte [current_sector]
xor dx, dx
mov dl, byte [boot_device] ; this we get from code at mbr.asm
.read_start:
mov di, 5
.read:
mov ax, 0x0210
call do_bios_call_13h
jnc .read_done
dec di
test di, di
jnz .read
mov si, msg_disk_read_fail
call panic
.read_done:
popa
mov sp, bp
pop bp
ret
; This function parses kernel header, setting DAP and other
; variables accordingly.
;
parse_kernel_header:
push bp
mov bp, sp
clc ; clear carry flag, we'll set it if kernel is found
pusha
mov si, 0x2000
.search:
cmp dword [si], 'nyan'
je .found_hdr
inc si
cmp si, 0x2200 ; sector size = 0x200
jl .search
; kernel was not found :(
popa
.ret:
mov sp, bp
pop bp
ret
.found_hdr:
; kernel was found :)
mov eax, dword [si+4]
mov dword [kernel_sectors_left], eax
sub si, 0x2000
mov word [kernel_entry_offset], si
mov si, msg_kernel_found
call write_serial
popa
stc
jmp .ret
; load_kernel function is basicly a loop going through
; extended disk read untill we've loaded the whole kernel.
load_kernel:
push bp
mov bp, sp
pusha
.start:
; reads happen 0x28 sectors at time MAX.
cmp dword [kernel_sectors_left], 0x28
jle .final_iteration
mov word [DAP.sector_count], 0x28
jmp .do_read
.final_iteration:
mov ax, word [kernel_sectors_left]
mov word [DAP.sector_count], ax
.do_read:
; extended disk read: int=0x13, al=0x42
mov dword [DAP.transfer_buffer], 0x2000
mov dl, byte [boot_device]
mov al, 0x42
mov si, DAP
call do_bios_call_13h
jc .fail
mov si, msg_loaded_block
call write_serial
; relocate sectors to 0x100000 onwards
; reloaction adjusts target address for us
call kernel_relocate
; adjust remaining sector count
xor eax, eax
mov ax, word [DAP.sector_count]
sub dword [kernel_sectors_left], eax
cmp dword [kernel_sectors_left], 0
jne .start
; kernel has been loaded
popa
mov sp, bp
pop bp
ret
.fail:
mov si, msg_disk_read_fail
call panic
kernel_relocate:
push bp
mov bp, sp
pusha
; relocate sectors to 0x100000 onwards
; We could also relocate less than 0x28 sectors on last read but
; it's less logic, easier code when it's like this,
; someday, and that day might never come, but someday I will optimize
; this and make it better
mov ecx, ((0x28 * 512) / 4) ; amount of dwords to reloacte
; I'd much more prefer movsd here, but that'd mean we'd need to
; either constantly swap between 32 and 16 bit mode, as atleast on
; qemu movsN does use ds:si, es:di on 32-bit unreal mode too. This
; practically means we could only load to address 0xF:FFFF at most,
; which is still in MMI/O space (usually MOBO BIOS ROM to be exact).
; swap to 32-bit mode would allow us to use esi, edi, but that'd mean
; we'd need to load our whole kernel to low memory first,
; and find enough space to somehow fit it here..
; that'd limit us a *LOT*.
;
; One way would be that constant swap between 16 and 32 bit mode,
; but that's not something I want to do.
;
.relocation_loop_start:
mov edx, dword [current_target_addr]
mov ebx, 0x2000
.loop:
mov eax, dword [ebx]
mov dword [edx], eax
add ebx, 4
add edx, 4
loop .loop
; adjust target address
inc edx
mov dword [current_target_addr], edx
popa
mov sp, bp
pop bp
ret
; Some pretty messages to print
msg_no_kernel:
db "Bootloader did not find kernel from disk :(", 0x0
msg_disk_read_fail:
db "Failed to read disk, firmware bug?", 0x0
msg_kernel_found:
db "Found kernel, loading...", 0x0A, 0x0D, 0
msg_loaded_block:
db "Loaded up to 20kb of kernel/os from disk...", 0x0A, 0x0D, 0
; =================================================================== ;
; Disk Address Packet format: ;
; ;
; Offset | Size | Desc ;
; 0 | 1 | Packet size ;
; 1 | 1 | Zero ;
; 2 | 2 | Sectors to read/write ;
; 4 | 4 | transfer-buffer 0xffff:0xffff ;
; 8 | 4 | lower 32-bits of 48-bit starting LBA ;
; 12 | 4 | upper 32-bits of 48-bit starting LBAs ;
; =================================================================== ;
DAP:
.size:
db 0x10
.zero:
db 0x00
.sector_count:
dw 0x0000
.transfer_buffer:
dd 0x00000000
.lower_lba:
dd 0x00000000
.higher_lba:
dd 0x00000000
times (USED_SECTORS * 512) - ($ - $$) db 0