Files
blog_os/blog/content/edition-3/posts/01-minimal-kernel/index.md
Philipp Oppermann 9080e69a09 Improve headings
2023-12-28 17:59:13 +01:00

607 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
+++
title = "Minimal Kernel"
weight = 1
path = "minimal-kernel"
date = 0000-01-01
draft = true
[extra]
chapter = "Bare Bones"
icon = '''
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-binary" viewBox="0 0 16 16">
<path d="M7.05 11.885c0 1.415-.548 2.206-1.524 2.206C4.548 14.09 4 13.3 4 11.885c0-1.412.548-2.203 1.526-2.203.976 0 1.524.79 1.524 2.203zm-1.524-1.612c-.542 0-.832.563-.832 1.612 0 .088.003.173.006.252l1.559-1.143c-.126-.474-.375-.72-.733-.72zm-.732 2.508c.126.472.372.718.732.718.54 0 .83-.563.83-1.614 0-.085-.003-.17-.006-.25l-1.556 1.146zm6.061.624V14h-3v-.595h1.181V10.5h-.05l-1.136.747v-.688l1.19-.786h.69v3.633h1.125z"/>
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
</svg>
'''
+++
The first step in creating our own operating system kernel is to create a [bare metal] Rust executable that does not depend on an underlying operating system.
For that we need to disable most of Rust's standard library and adjust various compilation settings.
The result is a minimal operating system kernel that forms the base for the following posts of this series.
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
<!-- more -->
This blog is openly developed on [GitHub].
If you have any problems or questions, please open an issue there.
You can also leave comments [at the bottom].
The complete source code for this post can be found in the [`post-3.1`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-3.1
<!-- toc -->
## Introduction
Kernels are the heart of an operating system.
They provide all the fundamental building blocks that are required for building higher-level programs.
Typical building blocks are threads, files, heap memory, timers, or sockets.
Other important tasks of a kernel are the isolation of different programs and the multiplexing of resources.
When writing an operating system kernel, we need to provide all of these building blocks ourselves.
This means that we can't use most of the [Rust standard library].
However, there are still a lot of Rust features that we _can_ use.
For example, we can use [iterators], [closures], [pattern matching], [`Option`] and [`Result`], [string formatting], and of course the [ownership system].
These features make it possible to write a kernel in a very expressive, high level way and worry less about [undefined behavior] or [memory safety].
[`Option`]: https://doc.rust-lang.org/core/option/
[`Result`]: https://doc.rust-lang.org/core/result/
[Rust standard library]: https://doc.rust-lang.org/std/
[iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html
[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html
[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html
[string formatting]: https://doc.rust-lang.org/core/macro.write.html
[ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
In this post, we create a minimal OS kernel that can be run without an underlying operating system.
Such an executable is often called a “freestanding” or “bare-metal” executable.
We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel.
## Disabling the Standard Library
By default, all Rust crates link the [standard library], which depends on the operating system for features such as threads, files, or networking.
It also depends on the C standard library `libc`, which closely interacts with OS services.
Since our plan is to write an operating system, we cannot use any OS-dependent libraries.
So we have to disable the automatic inclusion of the standard library, which we can do through the [`no_std` attribute].
[standard library]: https://doc.rust-lang.org/std/
[`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
We start by creating a new cargo application project.
The easiest way to do this is through the command line:
```
cargo new kernel --bin --edition 2021
```
We name the project `kernel` here, but of course you can choose your own name.
The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2021` flag specifies that we want to use the [2021 edition] of Rust for our crate.
When we run the command, cargo creates the following directory structure for us:
[2021 edition]: https://doc.rust-lang.org/edition-guide/rust-2021/index.html
```
kernel
├── Cargo.toml
└── src
└── main.rs
```
The `Cargo.toml` contains the crate configuration, for example the crate name, the [semantic version] number, and dependencies.
The `src/main.rs` file contains the root module of our crate and our `main` function.
You can compile your crate through `cargo build` and then run the compiled `kernel` binary in the `target/debug` subfolder.
[semantic version]: https://semver.org/
### The `no_std` Attribute
Right now our crate implicitly links the standard library.
Let's try to disable this by adding the [`no_std` attribute]:
```rust,hl_lines=3
// main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
```
When we try to build it now (by running `cargo build`), the following errors occur:
```
error: cannot find macro `println!` in this scope
--> src/main.rs:4:5
|
4 | println!("Hello, world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
[...]
```
The reason for the first error is that the [`println` macro] is part of the standard library, which we no longer include.
So we can no longer print things.
This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system.
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
So let's remove the printing and try again with an empty main function:
```rust,hl_lines=5
// main.rs
#![no_std]
fn main() {}
```
```
cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
[...]
```
The `println` error is gone, but the compiler is still missing a `#[panic_handler]` function and a _language item_.
### Panic Implementation
The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs.
The standard library provides its own panic handler function, but in a `no_std` environment we need to define one ourselves:
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
```rust,hl_lines=3 9-13
// in main.rs
use core::panic::PanicInfo;
#![no_std]
fn main() {}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
The [`PanicInfo` parameter][PanicInfo] contains the file and line where the panic happened and the optional panic message.
The handler function should never return, so it is marked as a [diverging function] by returning the [“never” type] `!`.
There is not much we can do in this function for now, so we just loop indefinitely.
[PanicInfo]: https://doc.rust-lang.org/core/panic/struct.PanicInfo.html
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
[“never” type]: https://doc.rust-lang.org/std/primitive.never.html
After defining a panic handler, only the `eh_personality` language item error remains:
```
cargo build
error: language item required, but not found: `eh_personality`
|
= note: this can occur when a binary crate with `#![no_std]` is compiled for a
target where `eh_personality` is defined in the standard library
= help: you may be able to compile for a target that doesn't need `eh_personality`,
specify a target with `--target` or in `.cargo/config`
```
### Disabling Unwinding
Language items are special functions and types that are required internally by the compiler.
They are normally provided by the standard library, which we disabled using the `#![no_std]` attribute.
The [`eh_personality` language item] marks a function that is used for implementing [stack unwinding].
By default, Rust uses unwinding to run the destructors of all live stack variables in case of a [panic].
This ensures that all used memory is freed and allows the parent thread to catch the panic and continue execution.
Unwinding, however, is a complex process and requires some OS-specific libraries, such as [libunwind] on Linux or [structured exception handling] on Windows.
[`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
[libunwind]: https://www.nongnu.org/libunwind/
[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling
While unwinding is very useful, it also has some drawbacks.
For example, it increases the size of the compiled executable because it requires additional context at runtime.
Because of these drawbacks, Rust provides an option to [abort on panic] instead.
[abort on panic]: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html#unwinding-the-stack-or-aborting-in-response-to-a-panic
We already use a custom panic handler that never returns, so we don't need unwinding for our kernel.
By disabling it, the `eh_personality` language item won't be required anymore.
There are multiple ways to set the panic strategy, the easiest is to use [cargo profiles]:
[cargo profiles]: https://doc.rust-lang.org/cargo/reference/profiles.html
```toml,hl_lines=3-7
# in Cargo.toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
```
This sets the panic strategy to `abort` for both the `dev` profile (used for `cargo build`) and the `release` profile (used for `cargo build --release`).
Now the `eh_personality` language item should no longer be required.
When we try to compile our kernel now, a new error occurs:
```
cargo build
error: requires `start` lang_item
```
Our kernel is missing the `start` language item, which defines the _entry point_ of the executable.
## Setting the Entry Point
The [entry point] of a program is the function that is called when the executable is started.
One might think that the `main` function is the first function called, however, most languages have a [runtime system], which is responsible for things such as garbage collection (e.g. in Java) or software threads (e.g. goroutines in Go).
This runtime needs to be called before `main`, since it needs to initialize itself.
[entry point]: https://en.wikipedia.org/wiki/Entry_point
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
In a typical Rust binary that links the standard library, execution starts in a C runtime library called [`crt0`] (“C runtime zero”), which sets up the environment for a C application.
This includes creating a [call stack] and placing the command line arguments in the right CPU registers.
The C runtime then invokes the [entry point of the Rust runtime][rt::lang_start], which is marked by the `start` language item.
Rust only has a very minimal runtime, which takes care of some small things such as setting up stack overflow guards or printing a backtrace on panic.
The runtime then finally calls the `main` function.
[`crt0`]: https://en.wikipedia.org/wiki/Crt0
[call stack]: https://en.wikipedia.org/wiki/Call_stack
[rt::lang_start]: hhttps://github.com/rust-lang/rust/blob/0d97f7a96877a96015d70ece41ad08bb7af12377/library/std/src/rt.rs#L59-L70
Since we're building an operating system kernel that should run without any underlying operating system, we don't want our kernel to depend on any Rust or C runtime.
To remove these dependencies, we need to do two things:
1. Instruct the compiler that we want to build for a bare-metal target environment. This removes the dependency on the C library.
2. Disable the Rust main function to remove the Rust runtime.
### Bare-Metal Target
By default Rust tries to build an executable that is able to run in your current system environment.
For example, if you're using Windows and an `x86_64` CPU, Rust tries to build a `.exe` Windows executable that uses `x86_64` instructions.
This environment is called your "host" system.
To describe different environments, Rust uses a string called [_target triple_].
You can see the target triple for your host system by running `rustc --version --verbose`:
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
```
rustc 1.68.1 (8460ca823 2023-03-20)
binary: rustc
commit-hash: 8460ca823e8367a30dda430efda790588b8c84d3
commit-date: 2023-03-20
host: x86_64-unknown-linux-gnu
release: 1.68.1
LLVM version: 15.0.6
```
The above output is from a `x86_64` Linux system.
We see that the `host` triple is `x86_64-unknown-linux-gnu`, which includes the CPU architecture (`x86_64`), the vendor (`unknown`), the operating system (`linux`), and the [ABI] (`gnu`).
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
By compiling for our host triple, the Rust compiler and the linker assume that there is an underlying operating system such as Linux or Windows that uses the C runtime by default, which requires the `start` language item.
To avoid the runtimes, we can compile for a different environment with no underlying operating system.
#### The `x86_64-unknown-none` Target
Rust supports a [variety of target systems][platform-support], including some bare-metal targets.
For example, the `thumbv7em-none-eabihf` target triple can be used to compile for an [embedded] [ARM] system with a `Cortex M4F` CPU, as used in the [Rust Embedded Book].
[platform-support]: https://doc.rust-lang.org/rustc/platform-support.html
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
[Rust Embedded Book]: https://docs.rust-embedded.org/book/intro/index.html
Our kernel should run on a bare-metal `x86_64` system, so the suitable target triple is [`x86_64-unknown-none`].
The `-none` suffix indicates that there is no underlying operating system.
To be able to compile for this target, we need to add it using [`rustup`].
[`x86_64-unknown-none`]: https://doc.rust-lang.org/rustc/platform-support/x86_64-unknown-none.html
<div class = "note"><details>
<summary><em>What is <code>rustup</code></em>?</summary>
The [`rustup`] tool is the [officially recommended] way of installing Rust.
It supports having multiple versions of Rust installed simultaneously and makes upgrading Rust easy.
It also provides access to optional tools and components such as [`rustfmt`] or [`rust-analyzer`].
This guide requires `rustup`, so please install it if you haven't already.
[`rustup`]: https://rustup.rs/
[officially recommended]: https://www.rust-lang.org/learn/get-started
[`rustfmt`]: https://github.com/rust-lang/rustfmt/
[`rust-analyzer`]: https://github.com/rust-lang/rust-analyzer
</details></div>
To download and set up the `x86_64-unknown-none` target, we use the following `rustup` command:
```
rustup target add x86_64-unknown-none
```
This downloads a pre-compiled copy of the `core` library for the target.
Afterwards, we can [cross compile] our executable for a bare metal environment by passing a `--target` argument:
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
```
cargo build --target x86_64-unknown-none
Compiling kernel v0.1.0 (/<...>/kernel)
error: requires `start` lang_item
```
We still get the error about a missing `start` language item because the custom target only removed the dependency on the C library.
To remove the dependency on the Rust runtime as well, we can use the `#[no_main]` attribute.
Before that, we can do a small cleanup.
The `x86_64-unknown-none` target defaults to `panic = "abort"`, so the we can remove the `profile.dev` and `profile.release` tables from our `Cargo.toml` again.
### The `#[no_main]` Attribute
To tell the Rust compiler that we don't want to use the normal entry point chain, we add the `#![no_main]` attribute.
```rust,hl_lines=4
// main.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
You might notice that we removed the `main` function.
The reason is that a `main` doesn't make sense without an underlying runtime that calls it.
Instead, we are now overwriting the operating system entry point with our own `_start` function:
```rust,hl_lines=3-6
// in main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
```
By using the `#[no_mangle]` attribute we disable the [name mangling] to ensure that the Rust compiler really outputs a function with the name `_start`.
Without the attribute, the compiler would generate some cryptic `_ZN3kernel4_start7hb173fedf945531caE` symbol to give every function an unique name.
The reason for naming the function `_start` is that this is the default entry point name for most systems.
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
We mark the function as `extern "C"` to tell the compiler that it should use the [C calling convention] for this function (instead of the unspecified Rust calling convention).
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
Like in our panic handler, the `!` return type means that the function is diverging, i.e. not allowed to ever return.
This is required because the entry point is not called by any function, but invoked directly by the operating system or bootloader.
So instead of returning, the entry point should e.g. invoke the [`exit` system call] of the operating system.
In our case, shutting down the machine could be a reasonable action, since there's nothing left to do if a freestanding binary returns.
For now, we fulfill the requirement by looping endlessly.
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
When we run `cargo build --target x86_64-unknown-none` now, it should finally compile without any errors:
```
cargo build --target x86_64-unknown-none
Compiling kernel v0.1.0 (/<...>/kernel)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
```
We successfully created a minimal bare-metal kernel executable! The compiled executable can be found at `target/x86_64-unknown-none/debug/kernel`.
There is no `.exe` extension even if you're on Windows because the `x86_64-unknown-none` target uses UNIX standards.
To build the kernel with optimizations, we can run:
```
cargo build --target x86_64-unknown-none --release
```
The compiled executable is placed at `target/x86_64-unknown-none/release/kernel` in this case.
In the next post we will cover how to turn this kernel into a bootable disk image that can be run in a virtual machine or on real hardware.
In the rest of this post, we will introduce some tools for examining our kernel executable.
These tools are very useful for debugging future issues, so it's good to know about them.
## Useful Tools
In this section, we will examine our kernel executable using the [`objdump`], [`nm`], and [`size`] tools.
[`objdump`]: https://www.man7.org/linux/man-pages/man1/objdump.1.html
[`nm`]: https://man7.org/linux/man-pages/man1/nm.1.html
[`size`]: https://man7.org/linux/man-pages/man1/size.1.html
If you're on a UNIX system, you might already have the above tools installed.
Otherwise (and on Windows), you can use the LLVM binutils shipped by `rustup` through the [`cargo-binutils`] crate.
To install it, run **`cargo install cargo-binutils`** and **`rustup component add llvm-tools-preview`**.
Afterwards, you can run the tools through `rust-nm`, `rust-objdump`, and `rust-strip`.
[`cargo-binutils`]: https://github.com/rust-embedded/cargo-binutils
### List Symbols using `nm`
We defined a `_start` function as the entry point of our kernel.
To verify that it is properly exposed in the executable, we can run `nm` to list all the symbols defined in the executable:
```
rust-nm target/x86_64-unknown-none/debug/kernel
0000000000201120 T _start
```
If we comment out the `_start` function or if we remove the `#[no_mangle]` attribute, the `_start` symbol is no longer there after recompiling:
```
rust-nm target/x86_64-unknown-none/debug/kernel
```
This way we can ensure that we set the `_start` function correctly.
### Inspect ELF File using `objdump`
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
[ELF file format]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
#### File Headers
Among other things, the ELF [file header] specifies the target architecture and the entry point address of the executable files.
To print the file header, we can use `objdump -f`:
[file header]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
```
rust-objdump -f target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
architecture: x86_64
start address: 0x0000000000001210
```
As expected, our kernel targets the `x86_64` CPU architecture.
The start address specifies the memory address of our `_start` function.
Here the function name `_start` becomes important.
If we rename the function to something else (e.g., `_start_here`) and recompile, we see that no start address is set in the ELF file anymore:
```bash,hl_lines=5
rust-objdump -f target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
architecture: x86_64
start address: 0x0000000000000000
```
#### Sections
Using `objdump -h`, we can print the various sections of our kernel executable:
```bash,hl_lines=12
rust-objdump -h target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .dynsym 00000018 00000000000001c8
2 .gnu.hash 0000001c 00000000000001e0
3 .hash 00000010 00000000000001fc
4 .dynstr 00000001 000000000000020c
5 .text 00000004 0000000000001210 TEXT
6 .dynamic 000000a0 0000000000002218
7 .debug_abbrev 0000010c 0000000000000000 DEBUG
8 .debug_info 000005ce 0000000000000000 DEBUG
9 .debug_aranges 00000040 0000000000000000 DEBUG
10 .debug_ranges 00000030 0000000000000000 DEBUG
11 .debug_str 00000492 0000000000000000 DEBUG
12 .debug_pubnames 000000bc 0000000000000000 DEBUG
13 .debug_pubtypes 0000036c 0000000000000000 DEBUG
14 .debug_frame 00000050 0000000000000000 DEBUG
15 .debug_line 00000059 0000000000000000 DEBUG
16 .comment 00000013 0000000000000000
17 .symtab 00000060 0000000000000000
18 .shstrtab 000000ce 0000000000000000
19 .strtab 00000022 0000000000000000
```
The `.text` section contains the program code, the other sections are not important right now.
The section dump is useful for debugging, for example for checking which section a pointer points to.
Most of the sections only contain debug information and are not needed for execution.
We can remove this debug information using `rust-strip`:
```
rust-strip target/x86_64-unknown-none/debug/kernel
rust-objdump -h target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .dynsym 00000018 00000000000001c8
2 .gnu.hash 0000001c 00000000000001e0
3 .hash 00000010 00000000000001fc
4 .dynstr 00000001 000000000000020c
5 .text 00000004 0000000000001210 TEXT
6 .dynamic 000000a0 0000000000002218
7 .shstrtab 00000034 0000000000000000
```
#### Disassembling
Sometimes we need to check the [assembly code] that certain functions compile to.
We can use the `objdump -d` command to print the `.text` section of an executable in assembly language:
[assembly code]: https://en.wikipedia.org/wiki/X86_assembly_language
```
rust-objdump -d target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
Disassembly of section .text:
0000000000001210 <_start>:
1210: eb 00 jmp 0x1212 <_start+0x2>
1212: eb fe jmp 0x1212 <_start+0x2>
```
We see that our `_start` function consists of just two [`jmp` instructions], which jump to the given address.
The first `jmp` command jumps to the second `jmp` command at address `1212`.
The second `jmp` command jumps to itself again, thereby representing the infinite loop that we've written in our `_start` function.
[`jmp` instructions]: https://www.felixcloutier.com/x86/jmp
As you probably noticed, the first `jmp` command is not really needed.
Such inefficiencies can happen in debug builds because the compiler does not optimize them.
If we disassemble the optimized release build, we see that the compiler indeed removed the unneeded `jmp`:
```
cargo build --target x86_64-unknown-none --release
rust-objdump -d target/x86_64-unknown-none/release/kernel
target/x86_64-unknown-none/release/kernel: file format elf64-x86-64
Disassembly of section .text:
0000000000001210 <_start>:
1210: eb fe jmp 0x1210 <_start>
```
We will use continue to use the above tools in future posts, as they're quite useful for debugging issues.
## What's next?
In the [next post], we will learn how to turn our minimal kernel in a bootable disk image, which can then be started in the [QEMU] virtual machine and on real hardware.
For this, we'll explore the boot process of `x86_64` systems and learn about the differences between UEFI and the legacy BIOS firmware.
[next post]: @/edition-3/posts/02-booting/index.md
[QEMU]: https://www.qemu.org/