mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-17 06:47:49 +00:00
Compare commits
110 Commits
main
...
db2d2bca19
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db2d2bca19 | ||
|
|
ce0c6c133b | ||
|
|
c40ac1b1d7 | ||
|
|
cc6d5eefb5 | ||
|
|
2642fa80e8 | ||
|
|
264fd90abe | ||
|
|
87b440c79a | ||
|
|
2c96cfe972 | ||
|
|
9adc094f43 | ||
|
|
1da47d3177 | ||
|
|
bc9d1a545a | ||
|
|
c73804789b | ||
|
|
233dec4caf | ||
|
|
6f1b982f3a | ||
|
|
f6d06bcf67 | ||
|
|
fb5eca717b | ||
|
|
0400ce692c | ||
|
|
d475912811 | ||
|
|
729a8241ea | ||
|
|
d7b93ab855 | ||
|
|
5af0499af5 | ||
|
|
ebf482379c | ||
|
|
91005553b3 | ||
|
|
06b7d345e4 | ||
|
|
b3eace1260 | ||
|
|
c61c37643a | ||
|
|
1aa7d21d8b | ||
|
|
d6885843e6 | ||
|
|
0b9231f0ba | ||
|
|
db47b27024 | ||
|
|
9c1babd027 | ||
|
|
ff3f055383 | ||
|
|
8740b619a5 | ||
|
|
a63c51c156 | ||
|
|
1ff447b378 | ||
|
|
83be6c7868 | ||
|
|
033be9ac25 | ||
|
|
91d65504db | ||
|
|
aeb72889ae | ||
|
|
c2fe9960a7 | ||
|
|
d6f424e338 | ||
|
|
843bd4ca87 | ||
|
|
b1f6a85a02 | ||
|
|
c48335747b | ||
|
|
81a1c0bded | ||
|
|
dfdbc6ec6d | ||
|
|
09f96c9221 | ||
|
|
aaae70974f | ||
|
|
268a5ccb7d | ||
|
|
f9f9b969bb | ||
|
|
943f950c54 | ||
|
|
01327746d1 | ||
|
|
acb478c0b5 | ||
|
|
08c84d2f59 | ||
|
|
9c11cebef7 | ||
|
|
b3b263db2a | ||
|
|
a8ef154d03 | ||
|
|
957855a18e | ||
|
|
ec1f80416b | ||
|
|
2e52b681ec | ||
|
|
8fbdf53598 | ||
|
|
72e4851bc7 | ||
|
|
76090f6656 | ||
|
|
cd7ac05395 | ||
|
|
d56f51a2d3 | ||
|
|
ae4c53fa75 | ||
|
|
de71899d19 | ||
|
|
6b1c01477c | ||
|
|
8e0f531334 | ||
|
|
a6b8623468 | ||
|
|
57bbb13e41 | ||
|
|
3dfc7ee84f | ||
|
|
c97c27f4e6 | ||
|
|
4aa5981252 | ||
|
|
b5bd0296bd | ||
|
|
7e0911b42e | ||
|
|
4027e61dab | ||
|
|
5e3062407f | ||
|
|
fd623fd033 | ||
|
|
d56977598e | ||
|
|
73b42d4747 | ||
|
|
4a86515a8d | ||
|
|
300a6f452a | ||
|
|
eb767523a5 | ||
|
|
ece6a9bb9d | ||
|
|
5c3015acc3 | ||
|
|
7cc3d5e3b9 | ||
|
|
376ce0ad95 | ||
|
|
b353866952 | ||
|
|
50683507da | ||
|
|
60a0b3bc28 | ||
|
|
798d5c58c5 | ||
|
|
e6c099ee5b | ||
|
|
874f9dbaed | ||
|
|
d45572f9fb | ||
|
|
7e86caf786 | ||
|
|
e8bfca0adb | ||
|
|
3a23f0555f | ||
|
|
352ba47971 | ||
|
|
c175d25048 | ||
|
|
d7f8dd78de | ||
|
|
f4eeda64d8 | ||
|
|
2147e67e3a | ||
|
|
8c87368eee | ||
|
|
645057fe0b | ||
|
|
4f108cc36e | ||
|
|
75a1d19b93 | ||
|
|
4ec30d4624 | ||
|
|
61aabc688c | ||
|
|
f39923545d |
@@ -29,7 +29,7 @@ skip_anchor_prefixes = [
|
||||
]
|
||||
|
||||
[extra]
|
||||
subtitle = "Philipp Oppermann's blog"
|
||||
subtitle = "by Philipp Oppermann"
|
||||
author = { name = "Philipp Oppermann" }
|
||||
default_language = "en"
|
||||
languages = ["en", "zh-CN", "zh-TW", "fr", "ja", "fa", "ru", "ko"]
|
||||
|
||||
@@ -4,9 +4,12 @@ This folder contains the content for the _"Writing an OS in Rust"_ blog.
|
||||
|
||||
## License
|
||||
|
||||
This folder is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License, available in [LICENSE-CC-BY-NC](LICENSE-CC-BY-NC) or under <https://creativecommons.org/licenses/by-nc/4.0/>.
|
||||
This folder is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License, available in [LICENSE-CC-BY-NC](LICENSE-CC-BY-NC) or under <https://creativecommons.org/licenses/by-nc/4.0/>. The following exceptions exist:
|
||||
|
||||
All _code examples_ between markdown code blocks denoted by three backticks (<code>\`\`\`</code>) are additionally licensed under either of
|
||||
- The post icons are taken from the [Bootstrap Icons](https://icons.getbootstrap.com/) project, which are [licensed under the MIT license by "The Bootstrap Authors"](https://github.com/twbs/icons/blob/main/LICENSE.md).
|
||||
- All files that have a custom license header. These files are licensed as described in this header.
|
||||
|
||||
In addition to the above license terms, all _code examples_ between markdown code blocks denoted by three backticks (<code>\`\`\`</code>) are additionally licensed under either of
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](../../LICENSE-APACHE) or
|
||||
https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
+++
|
||||
title = "Second Edition"
|
||||
template = "redirect-to-frontpage.html"
|
||||
aliases = ["second-edition/index.html"]
|
||||
+++
|
||||
|
||||
21
blog/content/edition-3/_index.md
Normal file
21
blog/content/edition-3/_index.md
Normal file
@@ -0,0 +1,21 @@
|
||||
+++
|
||||
template = "edition-3/index.html"
|
||||
+++
|
||||
|
||||
<h1>Writing an OS in Rust</h1>
|
||||
|
||||
<h1 style="visibility: hidden; height: 0px; margin: 0px; padding: 0px;">Writing an OS in Rust</h1>
|
||||
|
||||
<p>A blog by Philipp Oppermann <em class="gray">— Third Edition (Alpha Release)</em></p>
|
||||
|
||||
<div class="front-page-introduction">
|
||||
|
||||
This blog series creates a small operating system in the [Rust programming language](https://www.rust-lang.org/). Each post is a small tutorial and includes all needed code, so you can follow along if you like. The source code is also available in the corresponding [Github repository](https://github.com/phil-opp/blog_os).
|
||||
|
||||
<!-- alpha-warning -->
|
||||
|
||||
We explain how to create an operating system for the **`x86_64`** architecture step by step. Starting from scratch, we create a bootable OS kernel, implement basic input/output support, show how to test and debug our kernel, explain virtual memory management, and add support for multitasking and userspace programs.
|
||||
|
||||
Latest post: <!-- latest-post -->
|
||||
|
||||
</div>
|
||||
4
blog/content/edition-3/chapters/_index.md
Normal file
4
blog/content/edition-3/chapters/_index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
+++
|
||||
title = "Chapters"
|
||||
render = false
|
||||
+++
|
||||
6
blog/content/edition-3/chapters/bare-bones/_index.md
Normal file
6
blog/content/edition-3/chapters/bare-bones/_index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title = "Bare Bones"
|
||||
render = false
|
||||
+++
|
||||
|
||||
In this first chapter, we explain how to create an operating system for the `x86_64` architecture step for step. Starting from scratch, we first create a minimal Rust executable that doesn't depend on the standard library. We then turn it into a bootable OS kernel by combining it with a bootloader. The resulting disk image can then be launched in the [QEMU](https://www.qemu.org/) emulator or booted on a real machine.
|
||||
6
blog/content/edition-3/chapters/basic-i-o/_index.md
Normal file
6
blog/content/edition-3/chapters/basic-i-o/_index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
+++
|
||||
title = "Basic I/O"
|
||||
render = false
|
||||
+++
|
||||
|
||||
Soluta cum voluptatem fuga reprehenderit tenetur dicta rerum. Ullam minima eaque saepe voluptatum saepe in illum cumque. Debitis doloribus dolores dolores earum minima accusamus eius. Nostrum qui saepe ducimus laudantium temporibus.
|
||||
910
blog/content/edition-3/posts/01-minimal-kernel/index.md
Normal file
910
blog/content/edition-3/posts/01-minimal-kernel/index.md
Normal file
@@ -0,0 +1,910 @@
|
||||
+++
|
||||
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-01`][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-01
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## Introduction
|
||||
To write an operating system kernel, we need code that does not depend on any operating system features.
|
||||
This means that we can't use threads, files, heap memory, the network, random numbers, standard output, or any other features requiring OS abstractions or specific hardware.
|
||||
Which makes sense, since we're trying to write our own OS and our own drivers.
|
||||
|
||||
While this means that we can't use most of the [Rust standard library], 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 without worrying 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 order to create a minimal OS kernel in Rust, we start by creating an executable 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 blog_os --bin --edition 2018
|
||||
```
|
||||
|
||||
I named the project `blog_os`, 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 2018` flag specifies that we want to use the [2018 edition] of Rust for our crate.
|
||||
When we run the command, cargo creates the following directory structure for us:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
```
|
||||
blog_os
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
The `Cargo.toml` contains the crate configuration, for example the crate name, the author, 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 `blog_os` 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
|
||||
// main.rs
|
||||
|
||||
#![no_std]
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
```
|
||||
|
||||
When we try to build it now (by running `cargo build`), the following error occurs:
|
||||
|
||||
```
|
||||
error: cannot find macro `println!` in this scope
|
||||
--> src/main.rs:4:5
|
||||
|
|
||||
4 | println!("Hello, world!");
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
The reason for this 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
|
||||
// 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`
|
||||
```
|
||||
|
||||
Now the compiler is 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 it ourselves:
|
||||
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
```rust
|
||||
// in main.rs
|
||||
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
/// 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 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/nightly/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/nightly/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`
|
||||
```
|
||||
|
||||
### The `eh_personality` Language Item
|
||||
|
||||
Language items are special functions and types that are required internally by the compiler.
|
||||
For example, the [`Copy`] trait is a language item that tells the compiler which types have [_copy semantics_][`Copy`].
|
||||
When we look at the [implementation][copy code], we see it has the special `#[lang = "copy"]` attribute that defines it as a language item.
|
||||
|
||||
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299
|
||||
|
||||
While providing custom implementations of language items is possible, it should only be done as a last resort.
|
||||
The reason is that language items are highly unstable implementation details and not even type checked (so the compiler doesn't even check if a function has the right argument types).
|
||||
Fortunately, there is a more stable way to fix the above language item error.
|
||||
|
||||
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 complicated process and requires some OS specific libraries (e.g. [libunwind] on Linux or [structured exception handling] on Windows), so we don't want to use it for our operating system.
|
||||
|
||||
[`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
|
||||
|
||||
#### Disabling Unwinding
|
||||
|
||||
There are other use cases as well for which unwinding is undesirable, so Rust provides an option to [abort on panic] instead.
|
||||
This disables the generation of unwinding symbol information and thus considerably reduces binary size.
|
||||
There are multiple ways to disable unwinding, the easiest is to add the following lines to our `Cargo.toml`:
|
||||
|
||||
```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.
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
Now we fixed both of the above errors.
|
||||
However, if we try to compile it now, another error occurs:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
error: requires `start` lang_item
|
||||
```
|
||||
|
||||
Our program is missing the `start` language item, which defines the entry point.
|
||||
|
||||
### The `start` Language Item
|
||||
|
||||
One might think that the `main` function is the first function called when a program is run.
|
||||
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.
|
||||
|
||||
[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
|
||||
|
||||
Our freestanding executable does not have access to the Rust runtime and `crt0`, so we need to define our own entry point.
|
||||
Implementing the `start` language item wouldn't help, since it would still require `crt0`.
|
||||
Instead, we need to overwrite the `crt0` entry point directly.
|
||||
|
||||
#### Overwriting the Entry Point
|
||||
To tell the Rust compiler that we don't want to use the normal entry point chain, we add the `#![no_main]` attribute.
|
||||
|
||||
```rust
|
||||
#![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
|
||||
#[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 `_ZN3blog_os4_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.
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||||
|
||||
When we run `cargo build` now, we get an ugly _linker_ error.
|
||||
|
||||
## Linker Errors
|
||||
|
||||
The [linker] is a program that combines the generated code into an executable.
|
||||
Since the executable format differs between Linux, Windows, and macOS, each system has its own linker that throws a different error.
|
||||
The fundamental cause of the errors is the same: the default configuration of the linker assumes that our program depends on the C runtime, which it does not.
|
||||
|
||||
To solve the errors, we need to tell the linker that we want to build for a bare-metal target, where no underlying operating system or C runtime exist.
|
||||
As an alternative, it is also possible to disable the linking of the C runtime by passing a certain set of arguments to the linker.
|
||||
|
||||
### Linker Arguments
|
||||
|
||||
Linkers are very complex programs with a lot of configuration options.
|
||||
Each of the major operating systems (Linux, Windows, macOS) has its own linker implementation with different options, but all of them provide a way to disable the linking of the C runtime.
|
||||
By using these options, it is possible to create a freestanding executable that still runs on top of an existing operating system.
|
||||
|
||||
_This is not what we want for our kernel, so this section is only provided for completeness.
|
||||
Feel free to skip this section if you like._
|
||||
|
||||
In the subsections below, we explain the required linker arguments for each operating system.
|
||||
It's worth noting that creating a freestanding executable this way is probably not a good idea.
|
||||
The reason is that our executable still expects various things, for example that a stack is initialized when the `_start` function is called.
|
||||
Without the C runtime, some of these requirements might not be fulfilled, which might cause our program to fail, e.g. by causing a segmentation fault.
|
||||
|
||||
If you want to create a minimal binary that runs on top of an existing operating system, including `libc` and setting the `#[start]` attribute as described [here](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) is probably a better idea.
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
#### Linux
|
||||
</summary>
|
||||
|
||||
On Linux the following linker error occurs (shortened):
|
||||
|
||||
```
|
||||
error: linking with `cc` failed: exit code: 1
|
||||
|
|
||||
= note: "cc" […]
|
||||
= note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
|
||||
(.text+0x12): undefined reference to `__libc_csu_fini'
|
||||
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
|
||||
(.text+0x19): undefined reference to `__libc_csu_init'
|
||||
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
|
||||
(.text+0x25): undefined reference to `__libc_start_main'
|
||||
collect2: error: ld returned 1 exit status
|
||||
```
|
||||
|
||||
The problem is that the linker includes the startup routine of the C runtime by default, which is also called `_start`.
|
||||
It requires some symbols of the C standard library `libc` that we don't include due to the `no_std` attribute, therefore the linker can't resolve these references.
|
||||
To solve this, we can tell the linker that it should not link the C startup routine by passing the `-nostartfiles` flag.
|
||||
|
||||
One way to pass linker attributes via cargo is the `cargo rustc` command.
|
||||
The command behaves exactly like `cargo build`, but allows to pass options to `rustc`, the underlying Rust compiler.
|
||||
`rustc` has the `-C link-arg` flag, which passes an argument to the linker.
|
||||
Combined, our new build command looks like this:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
```
|
||||
|
||||
Now our crate builds as a freestanding executable on Linux!
|
||||
|
||||
We didn't need to specify the name of our entry point function explicitly since the linker looks for a function with the name `_start` by default.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
#### Windows
|
||||
</summary>
|
||||
|
||||
On Windows, the following linker error occurs (shortened):
|
||||
|
||||
```
|
||||
error: linking with `link.exe` failed: exit code: 1561
|
||||
|
|
||||
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
|
||||
= note: LINK : fatal error LNK1561: entry point must be defined
|
||||
```
|
||||
|
||||
The "entry point must be defined" error means that the linker can't find the entry point.
|
||||
On Windows, the default entry point name [depends on the used subsystem][windows-subsystems].
|
||||
For the `CONSOLE` subsystem the linker looks for a function named `mainCRTStartup` and for the `WINDOWS` subsystem it looks for a function named `WinMainCRTStartup`.
|
||||
To override the default and tell the linker to look for our `_start` function instead, we can pass an `/ENTRY` argument to the linker:
|
||||
|
||||
[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=/ENTRY:_start
|
||||
```
|
||||
|
||||
From the different argument format we clearly see that the Windows linker is a completely different program than the Linux linker.
|
||||
|
||||
Now a different linker error occurs:
|
||||
|
||||
```
|
||||
error: linking with `link.exe` failed: exit code: 1221
|
||||
|
|
||||
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
|
||||
= note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be
|
||||
defined
|
||||
```
|
||||
|
||||
This error occurs because Windows executables can use different [subsystems][windows-subsystems].
|
||||
For normal programs they are inferred depending on the entry point name: If the entry point is named `main`, the `CONSOLE` subsystem is used, and if the entry point is named `WinMain`, the `WINDOWS` subsystem is used.
|
||||
Since our `_start` function has a different name, we need to specify the subsystem explicitly:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
|
||||
```
|
||||
|
||||
We use the `CONSOLE` subsystem here, but the `WINDOWS` subsystem would work too.
|
||||
Instead of passing `-C link-arg` multiple times, we use `-C link-args` which takes a space separated list of arguments.
|
||||
|
||||
With this command, our executable should build successfully on Windows.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
#### macOS
|
||||
</summary>
|
||||
|
||||
On macOS, the following linker error occurs (shortened):
|
||||
|
||||
```
|
||||
error: linking with `cc` failed: exit code: 1
|
||||
|
|
||||
= note: "cc" […]
|
||||
= note: ld: entry point (_main) undefined.
|
||||
for architecture x86_64
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
This error message tells us that the linker can't find an entry point function with the default name `main` (for some reason all functions are prefixed with a `_` on macOS).
|
||||
To set the entry point to our `_start` function, we pass the `-e` linker argument:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start"
|
||||
```
|
||||
|
||||
The `-e` flag specifies the name of the entry point function.
|
||||
Since all functions have an additional `_` prefix on macOS, we need to set the entry point to `__start` instead of `_start`.
|
||||
|
||||
Now the following linker error occurs:
|
||||
|
||||
```
|
||||
error: linking with `cc` failed: exit code: 1
|
||||
|
|
||||
= note: "cc" […]
|
||||
= note: ld: dynamic main executables must link with libSystem.dylib
|
||||
for architecture x86_64
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
macOS [does not officially support statically linked binaries] and requires programs to link the `libSystem` library by default.
|
||||
To override this and link a static binary, we pass the `-static` flag to the linker:
|
||||
|
||||
[does not officially support statically linked binaries]: https://developer.apple.com/library/archive/qa/qa1118/_index.html
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static"
|
||||
```
|
||||
|
||||
This still does not suffice, as a third linker error occurs:
|
||||
|
||||
```
|
||||
error: linking with `cc` failed: exit code: 1
|
||||
|
|
||||
= note: "cc" […]
|
||||
= note: ld: library not found for -lcrt0.o
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
This error occurs because programs on macOS link to `crt0` (“C runtime zero”) by default.
|
||||
This is similar to the error we had on Linux and can be also solved by adding the `-nostartfiles` linker argument:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
```
|
||||
|
||||
Now our program should build successfully on macOS.
|
||||
</details>
|
||||
|
||||
### Building for a 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.49.0 (e1884a8e3 2020-12-29)
|
||||
binary: rustc
|
||||
commit-hash: e1884a8e3c3e813aada8254edfa120e85bf5ffca
|
||||
commit-date: 2020-12-29
|
||||
host: x86_64-unknown-linux-gnu
|
||||
release: 1.49.0
|
||||
```
|
||||
|
||||
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 causes the linker errors.
|
||||
So to avoid the linker errors, we can compile for a different environment with no underlying operating system.
|
||||
|
||||
An example for such a bare metal environment is the `thumbv7em-none-eabihf` target triple, which describes an [embedded] [ARM] system.
|
||||
The details are not important, all that matters is that the target triple has no underlying operating system, which is indicated by the `none` in the target triple.
|
||||
To be able to compile for this target, we need to add it in rustup:
|
||||
|
||||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
|
||||
|
||||
```
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
This downloads a pre-compiled copy of the `core` library for the target.
|
||||
Afterwards we can build our freestanding executable for the target:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
By passing a `--target` argument we [cross compile] our executable for a bare metal target system.
|
||||
Since the target system has no operating system, the linker does not try to link the C runtime and our build succeeds without any linker errors.
|
||||
|
||||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
## Kernel Target
|
||||
|
||||
We just saw that we can compile our executable for a embedded ARM system by passing a `--target` argument.
|
||||
Rust supports [many different target triples][platform-support], including `arm-linux-androideabi` for Android or [`wasm32-unknown-unknown` for WebAssembly](https://www.hellorust.com/setup/wasm-target/).
|
||||
|
||||
[platform-support]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
|
||||
|
||||
In order to create an operating system kernel, we need to choose a target that describes the environment on a bare-metal `x86_64` system.
|
||||
This requires some special configuration parameters (e.g. no underlying OS), so none of the officially supported target triples fit.
|
||||
Fortunately, Rust allows us to define [our own target][custom-targets] through a JSON file.
|
||||
For example, a JSON file that describes the `x86_64-unknown-linux-gnu` target looks like this:
|
||||
|
||||
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
|
||||
|
||||
```json
|
||||
{
|
||||
"llvm-target": "x86_64-unknown-linux-gnu",
|
||||
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
|
||||
"arch": "x86_64",
|
||||
"target-endian": "little",
|
||||
"target-pointer-width": "64",
|
||||
"target-c-int-width": "32",
|
||||
"os": "linux",
|
||||
"executables": true,
|
||||
"linker-flavor": "gcc",
|
||||
"pre-link-args": ["-m64"],
|
||||
"morestack": false
|
||||
}
|
||||
```
|
||||
|
||||
Most fields are required by LLVM to generate code for that platform.
|
||||
For example, the [`data-layout`] field defines the size of various integer, floating point, and pointer types.
|
||||
Then there are fields that Rust uses for conditional compilation, such as `target-pointer-width`.
|
||||
The third kind of fields define how the crate should be built.
|
||||
For example, the `pre-link-args` field specifies arguments passed to the [linker].
|
||||
For a full list of available fields and their meaning, check out the docs for Rust's internal [`Target`] and [`TargetOptions`] types.
|
||||
|
||||
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
|
||||
[`Target`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.Target.html
|
||||
[`TargetOptions`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.TargetOptions.html
|
||||
|
||||
We also target `x86_64` systems with our kernel, so our target specification will look very similar to the one above.
|
||||
Let's start by creating a `x86_64-blog_os.json` file (choose any name you like) with the common content:
|
||||
|
||||
```json
|
||||
{
|
||||
"llvm-target": "x86_64-unknown-none",
|
||||
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
|
||||
"arch": "x86_64",
|
||||
"target-endian": "little",
|
||||
"target-pointer-width": "64",
|
||||
"target-c-int-width": "32",
|
||||
"os": "none",
|
||||
"executables": true
|
||||
}
|
||||
```
|
||||
|
||||
Note that we changed the OS in the `llvm-target` and the `os` field to `none`, because our kernel will run on bare metal.
|
||||
|
||||
We add the following build-related entries:
|
||||
|
||||
- Override the default linker:
|
||||
|
||||
```json
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
```
|
||||
|
||||
Instead of using the platform's default linker (which might not support our custom target), we use the cross platform [LLD] linker that is shipped with Rust for linking our kernel.
|
||||
|
||||
[LLD]: https://lld.llvm.org/
|
||||
|
||||
- Abort on panic:
|
||||
|
||||
```json
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
This setting specifies that the target doesn't support [stack unwinding] on panic, so instead the program should abort directly.
|
||||
This has the same effect as the `panic = "abort"` option in our Cargo.toml, so we can remove it from there.
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
|
||||
- Disable the red zone:
|
||||
|
||||
```json
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
We're writing a kernel, so we'll need to handle interrupts at some point.
|
||||
To do that safely, we have to disable a certain stack pointer optimization called the _“red zone”_, because it would cause stack corruptions otherwise.
|
||||
For more information, see our separate post about [disabling the red zone].
|
||||
|
||||
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
- Disable SIMD:
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
The `features` field enables/disables target features.
|
||||
We disable the `mmx` and `sse` features by prefixing them with a minus and enable the `soft-float` feature by prefixing it with a plus.
|
||||
Note that there must be no spaces between different flags, otherwise LLVM fails to interpret the features string.
|
||||
|
||||
The `mmx` and `sse` features determine support for [Single Instruction Multiple Data (SIMD)] instructions, which can often speed up programs significantly.
|
||||
However, using the large SIMD registers in OS kernels leads to performance problems.
|
||||
The reason is that the kernel needs to restore all registers to their original state before continuing an interrupted program.
|
||||
This means that the kernel has to save the complete SIMD state to main memory on each system call or hardware interrupt.
|
||||
Since the SIMD state is very large (512–1600 bytes) and interrupts can occur very often, these additional save/restore operations considerably harm performance.
|
||||
To avoid this, we disable SIMD for our kernel (not for applications running on top!).
|
||||
|
||||
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
|
||||
|
||||
A problem with disabling SIMD is that floating point operations on `x86_64` require SIMD registers by default.
|
||||
To solve this problem, we add the `soft-float` feature, which emulates all floating point operations through software functions based on normal integers.
|
||||
|
||||
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
|
||||
After adding all the above entries, our full target specification file looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"llvm-target": "x86_64-unknown-none",
|
||||
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
|
||||
"arch": "x86_64",
|
||||
"target-endian": "little",
|
||||
"target-pointer-width": "64",
|
||||
"target-c-int-width": "32",
|
||||
"os": "none",
|
||||
"executables": true,
|
||||
"linker-flavor": "ld.lld",
|
||||
"linker": "rust-lld",
|
||||
"panic-strategy": "abort",
|
||||
"disable-redzone": true,
|
||||
"features": "-mmx,-sse,+soft-float"
|
||||
}
|
||||
```
|
||||
|
||||
## Building our Kernel
|
||||
|
||||
To build our kernel for our new custom target we pass the path to the JSON file as `--target` argument:
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json
|
||||
|
||||
error[E0463]: can't find crate for `core`
|
||||
```
|
||||
|
||||
It fails! The error tells us that the Rust compiler no longer finds the [`core` library].
|
||||
This library contains basic Rust types such as `Result`, `Option`, and iterators, and is implicitly linked to all `no_std` crates.
|
||||
|
||||
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
|
||||
The problem is that the core library is distributed together with the Rust compiler as a precompiled library.
|
||||
These precompiled versions are available through `rustup` for all officially supported targets.
|
||||
We already saw this above, when we [built our kernel for the `thumbv7em-none-eabihf` target](#building-for-a-bare-metal-target).
|
||||
For our custom target, however, we need to build the `core` library ourselves.
|
||||
|
||||
While `cargo` has built-in support for building the `core` library, this feature is still considered [_unstable_][cargo-unstable].
|
||||
Unstable features are only available in the "nightly" release channel of Rust, not on normal stable releases.
|
||||
So in order to build the `core` library, we need to install a nightly version of Rust first.
|
||||
|
||||
[cargo-unstable]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html
|
||||
|
||||
### Installing Rust Nightly
|
||||
Rust has three release channels: _stable_, _beta_, and _nightly_.
|
||||
The Rust Book explains the difference between these channels really well, so take a minute and [check it out](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains).
|
||||
Apart from the availability of unstable features, there is not really a difference between nightly and stable releases.
|
||||
Every 6 weeks, the current nightly is released on the beta channel and the current beta is released as stable.
|
||||
Since we will need some unstable features for our operating system (such as building `core`), we need to install a nightly version of Rust.
|
||||
|
||||
The recommend tool to manage Rust installations is [rustup].
|
||||
It allows you to install nightly, beta, and stable compilers side-by-side and makes it easy to update them.
|
||||
With rustup you can use a nightly compiler for the current directory by running:
|
||||
|
||||
```
|
||||
rustup override set nightly
|
||||
```
|
||||
|
||||
Alternatively, you can add a file called **`rust-toolchain`** to the project's root directory with the required Rust version:
|
||||
|
||||
```toml
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
```
|
||||
|
||||
After doing one of these things, both the `cargo` and `rustc` command should use a nightly version of Rust when invoked from within the current directory.
|
||||
You can verify that you have a nightly version installed and active by running `rustc --version`: The version number should contain `-nightly` at the end, for example:
|
||||
|
||||
[rustup]: https://www.rustup.rs/
|
||||
|
||||
```
|
||||
rustc 1.51.0-nightly (04caa632d 2021-01-30)
|
||||
```
|
||||
|
||||
<div class="note">
|
||||
|
||||
Note that this version number is just an example, your version should be newer.
|
||||
This post and the rest of the blog is regularly updated to always compile on the newest nightly version.
|
||||
So if something doesn't work try updating to the latest nightly by running `rustup update nightly`.
|
||||
|
||||
</div>
|
||||
|
||||
In addition to building `core`, using a nightly compiler allows us to opt-in to [various experimental features] by using so-called _feature flags_ at the top of our file.
|
||||
For example, we could enable the experimental [`asm!` macro] for inline assembly by adding `#![feature(asm)]` to the top of our `main.rs`.
|
||||
Note that such experimental features are completely unstable, which means that future Rust versions might change or remove them without prior warning.
|
||||
For this reason we will only use them if absolutely necessary.
|
||||
|
||||
[various experimental features]: https://doc.rust-lang.org/unstable-book/the-unstable-book.html
|
||||
[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
### The `build-std` Option
|
||||
|
||||
Now that we switched to nightly Rust, we are able use the [`build-std` feature] of cargo.
|
||||
It allows to build `core` and other standard library crates on demand, instead of using the precompiled versions shipped with the Rust installation.
|
||||
|
||||
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
|
||||
|
||||
To build the `core` library, we need to pass a `-Z build-std=core` argument to the `cargo build` command:
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json -Z build-std=core
|
||||
|
||||
error: "/…/rustlib/src/rust/Cargo.lock" does not exist,
|
||||
unable to build with the standard library, try:
|
||||
rustup component add rust-src
|
||||
```
|
||||
|
||||
It still fails.
|
||||
The problem is that cargo needs a copy of the Rust source code in order to recompile the `core` crate.
|
||||
The error message helpfully suggest to provide such a copy by installing the `rust-src` component.
|
||||
|
||||
Instead of running the suggested `rustup component add rust-src` command, we an also record the dependency on the `rust-src` component in our `rust-toolchain` file:
|
||||
|
||||
```toml
|
||||
# in rust-toolchain
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
```
|
||||
|
||||
This way, `rustup` will automatically download the required components so that no manual steps are necessary.
|
||||
You can also automatically install components such as [`rustfmt`] or [`clippy`] this way if you like to use them:
|
||||
|
||||
|
||||
```toml
|
||||
# in rust-toolchain
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src", "rustfmt", "clippy"]
|
||||
```
|
||||
|
||||
[`rustfmt`]: https://github.com/rust-lang/rustfmt
|
||||
[`clippy`]: https://github.com/rust-lang/rust-clippy
|
||||
|
||||
After installing the `rust-src` component (either manually or automatically), the build should finally succeeds:
|
||||
|
||||
```
|
||||
> cargo build --target x86_64-blog_os.json -Z build-std=core
|
||||
Compiling core v0.0.0 (/…/rust/src/libcore)
|
||||
Compiling rustc-std-workspace-core v1.99.0 (/…/rustc-std-workspace-core)
|
||||
Compiling compiler_builtins v0.1.32
|
||||
Compiling blog_os v0.1.0 (/…/blog_os)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
|
||||
```
|
||||
|
||||
We see that `cargo build` now builds the `core`, `compiler_builtins` (a dependency of `core`), and `rustc-std-workspace-core` (a dependency of `compiler_builtins`) libraries for our custom target.
|
||||
|
||||
### Memory-Related Intrinsics
|
||||
|
||||
The Rust compiler assumes that a certain set of built-in functions is available for all systems.
|
||||
Most of these functions are provided by the `compiler_builtins` crate that we just built.
|
||||
However, there are some memory-related functions in that crate that are not enabled by default because they are normally provided by the C library on the system.
|
||||
These functions include `memset`, which sets all bytes in a memory block to a given value, `memcpy`, which copies one memory block to another, and `memcmp`, which compares two memory blocks.
|
||||
While we didn't need any of these functions to compile our kernel right now, they will be required as soon as we add some more code to it (e.g. when copying structs around).
|
||||
|
||||
Since we can't link to the C library of the operating system, we need an alternative way to provide these functions to the compiler.
|
||||
One possible approach for this could be to implement our own `memset` etc.
|
||||
functions and apply the `#[no_mangle]` attribute to them (to avoid the automatic renaming during compilation).
|
||||
However, this is dangerous since the slightest mistake in the implementation of these functions could lead to bugs and undefined behavior.
|
||||
For example, you might get an endless recursion when implementing `memcpy` using a `for` loop because `for` loops implicitly call the [`IntoIterator::into_iter`] trait method, which might call `memcpy` again.
|
||||
So it's a good idea to reuse existing well-tested implementations instead of creating your own.
|
||||
|
||||
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
|
||||
|
||||
Fortunately, the `compiler_builtins` crate already contains implementations for all the needed functions, they are just disabled by default to not collide with the implementations from the C library.
|
||||
We can enable them by passing an additional `-Z build-std-features=compiler-builtins-mem` flag to `cargo`.
|
||||
Like the `build-std` flag, this [`build-std-features`] flag is still unstable, so it might change in the future.
|
||||
|
||||
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
|
||||
|
||||
The full build command now looks like this:
|
||||
|
||||
```
|
||||
cargo build --target x86_64-blog_os.json -Z build-std=core \
|
||||
-Z build-std-features=compiler-builtins-mem
|
||||
```
|
||||
|
||||
Behind the scenes, the new flag enables the [`mem` feature] of the `compiler_builtins` crate.
|
||||
The effect of this is that the `#[no_mangle]` attribute is applied to the [`memcpy` etc.
|
||||
implementations] of the crate, which makes them available to the linker.
|
||||
It's worth noting that these functions are already optimized using [inline assembly] on `x86_64`, so their performance should be much better than a custom loop-based implementation.
|
||||
|
||||
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
|
||||
[`memcpy` etc.
|
||||
implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
[inline assembly]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
With the additional `compiler-builtins-mem` flag, our kernel now has valid implementations for all compiler-required functions, so it will continue to compile even if our code gets more complex.
|
||||
|
||||
## A Shorter Build Command
|
||||
|
||||
Our build command is quite long now, so it's a bit cumbersome to type and difficult to remember.
|
||||
So let's try to shorten it!
|
||||
|
||||
### Setting Defaults
|
||||
|
||||
Since we want to always pass these flags to our build command, it would make sense to set them as default.
|
||||
Unfortunately, Cargo currently only supports changing the default build command through [`.cargo/config.toml`] configuration files.
|
||||
The problem with these files is that they are applied based on the current working directory, not based on the compiled project.
|
||||
This leads to [various problems][cargo-config-problems], for example that the settings also apply to all crates in subdirectories.
|
||||
These problems make `.cargo/config.toml` files unsuitable for our use case, since the code in the next post would be broken this way.
|
||||
|
||||
[`.cargo/config.toml`]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
[cargo-config-problems]: https://internals.rust-lang.org/t/problems-of-cargo-config-files-and-possible-solutions/12987
|
||||
|
||||
To fix these problems, I proposed to [move some `.cargo/config.toml` settings to `Cargo.toml`][internals-proposal] to make them crate-specific.
|
||||
This would allow us to set proper defaults for our kernel too.
|
||||
So let's hope that it is implemented soon :).
|
||||
Until then, we can use _aliases_ to shorten our build command.
|
||||
|
||||
[internals-proposal]: https://internals.rust-lang.org/t/proposal-move-some-cargo-config-settings-to-cargo-toml/13336
|
||||
|
||||
### Aliases
|
||||
|
||||
Cargo allows to define custom [command aliases], for example `cargo br` for `cargo build --release`.
|
||||
While these aliases are defined in a `.cargo/config.toml` file too, they apply only to the command-line invocation and don't affect the normal build process of other crates.
|
||||
Thus, we can use them without problems.
|
||||
|
||||
[command aliases]: https://doc.rust-lang.org/cargo/reference/config.html#alias
|
||||
|
||||
To shorten our build command using an alias, we first need to create a directory named `.cargo` in the crate's root (i.e. next to the `Cargo.toml`).
|
||||
In that directory, we create a new file named `config.toml` with the following content:
|
||||
|
||||
```toml
|
||||
[alias]
|
||||
kbuild = """build --target x86_64-blog_os.json -Z build-std=core \
|
||||
-Z build-std-features=compiler-builtins-mem"""
|
||||
```
|
||||
|
||||
This defines a new `kbuild` command (for "kernel build") that expands to the long build command of our kernel.
|
||||
Now we can build our kernel by running just:
|
||||
|
||||
```
|
||||
cargo kbuild
|
||||
```
|
||||
|
||||
The name of the alias doesn't matter, so you can also name the alias `kb` if you like it even shorter.
|
||||
Note that overriding the built-in `build` command is not possible.
|
||||
|
||||
One drawback of the alias approach is that you need to define a separate alias for every cargo subcommand (e.g. [`cargo check`], [`cargo doc`], or [`cargo clippy`][`clippy`]), which you want to use.
|
||||
You also need to adjust your IDE (e.g. [rust-analyzer]) to use a non-standard build/check command.
|
||||
So this approach is clearly just a workaround until proper package-specific defaults are implemented in Cargo.
|
||||
|
||||
[`cargo check`]: https://doc.rust-lang.org/cargo/commands/cargo-check.html
|
||||
[`cargo doc`]: https://doc.rust-lang.org/cargo/commands/cargo-doc.html
|
||||
[rust-analyzer]: https://rust-analyzer.github.io/
|
||||
|
||||
## 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
|
||||
926
blog/content/edition-3/posts/02-booting/index.md
Normal file
926
blog/content/edition-3/posts/02-booting/index.md
Normal file
@@ -0,0 +1,926 @@
|
||||
+++
|
||||
title = "Booting"
|
||||
weight = 2
|
||||
path = "booting"
|
||||
date = 0000-01-01
|
||||
draft = true
|
||||
|
||||
[extra]
|
||||
chapter = "Bare Bones"
|
||||
icon = '''
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-power" viewBox="0 0 16 16">
|
||||
<path d="M7.5 1v7h1V1h-1z"/>
|
||||
<path d="M3 8.812a4.999 4.999 0 0 1 2.578-4.375l-.485-.874A6 6 0 1 0 11 3.616l-.501.865A5 5 0 1 1 3 8.812z"/>
|
||||
</svg>
|
||||
'''
|
||||
|
||||
extra_content = ["uefi/index.md"]
|
||||
+++
|
||||
|
||||
In this post, we explore the boot process on both BIOS and UEFI-based systems.
|
||||
We combine the [minimal kernel] created in the previous post with a bootloader to create a bootable disk image.
|
||||
We then show how this image can be started in the [QEMU] emulator and run on real hardware.
|
||||
|
||||
[minimal kernel]: @/edition-3/posts/01-minimal-kernel/index.md
|
||||
[QEMU]: https://www.qemu.org/
|
||||
|
||||
<!-- 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-02`][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-02
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
## The Boot Process
|
||||
When you turn on a computer, it begins executing firmware code that is stored in motherboard [ROM].
|
||||
This code performs a [power-on self-test], detects available RAM, and pre-initializes the CPU and other hardware.
|
||||
Afterwards it looks for a bootable disk and starts booting the operating system kernel.
|
||||
|
||||
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
|
||||
[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test
|
||||
|
||||
On x86, there are two firmware standards: the “Basic Input/Output System“ (**[BIOS]**) and the newer “Unified Extensible Firmware Interface” (**[UEFI]**).
|
||||
The BIOS standard is old and outdated, but simple and well-supported on any x86 machine since the 1980s.
|
||||
UEFI, in contrast, is more modern and has much more features, but also more complex.
|
||||
|
||||
[BIOS]: https://en.wikipedia.org/wiki/BIOS
|
||||
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
|
||||
|
||||
### BIOS
|
||||
|
||||
Almost all x86 systems have support for BIOS booting, including most UEFI-based machines that support an emulated BIOS.
|
||||
This is great, because you can use the same boot logic across all machines from the last centuries.
|
||||
The drawback is that the standard is very old, for example the CPU is put into a 16-bit compatibility mode called [real mode] before booting so that archaic bootloaders from the 1980s would still work.
|
||||
Also, BIOS-compatibility will be slowly removed on newer UEFI machines over the next years (see below).
|
||||
|
||||
#### Boot Process
|
||||
|
||||
When you turn on a BIOS-based computer, it first loads the BIOS firmware from some special flash memory located on the motherboard.
|
||||
The BIOS runs self test and initialization routines of the hardware, then it looks for bootable disks.
|
||||
For that it loads the first disk sector (512 bytes) of each disk into memory, which contains the [_master boot record_] (MBR) structure.
|
||||
This structure has the following general format:
|
||||
|
||||
[_master boot record_]: https://en.wikipedia.org/wiki/Master_boot_record
|
||||
|
||||
| Offset | Field | Size |
|
||||
| ------ | ----------------- | ---- |
|
||||
| 0 | bootstrap code | 446 |
|
||||
| 446 | partition entry 1 | 16 |
|
||||
| 462 | partition entry 2 | 16 |
|
||||
| 478 | partition entry 3 | 16 |
|
||||
| 444 | partition entry 4 | 16 |
|
||||
| 510 | boot signature | 2 |
|
||||
|
||||
The bootstrap code is commonly called the _bootloader_ and responsible for loading and starting the operating system kernel.
|
||||
The four partition entries describe the [disk partitions] such as the `C:` partition on Windows.
|
||||
The boot signature field at the end of the structure specifies whether this disk is bootable or not.
|
||||
If it is bootable, the signature field must be set to the [magic bytes] `0xaa55`.
|
||||
It's worth noting that there are [many extensions][mbr-extensions] of the MBR format, which for example include a 5th partition entry or a disk signature.
|
||||
|
||||
[disk partitions]: https://en.wikipedia.org/wiki/Disk_partitioning
|
||||
[magic bytes]: https://en.wikipedia.org/wiki/Magic_number_(programming)
|
||||
[mbr-extensions]: https://en.wikipedia.org/wiki/Master_boot_record#Sector_layout
|
||||
|
||||
The BIOS itself only cares for the boot signature field.
|
||||
If it finds a disk with a boot signature equal to `0xaa55`, it directly passes control to the bootloader code stored at the beginning of the disk.
|
||||
This bootloader is then responsible for multiple things:
|
||||
|
||||
- **Loading the kernel from disk:** The bootloader has to determine the location of the kernel image on the disk and load it into memory.
|
||||
- **Initializing the CPU:** As noted above, all `x86_64` CPUs start up in a 16-bit [real mode] to be compatible with older operating systems.
|
||||
So in order to run current 64-bit operating systems, the bootloader needsn to switch the CPU from the 16-bit [real mode] first to the 32-bit [protected mode], and then to the 64-bit [long mode], where all CPU registers and the complete main memory are available.
|
||||
- **Querying system information:** The third job of the bootloader is to query certain information from the BIOS and pass it to the OS kernel.
|
||||
This, for example, includes information about the available main memory and graphical output devices.
|
||||
- **Setting up an execution environment:** Kernels are typically stored as normal executable files (e.g. in the [ELF] or [PE] format), which require some loading procedure.
|
||||
This includes setting up a [call stack] and a [page table].
|
||||
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[PE]: https://en.wikipedia.org/wiki/Portable_Executable
|
||||
[call stack]: https://en.wikipedia.org/wiki/Call_stack
|
||||
[real mode]: https://en.wikipedia.org/wiki/Real_mode
|
||||
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
|
||||
[long mode]: https://en.wikipedia.org/wiki/Long_mode
|
||||
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
|
||||
[page table]: https://en.wikipedia.org/wiki/Page_table
|
||||
|
||||
Some bootloaders also include a basic user interface for [choosing between multiple installed OSs][multi-booting] or entering a recovery mode.
|
||||
Since it is not possible to do all that within the available 446 bytes, most bootloaders are split into a small first stage, which is as small as possible, and a second stage, which is subsequently loaded by the first stage.
|
||||
|
||||
[multi-booting]: https://en.wikipedia.org/wiki/Multi-booting
|
||||
|
||||
Writing a BIOS bootloader is cumbersome as it requires assembly language and a lot of non insightful steps like _“write this magic value to this processor register”_.
|
||||
Therefore we don't cover bootloader creation in this post and instead use the existing [`bootloader`] crate to make our kernel bootable.
|
||||
If you are interested in building your own BIOS bootloader: Stay tuned, a set of posts on this topic is already planned! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built.
|
||||
-->
|
||||
|
||||
#### The Future of BIOS
|
||||
|
||||
As noted above, most modern systems still support booting operating systems written for the legacy BIOS firmware for backwards-compatibility.
|
||||
However, there are [plans to remove this support soon][end-bios-support].
|
||||
Thus, it is strongly recommended to make operating system kernels compatible with the newer UEFI standard too.
|
||||
Fortunately, it is possible to create a kernel that supports booting on both BIOS (for older systems) and UEFI (for modern systems).
|
||||
|
||||
[end-bios-support]: https://arstechnica.com/gadgets/2017/11/intel-to-kill-off-the-last-vestiges-of-the-ancient-pc-bios-by-2020/
|
||||
|
||||
### UEFI
|
||||
|
||||
The Unified Extensible Firmware Interface (UEFI) replaces the classical BIOS firmware on most modern computers.
|
||||
The specification provides lots of useful features that make bootloader implementations much simpler:
|
||||
|
||||
- It supports initializing the CPU directly into 64-bit mode, instead of starting in a DOS-compatible 16-bit mode like the BIOS firmware.
|
||||
- It understands disk partitions and executable files.
|
||||
Thus it is able to fully load the bootloader from disk into memory (no 512-byte "first stage" is required anymore).
|
||||
- A standardized [specification][uefi-specification] minimizes the differences between systems.
|
||||
This isn't the case for the legacy BIOS firmware, so that bootloaders often have to try different methods because of hardware differences.
|
||||
- The specification is independent of the CPU architecture, so that the same interface can be used to boot on `x86_64` and e.g. `ARM` CPUs.
|
||||
- It natively supports network booting without requiring additional drivers.
|
||||
|
||||
[uefi-specification]: https://uefi.org/specifications
|
||||
|
||||
The UEFI standard also tries to make the boot process safer through a so-called _"secure boot"_ mechanism.
|
||||
The idea is that the firmware only allows loading bootloaders that are signed by a trusted [digital signature].
|
||||
Thus, malware should be prevented from compromising the early boot process.
|
||||
|
||||
[digital signature]: https://en.wikipedia.org/wiki/Digital_signature
|
||||
|
||||
#### Issues & Criticism
|
||||
|
||||
While most of the UEFI specification sounds like a good idea, there are also many issues with the standard.
|
||||
The main issue for most people is the fear that the _secure boot_ mechanism can be used to [lock users into the Windows operating system][uefi-secure-boot-lock-in] and thus prevent the installation of alternative operating systems such as Linux.
|
||||
|
||||
[uefi-secure-boot-lock-in]: https://arstechnica.com/information-technology/2015/03/windows-10-to-make-the-secure-boot-alt-os-lock-out-a-reality/
|
||||
|
||||
Another point of criticism is that the large number of features make the UEFI firmware very complex, which increases the chance that there are some bugs in the firmware implementation itself.
|
||||
This can lead to security problems because the firmware has complete control over the hardware.
|
||||
For example, a vulnerability in the built-in network stack of an UEFI implementation can allow attackers to compromise the system and e.g. silently observe all I/O data.
|
||||
The fact that most UEFI implementations are not open-source makes this issue even more problematic, since there is no way to audit the firmware code for potential bugs.
|
||||
|
||||
While there are open firmware projects such as [coreboot] that try to solve these problems, there is no way around the UEFI standard on most modern consumer computers.
|
||||
So we have to live with these drawbacks for now if we want to build a widely compatible bootloader and operating system kernel.
|
||||
|
||||
[coreboot]: https://www.coreboot.org/
|
||||
|
||||
#### Boot Process
|
||||
|
||||
The UEFI boot process works in the following way:
|
||||
|
||||
- After powering on and self-testing all components, the UEFI firmware starts looking for special bootable disk partitions called [EFI system partitions].
|
||||
These partitions must be formatted with the [FAT file system] and assigned a special ID that indicates them as EFI system partition.
|
||||
The UEFI standard understands both the [MBR] and [GPT] partition table formats for this, at least theoretically.
|
||||
In practice, some UEFI implementations seem to [directly switch to BIOS-style booting when an MBR partition table is used][mbr-csm], so it is recommended to only use the GPT format with UEFI.
|
||||
- If the firmware finds a EFI system partition, it looks for an executable file named `efi\boot\bootx64.efi` (on x86_64 systems) in it.
|
||||
This executable must use the [Portable Executable (PE)] format, which is common in the Windows world.
|
||||
- It then loads the executable from disk to memory, sets up the execution environment (CPU state, page tables, etc.) in a standardized way, and finally jumps to the entry point of the loaded executable.
|
||||
|
||||
[MBR]: https://en.wikipedia.org/wiki/Master_boot_record
|
||||
[GPT]: https://en.wikipedia.org/wiki/GUID_Partition_Table
|
||||
[mbr-csm]: https://bbs.archlinux.org/viewtopic.php?id=142637
|
||||
[EFI system partitions]: https://en.wikipedia.org/wiki/EFI_system_partition
|
||||
[FAT file system]: https://en.wikipedia.org/wiki/File_Allocation_Table
|
||||
[Portable Executable (PE)]: https://en.wikipedia.org/wiki/Portable_Executable
|
||||
|
||||
From this point on, the loaded executable has control.
|
||||
Typically, this executable is a bootloader that then loads the actual operating system kernel.
|
||||
Theoretically, it would also be possible to let the UEFI firmware load the kernel directly without a bootloader in between, but this would make it more difficult to port the kernel to other architectures.
|
||||
|
||||
Bootloaders and kernels typically need additional information about the system, for example the amount of available memory.
|
||||
For this reason, the UEFI firmware passes a pointer to a special _system table_ as an argument when invoking the bootloader entry point function.
|
||||
Using this table, the bootloader can query various system information and even invoke special functions provided by the UEFI firmware, for example for accessing the hard disk.
|
||||
|
||||
#### How we will use UEFI
|
||||
|
||||
As it is probably clear at this point, the UEFI interface is very powerful and complex.
|
||||
The wide range of functionality makes it even possible to write an operating system directly as an UEFI application, using the UEFI services provided by the system table instead of creating own drivers.
|
||||
In practice, however, most operating systems use UEFI only for the bootloader since own drivers give you better performance and more control over the system.
|
||||
We will also follow this path for our OS implementation.
|
||||
|
||||
To keep this post focused, we won't cover the creation of an UEFI bootloader here.
|
||||
Instead, we will use the already mentioned [`bootloader`] crate, which allows loading our kernel on both UEFI and BIOS systems.
|
||||
If you're interested in how to create an UEFI bootloader yourself, check out our extra post about [**UEFI Booting**].
|
||||
|
||||
[**UEFI Booting**]: @/edition-3/posts/02-booting/uefi/index.md
|
||||
|
||||
### The Multiboot Standard
|
||||
|
||||
To avoid that every operating system implements its own bootloader that is only compatible with a single OS, the [Free Software Foundation] created an open bootloader standard called [Multiboot] in 1995.
|
||||
The standard defines an interface between the bootloader and operating system, so that any Multiboot compliant bootloader can load any Multiboot compliant operating system on both BIOS and UEFI systems.
|
||||
The reference implementation is [GNU GRUB], which is the most popular bootloader for Linux systems.
|
||||
|
||||
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
|
||||
[Multiboot]: https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html
|
||||
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
|
||||
|
||||
To make a kernel Multiboot compliant, one just needs to insert a so-called [Multiboot header] at the beginning of the kernel file.
|
||||
This makes it very easy to boot an OS in GRUB.
|
||||
However, GRUB and the Multiboot standard have some problems too:
|
||||
|
||||
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
|
||||
|
||||
- The standard is designed to make the bootloader simple instead of the kernel.
|
||||
For example, the kernel needs to be linked with an [adjusted default page size], because GRUB can't find the Multiboot header otherwise.
|
||||
Another example is that the [boot information], which is passed to the kernel, contains lots of architecture dependent structures instead of providing clean abstractions.
|
||||
- The standard supports only the 32-bit protected mode on BIOS systems.
|
||||
This means that you still have to do the CPU configuration to switch to the 64-bit long mode.
|
||||
- For UEFI systems, the standard provides very little added value as it simply exposes the normal UEFI interface to kernels.
|
||||
- Both GRUB and the Multiboot standard are only sparsely documented.
|
||||
- GRUB needs to be installed on the host system to create a bootable disk image from the kernel file.
|
||||
This makes development on Windows or Mac more difficult.
|
||||
|
||||
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
|
||||
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
|
||||
|
||||
Because of these drawbacks we decided to not use GRUB or the Multiboot standard for this series.
|
||||
However, we plan to add Multiboot support to our [`bootloader`] crate, so that it becomes possible to load your kernel on a GRUB system too.
|
||||
If you're interested in writing a Multiboot compliant kernel, check out the [first edition] of this blog series.
|
||||
|
||||
[first edition]: @/edition-1/_index.md
|
||||
|
||||
## Bootable Disk Image
|
||||
|
||||
We now know that most operating system kernels are loaded by bootloaders, which are small programs that initialize the hardware to reasonable defaults, load the kernel from disk, and provide it with some fundamental information about the underlying system.
|
||||
In this section, we will learn how to combine the [minimal kernel] we created in the previous post with the `bootloader` crate in order to create a bootable disk image.
|
||||
|
||||
### The `bootloader` Crate
|
||||
|
||||
Since bootloaders quite complex on their own, we won't create our own bootloader here (but we are planning a separate series of posts on this).
|
||||
Instead, we will boot our kernel using the [`bootloader`] crate.
|
||||
This crate supports both BIOS and UEFI booting, provides all the necessary system information we need, and creates a reasonable default execution environment for our kernel.
|
||||
This way, we can focus on the actual kernel design in the following posts instead of spending a lot of time on system initialization.
|
||||
|
||||
[`bootloader`]: https://crates.io/crates/bootloader
|
||||
|
||||
To use the `bootloader` crate, we first need to add a dependency on it:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.10.1"
|
||||
```
|
||||
|
||||
For normal Rust crates, this step would be all that's needed for adding them as a dependency.
|
||||
However, the `bootloader` crate is a bit special.
|
||||
The problem is that it needs access to our kernel _after compilation_ in order to create a bootable disk image.
|
||||
However, cargo has no support for automatically running code after a successful build, so we need some manual build code for this.
|
||||
(There is a proposal for [post-build scripts] that would solve this issue, but it is not clear yet whether the Cargo team wants to add such a feature.)
|
||||
|
||||
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
|
||||
|
||||
#### Receiving the Boot Information
|
||||
|
||||
Before we look into the bootable disk image creation, we update need to update our `_start` entry point to be compatible with the `bootloader` crate.
|
||||
As we already mentioned above, bootloaders commonly pass additional system information when invoking the kernel, such as the amount of available memory.
|
||||
The `bootloader` crate also follows this convention, so we need to update our `_start` entry point to expect an additional argument.
|
||||
|
||||
The [`bootloader` documentation][`BootInfo`] specifies that a kernel entry point should have the following signature:
|
||||
|
||||
[`BootInfo`]: https://docs.rs/bootloader/0.10.1/bootloader/boot_info/struct.BootInfo.html
|
||||
|
||||
```rust
|
||||
extern "C" fn(boot_info: &'static mut bootloader::BootInfo) -> ! { ...
|
||||
}
|
||||
```
|
||||
|
||||
The only difference to our `_start` entry point is the additional `boot_info` argument, which is passed by the `bootloader` crate.
|
||||
This argument is a mutable reference to a [`bootloader::BootInfo`] type, which provides various information about the system.
|
||||
|
||||
[`bootloader::BootInfo`]: https://docs.rs/bootloader/0.10.1/bootloader/boot_info/struct.BootInfo.html
|
||||
|
||||
<div class="note"><details>
|
||||
<summary><h5>About <code>extern "C"</code> and <code>!</code></h5></summary>
|
||||
|
||||
The [`extern "C"`] qualifier specifies that the function should use the same [ABI] and [calling convention] as C code.
|
||||
It is common to use this qualifier when communicating across different executables because C has a stable ABI that is guaranteed to never change.
|
||||
Normal Rust functions, on the other hand, don't have a stable ABI, so they might change it the future (e.g. to optimize performance) and thus shouldn't be used across different executables.
|
||||
|
||||
[`extern "C"`]: https://doc.rust-lang.org/reference/items/functions.html#extern-function-qualifier
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
[calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
|
||||
The `!` return type indicates that the function is [diverging], which means that it must never return.
|
||||
The `bootloader` requires this because its code might no longer be valid after the kernel modified the system state such as the [page tables].
|
||||
|
||||
[diverging]: https://doc.rust-lang.org/rust-by-example/fn/diverging.html
|
||||
[page tables]: @/edition-2/posts/08-paging-introduction/index.md
|
||||
|
||||
</details></div>
|
||||
|
||||
While we could simply add the additional argument to our `_start` function, it would result in very fragile code.
|
||||
The problem is that because the `_start` function is called externally from the bootloader, no checking of the function signature occurs.
|
||||
So no compilation error occurs, even if the function signature completely changed after updating to a newer `bootloader` version.
|
||||
At runtime, however, the code would fail or introduce undefined behavior.
|
||||
|
||||
To avoid these issues and make sure that the entry point function has always the correct signature, the `bootloader` crate provides an [`entry_point`] macro that provides a type-checked way to define a Rust function as the entry point.
|
||||
This way, the function signature is checked at compile time so that no runtime error can occur.
|
||||
|
||||
[`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html
|
||||
|
||||
To use the `entry_point` macro, we rewrite our entry point function in the following way:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
use bootloader::{entry_point, BootInfo};
|
||||
|
||||
entry_point!(kernel_main);
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
We no longer need to use `extern "C"` or `no_mangle` for our entry point, as the macro defines the actual lower-level `_start` entry point for us.
|
||||
The `kernel_main` function is now a completely normal Rust function, so we can choose an arbitrary name for it.
|
||||
Since the signature of the function is enforced by the macro, a compilation error occurs when it e.g. has the wrong argument type.
|
||||
|
||||
After adjusting our entry point for the `bootloader` crate, we can now look into how to create a bootable disk image from our kernel.
|
||||
|
||||
### Creating a Disk Image
|
||||
|
||||
The [docs of the `bootloader` crate][`bootloader` docs] describes how to create a bootable disk image for a kernel.
|
||||
The first step is to find the directory where cargo placed the source code of the `bootloader` dependency.
|
||||
Then, a special build command needs to be executed in that directory, passing the paths to the kernel binary and its `Cargo.toml` as arguments.
|
||||
This will result in multiple disk image files as output, which can be used to boot the kernel on BIOS and UEFI systems.
|
||||
|
||||
[`bootloader` docs]: https://docs.rs/bootloader/0.10.1/bootloader/
|
||||
|
||||
#### A `boot` crate
|
||||
|
||||
Since following these steps manually is cumbersome, we create a script to automate it.
|
||||
For that we create a new `boot` crate in a subdirectory, in which we will implement the build steps:
|
||||
|
||||
```
|
||||
cargo new --bin boot
|
||||
```
|
||||
|
||||
This command creates a new `boot` subfolder with a `Cargo.toml` and a `src/main.rs` in it.
|
||||
Since this new cargo project will be tightly coupled with our main project, it makes sense to combine the two crates as a [cargo workspace].
|
||||
This way, they will share the same `Cargo.lock` for their dependencies and place their compilation artifacts in a common `target` folder.
|
||||
To create such a workspace, we add the following to the `Cargo.toml` of our main project:
|
||||
|
||||
[cargo workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[workspace]
|
||||
members = ["boot"]
|
||||
```
|
||||
|
||||
After creating the workspace, we can begin the implementation of the `boot` crate.
|
||||
Note that the crate will be invoked as part as our build process, so it can be a normal Rust executable that runs on our host system.
|
||||
This means that is has a classical `main` function and can use standard library types such as [`Path`] or [`Command`] without problems.
|
||||
|
||||
[`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
|
||||
[`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html
|
||||
|
||||
#### Locating the `bootloader` Source
|
||||
|
||||
The first step in creating the bootable disk image is to to locate where cargo put the source code of the `bootloader` dependency.
|
||||
For that we can use cargo's [`cargo metadata`] subcommand, which outputs all kinds of information about a cargo project as a JSON object.
|
||||
Among other things, it contains the manifest path (i.e. the path to the `Cargo.toml`) of all dependencies, including the `bootloader` crate.
|
||||
|
||||
[`cargo metadata`]: https://doc.rust-lang.org/cargo/commands/cargo-metadata.html
|
||||
|
||||
To keep this post short, we won't include the code to parse the JSON output and to locate the right entry here.
|
||||
Instead, we created a small crate named [`bootloader-locator`] that wraps the needed functionality in a simple [`locate_bootloader`] function.
|
||||
Let's add that crate as a dependency and use it:
|
||||
|
||||
[`bootloader-locator`]: https://docs.rs/bootloader-locator/0.0.4/bootloader_locator/index.html
|
||||
[`locate_bootloader`]: https://docs.rs/bootloader-locator/0.0.4/bootloader_locator/fn.locate_bootloader.html
|
||||
|
||||
```toml
|
||||
# in boot/Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader-locator = "0.0.4"
|
||||
```
|
||||
|
||||
```rust
|
||||
// in boot/src/main.rs
|
||||
|
||||
use bootloader_locator::locate_bootloader; // new
|
||||
|
||||
pub fn main() {
|
||||
let bootloader_manifest = locate_bootloader("bootloader").unwrap();
|
||||
dbg!(bootloader_manifest);
|
||||
}
|
||||
```
|
||||
|
||||
The `locate_bootloader` function takes the name of the bootloader dependency as argument to allow alternative bootloader crates that are named differently.
|
||||
Since the function might fail, we use the [`unwrap`] method to [panic] on an error.
|
||||
Panicking is ok here because the `boot` crate is only part of our build process.
|
||||
|
||||
[`unwrap`]: https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
If you're interested in how the `locate_bootloader` function works, [check out its source code][locate_bootloader source].
|
||||
It first executes the `cargo metadata` command and parses it's result as JSON using the [`json` crate].
|
||||
Then it traverses the parsed metadata to find the `bootloader` dependency and return its manifest path.
|
||||
|
||||
[locate_bootloader source]: https://docs.rs/crate/bootloader-locator/0.0.4/source/src/lib.rs
|
||||
[`json` crate]: https://docs.rs/json/0.12.4/json/
|
||||
|
||||
Let's try to run it to see whether it works.
|
||||
If everything succeeds, the [`dbg!`] macro should print the path to the `bootloader` source code.
|
||||
Note that we need to run the `boot` binary from the root directory of our workspace, not from within the `boot` directory.
|
||||
Otherwise the `locate_bootloader` function would operate on the `boot/Cargo.toml`, where it won't find a bootloader dependency.
|
||||
|
||||
[`dbg!`]: https://doc.rust-lang.org/std/macro.dbg.html
|
||||
|
||||
To run the `boot` crate from our workspace root (i.e. the kernel directory), we can pass a [`--package`] argument to `cargo run`:
|
||||
|
||||
[`--package`]: https://doc.rust-lang.org/cargo/commands/cargo-run.html#package-selection
|
||||
|
||||
```
|
||||
> cargo run --package boot
|
||||
[boot/src/main.rs:5] bootloader_manifest = "/.../.cargo/.../bootloader-.../Cargo.toml"
|
||||
```
|
||||
|
||||
It worked! We see that the bootloader source code lives somewhere in the `.cargo` directory in our user directory.
|
||||
By querying the source code for the exact bootloader version that our kernel is using, we ensure that the bootloader and the kernel use the exact same version of the `BootInfo` type.
|
||||
This is important because the `BootInfo` type is not stable yet, so undefined behavior can occur when when using different `BootInfo` versions.
|
||||
|
||||
#### Running the Build Command
|
||||
|
||||
The next step is to run the build command of the bootloader.
|
||||
From the [`bootloader` docs] we learn that the crate requires the following build command:
|
||||
|
||||
```
|
||||
cargo builder --kernel-manifest path/to/kernel/Cargo.toml \
|
||||
--kernel-binary path/to/kernel_bin
|
||||
```
|
||||
|
||||
In addition, the docs recommend to use the `--target-dir` and `--out-dir` arguments when building the bootloader as a dependency to override where cargo places the compilation artifacts.
|
||||
|
||||
Let's try to invoke that command from our `main` function.
|
||||
For that we use the [`process::Command`] type of the standard library, which allows us to spawn new processes and wait for their results:
|
||||
|
||||
[`process::Command`]: https://doc.rust-lang.org/std/process/struct.Command.html
|
||||
|
||||
```rust
|
||||
// in boot/src/main.rs
|
||||
|
||||
use std::process::Command; // new
|
||||
|
||||
pub fn main() {
|
||||
let bootloader_manifest = locate_bootloader("bootloader").unwrap();
|
||||
|
||||
// new code below
|
||||
|
||||
let kernel_binary = todo!();
|
||||
let kernel_manifest = todo!();
|
||||
let target_dir = todo!();
|
||||
let out_dir = todo!();
|
||||
|
||||
// create a new build command; use the `CARGO` environment variable to
|
||||
// also support non-standard cargo versions
|
||||
let mut build_cmd = Command::new(env!("CARGO"));
|
||||
|
||||
// pass the arguments
|
||||
build_cmd.arg("builder");
|
||||
build_cmd.arg("--kernel-manifest").arg(&kernel_manifest);
|
||||
build_cmd.arg("--kernel-binary").arg(&kernel_binary);
|
||||
build_cmd.arg("--target-dir").arg(&target_dir);
|
||||
build_cmd.arg("--out-dir").arg(&out_dir);
|
||||
|
||||
// set the working directory
|
||||
let bootloader_dir = bootloader_manifest.parent().unwrap();
|
||||
build_cmd.current_dir(&bootloader_dir);
|
||||
|
||||
// run the command
|
||||
let exit_status = build_cmd.status().unwrap();
|
||||
if !exit_status.success() {
|
||||
panic!("bootloader build failed");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`Command::new`] function to create a new [`process::Command`].
|
||||
Instead of hardcoding the command name "cargo", we use the [`CARGO` environment variable] that cargo sets when compiling the `boot` crate.
|
||||
This way, we ensure that we use the exact same cargo version for compiling the `bootloader` crate, which is important when using non-standard cargo versions, e.g. through rustup's [toolchain override shorthands].
|
||||
Since the environment variable is set at compile time, we use the compiler-builtin [`env!`] macro to retrieve its value.
|
||||
|
||||
[`Command::new`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.new
|
||||
[`CARGO` environment variable]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
|
||||
[toolchain override shorthands]: https://rust-lang.github.io/rustup/overrides.html#toolchain-override-shorthand
|
||||
[`env!`]: https://doc.rust-lang.org/std/macro.env.html
|
||||
|
||||
After creating the `Command` type, we pass all the required arguments by calling the [`Command::arg`] method.
|
||||
Most of the paths are still set to [`todo!()`] as a placeholder and will be filled out in a moment.
|
||||
|
||||
[`Command::arg`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.arg
|
||||
[`todo!()`]: https://doc.rust-lang.org/std/macro.todo.html
|
||||
|
||||
Since the build command needs to be run inside the source directory of the `bootloader` crate, we use the [`Command::current_dir`] method to set the working directory accordingly.
|
||||
We can determine the `bootloader_dir` path from the `bootloader_manifest` path by using the [`Path::parent`] method.
|
||||
Since not all paths have a parent directory (e.g. the path `/` has not), the `parent()` call can fail.
|
||||
However, this should never happen for the `bootloader_manifest` path, so we use the [`Option::unwrap`] method that panics on `None`.
|
||||
|
||||
[`Command::current_dir`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.current_dir
|
||||
[`Path::parent`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.parent
|
||||
[`Option::unwrap`]: https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap
|
||||
|
||||
After setting the arguments and the working directory, we use the [`Command::status`] method to execute the command and wait for its exit status.
|
||||
Through the [`ExitStatus::success`] method we verify that the command was successful.
|
||||
If not we use the [`panic!`] macro to cause a panic.
|
||||
|
||||
[`Command::current_dir`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.current_dir
|
||||
[`Command::status`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.status
|
||||
[`ExitStatus::success`]: https://doc.rust-lang.org/std/process/struct.ExitStatus.html#method.success
|
||||
[`panic!`]: https://doc.rust-lang.org/std/macro.panic.html
|
||||
|
||||
#### Filling in the Paths
|
||||
|
||||
We still need to fill in the paths we marked as `todo!` above.
|
||||
We start with the path to the kernel binary:
|
||||
|
||||
```rust
|
||||
// in `main` in boot/src/main.rs
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
// TODO: don't hardcore this
|
||||
let kernel_binary = Path::new("target/x86_64-blog_os/debug/blog_os").canonicalize().unwrap();
|
||||
```
|
||||
|
||||
By default, cargo places our compiled kernel executable in a subdirectory of the `target` folder.
|
||||
The `x86_64_blog_os` is the name of our target JSON file and the `debug` indicates that this was a build with debug information and without optimizations.
|
||||
For now we simply hardcode the path to keep things simple, but we will make it more flexible later in this post.
|
||||
|
||||
Since we're going to need an absolute path, we use the [`Path::canonicalize`] method to get the full path to the file.
|
||||
We use [`unwrap`] to panic if the file doesn't exist.
|
||||
|
||||
[`Path::canonicalize`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.canonicalize
|
||||
[`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
|
||||
|
||||
To fill in the other path variables, we utilize another environment variable that cargo passes on build:
|
||||
|
||||
```rust
|
||||
// in `main` in boot/src/main.rs
|
||||
|
||||
// the path to the root of this crate, set by cargo
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
// we know that the kernel lives in the parent directory
|
||||
let kernel_dir = manifest_dir.parent().unwrap();
|
||||
|
||||
let kernel_manifest = kernel_dir.join("Cargo.toml");
|
||||
// use the same target folder for building the bootloader
|
||||
let target_dir = kernel_dir.join("target");
|
||||
// place the resulting disk image next to our kernel binary
|
||||
let out_dir = kernel_binary.parent().unwrap();
|
||||
```
|
||||
|
||||
The [`CARGO_MANIFEST_DIR`] environment variable always points to the `boot` directory, even if the crate is built from a different directory (e.g. via cargo's `--manifest-path` argument).
|
||||
This gives use a good starting point for creating the paths we care about since we know that our kernel lives in the [parent][`Path::parent`] directory.
|
||||
|
||||
[`CARGO_MANIFEST_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
|
||||
|
||||
From the `kernel_dir`, we can then construct the `kernel_manifest` and `target_dir` paths using the [`Path::join`] method.
|
||||
For the `out_dir` binding, we use the parent directory of the `kernel_binary` path.
|
||||
This way, the bootloader will create the disk image files next to our kernel executable.
|
||||
|
||||
[`Path::join`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.join
|
||||
|
||||
#### Creating the Disk Images
|
||||
|
||||
There is one last step before we can create the bootable disk images: The `bootloader` build requires the [rustup component] `llvm-tools-preview`.
|
||||
To install it, we can either run `rustup component add llvm-tools-preview` or specify it in our `rust-toolchain` file:
|
||||
|
||||
[rustup component]: https://rust-lang.github.io/rustup/concepts/components.html
|
||||
|
||||
```toml
|
||||
# in rust-toolchain
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src", "rustfmt", "clippy", "llvm-tools-preview"]
|
||||
```
|
||||
|
||||
After that can finally use our `boot` crate to create some bootable disk images from our kernel:
|
||||
|
||||
```bash
|
||||
> cargo kbuild
|
||||
> cargo run --package boot
|
||||
```
|
||||
|
||||
We first compile our kernel through `cargo kbuild` to ensure that the kernel binary is up to date.
|
||||
Then we run our `boot` crate through `cargo run --package boot`, which takes the kernel binary and builds the bootloader around it.
|
||||
The result are some disk image files named `bootimage-*` next to our kernel binary inside `target/x86_64-blog_os/debug`.
|
||||
Note that the command will only work from the root directory of our project.
|
||||
This is because we hardcoded the `kernel_binary` path in our `main` function.
|
||||
We will fix this later in the post, but first it is time to actually run our kernel!
|
||||
|
||||
From the [`bootloader` docs], we learn that the bootloader the following disk images:
|
||||
|
||||
- A BIOS boot image named `bootimage-bios-<bin_name>.img`.
|
||||
- Multiple images suitable for UEFI booting
|
||||
- An EFI executable named `bootimage-uefi-<bin_name>.efi`.
|
||||
- A FAT partition image named `bootimage-uefi-<bin_name>.fat`, which contains the EFI executable under `efi\boot\bootx64.efi`.
|
||||
- A GPT disk image named `bootimage-uefi-<bin_name>.img`, which contains the FAT image as EFI system partition.
|
||||
|
||||
In general, the `.img` files are the ones that you want to copy to an USB stick in order to boot from it.
|
||||
The other files are useful for booting the kernel in virtual machines such as [QEMU].
|
||||
The `<bin_name>` placeholder is the binary name of the kernel, i.e. `blog_os` or the crate name you chose.
|
||||
|
||||
## Running our Kernel
|
||||
|
||||
After creating a bootable disk image for our kernel, we are finally able to run it.
|
||||
Before we learn how to run it on real hardware, we start by running it inside the [QEMU] system emulator.
|
||||
This has multiple advantages:
|
||||
|
||||
- We can't break anything: Our kernel has full hardware access, so that a bug might have serious consequences on read hardware.
|
||||
- We don't need a separate computer: QEMU runs as a normal program on our development computer.
|
||||
- The edit-test cycle is much faster: We don't need to copy the disk image to bootable usb stick on every kernel change.
|
||||
- It's possible to debug our kernel via QEMU's debug tools and GDB.
|
||||
|
||||
We will still learn how to boot our kernel on real hardware later in this post, but for now we focus on QEMU.
|
||||
For that you need to install QEMU on your machine as described on the [QEMU download page].
|
||||
|
||||
[QEMU download page]: https://www.qemu.org/download/
|
||||
|
||||
### Running in QEMU
|
||||
|
||||
After installing QEMU, you can run `qemu-system-x86_64 --version` in a terminal to verify that it is installed.
|
||||
Then you can run the BIOS disk image of our kernel through the following command:
|
||||
|
||||
```
|
||||
qemu-system-x86_64 -drive \
|
||||
format=raw,file=target/x86_64-blog_os/debug/bootimage-bios-blog_os.img
|
||||
```
|
||||
|
||||
As a result, you should see a window open that looks like this:
|
||||
|
||||

|
||||
|
||||
This output comes from the bootloader.
|
||||
As we see, the last line is _"Jumping to kernel entry point at […]"_.
|
||||
This is the point where the `_start` function of our kernel is called.
|
||||
Since we currently only `loop {}` in that function nothing else happens, so it is expected that we don't see any additional output.
|
||||
|
||||
Running the UEFI disk image works in a similar way, but we need to pass some additional files to QEMU to emulate an UEFI firmware.
|
||||
This is necessary because QEMU does not support emulating an UEFI firmware natively.
|
||||
The files that we need are provided by the [Open Virtual Machine Firmware (OVMF)][OVMF] project, which is a sub-project of [TianoCore] and implements UEFI support for virtual machines.
|
||||
Unfortunately, the project is only [sparsely documented][ovmf-whitepaper] and does not even have a clear homepage.
|
||||
|
||||
[OVMF]: https://github.com/tianocore/tianocore.github.io/wiki/OVMF
|
||||
[TianoCore]: https://www.tianocore.org/
|
||||
[ovmf-whitepaper]: https://www.linux-kvm.org/downloads/lersek/ovmf-whitepaper-c770f8c.txt
|
||||
|
||||
The easiest way to work with OVMF is to download pre-built images of the code.
|
||||
We provide such images in the [`rust-osdev/ovmf-prebuilt`] repository, which is updated daily from [Gerd Hoffman's RPM builds](https://www.kraxel.org/repos/).
|
||||
The compiled OVMF are provided as [GitHub releases][ovmf-prebuilt-releases].
|
||||
|
||||
[`rust-osdev/ovmf-prebuilt`]: https://github.com/rust-osdev/ovmf-prebuilt/
|
||||
[ovmf-prebuilt-releases]: https://github.com/rust-osdev/ovmf-prebuilt/releases/latest
|
||||
|
||||
To run our UEFI disk image in QEMU, we need the `OVMF_pure-efi.fd` file (other files might work as well).
|
||||
After downloading it, we can then run our UEFI disk image using the following command:
|
||||
|
||||
```
|
||||
qemu-system-x86_64 -drive \
|
||||
format=raw,file=target/x86_64-blog_os/debug/bootimage-uefi-blog_os.img \
|
||||
-bios /path/to/OVMF_pure-efi.fd,
|
||||
```
|
||||
|
||||
If everything works, this command opens a window with the following content:
|
||||
|
||||
|
||||

|
||||
|
||||
The output is a bit different than with the BIOS disk image.
|
||||
Among other things, it explicitly mentions that this is an UEFI boot right on top.
|
||||
|
||||
### Screen Output
|
||||
|
||||
While we see some screen output from the bootloader, our kernel still does nothing.
|
||||
Let's fix this by trying to output something to the screen from our kernel too.
|
||||
|
||||
Screen output works through a so-called [_framebuffer_].
|
||||
A framebuffer is a memory region that contains the pixels that should be shown on the screen.
|
||||
The graphics card automatically reads the contents of this region on every screen refresh and updates the shown pixels accordingly.
|
||||
|
||||
[_framebuffer_]: https://en.wikipedia.org/wiki/Framebuffer
|
||||
|
||||
Since the size, pixel format, and memory location of the framebuffer can vary between different systems, we need to find out these parameters first.
|
||||
The easiest way to do this is to read it from the [boot information structure][`BootInfo`] that the bootloader passes as argument to our kernel entry point:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_ref() {
|
||||
let info = framebuffer.info();
|
||||
let buffer = framebuffer.buffer();
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Even though most systems support a framebuffer, some might not.
|
||||
The [`BootInfo`] type reflects this by specifying its `framebuffer` field as an [`Option`].
|
||||
Since screen output won't be essential for our kernel (there are other possible communication channels such as serial ports), we use an [`if let`] statement to run the framebuffer code only if a framebuffer is available.
|
||||
|
||||
[`Option`]: https://doc.rust-lang.org/std/option/enum.Option.html
|
||||
[`if let`]: https://doc.rust-lang.org/reference/expressions/if-expr.html#if-let-expressions
|
||||
|
||||
The [`FrameBuffer`] type provides two methods: The `info` method returns a [`FrameBufferInfo`] instance with all kinds of information about the framebuffer format, including the pixel type and the screen resolution.
|
||||
The `buffer` method returns the actual framebuffer content in form of a mutable byte [slice].
|
||||
|
||||
[`FrameBuffer`]: https://docs.rs/bootloader/0.10.1/bootloader/boot_info/struct.FrameBuffer.html
|
||||
[`FrameBufferInfo`]: https://docs.rs/bootloader/0.10.1/bootloader/boot_info/struct.FrameBufferInfo.html
|
||||
[slice]: https://doc.rust-lang.org/std/primitive.slice.html
|
||||
|
||||
We will look into programming the framebuffer in detail in the next post.
|
||||
For now, let's just try setting the whole screen to some color.
|
||||
For this, we just set every pixel in the byte slice to some fixed value:
|
||||
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
for byte in framebuffer.buffer_mut() {
|
||||
*byte = 0x90;
|
||||
}
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
While it depends on the pixel color format how these values are interpreted, the result will likely be some shade of gray since we set the same value for every color channel (e.g. in the RGB color format).
|
||||
|
||||
After running `cargo kbuild` and then our `boot` script again, we can boot the new version in QEMU.
|
||||
We see that our guess that the whole screen would turn gray was right:
|
||||
|
||||

|
||||
|
||||
We finally see some output from our own little kernel!
|
||||
|
||||
You can try experimenting with the pixel bytes if you like, for example by increasing the pixel value on each loop iteration:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
let mut value = 0x90;
|
||||
for byte in framebuffer.buffer_mut() {
|
||||
*byte = value;
|
||||
value = value.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`wrapping_add`] method here because Rust panics on implicit integer overflow (at least in debug mode).
|
||||
By adding a prime number, we try to add some variety.
|
||||
The result looks as follows:
|
||||
|
||||

|
||||
|
||||
### Booting on Real Hardware
|
||||
|
||||
To boot on real hardware, you first need to write either the `bootimage-uefi-blog_os.img` or the `bootimage-bios-blog_os.img` disk image to an USB stick.
|
||||
This deletes everything on the stick, so be careful.
|
||||
The actual steps to do this depend on your operating system.
|
||||
|
||||
|
||||
#### Unix-like
|
||||
|
||||
On any Unix-like host OS (including both Linux and macOS), you can use the `dd` command to write the disk image directly to a USB drive.
|
||||
First run either `sudo fdisk -l` (on Linux) or `diskutil list` (on a Mac) to get info about where in `/dev` the file representing your device is located.
|
||||
After that, open a terminal window and run either of the following commands:
|
||||
|
||||
##### Linux
|
||||
```
|
||||
# replace /dev/sdX with device filename as revealed by "sudo fdisk -l"
|
||||
$ sudo dd if=boot-uefi-blog_os.img of=/dev/sdX
|
||||
```
|
||||
|
||||
##### macOS
|
||||
```
|
||||
# replace /dev/diskX with device filename as revealed by "diskutil list"
|
||||
$ sudo dd if=boot-uefi-blog_os.img of=/dev/diskX
|
||||
```
|
||||
|
||||
**WARNING**: Be very careful when running this command.
|
||||
If you specify the wrong device as the `of=` parameter, you could end up wiping your system clean, so make sure the device you run it on is a removable one.
|
||||
|
||||
#### Windows
|
||||
|
||||
On Windows, you can use the [Rufus] tool, which is developed as an open-source project [on GitHub][rufus-github].
|
||||
After downloading it you can directly run it, there's no installation necessary.
|
||||
In the interface, you select the USB stick you want to write to
|
||||
|
||||
[Rufus]: https://rufus.ie/
|
||||
[rufus-github]: https://github.com/pbatard/rufus
|
||||
|
||||
## Support for `cargo run`
|
||||
|
||||
- take `kernel_binary` path as argument instead of hardcoding it
|
||||
- set `boot` crate as runner in `.cargo/config` (for no OS targets only)
|
||||
- add `krun` alias
|
||||
|
||||
|
||||
|
||||
### Only create disk images
|
||||
|
||||
- Add support for new `--no-run` arg to `boot` crate
|
||||
- Add `cargo disk-image` alias for `cargo run --package boot -- --no-run`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# OLD
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
For running `bootimage` and building the bootloader, you need to have the `llvm-tools-preview` rustup component installed.
|
||||
You can do so by executing `rustup component add llvm-tools-preview`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Real Machine
|
||||
|
||||
It is also possible to write it to an USB stick and boot it on a real machine:
|
||||
|
||||
```
|
||||
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.img of=/dev/sdX && sync
|
||||
```
|
||||
|
||||
Where `sdX` is the device name of your USB stick.
|
||||
**Be careful** to choose the correct device name, because everything on that device is overwritten.
|
||||
|
||||
After writing the image to the USB stick, you can run it on real hardware by booting from it.
|
||||
You probably need to use a special boot menu or change the boot order in your BIOS configuration to boot from the USB stick.
|
||||
Note that it currently doesn't work for UEFI machines, since the `bootloader` crate has no UEFI support yet.
|
||||
|
||||
### Using `cargo run`
|
||||
|
||||
To make it easier to run our kernel in QEMU, we can set the `runner` configuration key for cargo:
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[target.'cfg(target_os = "none")']
|
||||
runner = "bootimage runner"
|
||||
```
|
||||
|
||||
The `target.'cfg(target_os = "none")'` table applies to all targets that have set the `"os"` field of their target configuration file to `"none"`.
|
||||
This includes our `x86_64-blog_os.json` target.
|
||||
The `runner` key specifies the command that should be invoked for `cargo run`.
|
||||
The command is run after a successful build with the executable path passed as first argument.
|
||||
See the [cargo documentation][cargo configuration] for more details.
|
||||
|
||||
The `bootimage runner` command is specifically designed to be usable as a `runner` executable.
|
||||
It links the given executable with the project's bootloader dependency and then launches QEMU.
|
||||
See the [Readme of `bootimage`] for more details and possible configuration options.
|
||||
|
||||
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
|
||||
|
||||
Now we can use `cargo run` to compile our kernel and boot it in QEMU.
|
||||
|
||||
## What's next?
|
||||
|
||||
In the next post, we will explore the VGA text buffer in more detail and write a safe interface for it.
|
||||
We will also add support for the `println` macro.
|
||||
BIN
blog/content/edition-3/posts/02-booting/qemu-bios.png
Normal file
BIN
blog/content/edition-3/posts/02-booting/qemu-bios.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
blog/content/edition-3/posts/02-booting/qemu-gray.png
Normal file
BIN
blog/content/edition-3/posts/02-booting/qemu-gray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
blog/content/edition-3/posts/02-booting/qemu-uefi.png
Normal file
BIN
blog/content/edition-3/posts/02-booting/qemu-uefi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
blog/content/edition-3/posts/02-booting/qemu-wrapping-add.png
Normal file
BIN
blog/content/edition-3/posts/02-booting/qemu-wrapping-add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
1107
blog/content/edition-3/posts/02-booting/uefi/index.md
Normal file
1107
blog/content/edition-3/posts/02-booting/uefi/index.md
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
25
blog/content/edition-3/posts/03-screen-output/index.md
Normal file
25
blog/content/edition-3/posts/03-screen-output/index.md
Normal file
@@ -0,0 +1,25 @@
|
||||
+++
|
||||
title = "Screen Output"
|
||||
weight = 3
|
||||
path = "screen-output"
|
||||
date = 0000-01-01
|
||||
draft = true
|
||||
|
||||
[extra]
|
||||
chapter = "Basic I/O"
|
||||
icon = '''<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-display" viewBox="0 0 16 16">
|
||||
<path d="M0 4s0-2 2-2h12s2 0 2 2v6s0 2-2 2h-4c0 .667.083 1.167.25 1.5H11a.5.5 0 0 1 0 1H5a.5.5 0 0 1 0-1h.75c.167-.333.25-.833.25-1.5H2s-2 0-2-2V4zm1.398-.855a.758.758 0 0 0-.254.302A1.46 1.46 0 0 0 1 4.01V10c0 .325.078.502.145.602.07.105.17.188.302.254a1.464 1.464 0 0 0 .538.143L2.01 11H14c.325 0 .502-.078.602-.145a.758.758 0 0 0 .254-.302 1.464 1.464 0 0 0 .143-.538L15 9.99V4c0-.325-.078-.502-.145-.602a.757.757 0 0 0-.302-.254A1.46 1.46 0 0 0 13.99 3H2c-.325 0-.502.078-.602.145z"/>
|
||||
</svg>'''
|
||||
+++
|
||||
|
||||
Dolores qui incidunt sit fugiat amet consequatur. Qui ab vel et molestias ex nemo corporis consequatur. Quia consequuntur itaque sequi quia autem. Maxime vel quis maxime at. Tenetur eveniet velit dolor quidem temporibus tenetur.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
<!-- toc -->
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
|
||||
<!-- TODO: update relative link in 02-booting/uefi/index.md when this post is finished -->
|
||||
23
blog/content/edition-3/posts/04-keyboard-and-serial/index.md
Normal file
23
blog/content/edition-3/posts/04-keyboard-and-serial/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
+++
|
||||
title = "Keyboard & Serial"
|
||||
weight = 4
|
||||
path = "keyboard-and-serial"
|
||||
date = 0000-01-01
|
||||
draft = true
|
||||
|
||||
[extra]
|
||||
chapter = "Basic I/O"
|
||||
icon = '''<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-keyboard" viewBox="0 0 16 16">
|
||||
<path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h12zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2z"/>
|
||||
<path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75v-.5zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25v-.5zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75v-.5zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75v-.5zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75v-.5zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75v-.5zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75v-.5zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75v-.5zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75v-.5zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25v-.5z"/>
|
||||
</svg>'''
|
||||
+++
|
||||
|
||||
Dolores qui incidunt sit fugiat amet consequatur. Qui ab vel et molestias ex nemo corporis consequatur. Quia consequuntur itaque sequi quia autem. Maxime vel quis maxime at. Tenetur eveniet velit dolor quidem temporibus tenetur.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
<!-- toc -->
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
23
blog/content/edition-3/posts/05-simple-shell/index.md
Normal file
23
blog/content/edition-3/posts/05-simple-shell/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
+++
|
||||
title = "Simple Shell"
|
||||
weight = 5
|
||||
path = "simple-shell"
|
||||
date = 0000-01-01
|
||||
draft = true
|
||||
|
||||
[extra]
|
||||
chapter = "Basic I/O"
|
||||
icon = '''<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-terminal" viewBox="0 0 16 16">
|
||||
<path d="M6 9a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3A.5.5 0 0 1 6 9zM3.854 4.146a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2z"/>
|
||||
<path d="M2 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H2zm12 1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h12z"/>
|
||||
</svg>'''
|
||||
+++
|
||||
|
||||
Dolores qui incidunt sit fugiat amet consequatur. Qui ab vel et molestias ex nemo corporis consequatur. Quia consequuntur itaque sequi quia autem. Maxime vel quis maxime at. Tenetur eveniet velit dolor quidem temporibus tenetur.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
<!-- toc -->
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
23
blog/content/edition-3/posts/06-basic-games/index.md
Normal file
23
blog/content/edition-3/posts/06-basic-games/index.md
Normal file
@@ -0,0 +1,23 @@
|
||||
+++
|
||||
title = "Basic Games"
|
||||
weight = 6
|
||||
path = "basic-games"
|
||||
date = 0000-01-01
|
||||
draft = true
|
||||
|
||||
[extra]
|
||||
chapter = "Basic I/O"
|
||||
icon = '''<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-controller" viewBox="0 0 16 16">
|
||||
<path d="M11.5 6.027a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm2.5-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0zm-1.5 1.5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1zm-6.5-3h1v1h1v1h-1v1h-1v-1h-1v-1h1v-1z"/>
|
||||
<path d="M3.051 3.26a.5.5 0 0 1 .354-.613l1.932-.518a.5.5 0 0 1 .62.39c.655-.079 1.35-.117 2.043-.117.72 0 1.443.041 2.12.126a.5.5 0 0 1 .622-.399l1.932.518a.5.5 0 0 1 .306.729c.14.09.266.19.373.297.408.408.78 1.05 1.095 1.772.32.733.599 1.591.805 2.466.206.875.34 1.78.364 2.606.024.816-.059 1.602-.328 2.21a1.42 1.42 0 0 1-1.445.83c-.636-.067-1.115-.394-1.513-.773-.245-.232-.496-.526-.739-.808-.126-.148-.25-.292-.368-.423-.728-.804-1.597-1.527-3.224-1.527-1.627 0-2.496.723-3.224 1.527-.119.131-.242.275-.368.423-.243.282-.494.575-.739.808-.398.38-.877.706-1.513.773a1.42 1.42 0 0 1-1.445-.83c-.27-.608-.352-1.395-.329-2.21.024-.826.16-1.73.365-2.606.206-.875.486-1.733.805-2.466.315-.722.687-1.364 1.094-1.772a2.34 2.34 0 0 1 .433-.335.504.504 0 0 1-.028-.079zm2.036.412c-.877.185-1.469.443-1.733.708-.276.276-.587.783-.885 1.465a13.748 13.748 0 0 0-.748 2.295 12.351 12.351 0 0 0-.339 2.406c-.022.755.062 1.368.243 1.776a.42.42 0 0 0 .426.24c.327-.034.61-.199.929-.502.212-.202.4-.423.615-.674.133-.156.276-.323.44-.504C4.861 9.969 5.978 9.027 8 9.027s3.139.942 3.965 1.855c.164.181.307.348.44.504.214.251.403.472.615.674.318.303.601.468.929.503a.42.42 0 0 0 .426-.241c.18-.408.265-1.02.243-1.776a12.354 12.354 0 0 0-.339-2.406 13.753 13.753 0 0 0-.748-2.295c-.298-.682-.61-1.19-.885-1.465-.264-.265-.856-.523-1.733-.708-.85-.179-1.877-.27-2.913-.27-1.036 0-2.063.091-2.913.27z"/>
|
||||
</svg>'''
|
||||
+++
|
||||
|
||||
Dolores qui incidunt sit fugiat amet consequatur. Qui ab vel et molestias ex nemo corporis consequatur. Quia consequuntur itaque sequi quia autem. Maxime vel quis maxime at. Tenetur eveniet velit dolor quidem temporibus tenetur.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
<!-- toc -->
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
7
blog/content/edition-3/posts/_index.md
Normal file
7
blog/content/edition-3/posts/_index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
+++
|
||||
title = "Posts"
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
page_template = "edition-3/page.html"
|
||||
+++
|
||||
1158
blog/sass/css/edition-3/main.scss
Normal file
1158
blog/sass/css/edition-3/main.scss
Normal file
File diff suppressed because it is too large
Load Diff
99
blog/static/js/edition-3/main.js
Normal file
99
blog/static/js/edition-3/main.js
Normal file
@@ -0,0 +1,99 @@
|
||||
window.onload = function () {
|
||||
let container = document.querySelector('#toc-aside');
|
||||
if (container != null) {
|
||||
resize_toc(container);
|
||||
toc_scroll_position(container);
|
||||
window.onscroll = function () { toc_scroll_position(container) };
|
||||
}
|
||||
|
||||
let theme = localStorage.getItem("theme");
|
||||
if (theme != null) {
|
||||
setTimeout(() => {
|
||||
set_giscus_theme(theme)
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function resize_toc(container) {
|
||||
let containerHeight = container.clientHeight;
|
||||
|
||||
let resize = function () {
|
||||
if (containerHeight > document.documentElement.clientHeight - 100) {
|
||||
container.classList.add('coarse');
|
||||
} else {
|
||||
container.classList.remove('coarse');
|
||||
}
|
||||
};
|
||||
resize();
|
||||
|
||||
let resizeId;
|
||||
window.onresize = function () {
|
||||
clearTimeout(resizeId);
|
||||
resizeId = setTimeout(resize, 300);
|
||||
};
|
||||
}
|
||||
|
||||
function toc_scroll_position(container) {
|
||||
if (container.offsetParent === null) {
|
||||
// skip computation if ToC is not visible
|
||||
return;
|
||||
}
|
||||
|
||||
// remove active class for all items
|
||||
for (item of container.querySelectorAll("li")) {
|
||||
item.classList.remove("active");
|
||||
}
|
||||
|
||||
// look for active item
|
||||
let site_offset = document.documentElement.scrollTop;
|
||||
let current_toc_item = null;
|
||||
for (item of container.querySelectorAll("li")) {
|
||||
if (item.offsetParent === null) {
|
||||
// skip items that are not visible
|
||||
continue;
|
||||
}
|
||||
let anchor = item.firstElementChild.getAttribute("href");
|
||||
let heading = document.querySelector(anchor);
|
||||
if (heading.offsetTop <= (site_offset + document.documentElement.clientHeight / 3)) {
|
||||
current_toc_item = item;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set active class for current ToC item
|
||||
if (current_toc_item != null) {
|
||||
current_toc_item.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_lights() {
|
||||
if (document.documentElement.getAttribute("data-theme") === "dark") {
|
||||
set_theme("light")
|
||||
} else if (document.documentElement.getAttribute("data-theme") === "light") {
|
||||
set_theme("dark")
|
||||
} else {
|
||||
set_theme(window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark")
|
||||
}
|
||||
}
|
||||
|
||||
function set_theme(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme)
|
||||
set_giscus_theme(theme)
|
||||
localStorage.setItem("theme", theme)
|
||||
}
|
||||
|
||||
function clear_theme_override() {
|
||||
document.documentElement.removeAttribute("data-theme");
|
||||
set_giscus_theme("preferred_color_scheme")
|
||||
localStorage.removeItem("theme")
|
||||
}
|
||||
|
||||
function set_giscus_theme(theme) {
|
||||
let comment_form = document.querySelector("iframe.giscus-frame");
|
||||
if (comment_form != null) {
|
||||
comment_form.contentWindow.postMessage({
|
||||
giscus: { setConfig: { theme: theme } }
|
||||
}, "https://giscus.app")
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
<script async src="/js/edition-2/main.js"></script>
|
||||
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
<title>{% block title %}{% endblock title %} (Second Edition)</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -34,7 +34,7 @@
|
||||
<header class="masthead">
|
||||
<div style="position:relative">
|
||||
<h2 class="masthead-title">
|
||||
<a href="{{ config.base_url | safe }}" title="Home">{{ config.title | safe }}</a>
|
||||
<a href="{{ config.base_url | safe }}" title="Home">{{ config.title | safe }} (Second Edition)</a>
|
||||
</h2>
|
||||
<p><small>{{ config.extra.subtitle | replace(from=" ", to=" ") | safe }}</small></p>
|
||||
{% block header %}{% endblock header %}
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
|
||||
{% block after_main %}
|
||||
<aside class="page-aside-right">
|
||||
<div class="block" id="language-selector">
|
||||
{% if section.translations -%}
|
||||
<div class="block" id="language-selector">
|
||||
<h2>Other Languages</h2>
|
||||
{% set translations = section.translations | group_by(attribute="lang") %}
|
||||
@@ -80,6 +82,8 @@
|
||||
{%- endif -%}
|
||||
{% endfor %}</ul>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2>Recent Updates</h2>
|
||||
{% include "auto/recent-updates.html" %}
|
||||
|
||||
@@ -42,3 +42,4 @@
|
||||
</ul>
|
||||
</details>
|
||||
{% endmacro toc %}
|
||||
|
||||
|
||||
25
blog/templates/edition-2/news-page.html
Normal file
25
blog/templates/edition-2/news-page.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "edition-2/base.html" %}
|
||||
|
||||
{% import "snippets.html" as snippets %}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ config.title }}{% endblock title %}
|
||||
|
||||
{% block main %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
<time datetime="{{ page.date | date(format="%Y-%m-%d") }}" class="post-date">
|
||||
{{ page.date | date(format="%b %d, %Y") }}
|
||||
{% if page.extra.updated %} (updated on {{ page.extra.updated | date(format="%b %d, %Y") }}) {% endif %}
|
||||
</time>
|
||||
{{ page.content | safe }}
|
||||
{% endblock main %}
|
||||
|
||||
{% block after_main %}
|
||||
<hr>
|
||||
<section>
|
||||
<h2 id="comments">Comments</h2>
|
||||
{{ snippets::giscus(search_term=page.title ~ " (News Post)", lang=page.lang) }}
|
||||
</section>
|
||||
|
||||
{% endblock after_main %}
|
||||
|
||||
|
||||
14
blog/templates/edition-3/base.html
Normal file
14
blog/templates/edition-3/base.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "edition-3/foundation.html" %}
|
||||
|
||||
{% block masthead %}
|
||||
<header class="masthead">
|
||||
<div style="position:relative">
|
||||
<h2 class="masthead-title">
|
||||
<a href="{{ config.base_url | safe }}/edition-3" title="Home">{{ config.title | safe }}</a>
|
||||
</h2>
|
||||
<p><small>{{ config.extra.subtitle | replace(from=" ", to=" ") | safe }}
|
||||
— Third Edition (Alpha Release)</small></p>
|
||||
{% block header %}{% endblock header %}
|
||||
</div>
|
||||
</header>
|
||||
{% endblock masthead %}
|
||||
22
blog/templates/edition-3/extra.html
Normal file
22
blog/templates/edition-3/extra.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "edition-3/base.html" %}
|
||||
|
||||
{% import "snippets.html" as snippets %}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ config.title }}{% endblock title %}
|
||||
|
||||
{% block description -%}
|
||||
{{ page.summary | safe | striptags | truncate(length=150) }}
|
||||
{%- endblock description %}
|
||||
|
||||
{% block main %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ page.content | safe }}
|
||||
{% endblock main %}
|
||||
|
||||
{% block after_main %}
|
||||
<hr>
|
||||
<section>
|
||||
<h2 id="comments">Comments</h2>
|
||||
{{ snippets::giscus(search_term=page.title ~ " (Extra Post)", lang=page.lang) }}
|
||||
</section>
|
||||
{% endblock after_main %}
|
||||
60
blog/templates/edition-3/foundation.html
Normal file
60
blog/templates/edition-3/foundation.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="{% if lang %}{{ lang }}{% else %}{{ config.default_language }}{% endif %}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}">
|
||||
<meta name="author" content="{{ config.extra.author.name }}">
|
||||
|
||||
{% if current_url %}
|
||||
<link rel="canonical" href="{{ current_url | safe }}" />
|
||||
{% endif %}
|
||||
<link href="/css/edition-3/main.css" rel="stylesheet">
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS feed for os.phil-opp.com"
|
||||
href="{{ config.base_url | safe }}/rss.xml" />
|
||||
|
||||
<script>
|
||||
let theme = localStorage.getItem("theme");
|
||||
if (theme != null) {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script async src="/js/edition-3/main.js"></script>
|
||||
|
||||
<title>{% block title %}{% endblock title %} (Third Edition - Alpha)</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container content">
|
||||
{% block masthead %}{% endblock masthead %}
|
||||
|
||||
<div>
|
||||
{% block toc_aside %}{% endblock toc_aside %}
|
||||
<main>{% block main %}{% endblock main %}</main>
|
||||
</div>
|
||||
|
||||
<div>{% block after_main %}{% endblock after_main %}</div>
|
||||
|
||||
<footer class="footer">
|
||||
<hr>
|
||||
<div class="theme-switch">
|
||||
<div class="light-switch" onclick="toggle_lights()" title="Switch between light and dark theme"></div>
|
||||
<div class="light-switch-reset" onclick="clear_theme_override()"
|
||||
title="Clear the theme override and go back to the system theme"></div>
|
||||
</div>
|
||||
<small>
|
||||
© <time datetime="2022">2022</time>. All rights reserved.
|
||||
<a class="spaced" href="https://github.com/phil-opp/blog_os#license">License</a>
|
||||
<a class="spaced" href="{{ get_url(path=" @/pages/contact.md") | safe }}">Contact</a>
|
||||
</small>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
175
blog/templates/edition-3/index.html
Normal file
175
blog/templates/edition-3/index.html
Normal file
@@ -0,0 +1,175 @@
|
||||
{% extends "edition-3/foundation.html" %}
|
||||
|
||||
{% import "edition-3/macros.html" as macros %}
|
||||
{% import "snippets.html" as snippets %}
|
||||
|
||||
{% block title %}{{ config.title }}{% endblock title %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
{% set posts_section = get_section(path = "edition-3/posts/_index.md") %}
|
||||
{% set posts = posts_section.pages %}
|
||||
|
||||
{{ section.content
|
||||
| replace(from="<!-- latest-post -->", to=macros::latest_post(posts=posts))
|
||||
| replace(from="<!-- alpha-warning -->", to=macros::alpha_warning())
|
||||
| safe
|
||||
}}
|
||||
|
||||
{%- set chapter = "none" -%}
|
||||
{%- for post in posts -%}
|
||||
{%- if post.extra["chapter"] != chapter -%}
|
||||
{%- if not loop.first -%}
|
||||
</ul>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
|
||||
{# Begin new chapter #}
|
||||
{%- set_global chapter = post.extra["chapter"] -%}
|
||||
|
||||
{% set chapter_slug = chapter | slugify %}
|
||||
<div class="posts {{chapter_slug}}">
|
||||
{% set chapter_section = get_section(path = "edition-3/chapters/" ~ chapter_slug ~ "/_index.md" ) %}
|
||||
<h2>{{ chapter_section.title }}</h2>
|
||||
<div class="chapter-introduction">
|
||||
{{ chapter_section.content | safe }}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{%- endif -%}
|
||||
|
||||
<li>{{ macros::post_link(page=post) }}</li>
|
||||
|
||||
{% if loop.last %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="after-posts">
|
||||
<h2>Subscribe</h2>
|
||||
<p>Receive notifications about new posts and other major changes! You can either:</p>
|
||||
|
||||
<ul>
|
||||
<li>Subscribe to our <a href="/rss.xml">RSS/Atom Feed</a>,</li>
|
||||
<li>Subscribe to <a href="https://github.com/phil-opp/blog_os/issues/479">this GitHub issue</a>, or</li>
|
||||
<li>Subscribe to our <a href="https://tinyletter.com/phil-opp/">email newsletter</a>.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="frontpage-section">
|
||||
<h2>Status Updates</h2>
|
||||
{% set status_updates = get_section(path = "status-update/_index.md") %}
|
||||
<p>{{ status_updates.description }}</p>
|
||||
<ul>
|
||||
{% include "auto/status-updates-truncated.html" %}
|
||||
<li><a href="{{ get_url(path=" @/status-update/_index.md") | safe }}"><em>view all »</em></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="frontpage-section">
|
||||
<h2>Previous Editions</h2>
|
||||
<p>You are currently viewing the third edition of “Writing an OS in Rust”. In case you are interested in the older
|
||||
editions, you can still find them here:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><strong><a href="{{ get_url(path=" @/edition-2/_index.md")}}">Second Edition:</a></strong> The second
|
||||
edition is based on older version of the <code>bootloader</code> crate, which uses the hardware-provided
|
||||
VGA text buffer instead of a pixel-based framebuffer for screen output. Instead of the APIC, the legacy
|
||||
PIC is used for implementing hardware interrupts. The second edition only works on BIOS-based systems,
|
||||
not on the newer UEFI standard. <a class="read-more" href="{{ get_url(path = " edition-2") | safe
|
||||
}}"><em>read the second edition »</em></a></p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong><a href="{{ get_url(path=" @/edition-1/_index.md")}}">First Edition:</a></strong> The first
|
||||
edition was already started in 2015. It is very different in many aspects, for example it builds upon
|
||||
the GRUB bootloader instead of using the `bootloader` crate. This means that it requires you to manually
|
||||
write some assembly code and do an elaborate remap of the kernel's virtual pages in order to improve
|
||||
safety.<a class="read-more" href="{{ get_url(path = " edition-1") | safe
|
||||
}}"><em>read the first edition »</em></a></p>
|
||||
</li>
|
||||
</ul>
|
||||
<p><em>Note that the older editions are no longer updated and might no longer work or contain outdated
|
||||
information.</em></p>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<h2>Support Me</h2>
|
||||
{{ snippets::support() }}
|
||||
</div>
|
||||
{% endblock main %}
|
||||
|
||||
{% block after_main %}
|
||||
<aside class="page-aside-right">
|
||||
{% if section.translations | length > 1 %}
|
||||
<div class="block" id="language-selector">
|
||||
<h2>Other Languages</h2>
|
||||
{% set translations = section.translations | group_by(attribute="lang") %}
|
||||
<ul>{%- for lang_code in config.extra.languages -%}
|
||||
{%- if translations[lang_code] and lang_code != lang -%}
|
||||
{%- set translation = translations[lang_code].0 -%}
|
||||
<li data-lang-switch-to="{{ translation.lang }}" class=""><a href="{{ translation.permalink | safe }}">
|
||||
{{ trans(key="lang_name", lang = translation.lang) }}
|
||||
</a></li>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="block">
|
||||
<h2>Recent Updates</h2>
|
||||
{% include "auto/recent-updates.html" %}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2>Repository</h2>
|
||||
<div class="gh-repo-box">
|
||||
<div>
|
||||
<svg viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4 9H3V8h1v1zm0-3H3v1h1V6zm0-2H3v1h1V4zm0-2H3v1h1V2zm8-1v12c0 .55-.45 1-1 1H6v2l-1.5-1.5L3 16v-2H1c-.55 0-1-.45-1-1V1c0-.55.45-1 1-1h10c.55 0 1 .45 1 1zm-1 10H1v2h2v-1h3v1h5v-2zm0-10H2v9h9V1z">
|
||||
</path>
|
||||
</svg>
|
||||
<a href="https://github.com/phil-opp/blog_os" class="repo-link">
|
||||
<span title="blog_os">phil-opp/blog_os</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="subtitle">
|
||||
Writing an OS in Rust
|
||||
</p>
|
||||
|
||||
<p class="stars-forks">
|
||||
<a href="https://github.com/phil-opp/blog_os/stargazers" class="stars">
|
||||
<svg aria-label="stars" viewBox="0 0 14 16" version="1.1" width="14" height="16" role="img">
|
||||
<path fill-rule="evenodd"
|
||||
d="M14 6l-4.9-.64L7 1 4.9 5.36 0 6l3.6 3.26L2.67 14 7 11.67 11.33 14l-.93-4.74L14 6z">
|
||||
</path>
|
||||
</svg>
|
||||
{% include "auto/stars.html" %}
|
||||
</a>
|
||||
<a href="https://github.com/phil-opp/blog_os/network/members" class="forks">
|
||||
<svg aria-label="forks" viewBox="0 0 10 16" version="1.1" width="10" height="16" role="img">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8 1a1.993 1.993 0 0 0-1 3.72V6L5 8 3 6V4.72A1.993 1.993 0 0 0 2 1a1.993 1.993 0 0 0-1 3.72V6.5l3 3v1.78A1.993 1.993 0 0 0 5 15a1.993 1.993 0 0 0 1-3.72V9.5l3-3V4.72A1.993 1.993 0 0 0 8 1zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zm3 10c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zm3-10c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2z">
|
||||
</path>
|
||||
</svg>
|
||||
{% include "auto/forks.html" %}
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/sponsors/phil-opp" class="sponsor">
|
||||
<svg viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true">
|
||||
<path fill-rule="evenodd"
|
||||
d="M9 2c-.97 0-1.69.42-2.2 1-.51.58-.78.92-.8 1-.02-.08-.28-.42-.8-1-.52-.58-1.17-1-2.2-1-1.632.086-2.954 1.333-3 3 0 .52.09 1.52.67 2.67C1.25 8.82 3.01 10.61 6 13c2.98-2.39 4.77-4.17 5.34-5.33C11.91 6.51 12 5.5 12 5c-.047-1.69-1.342-2.913-3-3z">
|
||||
</path>
|
||||
</svg>
|
||||
Sponsor
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{% endblock after_main %}
|
||||
89
blog/templates/edition-3/macros.html
Normal file
89
blog/templates/edition-3/macros.html
Normal file
@@ -0,0 +1,89 @@
|
||||
{% macro latest_post(posts) %}
|
||||
{% set post = posts|last %}
|
||||
{% if post %}
|
||||
<strong><a href="{{ post.path | safe }}">{{ post.title }}</a></strong>
|
||||
{% else %}
|
||||
<em>none yet, stay tuned!</em>
|
||||
{% endif %}
|
||||
{% endmacro latest_post %}
|
||||
|
||||
{% macro post_link(page) %}
|
||||
<div>
|
||||
{% set translations = page.translations | filter(attribute="lang", value=lang) -%}
|
||||
{%- if translations -%}
|
||||
{%- set post = get_page(path = translations.0.path) -%}
|
||||
{%- else -%}
|
||||
{%- set post = page -%}
|
||||
{%- set not_translated = true -%}
|
||||
{%- endif -%}
|
||||
<h3 class="post-list-title"><a href="{{ post.path | safe }}">{{ post.title }}</a></h3>
|
||||
<span class="post-list-icon">
|
||||
{%- if post.extra.icon -%}{{post.extra.icon | safe}}{%- endif -%}
|
||||
</span>
|
||||
<div class="post-summary">
|
||||
{{ post.summary | safe }}
|
||||
<a class="read-more" href="{{ post.path | safe }}"><em>read more »</em></a>
|
||||
|
||||
{% if page.extra.extra_content %}
|
||||
<aside class="post-extra-content">
|
||||
<h4>Extra Content:</h4>
|
||||
{% for name in page.extra.extra_content %}
|
||||
{% set path = page.relative_path | split(pat="/") | slice(end=-1) | concat(with=name) | join(sep="/") %}
|
||||
|
||||
{% set extra_page = get_page(path = path) %}
|
||||
|
||||
<span class="post-list-extra-post-icon">
|
||||
{%- if extra_page.extra.icon -%}{{extra_page.extra.icon | safe}}{%- endif -%}
|
||||
</span>
|
||||
<a href="{{ extra_page.path | safe }}">{{ extra_page.title }}</a>{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
{%- if lang and not_translated and lang != config.default_language -%}
|
||||
<aside class="no-translation">
|
||||
(This post is not translated yet.)
|
||||
</aside>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro post_link %}
|
||||
|
||||
{% macro toc(toc) %}
|
||||
<details id="toc-inline">
|
||||
<summary><b>Table of Contents</b></summary>
|
||||
<ul>
|
||||
{% for h2 in toc %}<li>
|
||||
<a href="#{{h2.id | safe}}">{{ h2.title | safe }}</a>
|
||||
{% if h2.children %}<ul>
|
||||
{% for h3 in h2.children %}<li>
|
||||
<a href="#{{h3.id | safe}}">{{ h3.title | safe }}</a>
|
||||
</li>{% endfor %}
|
||||
</ul>{% endif %}
|
||||
</li>{% endfor %}
|
||||
<li class="toc-comments-link"><a href="#comments">Comments</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<div class="theme-switch-inline">
|
||||
Switch between light and dark mode:
|
||||
<span class="switches">
|
||||
<span class="light-switch" onclick="toggle_lights()" title="Switch between light and dark theme"></span>
|
||||
<span class="light-switch-reset" onclick="clear_theme_override()"
|
||||
title="Clear the theme override and go back to the system theme"></span>
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro toc %}
|
||||
|
||||
{% macro alpha_warning() %}
|
||||
<div class="warning">
|
||||
<p>
|
||||
This is an <strong>early preview</strong> of the upcoming <em>third edition</em> of this guide. The edition is
|
||||
still in alpha state, so things might be still in progress, not work, or change without warning!
|
||||
</p>
|
||||
<p>
|
||||
For a more stable experience, check out the current <a href="{{ get_url(path = " @/edition-2/_index.md") | safe
|
||||
}}"><strong>Second Edition</strong></a>.
|
||||
</p>
|
||||
</div>
|
||||
{% endmacro alpha_warning %}
|
||||
157
blog/templates/edition-3/page.html
Normal file
157
blog/templates/edition-3/page.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "edition-3/base.html" %}
|
||||
|
||||
{% import "edition-3/macros.html" as macros %}
|
||||
{% import "snippets.html" as snippets %}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ config.title }}{% endblock title %}
|
||||
{% block header %}
|
||||
{% if lang != "en" -%}
|
||||
<aside id="all-posts-link"><a href="{{ get_url(path=" @/edition-3/_index.md") }}/{{ lang }}" title="All Posts">{{
|
||||
trans(key="all_posts", lang=lang) }}</a></aside>
|
||||
{%- else -%}
|
||||
<aside id="all-posts-link"><a href="{{ get_url(path=" @/edition-3/_index.md") }}" title="All Posts">{{
|
||||
trans(key="all_posts", lang=lang) }}</a></aside>
|
||||
{%- endif %}
|
||||
{% endblock header %}
|
||||
|
||||
{% block description -%}
|
||||
{{ page.summary | safe | striptags | truncate(length=150) }}
|
||||
{%- endblock description %}
|
||||
|
||||
{% block toc_aside %}
|
||||
<aside id="toc-aside" class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
<h2>{{ trans(key="toc", lang=lang) }}</h2>
|
||||
<ol>
|
||||
{% for h2 in page.toc %}<li>
|
||||
<a href="#{{h2.id | safe}}">{{ h2.title | safe }}</a>
|
||||
{% if h2.children %}<ol>
|
||||
{% for h3 in h2.children %}<li>
|
||||
<a href="#{{h3.id | safe}}">{{ h3.title | safe }}</a>
|
||||
</li>{% endfor %}
|
||||
</ol>{% endif %}
|
||||
</li>{% endfor %}
|
||||
<li class="toc-comments-link"><a href="#comments">{{ trans(key="comments", lang=lang) }}</a></li>
|
||||
</ol>
|
||||
</aside>
|
||||
{% endblock toc_aside %}
|
||||
|
||||
{% block main %}
|
||||
<div class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
<div class="post-title">
|
||||
<h1>{{ page.title }}</h1><span class="post-icon">
|
||||
{%- if page.extra.icon -%}{{page.extra.icon | safe}}{%- endif -%}
|
||||
</span>
|
||||
</div>
|
||||
<time datetime="{{ page.date | date(format=" %Y-%m-%d") }}" class="post-date">
|
||||
{{ page.date | date(format="%b %d, %Y") }}
|
||||
{% if page.extra.updated %} (updated on {{ page.extra.updated | date(format="%b %d, %Y") }}) {% endif %}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
{{ macros::alpha_warning() }}
|
||||
|
||||
{% if page.extra.warning %}
|
||||
<div class="warning">
|
||||
{% if page.extra.warning_short %} <b>{{ page.extra.warning_short }}</b> {% endif %}
|
||||
{{ page.extra.warning | markdown(inline=true) | safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- if page.lang != "en" %}
|
||||
<div class="warning{% if page.extra.rtl %} right-to-left{% endif %}">
|
||||
{% set translations = page.translations | filter(attribute="lang", value="en") %}
|
||||
{% set original = translations.0 %}
|
||||
<p>
|
||||
<b>{{ trans(key="translated_content", lang=lang) }}</b>
|
||||
{{ trans(key="translated_content_notice", lang=lang) |
|
||||
replace(from="_original.permalink_", to=original.permalink) |
|
||||
replace(from="_original.title_", to=original.title) | safe }}
|
||||
</p>
|
||||
{%- if page.extra.translators %}
|
||||
<p>
|
||||
{{ trans(key="translated_by", lang=lang) }} {% for user in page.extra.translators -%}
|
||||
{%- if not loop.first -%}
|
||||
{%- if loop.last %} {{ trans(key="word_separator", lang=lang) }} {% else %}, {% endif -%}
|
||||
{%- endif -%}
|
||||
<a href="https://github.com/{{user}}">@{{user}}</a>
|
||||
{%- endfor %}.
|
||||
|
||||
{%- if page.extra.translation_contributors %}
|
||||
<span class="translation_contributors">
|
||||
{{ trans(key="translation_contributors", lang=lang) }} {% for user in page.extra.translation_contributors
|
||||
-%}
|
||||
{%- if not loop.first -%}
|
||||
{%- if loop.last %} {{ trans(key="word_separator", lang=lang) }} {% else %}, {% endif -%}
|
||||
{%- endif -%}
|
||||
<a href="https://github.com/{{user}}">@{{user}}</a>
|
||||
{%- endfor %}.
|
||||
</span>
|
||||
{% endif -%}
|
||||
</p>
|
||||
{% endif -%}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
{{ page.content | replace(from="<!-- toc -->", to=macros::toc(toc=page.toc)) | safe }}
|
||||
</div>
|
||||
|
||||
<div class="post-footer-support{% if page.extra.rtl %} right-to-left{% endif %}">
|
||||
<h2>Support Me</h2>
|
||||
{{ snippets::support() }}
|
||||
</div>
|
||||
|
||||
{% if not page.extra.hide_next_prev %}
|
||||
<hr>
|
||||
<div class="PageNavigation">
|
||||
{% if page.lower %}
|
||||
<a class="prev" href="{{ page.lower.path | safe }}">« {{ page.lower.title }}</a>
|
||||
{% endif %}
|
||||
{% if page.higher %}
|
||||
<a class="next" href="{{ page.higher.path | safe }}">{{ page.higher.title }} »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<section>
|
||||
<h2 id="comments" class="{% if page.extra.rtl %}right-to-left{% endif %}">{{ trans(key="comments", lang=lang) }}
|
||||
</h2>
|
||||
|
||||
{% if page.extra.comments_search_term %}
|
||||
{% set search_term=page.extra.comments_search_term %}
|
||||
{% elif page.lang != "en" %}
|
||||
{% set translations = page.translations | filter(attribute="lang", value="en") %}
|
||||
{% set original = translations.0 %}
|
||||
{% set search_term=original.title ~ " (" ~ page.lang ~ ")" %}
|
||||
{% else %}
|
||||
{% set search_term=page.title %}
|
||||
{% endif %}
|
||||
{{ snippets::giscus(search_term=search_term, lang=page.lang) }}
|
||||
|
||||
{%- if page.lang != "en" %}
|
||||
<p class="{% if page.extra.rtl %}right-to-left{% endif %}">
|
||||
{{ trans(key="comments_notice", lang=lang) }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<aside class="page-aside-right">
|
||||
{% if page.translations | length > 1-%}
|
||||
<div class="block" id="language-selector">
|
||||
<h2>Other Languages</h2>
|
||||
{% set translations = page.translations | group_by(attribute="lang") %}
|
||||
<ul>{%- for lang_code in config.extra.languages -%}{%- if translations[lang_code] -%}
|
||||
{%- set translation = translations[lang_code] | first -%}
|
||||
{%- if translation and lang_code != lang -%}
|
||||
<li data-lang-switch-to="{{ translation.lang }}" class=""><a href="{{ translation.permalink | safe }}">
|
||||
{{ trans(key="lang_name", lang = translation.lang) }}
|
||||
</a></li>
|
||||
{%- endif -%}
|
||||
{%- endif -%}{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</aside>
|
||||
|
||||
{% endblock main %}
|
||||
1
blog/templates/edition-3/raw.html
Normal file
1
blog/templates/edition-3/raw.html
Normal file
@@ -0,0 +1 @@
|
||||
{{ page.content | safe }}
|
||||
@@ -1,25 +1 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% import "snippets.html" as snippets %}
|
||||
|
||||
{% block title %}{{ page.title }} | {{ config.title }}{% endblock title %}
|
||||
|
||||
{% block main %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
<time datetime="{{ page.date | date(format="%Y-%m-%d") }}" class="post-date">
|
||||
{{ page.date | date(format="%b %d, %Y") }}
|
||||
{% if page.extra.updated %} (updated on {{ page.extra.updated | date(format="%b %d, %Y") }}) {% endif %}
|
||||
</time>
|
||||
{{ page.content | safe }}
|
||||
{% endblock main %}
|
||||
|
||||
{% block after_main %}
|
||||
<hr>
|
||||
<section>
|
||||
<h2 id="comments">Comments</h2>
|
||||
{{ snippets::giscus(search_term=page.title ~ " (News Post)", lang=page.lang) }}
|
||||
</section>
|
||||
|
||||
{% endblock after_main %}
|
||||
|
||||
|
||||
{% extends "edition-2/news-page.html" %}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="canonical" href="{{ config.base_url | safe }}" />
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="refresh" content="0;url={{ config.base_url | safe }}" />
|
||||
</head>
|
||||
|
||||
</html>
|
||||
@@ -3,10 +3,7 @@
|
||||
Creating and <a href="{{ get_url(path="@/status-update/_index.md") }}">maintaining</a> this blog and the associated libraries is a lot of work, but I really enjoy doing it. By supporting me, you allow me to invest more time in new content, new features, and continuous maintenance.
|
||||
</p>
|
||||
<p>
|
||||
The best way to support me is to <a href="https://github.com/sponsors/phil-opp"><em>sponsor me on GitHub</em></a>, since they don't charge any fees. If you prefer other platforms, I also have <a href="https://www.patreon.com/phil_opp"><em>Patreon</em></a> and <a href="https://donorbox.org/phil-opp"><em>Donorbox</em></a> accounts. The latter is the most flexible as it supports multiple currencies and one-time contributions.
|
||||
</p>
|
||||
<p>
|
||||
Thank you!
|
||||
The best way to support me is to <a href="https://github.com/sponsors/phil-opp"><em>sponsor me on GitHub</em></a>. Thank you!
|
||||
</p>
|
||||
{% endmacro support %}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
<div>
|
||||
<h2>Thank You!</h2>
|
||||
<p>Thanks a lot to all the contributors this month!</p>
|
||||
<p>I also want to thank all the people who support me on <a href="https://github.com/sponsors/phil-opp">GitHub</a>, <a href="https://www.patreon.com/phil_opp">Patreon</a>, and <a href="https://donorbox.org/phil-opp">Donorbox</a>. It means a lot to me!</p>
|
||||
<p>I also want to thank all the people who support me on <a href="https://github.com/sponsors/phil-opp">GitHub</a>,
|
||||
<a href="https://www.patreon.com/phil_opp">Patreon</a>, and <a
|
||||
href="https://donorbox.org/phil-opp">Donorbox</a>. It means a lot to me!
|
||||
</p>
|
||||
</div>
|
||||
{% endblock main %}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
<p>{{ section.description }}</p>
|
||||
{% endblock introduction %}
|
||||
|
||||
<div class="status-update-list"><ul>
|
||||
<div class="status-update-list">
|
||||
<ul>
|
||||
{% include "auto/status-updates.html" %}
|
||||
{% for page in section.pages %}
|
||||
<li><b><a href="{{ page.path | safe }}">{{ page.title }}</a></b></li>
|
||||
{% endfor %}
|
||||
</ul></div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock main %}
|
||||
|
||||
Reference in New Issue
Block a user