From 668fa121eb74d3317a73c2289f8805aaf1c84d7e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Duje=20Mihanovi=C4=87?= Date: Tue, 7 Sep 2021 20:33:39 +0200 Subject: [PATCH] Document the code --- boot.s | 112 +++++++++++++++++++++++++++--------------------- how-to-compile | 10 +++++ kernel/entry.s | 15 +++++-- kernel/kernel.c | 4 +- 4 files changed, 86 insertions(+), 55 deletions(-) create mode 100644 how-to-compile diff --git a/boot.s b/boot.s index a40b750..de5887d 100644 --- a/boot.s +++ b/boot.s @@ -1,32 +1,33 @@ - bits 16 - org 7C00h + bits 16 ; boot sectors run in real mode + org 7C00h ; BIOS loads us at 0x7c00 -KERNEL_OFFSET equ 1000h +KERNEL_OFFSET equ 1000h ; where we will load our kernel - mov [BOOT_DRIVE], dl + mov [BOOT_DRIVE], dl ; BIOS puts the number of our boot drive in dl, so we will want to remember this - mov bp, 9000h + mov bp, 9000h ; initialize stack at a spot sufficiently distanced from this code mov sp, bp mov di, 0 - xor ax, ax - int 13h - jc reset_error - - mov ah, 2 - mov al, 10 - xor ch, ch - mov cl, 2 - xor dh, dh - xor bx, bx - mov es, bx - mov bx, KERNEL_OFFSET - int 13h - jc read_error - cmp al, 10 - jl read_error - jmp switch_to_pm + xor ax, ax ; clear accumulator + int 13h ; BIOS disk services, with a cleared accumulator this will reset the disk controller + jc reset_error ; halt if controller reset fails + + mov ah, 2 ; instruct BIOS's interrupt 13h to read sectors + mov al, 10 ; load 10 sectors, might be excessive (for now) but it's still good to do so + xor ch, ch ; read from 1st cylinder + mov cl, 2 ; start reading from 2nd sector, right after the boot sector + xor dh, dh ; read from 1st head + xor bx, bx ; clear B register + mov es, bx ; clear extended segment + mov bx, KERNEL_OFFSET ; put the sectors in our desired offset + ; dl holds the number of the drive to read from, but BIOS already filled this in + int 13h ; do the read + jc read_error ; halt if read fails + cmp al, 10 ; make sure we actually read 10 sectors + jl read_error ; halt if we didn't + jmp switch_to_pm ; if all is good, begin the switch to 32-bit protected mode reset_error mov bx, 0B800h @@ -47,64 +48,77 @@ halt switch_to_pm mov bx, 0B800h - mov es, bx - mov byte [es:0], 'L' + mov es, bx ; set extra segment to starting address of video RAM + mov byte [es:0], 'L' ; print an L to screen, to let us know that we actually got here - cli - xor ax, ax - mov ds, ax - lgdt [gdt_desc] + cli ; disable interrupts + xor ax, ax ; clear accumulator + mov ds, ax ; clear data segment, this makes sure the next instruction reads from the right place + lgdt [gdt_desc] ; load the Global Descriptor Table - mov eax, cr0 - or eax, 1 - mov cr0, eax - jmp CODE_SEG:protected + mov eax, cr0 ; move Control Register 0 to accumulator + or eax, 1 ; flip the bit which controls memory protection + mov cr0, eax ; move the accumulator back to CR0 + jmp CODE_SEG:protected ; not quite there yet, need to do a far jump to clean the pipeline from any 16-bit instructions + ; note that the jump does not have to be physically far away, it just needs to use a segmented address - bits 32 + bits 32 ; we are finally in 32-bit mode protected - mov ax, DATA_SEG + mov ax, DATA_SEG ; put the selector for the data segment in the accumulator + ; in real mode, segmentation works by taking one of these segment registers, shifting it right by 4 bits or 1 hex digit + ; and then adding a 16-bit offset to form a 20-bit address + ; example: 1234h:5678h + ; 1234h is shifted to 12340h, 12340h + 5678h is 179B8h + ; in 32-bit protected mode, these segment registers do not hold the segment address itself, but a selector in the GDT + ; so we have to update the segment registers accordingly mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax + ; reinitialize the stack at a safe location mov ebp, 090000h mov esp, ebp + ; print a string to let us know that we survived the switch mov ebx, pm_success call pmprint + ; transfer control to the kernel call KERNEL_OFFSET + ; above call should not return in normal circumstances, but if it does hang forever jmp $ pmprint - pusha - mov edx, video_memory + pusha ; save registers to stack + mov edx, video_memory ; initialize dx with location of VRAM .loop - mov al, [ebx] - mov ah, 0Fh + mov al, [ebx] ; read next char and put it in al + mov ah, 0Fh ; puts the VGA text mode color white on black into ah - cmp al, 0 - je .done + cmp al, 0 ; if the next character is null, we reached end of string + je .done ; so return the instruction - mov [edx], al - mov [edx+1], ah + mov [edx], al ; otherwise put the next character in the video memory + mov [edx+1], ah ; do the same for its color - inc ebx - add edx, 2 + inc ebx ; point to next character in string + add edx, 2 ; point to next character in VRAM - jmp .loop + jmp .loop ; go back to the loop .done - popa - ret + popa ; restore registers from stack + ret ; return pm_success db "Now in protected mode", 0 video_memory equ 0B8000h +; the actual Global Descriptor Table +; refer to Volume 3, Chapter 2, 2.1.1 of Intel's 64 and IA-32 Software Developer's Manual for more info gdt_start null_seg dq 0 @@ -129,7 +143,7 @@ gdt_desc CODE_SEG equ code_seg - gdt_start DATA_SEG equ data_seg - gdt_start -BOOT_DRIVE db 0 +BOOT_DRIVE db 0 ; reserve a spot in RAM for our boot drive variable times 510-($-$$) db 0 - dw 0AA55h + dw 0AA55h ; MBR signature diff --git a/how-to-compile b/how-to-compile new file mode 100644 index 0000000..bcd0b8b --- /dev/null +++ b/how-to-compile @@ -0,0 +1,10 @@ +1. Compile an i686-elf GCC and binutils following the instructions on this page: + https://wiki.osdev.org/GCC_Cross-Compiler + +2. Install yasm, or modify the makefile to use nasm if you prefer it + +3. Run make in the source root + +4. Run boot.img with some emulator, I use qemu: + qemu-system-i386 boot.img + You can use the x86_64 qemu, Bochs, VirtualBox or something else if you prefer diff --git a/kernel/entry.s b/kernel/entry.s index 236b81e..a02df92 100644 --- a/kernel/entry.s +++ b/kernel/entry.s @@ -1,5 +1,12 @@ - bits 32 - extern _start +; when our kernel source has functions before _start and we blindly transfer control to the beginning +; of the kernel binary, it will wrongly execute the other functions +; the solution is to link a small assembly routine (this) which executes a known label within the kernel +; so that this routine comes before the kernel in the resulting binary +; we cannot link the boot sector code and the kernel because the former needs to be a raw binary, while the +; kernel is compiled into an ELF object file which contains some metadata on the kernel code - call _start - jmp $ + bits 32 ; this is started in protected mode + extern _start ; the known label in the kernel source we will execute is, well, not in this assembly routine + + call _start ; call the known label + jmp $ ; it should never return, but hang forever if it does diff --git a/kernel/kernel.c b/kernel/kernel.c index 87fbef9..e04b875 100644 --- a/kernel/kernel.c +++ b/kernel/kernel.c @@ -1,5 +1,5 @@ void _start(void) { - char *video_memory = (char *) 0xB8000; - *video_memory = 'A'; + char *video_memory = (char *) 0xB8000; /* VGA VRAM starts at 0xB8000 */ + *video_memory = 'A'; /* put an A at the beginning of the VRAM */ } -- 2.39.5