Some small improvements

This commit is contained in:
Philipp Oppermann
2015-08-13 11:58:17 +02:00
parent 726b8b65b6
commit ed5ace118b
2 changed files with 30 additions and 15 deletions

View File

@@ -3,13 +3,13 @@ layout: post
title: '[DRAFT] A minimal x86 kernel in small steps' title: '[DRAFT] A minimal x86 kernel in small steps'
related_posts: null 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. 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/ [Rust]: http://www.rust-lang.org/
[create an issue]: https://github.com/phil-opp/phil-opp.github.io/issues [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 ## 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). 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 section .text
bits 32 bits 32
start: start:
; print `OK` to screen
mov dword [0xb8000], 0x2f4b2f4f mov dword [0xb8000], 0x2f4b2f4f
hlt 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. - `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 - 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). - `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 - `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: 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 [ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[linker script]: https://sourceware.org/binutils/docs/ld/Scripts.html [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 ## 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: 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 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`) 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`) 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. 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 $(kernel) build/isofiles/boot/
@cp $(grub_cfg) build/isofiles/boot/grub @cp $(grub_cfg) build/isofiles/boot/grub
@grub-mkrescue -o $(iso) build/isofiles 2> /dev/null @grub-mkrescue -o $(iso) build/isofiles 2> /dev/null
@rm -r build/isofiles
$(kernel): $(assembly_object_files) $(linker_script) $(kernel): $(assembly_object_files) $(linker_script)
@ld -n -T $(linker_script) -o $(kernel) $(assembly_object_files) @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 `$<` 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). - 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]. In the [next post] we will create a page table and do some CPU configuration to switch to [Long Mode].

View File

@@ -3,7 +3,7 @@ layout: post
title: '[DRAFT] Entering Long Mode in small steps' title: '[DRAFT] Entering Long Mode in small steps'
related_posts: null 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. 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 ```nasm
... ...
; Prints `ERR: ` and the given error code to screen and hangs.
; parameter: error code (in ascii) in al ; parameter: error code (in ascii) in al
error: error:
mov dword [0xb8000], 0x4f524f45 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 section .bss
stack_bottom: stack_bottom:
resb 64 resb 64
stack_top: 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`: 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 bits 32
start: start:
mov esp, stack_top 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. 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 bits 32
_start: _start:
mov esp, stack_top mov esp, stack_top
call check_multiboot call check_multiboot
call check_cpuid call check_cpuid
call check_long_mode 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 section .bss
align 4096 align 4096
p4_table: p4_table:
resb 4096 resb 4096
p3_table: p3_table:
resb 4096 resb 4096
stack_bottom: stack_bottom:
resb 64 resb 64
stack_top: 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: 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: start:
mov esp, stack_top mov esp, stack_top
call check_multiboot call check_multiboot
call check_cpuid call check_cpuid
call check_long_mode call check_long_mode
call setup_page_tables ; new call setup_page_tables ; new
call enable_paging ; new call enable_paging ; new
; write OK ; print `OK` to screen
mov dword [0xb8000], 0x2f4b2f4f mov dword [0xb8000], 0x2f4b2f4f
hlt hlt
... ...
@@ -336,7 +344,7 @@ start:
; load the 64-bit GDT ; load the 64-bit GDT
lgdt [gdt64.pointer] 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: 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 ds, ax
mov es, 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: 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 dq (1<<44) | (1<<47) | (1<<41) | (1<<43) | (1<<53) ; code segment
.data: equ $ - gdt64 ; new .data: equ $ - gdt64 ; new
dq (1<<44) | (1<<47) | (1<<41) ; data segment 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: 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 bits 64
long_mode_start: long_mode_start:
; write OKAY ; print `OKAY` to screen
mov rax, 0x2f592f412f4b2f4f mov rax, 0x2f592f412f4b2f4f
mov qword [0xb8000], rax mov qword [0xb8000], rax
hlt
bits 32 bits 32
... ...