diff --git a/_posts/2015-07-22-rust-os-boot.md b/_posts/2015-07-22-rust-os-boot.md index adc55687..a7d25399 100644 --- a/_posts/2015-07-22-rust-os-boot.md +++ b/_posts/2015-07-22-rust-os-boot.md @@ -3,13 +3,13 @@ layout: post title: '[DRAFT] A minimal x86 kernel in small steps' related_posts: null --- -This post explains how to create a minimal x86 operating system kernel. In fact, it will just boot and write `OK` to the screen. The following blog posts we will extend it using the [Rust] programming language. +This post explains how to create a minimal x86 operating system kernel. In fact, it will just boot and print `OK` to the screen. The following blog posts we will extend it using the [Rust] programming language. I tried to explain everything in detail and to keep the code as simple as possible. If you have any questions, suggestions or other issues, please leave a comment or [create an issue] on Github. The source code is available in a [repository][source code], too. [Rust]: http://www.rust-lang.org/ [create an issue]: https://github.com/phil-opp/phil-opp.github.io/issues -[source code]: https://github.com/phil-opp/blogOS/tree/multiboot_bootstrap +[source code]: https://github.com/phil-opp/blogOS/tree/multiboot_bootstrap/src/arch/x86_64 ## Overview When you turn on a computer, it loads the BIOS. It first runs self test and initialization routines of the hardware. Then it looks for bootable devices. If it finds one, the control is transferred to its _bootloader_, which is a small portion of executable code stored at the device's beginning. The bootloader has to determine the location of the kernel image on the device and load it into memory. It also needs to switch the CPU to the so-called [Protected Mode] because x86 CPUs start in the very limited [Real Mode] by default (to be compatible to programs from 1978). @@ -85,6 +85,7 @@ global start section .text bits 32 start: + ; print `OK` to screen mov dword [0xb8000], 0x2f4b2f4f hlt ``` @@ -93,7 +94,7 @@ There are some new commands: - `global` exports a label (makes it public). As `start` will be the entry point of our kernel, it needs to be public. - the `.text` section is the default section for executable code - `bits 32` specifies that the following lines are 32-bit instructions. It's needed because the CPU is still in [Protected mode] when GRUB starts our kernel. When we switch to [Long mode] in the [next post] we can use `bits 64` (64-bit instructions). -- the `mov dword` instruction moves the 32bit constant `0x2f4f2f4b` to the memory at address `b8000` (it writes `OK` to the screen, an explanation follows in the [next post]) +- the `mov dword` instruction moves the 32bit constant `0x2f4f2f4b` to the memory at address `b8000` (it prints `OK` to the screen, an explanation follows in the next posts) - `hlt` is the halt instruction and causes the CPU to stop Through assembling, viewing and disassembling it we can see the CPU [Opcodes] in action: @@ -159,7 +160,7 @@ Idx Name Size VMA LMA File off Algn [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format [linker script]: https://sourceware.org/binutils/docs/ld/Scripts.html -[^Linker 1M]: We don't want to load the kernel to e.g. `0x0` because there are many special memory areas below the 1MB mark (for example the so-called VGA buffer at `0xb8000`, that we use to write `OK` to the screen). +[^Linker 1M]: We don't want to load the kernel to e.g. `0x0` because there are many special memory areas below the 1MB mark (for example the so-called VGA buffer at `0xb8000`, that we use to print `OK` to the screen). ## Creating the ISO The last step is to create a bootable ISO image with GRUB. We need to create the following directory structure and copy the `kernel.bin` to the right place: @@ -203,7 +204,7 @@ Notice the green `OK` in the upper left corner. Let's summarize what happens: 2. the bootloader reads the kernel executable and finds the Multiboot header 3. it copies the `.boot` and `.text` sections to memory (to addresses `0x100000` and `0x100020`) 4. it jumps to the entry point (`0x100020`, you can obtain it through `objdump -f`) -5. our kernel writes the green `OK` and stops the CPU +5. our kernel prints the green `OK` and stops the CPU You can test it on real hardware, too. Just burn the ISO to a disk or USB stick and boot from it. @@ -254,6 +255,7 @@ $(iso): $(kernel) @cp $(kernel) build/isofiles/boot/ @cp $(grub_cfg) build/isofiles/boot/grub @grub-mkrescue -o $(iso) build/isofiles 2> /dev/null + @rm -r build/isofiles $(kernel): $(assembly_object_files) $(linker_script) @ld -n -T $(linker_script) -o $(kernel) $(assembly_object_files) @@ -270,7 +272,9 @@ Some comments (see the [Makefile tutorial] if you don't know `make`): - the `$<` and `$@` in the assembly target are [automatic variables] - the Makefile has rudimentary multi-architecture support, e.g. `make arch=mips iso` tries to create an ISO for MIPS (it will fail of course as we don't support MIPS yet). -Now we can invoke `make` and all updated assembly files are compiled and linked. The `make iso` command also creates the ISO image and `make run` will additionally start QEMU. Nice :) +Now we can invoke `make` and all updated assembly files are compiled and linked. The `make iso` command also creates the ISO image and `make run` will additionally start QEMU. Nice! + +## What's next? In the [next post] we will create a page table and do some CPU configuration to switch to [Long Mode]. diff --git a/_posts/2015-08-10-entering-longmode.md b/_posts/2015-08-10-entering-longmode.md index 91f0ef72..c0f20365 100644 --- a/_posts/2015-08-10-entering-longmode.md +++ b/_posts/2015-08-10-entering-longmode.md @@ -3,7 +3,7 @@ layout: post title: '[DRAFT] Entering Long Mode in small steps' related_posts: null --- -In the [last post] we created a minimal multiboot kernel. It just writes `OK` and hangs. Let's extend it! The goal is to call 64-bit [Rust] code. But the CPU is currently in [Protected Mode] and allows only 32bit instructions and up to 4GiB memory. So we need to setup _Paging_ and switch to the 64-bit [Long Mode] first. +In the [last post] we created a minimal multiboot kernel. It just prints `OK` and hangs. Let's extend it! The goal is to call 64-bit [Rust] code. But the CPU is currently in [Protected Mode] and allows only 32bit instructions and up to 4GiB memory. So we need to setup _Paging_ and switch to the 64-bit [Long Mode] first. I tried to explain everything in detail and to keep the code as simple as possible. If you have any questions, suggestions or other issues, please leave a comment or [create an issue] on Github. The source code is available in a [repository][source code], too. @@ -20,6 +20,7 @@ To avoid bugs and strange errors on old CPUs we should test if the processor sup ```nasm ... +; Prints `ERR: ` and the given error code to screen and hangs. ; parameter: error code (in ascii) in al error: mov dword [0xb8000], 0x4f524f45 @@ -36,7 +37,7 @@ Now we will add some check _functions_. A function is just a normal label with a ... section .bss stack_bottom: -resb 64 + resb 64 stack_top: ``` A stack doesn't need to be initialized with data because we will `pop` only if we `pushed` before. By using the [.bss] section and the `resb` (reserve byte) command, we just declare the length of the uninitialized data (64 byte) and avoid saving needless bytes in the executable. When loading the executable, GRUB will create the section and the stack in memory. To use it, we update the stack pointer right after `start`: @@ -48,6 +49,8 @@ section .text bits 32 start: mov esp, stack_top + + ; print `OK` to screen ... ``` We use `stack_top` because the stack grows downwards: A `push eax` subtracts 4 from `esp` and does a `mov [esp], eax` afterwards (`eax` is a general purpose register). Now we have a valid stack pointer and are able to call functions. @@ -124,9 +127,12 @@ section .text bits 32 _start: mov esp, stack_top + call check_multiboot call check_cpuid call check_long_mode + + ; print `OK` to screen ... ``` @@ -195,11 +201,11 @@ The `huge page` bit is now very useful to us. It creates a 2MiB (when used in P2 section .bss align 4096 p4_table: -resb 4096 + resb 4096 p3_table: -resb 4096 + resb 4096 stack_bottom: -resb 64 + resb 64 stack_top: ``` The `resb` command reserves the specified amount of bytes without initializing them, so the 8KiB don't need to be saved in the executable. The `align 4096` ensures that the page tables are page aligned. When GRUB creates the `.bss` section in memory, it will initialize it to `0`. So our `p4_table` is already valid (it contains 512 non-present entries) but not very useful. Let's link its first entry to the `p3_table` and map the first P3 entry to a huge page: @@ -259,13 +265,15 @@ Let's call our new functions in `start`: ... start: mov esp, stack_top + call check_multiboot call check_cpuid call check_long_mode + call setup_page_tables ; new call enable_paging ; new - ; write OK + ; print `OK` to screen mov dword [0xb8000], 0x2f4b2f4f hlt ... @@ -336,7 +344,7 @@ start: ; load the 64-bit GDT lgdt [gdt64.pointer] - ; write OK + ; print `OK` to screen ... ``` When you still see the green `OK`, everything went fine and the new GDT is loaded. I don't want to frustrate you, but we still can't execute 64-bit code… The selector registers like the code selector `cs`, the data selector `ds`, the stack selector `ss`, and the extra selector `es` still have the values from the old GDT. To update them, we need to load them with the GDT index (in bytes) of the desired segment. In our case the code segment starts at byte 8 of the GDT and the data segment at byte 16. Let's try it: @@ -351,7 +359,7 @@ When you still see the green `OK`, everything went fine and the new GDT is loade mov ds, ax mov es, ax - ; write OK + ; print `OK` to screen ... ``` It should still work. The segment selectors are only 16-bits large, so we use the ax subregister. Notice that we didn't update the code selector `cs`. We will do that later. First we should replace this hardcoded `16` by adding some labels to our GDT: @@ -364,6 +372,8 @@ gdt64: dq (1<<44) | (1<<47) | (1<<41) | (1<<43) | (1<<53) ; code segment .data: equ $ - gdt64 ; new dq (1<<44) | (1<<47) | (1<<41) ; data segment +.pointer: + ... ``` Now we can use `gdt64.data` instead of 16 and `gdt64.code` instead of 8. These labels will still work if we modify the GDT. So let's do the last step and enter the true 64-bit mode (finally)! We just need to load `cs` with `gdt64.code`. But we can't do it through `mov`. The only way to reload the code selector is a _far jump_ or a _far return_. These instructions work like a normal jump/return but change the code selector. We will use a far jump to a long mode label: @@ -381,9 +391,10 @@ Now we can use `gdt64.data` instead of 16 and `gdt64.code` instead of 8. These l bits 64 long_mode_start: - ; write OKAY + ; print `OKAY` to screen mov rax, 0x2f592f412f4b2f4f mov qword [0xb8000], rax + hlt bits 32 ...