diff --git a/src/bootloader/mbr.s b/src/bootloader/mbr.s index 8db254b..edf6cb3 100644 --- a/src/bootloader/mbr.s +++ b/src/bootloader/mbr.s @@ -5,37 +5,122 @@ [org 0x7c00] ; Address where the code expects to be loaded in. The BIOS always loads us here [bits 16] ; All x86 CPUs start in 16 bit (aka "real") mode, so we tell nasm to emit 16-bit code -; We print a simple startup message using the ISRs -; from the BIOS -mov si, startup_msg -call print +start: + ; Copied from https://github.com/limine-bootloader/limine/blob/trunk/stage1/hdd/bootsect.asm. + ; Quote: + ; Some BIOSes will do a funny and decide to overwrite bytes of code in + ; the section where a FAT BPB would be, potentially overwriting + ; bootsector code. Avoid that by filling the BPB area with dummy values. + ; Some of the values have to be set to certain values in order to boot + ; on even quirkier machines. + ; Source: https://github.com/freebsd/freebsd-src/blob/82a21151cf1d7a3e9e95b9edbbf74ac10f386d6a/stand/i386/boot2/boot1.S + jmp skip_bpb + nop + bpb: + times 3-($-$$) db 0 + .bpb_oem_id: db "TSOS " + .bpb_sector_size: dw 512 + .bpb_sects_per_cluster: db 0 + .bpb_reserved_sects: dw 0 + .bpb_fat_count: db 0 + .bpb_root_dir_entries: dw 0 + .bpb_sector_count: dw 0 + .bpb_media_type: db 0 + .bpb_sects_per_fat: dw 0 + .bpb_sects_per_track: dw 18 + .bpb_heads_count: dw 2 + .bpb_hidden_sects: dd 0 + .bpb_sector_count_big: dd 0 + .bpb_drive_num: db 0 + .bpb_reserved: db 0 + .bpb_signature: db 0 + .bpb_volume_id: dd 0 + .bpb_volume_label: db "TSOS " + .bpb_filesystem_type: times 8 db 0 + ; The skip_bpb and initialize_cs code is adapted + ; from the Limine bootloader as well + skip_bpb: + cli + cld + jmp 0x0000:initialise_cs + initialise_cs: + xor si, si + mov ds, si + mov es, si + mov ss, si + sti + ; We're not made for floppy disks, these are dead anyways. + ; So if the value the BIOS passed is <0x80, just assume it has passed + ; an incorrect value. + cmp dl, 0x80 + jb invalid_boot_device + ; Values above 0x8f are dubious so we assume we weren't booted properly + ; for those either + cmp dl, 0x8f + ja invalid_boot_device + continue: + ; First off, we setup the stack by setting the + ; base pointer to address 0x8000. The address + ; itself doesn't matter as long as it's far + ; enough away from memory already in use by + ; the BIOS + mov bp, 0x8000 + mov sp, bp ; The stack starts out empty, so sp == bp -; Now we setup the stack by setting the -; base pointer to address 0x8000. The -; address itself doesn't matter as long -; as it's far enough away from memory already -; in use by the BIOS -mov bp, 0x8000 -mov sp, bp ; The stack starts out empty, so sp == bp + ; Since we have a stack, we can now call functions, + ; so we print a simple startup message using BIOS + ; routines + mov si, startup_msg + call bios_print + ; We also inform the user we're loading a few + ; sectors from the boot drive + mov si, disk_read_info + and dx, 0xff ; Gets rid of the high 8 bits of dx so + ; we only print the hex value of dh, + ; which is the type of storage device + ; we're booting from (0 = floppy, + ; 1 = floppy2, 0x80 = hdd, 0x81 = hdd2) + call bios_print + call bios_printh + call bios_newline + ; We load data from the current boot drive. The data is copied + ; to memory starting at address 0x9000 + mov bx, 0x9000 + mov dh, 2 ; Read 3 sectors (2 for our dummy sectors, 1 for our variables) + ; The dl register is already set by the BIOS + call load_disk -jmp $ ; Keeps jumping at the current address (loops forever) + ; Now we retrieve the test data we placed in the other + ; sectors and print it in hexadecimal format + mov dx, [0x9000] + call bios_printh ; Should print 0xDADA + call bios_newline + mov dx, [0x9000 + 512] + call bios_printh ; Should print OxFACE -; These two lines never execute (for now), but they might once -; we jump into the kernel, so we disable interrupts and halt the -; CPU, just in case -cli -hlt + endless_loop: + jmp $ ; Keeps jumping at the current address (loops forever) + + invalid_boot_device: + mov si, invalid_boot_device_msg + call bios_println -; Now we include our "function definitions" (after the halting, so -; they're never executed unless explicitly called) -%include "src/bootloader/util/tty.s" + ; Now we include our "function definitions" (after the + ; loop, so they're never executed unless explicitly called) + %include "src/bootloader/util/disk.s" + %include "src/bootloader/util/io.s" -; Here we define our variables: They need to be defined after the -; halting because otherwise they will be executed as code! -startup_msg: db "TSOS - Bootloader: Starting up", 0xA, 0xD, 0x0 + ; Here we define our variables: They need to be defined after the + ; halting because otherwise they will be executed as code + startup_msg: db "TSOS is starting up", 0xA, 0xD, 0 + invalid_boot_device_msg: db "Invalid boot device", 0 ; padding and magic number times 510 - ($-$$) db 0 -dw 0xaa55 \ No newline at end of file +dw 0xaa55 + +; We add more sectors to our binary so we can read them +times 256 dw 0xdada ; sector 2 = 512 bytes +times 256 dw 0xface ; sector 3 = 512 bytes diff --git a/src/bootloader/util/disk.s b/src/bootloader/util/disk.s new file mode 100644 index 0000000..809273e --- /dev/null +++ b/src/bootloader/util/disk.s @@ -0,0 +1,48 @@ +; Utilities to read data from the disk. Used to load the kernel +; into main memory + + +load_disk: + ; Loads 'dh' sectors from drive 'dl' (this + ; register is set by the BIOS before calling + ; the bootloader) into es:bx + pusha + push dx + mov ah, 0x2 ; Perform a read operation + mov al, dh ; Number of sectors + mov cl, 0x2 ; 0x1 is us, so 0x2 is the first sector we want + mov ch, 0x0 ; Cylinder 0 + mov dh, 0x0 ; Head position is 0 too + int 0x13 ; Once this returns, the data will be in es:bx + jc disk_error ; If an error occurs, the carry bit is set + pop dx + ; The BIOS tells us how many sectors have been + ; actually read. If the desired and actual value + ; don't match, an error occurred + cmp al, dh + jne sectors_error + popa + ret + + +disk_error: + mov si, disk_read_error_msg + call bios_print + mov dh, ah ; Error code is in ah + call bios_printh + call bios_newline + jmp disk_loop + + +sectors_error: + mov si, disk_sectors_error_msg + call bios_println + + +disk_loop: + jmp $ + + +disk_read_info: db "Booting from disk type ", 0 +disk_read_error_msg: db "Read error: ", 0 +disk_sectors_error_msg: db "Read error (wrong number of sectors)", 0 diff --git a/src/bootloader/util/io.s b/src/bootloader/util/io.s new file mode 100644 index 0000000..f7ea815 --- /dev/null +++ b/src/bootloader/util/io.s @@ -0,0 +1,192 @@ +; Some utilities to deal with the TTY using the BIOS +; during real mode and writing directly to VGA memory +; once we switch to protected mode +[bits 16] + +; I/O routines based on BIOS interrupt 13 + +bios_newline: + ; Points the TTY cursor to the next + ; line + pusha + ; Source: http://www.techhelpmanual.com/118-int_10h_03h__query_cursor_position_and_size.html + mov ah, 0x3 ; Get cursor position + mov bh, 0x0 ; Page 0 + int 0x10 + ; Go to the next row + ; Source: http://www.techhelpmanual.com/117-int_10h_02h__set_cursor_position.html + mov ah, 0x2 ; Set cursor position + mov bh, 0x0 ; Page 0 + xor dl, dl ; Goes to column 0 (i.e. start of the line) + inc dh ; Goes to the next row + int 0x10 + popa + ret + + +bios_print: + ; Prints a null-terminated string whose address + ; is located in the si register + pusha + mov ah, 0xe ; Set the screen in TTY mode + bios_print_loop: + mov al, [si] + int 0x10 ; Writes the content of al to the screen + inc si + cmp byte [si], 0x0 ; If we got to the null byte, we're done + jne bios_print_loop ; Otherwise, we run this again + popa + ret + + +bios_println: + ; Prints a null-terminated string whose address + ; is located in the si register and sets the TTY + ; cursor to the next line + pusha + call bios_print + call bios_newline + popa + ret + + +bios_printh: + ; Prints the value of dx in hexadecimal format + pusha + xor cx, cx ; This serves as our index and loop variable + bios_printh_loop: + cmp cx, 4 ; loop 4 times + je bios_printh_end + + ; Here we extract the last digit from dx using + ; a bitmask, with ax as our working register, + ; and convert it to ASCII by adding 30 to it if + ; it's less than 9 (meaning it's a digit) or 37 + ; if if it's a letter (that's because letters and + ; numbers are 7 digits apart in the ASCII table) + mov ax, dx + and ax, 0xf + add al, 0x30 + cmp al, 0x39 + jle bios_printh_step2 + add al, 7 + + bios_printh_step2: + ; Now we start filling our string variable (starting from the + ; back, since we are extracting digits from the end of the + ; number) and then rotate the number by 4 bits to access the + ; next digit. This works and is the same as the more common + ; modulo division because each hexadecimal digits represents + ; exactly 4 bits and we can take advantage of the CPU's much + ; faster bitwise operations rather than burden ourselves with + ; a costly modulo 10 division (which would take hundreds of + ; clock cycles, as opposed to it only taking one for a rotate + ; operation) + mov si, HEX_OUT + 5 ; We skip the '0x' part and jump to the last digit + sub si, cx ; We subtract si by cx so we land on the right digit + mov [si], al ; We copy the character in al to the character pointed to by si + ror dx, 4 ; With an example input: 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 + inc cx + jmp bios_printh_loop + + bios_printh_end: + ; Since we have a string now, we just call our + ; handy routine to print it out. We're done! + mov si, HEX_OUT + call bios_print + popa + ret + + +HEX_OUT: db '0x0000', 0 ; reserve memory for our new string + +[bits 32] + +; I/O routines that directly manipulate video memory +VMEM_START: equ 0xb8000 ; Video memory always starts at this address +; A character on the screen in VGA text mode is composed of 2 bytes: +; the first byte is the ASCII codepoint to be printed, while the next +; octet represents additional formatting information (color, blink, +; underline, etc.). More info: https://en.wikipedia.org/wiki/VGA_text_mode +WHITE_ON_BLACK: equ 0x0f + + +vga_print: + ; Prints a null-terminated string located + ; on the esi register + pusha + mov edx, VMEM_START + + vga_print_loop: + mov al, [esi] + mov ah, WHITE_ON_BLACK + cmp al, 0 + je vga_print_done ; If we're at the null byte, we exit + mov [edx], ax ; Write the 2-byte character to video memory + inc esi ; Go to the next character in the string + add ebx, 2 ; Go to the next character in video memory + jmp vga_print_loop + + vga_print_done: + popa + ret + + +vga_println: + ; Prints a null-terminated string located on the + ; esi register and terminates it with a newline + pusha + call vga_print + mov esi, NEWLINE + call vga_print + popa + ret + + +vga_printh: + ; Prints the value of edx in hexadecimal format + pusha + xor ecx, ecx ; This serves as our index and loop counter + vga_printh_loop: + cmp ecx, 8 ; loop 8 times + je vga_printh_end + ; Here we extract the last digit from edx using + ; a bitmask, with eax as our working register, + ; and convert it to ASCII by adding 30 to it if + ; it's less than 9 (meaning it's a digit) or 37 + ; if if it's a letter (that's because letters and + ; numbers are 7 digits apart in the ASCII table) + mov eax, edx + and eax, 0xf + add ax, 0x30 + cmp ax, 0x39 + jle vga_printh_step2 + add ax, 7 + + vga_printh_step2: + ; Now we start filling our string variable (starting from the + ; back, since we are extracting digits from the end of the + ; number) and then rotate the number by 4 bits to access the + ; next digit. This works and is the same as the more common + ; modulo division because each hexadecimal digits represents + ; exactly 4 bits and we can take advantage of the CPU's much + ; faster bitwise operations rather than burden ourselves with + ; a costly modulo 10 division (which would take hundreds of + ; clock cycles, as opposed to it only taking one for a rotate + ; operation) + mov esi, HEX_OUT_LONG + 9 ; We skip the '0x' part and jump to the last digit + sub esi, ecx ; We subtract esi by ecx so we land on the right digit + mov [esi], ax ; We copy the character in ax to the character pointed to by esi + ror edx, 4 ; With an example input: 0x1234 -> 0x4123 -> 0x3412 -> 0x2341 -> 0x1234 + inc ecx + jmp vga_printh_loop + + vga_printh_end: + mov esi, HEX_OUT_LONG + call vga_print + popa + ret + + +NEWLINE: db 0xA, 0xD +HEX_OUT_LONG: db '0x00000000', 0 \ No newline at end of file diff --git a/src/bootloader/util/tty.s b/src/bootloader/util/tty.s deleted file mode 100644 index ce0ca79..0000000 --- a/src/bootloader/util/tty.s +++ /dev/null @@ -1,44 +0,0 @@ -; Some utilities to deal with the TTY using the BIOS -; during real mode - -next_line: - ; Points the TTY cursor to the next - ; line - pusha - ; Source: http://www.techhelpmanual.com/118-int_10h_03h__query_cursor_position_and_size.html - mov ah, 0x3 ; Get cursor position - mov bh, 0x0 ; Page 0 - int 0x10 - ; Go to the next row - ; Source: http://www.techhelpmanual.com/117-int_10h_02h__set_cursor_position.html - mov ah, 0x2 ; Set cursor position - mov bh, 0x0 ; Page 0 - xor dl, dl ; Goes to column 0 (i.e. start of the line) - inc dh ; Goes to the next row - int 0x10 - popa - ret - - -print: - ; Prints a null-terminated string whose address - ; is located in the si register - pusha - mov ah, 0xe ; Set the screen in TTY mode - print_loop: - mov al, [si] - int 0x10 ; Writes the content of al to the screen - inc si - cmp byte [si], 0x0 ; If we got to the null byte, we're done - jne print_loop ; Otherwise, we run this again - popa - ret - - -println: - ; Prints a null-terminated string whose address - ; is located in the si register and sets the TTY - ; cursor to the next line - call print - call next_line - ret \ No newline at end of file