Compare commits

...

12 Commits

Author SHA1 Message Date
Philipp Oppermann
c81a2e11db Improve first half of post 2023-03-25 20:24:02 +01:00
Philipp Oppermann
3ff7f48257 Highlight changed lines in code examples + other improvements 2023-03-25 19:10:59 +01:00
Philipp Oppermann
dba81f6d1c Rewrite introduction 2023-03-25 18:30:19 +01:00
Philipp Oppermann
ae6f001eae Use new branch naming scheme for edition-3 posts 2023-03-25 17:36:33 +01:00
Philipp Oppermann
5c00dbcb60 Merge branch 'main' into edition-3 2023-03-25 16:28:01 +01:00
Philipp Oppermann
79be18bfb5 Merge edition-3 changes of PR #1187 2023-03-25 16:07:21 +01:00
Philipp Oppermann
dd4e872f82 Remove outdated paragraph with dead link in first edition 2023-03-25 15:49:47 +01:00
Philipp Oppermann
d8ad83528d Merge pull request #1190 from somebodyLi/main
fix(edition-2-posts-01-zh-CN): translate issue
2023-03-09 18:08:14 +01:00
Jie Wei
7c1b57a663 [Translation][Chinese] post-09 (edition-2) (#1189) 2023-02-26 12:29:42 +01:00
Ritchie
559b2a195d fix(edition-2-posts-01-zh-CN): translate issue 2023-02-16 19:04:49 +08:00
Philipp Oppermann
e56c635c13 Merge pull request #1188 from phil-opp/fix-broken-links
Fix broken links
2023-01-26 10:30:14 +01:00
Kenny Strawn
6cf6e32ee9 Fix broken links 2023-01-26 10:14:21 +01:00
14 changed files with 1218 additions and 297 deletions

View File

@@ -422,7 +422,7 @@ extern "C" fn divide_by_zero_handler() -> ! {
```
We register a single handler function for a [divide by zero error] \(index 0). Like the name says, this exception occurs when dividing a number by 0. Thus we have an easy way to test our new exception handler.
[divide by zero error]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[divide by zero error]: https://wiki.osdev.org/Exceptions#Division_Error
However, it doesn't work this way:

View File

@@ -31,8 +31,6 @@ nightly
[rustup]: https://www.rustup.rs/
The code from this post (and all following) is [automatically tested](https://travis-ci.org/phil-opp/blog_os) every day and should always work for the newest nightly. If it doesn't, please [file an issue](https://github.com/phil-opp/blog_os/issues).
## Creating a Cargo project
[Cargo] is Rust's excellent package manager. Normally you would call `cargo new` when you want to create a new project folder. We can't use it because our folder already exists, so we need to do it manually. Fortunately we only need to add a cargo configuration file named `Cargo.toml`:

View File

@@ -128,7 +128,7 @@ First Exception | Second Exception
[Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

View File

@@ -236,7 +236,7 @@ rustup target add thumbv7em-none-eabihf
cargo build --target thumbv7em-none-eabihf
```
我们传递了 `--target` 参数,来为裸机目标系统**交叉编译**[cross compile](https://en.wikipedia.org/wiki/Cross_compiler))我们的程序。我们的目标并不包括操作系统,所以链接器不会试着链接 C 语言运行环境,因此构建过程成功完成,不会产生链接器错误。
我们传递了 `--target` 参数,来为裸机目标系统**交叉编译**[cross compile](https://en.wikipedia.org/wiki/Cross_compiler))我们的程序。我们的目标并不包括操作系统,所以链接器不会试着链接 C 语言运行环境,因此构建过程成功完成,不会产生链接器错误。
我们将使用这个方法编写自己的操作系统内核。我们不会编译到 `thumbv7em-none-eabihf`,而是使用描述 `x86_64` 环境的**自定义目标**[custom target](https://doc.rust-lang.org/rustc/targets/custom.html))。在下一篇文章中,我们将详细描述一些相关的细节。
@@ -424,7 +424,7 @@ use core::panic::PanicInfo;
#[no_mangle] // 不重整函数名
pub extern "C" fn _start() -> ! {
// 因为编译器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
// 因为链接器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
// 默认命名为 `_start`
loop {}
}

View File

@@ -890,7 +890,7 @@ fn test_println() {
標準ライブラリのテストフレームワークは、[`#[should_panic]`属性][should_panic]をサポートしています。これを使うと、失敗しなければならないテストを作ることができます。これは、例えば、関数が無効な引数を渡されたときに失敗することを確かめる場合などに便利です。残念なことに、この機能は標準ライブラリのサポートを必要とするため、`#[no_std]`クレートではこの属性はサポートされていません。
[should_panic]: https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html#testing-panics
[should_panic]: https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html#パニックをテストする
`#[should_panic]`属性は使えませんが、パニックハンドラから成功のエラーコードで終了するような結合テストを作れば、似たような動きをさせることはできます。そのようなテストを`should_panic`という名前で作ってみましょう:

View File

@@ -138,7 +138,7 @@ extern "x86-interrupt" fn double_fault_handler(
[Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

View File

@@ -132,7 +132,7 @@ CPUはダブルフォルトハンドラを呼べるようになったので、
[ゼロ除算],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反] | [無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
[ページフォルト] | [ページフォルト],<br>[無効TSS],<br>[セグメント不在],<br>[スタックセグメントフォルト],<br>[一般保護違反]
[ゼロ除算]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[ゼロ除算]: https://wiki.osdev.org/Exceptions#Division_Error
[無効TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[セグメント不在]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[スタックセグメントフォルト]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

View File

@@ -135,7 +135,7 @@ _“예외 처리 함수를 호출하는 것에 실패했을 때”_ 라는 게
[Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

View File

@@ -129,7 +129,7 @@ First Exception | Second Exception
[Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault]
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

View File

@@ -137,7 +137,7 @@ extern "x86-interrupt" fn double_fault_handler(
| [Divide-by-zero],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] | [Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] |
| [Page Fault] | [Page Fault],<br>[Invalid TSS],<br>[Segment Not Present],<br>[Stack-Segment Fault],<br>[General Protection Fault] |
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error
[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS
[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present
[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault

File diff suppressed because it is too large Load Diff

View File

@@ -26,23 +26,26 @@ The result is a minimal operating system kernel that forms the base for the foll
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.
The complete source code for this post can be found in the [`post-3.1`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
[post branch]: https://github.com/phil-opp/blog_os/tree/post-3.1
<!-- toc -->
## Introduction
To write an operating system kernel, we need code that does not depend on any operating system features.
So 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.
Kernels are the heart of an operating system.
They provide all the fundamental building blocks that are required for building higher-level programs.
Typical building blocks are threads, files, heap memory, timers, or sockets.
Other important tasks of a kernel are the isolation of different programs and the multiplexing of resources.
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.
When writing an operating system kernel, we need to provide all of these building blocks ourselves.
This means that we can't use most of the [Rust standard library].
However, there are still a lot of Rust features that we _can_ use.
For example, we can use [iterators], [closures], [pattern matching], [`Option`] and [`Result`], [string formatting], and of course the [ownership system].
These features make it possible to write a kernel in a very expressive, high level way without worrying about [undefined behavior] or [memory safety].
These features make it possible to write a kernel in a very expressive, high level way and worry less about [undefined behavior] or [memory safety].
[`Option`]: https://doc.rust-lang.org/core/option/
[`Result`]: https://doc.rust-lang.org/core/result/
@@ -55,7 +58,7 @@ These features make it possible to write a kernel in a very expressive, high lev
[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.
In this post, we create a minimal OS kernel that can be run without an underlying operating system.
Such an executable is often called a “freestanding” or “bare-metal” executable.
We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel.
@@ -99,7 +102,7 @@ You can compile your crate through `cargo build` and then run the compiled `kern
Right now our crate implicitly links the standard library.
Let's try to disable this by adding the [`no_std` attribute]:
```rust
```rust,hl_lines=3
// main.rs
#![no_std]
@@ -109,7 +112,7 @@ fn main() {
}
```
When we try to build it now (by running `cargo build`), the following error occurs:
When we try to build it now (by running `cargo build`), the following errors occur:
```
error: cannot find macro `println!` in this scope
@@ -124,8 +127,7 @@ error: language item required, but not found: `eh_personality`
[...]
```
There are multiple errors.
The reason for the first one is that the [`println` macro] is part of the standard library, which we no longer include.
The reason for the first error is that the [`println` macro] is part of the standard library, which we no longer include.
So we can no longer print things.
This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system.
@@ -134,7 +136,7 @@ This makes sense, since `println` writes to [standard output], which is a specia
So let's remove the printing and try again with an empty main function:
```rust
```rust,hl_lines=5
// main.rs
#![no_std]
@@ -155,15 +157,19 @@ The `println` error is gone, but the compiler is still missing a `#[panic_handle
### 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:
The standard library provides its own panic handler function, but in a `no_std` environment we need to define one ourselves:
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
```rust
```rust,hl_lines=3 9-13
// in main.rs
use core::panic::PanicInfo;
#![no_std]
fn main() {}
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
@@ -219,7 +225,7 @@ There are multiple ways to set the panic strategy, the easiest is to use [cargo
[cargo profiles]: https://doc.rust-lang.org/cargo/reference/profiles.html
```toml
```toml,hl_lines=3-7
# in Cargo.toml
[profile.dev]
@@ -232,9 +238,7 @@ 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.
Now we fixed both of the above errors.
However, if we try to compile it now, another error occurs:
When we try to compile our kernel now, a new error occurs:
```
cargo build
@@ -281,13 +285,13 @@ You can see the target triple for your host system by running `rustc --version -
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
```
rustc 1.66.0 (69f9c33d7 2022-12-12)
rustc 1.68.1 (8460ca823 2023-03-20)
binary: rustc
commit-hash: 69f9c33d71c871fc16ac445211281c6e7a340943
commit-date: 2022-12-12
commit-hash: 8460ca823e8367a30dda430efda790588b8c84d3
commit-date: 2023-03-20
host: x86_64-unknown-linux-gnu
release: 1.66.0
LLVM version: 15.0.2
release: 1.68.1
LLVM version: 15.0.6
```
The above output is from a `x86_64` Linux system.
@@ -336,7 +340,9 @@ We still get the error about a missing `start` language item because we're still
To tell the Rust compiler that we don't want to use the normal entry point chain, we add the `#![no_main]` attribute.
```rust
```rust,hl_lines=4
// main.rs
#![no_std]
#![no_main]
@@ -353,7 +359,9 @@ 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
```rust,hl_lines=3-6
// in main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
@@ -398,7 +406,8 @@ cargo build --target x86_64-unknown-none --release
The compiled executable is placed at `target/x86_64-unknown-none/release/kernel` in this case.
In the next post we will cover how to turn this kernel into a bootable disk image that can be run in a virtual machine or on real hardware.
But before that, we will examine our executable to verify that it looks as expected.
In the rest of this post, we will introduce some tools for examining our kernel executable.
These tools are very useful for debugging future issues, so it's good to know about them.
## Useful Tools
@@ -408,8 +417,8 @@ In this section, we will examine our kernel executable using the [`objdump`], [`
[`nm`]: https://man7.org/linux/man-pages/man1/nm.1.html
[`size`]: https://man7.org/linux/man-pages/man1/size.1.html
If you're on a UNIX system, you might already have the `nm`, `objdump`, and `strip` tools installed.
Otherwise, you can use the LLVM binutils shipped by `rustup` through the [`cargo-binutils`] crate.
If you're on a UNIX system, you might already have the above tools installed.
Otherwise (and on Windows), you can use the LLVM binutils shipped by `rustup` through the [`cargo-binutils`] crate.
To install it, run **`cargo install cargo-binutils`** and **`rustup component add llvm-tools-preview`**.
Afterwards, you can run the tools through `rust-nm`, `rust-objdump`, and `rust-strip`.
@@ -422,26 +431,26 @@ To verify that it is properly exposed in the executable, we can run `nm` to list
```
rust-nm target/x86_64-unknown-none/debug/kernel
0000000000002218 d _DYNAMIC
0000000000001210 T _start
0000000000201120 T _start
```
If we comment out the `_start` function or if we remove the `#[no_mangle]` attribute, the `_start` symbol is no longer there after recompiling:
```
rust-nm target/x86_64-unknown-none/debug/kernel
0000000000002218 d _DYNAMIC
```
This way we can ensure that we set the `_start` function correctly.
### `objdump`
The `objdump` tool can inspect different parts of executables that use the [ELF file format].
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
[ELF file format]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
#### File Headers
Upon other things, the ELF [file header] specifies the target architecture and the entry point address of the executable files.
Among other things, the ELF [file header] specifies the target architecture and the entry point address of the executable files.
To print the file header, we can use `objdump -f`:
[file header]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
@@ -459,7 +468,7 @@ The start address specifies the memory address of our `_start` function.
Here the function name `_start` becomes important.
If we rename the function to something else (e.g., `_start_here`) and recompile, we see that no start address is set in the ELF file anymore:
```
```hl_lines=5
rust-objdump -f target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64

View File

@@ -29,12 +29,12 @@ We then show how this image can be started in the [QEMU] emulator and run on rea
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.
The complete source code for this post can be found in the [`post-3.2`][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
[post branch]: https://github.com/phil-opp/blog_os/tree/post-3.2
<!-- toc -->
@@ -47,8 +47,8 @@ Afterwards it looks for a bootable disk and starts booting the operating system
[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.
The BIOS standard is outdated and not standardized, but relatively simple and supported on almost any x86 machine since the 1980s.
UEFI, in contrast, is more modern and has much more features, but also more complex and only runs on fairly recent hardware (built since ~2012).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
@@ -116,8 +116,10 @@ Since it is not possible to do all that within the available 446 bytes, most boo
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.
-->
(If you are interested in building your own BIOS bootloader, you can look through the [BIOS source code] of the `bootloader` crate on GitHub, which is mostly written in Rust and has only about 50 lines of assembly code.)
[BIOS source code]: https://github.com/rust-osdev/bootloader/tree/main/bios
#### The Future of BIOS
@@ -152,9 +154,7 @@ Thus, malware should be prevented from compromising the early boot process.
#### 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/
The main issue for most people is the fear that the _secure boot_ mechanism could be used to lock users into a specific operating system (e.g. Windows) and thus prevent the installation of alternative operating systems.
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.
@@ -174,7 +174,7 @@ The UEFI boot process works in the following way:
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.
- If the firmware finds an EFI system partition, it looks for an executable file named `efi\boot\bootx64.efi` (on x86_64 systems).
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.
@@ -218,13 +218,13 @@ The reference implementation is [GNU GRUB], which is the most popular bootloader
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:
However, GRUB and the Multiboot standard have some issues 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.
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.
@@ -236,7 +236,7 @@ This makes development on Windows or Mac more difficult.
[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.
However, we might add Multiboot support to our [`bootloader`] crate at some point, 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
@@ -246,109 +246,89 @@ If you're interested in writing a Multiboot compliant kernel, check out the [fir
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.
The [`bootloader`] crate supports both BIOS and UEFI booting on `x86_64` 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
### The `bootloader_api` Crate
To use the `bootloader` crate, we first need to add a dependency on it:
In order to make our kernel compatible with the `bootloader` crate, we first need to add a dependency on the [`bootloader_api`] crate:
```toml
[`bootloader`]: https://docs.rs/bootloader/latest/bootloader/
[`bootloader_api`]: https://docs.rs/bootloader_api/latest/bootloader_api/
```toml,hl_lines=4
# in Cargo.toml
[dependencies]
bootloader = "0.10.1"
bootloader_api = "0.11.2"
```
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.)
Now we need to replace our custom `_start` entry point function with [`bootloader_api::entry_point`] macro. This macro instructs the compiler to create a special `.bootloader-config` section with encoded configuration options in the resulting executable, which is later read by the bootloader implementation.
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
[`bootloader_api::entry_point`]: https://docs.rs/bootloader_api/latest/bootloader_api/macro.entry_point.html
#### Receiving the Boot Information
We will take a closer look at the `entry_point` macro and the different configuration options later. For now, we just use the default setup:
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.
```rust,hl_lines=3 6-8
// in main.rs
The [`bootloader` documentation][`BootInfo`] specifies that a kernel entry point should have the following signature:
bootloader_api::entry_point!(kernel_main);
[`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) -> ! {
// ↓ this replaces the `_start` function ↓
fn kernel_main(bootinfo: &'static mut bootloader_api::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.
There are a few notable things:
After adjusting our entry point for the `bootloader` crate, we can now look into how to create a bootable disk image from our kernel.
- The `kernel_main` function is just a normal Rust function with an arbitrary name. No `#[no_mangle]` attribute is needed anymore since the `entry_point` macro handles this internally.
- Like before, our entry point function is [diverging], i.e. it must never return. We ensure this by looping endlessly.
- There is a new [`BootInfo`] argument, which the bootloader fills with various system information. We will use this argument later.
- The `entry_point` macro verifies that the `kernel_main` function has the correct arguments and return type, otherwise a compile error will occur. This is important because undefined behavior might occur when the function signature does not match the bootloader's expectations.
[diverging]: https://doc.rust-lang.org/rust-by-example/fn/diverging.html
[`BootInfo`]: https://docs.rs/bootloader_api/latest/bootloader_api/info/struct.BootInfo.html
To verify that the `entry_point` macro worked as expected, we can use the `objdump` tool as [described in the previous post][objdump-prev]. First, we recompile using `cargo build --target x86_64-unknown-none`, then we inspect the section headers using `objdump` or `rust-objdump`:
[objdump-prev]: @/edition-3/posts/01-minimal-kernel/index.md#objdump
```hl_lines=8
rust-objdump -h target/x86_64-unknown-none/debug/kernel
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .bootloader-config 0000007c 0000000000200120 DATA
2 .text 00000075 00000000002011a0 TEXT
3 .debug_abbrev 000001c8 0000000000000000 DEBUG
4 .debug_info 00000b56 0000000000000000 DEBUG
5 .debug_aranges 00000090 0000000000000000 DEBUG
6 .debug_ranges 00000040 0000000000000000 DEBUG
7 .debug_str 00000997 0000000000000000 DEBUG
8 .debug_pubnames 0000014c 0000000000000000 DEBUG
9 .debug_pubtypes 00000548 0000000000000000 DEBUG
10 .debug_frame 000000b0 0000000000000000 DEBUG
11 .debug_line 0000012c 0000000000000000 DEBUG
12 .comment 00000013 0000000000000000
13 .symtab 000000a8 0000000000000000
14 .shstrtab 000000b8 0000000000000000
15 .strtab 000000cd 0000000000000000
```
We see that there is indeed a new `.bootloader-config` section of size `0x7c` in our kernel executable. This means that 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 [docs of the `bootloader` crate][`bootloader` docs] describe 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/
[`bootloader` docs]: https://docs.rs/bootloader/0.11.0/bootloader/
#### A `boot` crate
@@ -380,57 +360,48 @@ This means that is has a classical `main` function and can use standard library
[`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
#### Artifact Dependencies
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.
The first step in creating the bootable disk image is to enable support for [artifact dependencies](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) from inside your kernel's `.cargo/config.toml` because we're going to need that support later:
[`cargo metadata`]: https://doc.rust-lang.org/cargo/commands/cargo-metadata.html
```toml
# in .cargo/config.toml
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:
[unstable]
bindeps = true
```
[`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
After this, you need to add an artifact dependency on your kernel from inside the boot crate. This tells the bootloader crate where the source code to your kernel resides:
```toml
# in boot/Cargo.toml
[dependencies]
bootloader-locator = "0.0.4"
kernel = { path = "..", artifact = "bin", target = "x86_64-unknown-none" }
```
Finally, you need to add a dependency on the main `bootloader` crate. Previous versions used `bootloader_locator` instead, but now, thanks to artifact dependencies, that is no longer necessary.
```toml
# in boot/Cargo.toml
[dependencies]
bootloader = "0.11.0"
```
We can see how this works by printing the Cargo-generated environment variable pointing to the absolute path of the kernel binary
```rust
// in boot/src/main.rs
use bootloader_locator::locate_bootloader; // new
use std::path::Path; // new
pub fn main() {
let bootloader_manifest = locate_bootloader("bootloader").unwrap();
dbg!(bootloader_manifest);
let kernel_binary = Path::new(env!("CARGO_BIN_FILE_KERNEL_kernel"));
dbg!(kernel_binary);
}
```
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.
The `CARGO_BIN_FILE_KERNEL_kernel` environment variable is defined by Cargo as the absolute path to the binary file created after compiling an artifact dependency — and in this case, the binary file it points to is your kernel's binary. This makes it very easy to begin the process of boot image creation, as explained in detail below.
[`dbg!`]: https://doc.rust-lang.org/std/macro.dbg.html
@@ -440,163 +411,100 @@ To run the `boot` crate from our workspace root (i.e. the kernel directory), we
```
> cargo run --package boot
[boot/src/main.rs:5] bootloader_manifest = "/.../.cargo/.../bootloader-.../Cargo.toml"
[boot/src/main.rs:5] kernel_path = "/.../target/x86_64-unknown-none/debug/deps/artifact/kernel-.../bin/kernel-..."
```
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.
It worked! We see that the kernel binary lives somewhere in the dependency tree of our `boot` crate.
By depending on the kernel as a binary dependency of `boot`, 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
#### Building a Boot Image
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:
The next step is to actually build the boot image.
From the [`bootloader` docs] we learn that the crate defines two completely unique bootloader objects: `BiosBoot` for BIOS and `UefiBoot` for UEFI. To keep it simple, we will support both, although it's possible to choose which to exclusively support later to keep your workflow streamlined as your kernel becomes more complex.
```
cargo builder --kernel-manifest path/to/kernel/Cargo.toml \
--kernel-binary path/to/kernel_bin
```toml
# in boot/Cargo.toml
[dependencies]
bootloader = "0.11.0"
kernel = { path = "..", artifact = "bin", target = "x86_64-unknown-none" }
```
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
Once all dependencies are accounted for, it's time to put everything together:
```rust
// in boot/src/main.rs
use std::process::Command; // new
// new
use bootloader::{BiosBoot, UefiBoot}
use std::{path::Path, process::exit};
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!();
let kernel_dir = todo!();
let bios_image = todo!();
let uefi_image = 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"));
// invoke UEFI boot image builder
let uefi = UefiBoot::new(&kernel_binary);
// 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);
// invoke BIOS boot image builder
let bios = BiosBoot::new(&kernel_binary);
// set the working directory
let bootloader_dir = bootloader_manifest.parent().unwrap();
build_cmd.current_dir(&bootloader_dir);
// attempt to create UEFI boot image
if let Err(e) = uefi.create_disk_image(&uefi_path) {
eprintln!("{:#?}", &e);
exit(1)
}
// run the command
let exit_status = build_cmd.status().unwrap();
if !exit_status.success() {
panic!("bootloader build failed");
// attempt to create BIOS boot image
if let Err(e) = bios.create_disk_image(&bios_path) {
eprintln!("{:#?}", &e);
exit(1)
}
}
```
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.
We use both the `UefiBoot` and `BiosBoot` types to create disk images for the BIOS and UEFI implementations, respectively. By using the `if let` syntax, we can exit the build gracefully whenever an error occurs.
[`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 `UefiBoot` and `BiosBoot` types using the `CARGO_BIN_FILE_KERNEL_kernel` environment variable that we went over previously as the constructor argument for both, we now are ready for the next step.
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.
#### Filling in the Blanks
[`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:
We still need to fill in the paths we marked as `todo!` above. Like with the kernel binary, we can also use the `env!()` builtin for this, since another environment variable can also be used as a reference point for determining the filenames for the disk images:
```rust
// in `main` in boot/src/main.rs
use std::path::Path;
// we know that the kernel lives in the parent directory of the `boot` crate
let kernel_dir = Path::new(env!("CARGO_MANIFEST_DIR")).manifest_dir.parent().unwrap();
// 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();
// use the above as a target folder in which to place both the BIOS and UEFI disk images
let bios_image = kernel_dir.join("bootimage-bios-blog_os.img");
let uefi_image = kernel_dir.join("bootimage-uefi-blog_os.img");
```
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.
[`Path::parent`]: https://doc.rust-lang.org/std/path/struct.Path.html
[`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.
From the `kernel_dir`, we can then construct the `bios_image` and `uefi_image` paths using the [`Path::join`] method.
[`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:
To install it, we can either run `rustup component add llvm-tools-preview` or specify it in our `rust-toolchain.toml` file:
[rustup component]: https://rust-lang.github.io/rustup/concepts/components.html
```toml
# in rust-toolchain
# in rust-toolchain.toml
[toolchain]
channel = "nightly"
@@ -606,28 +514,15 @@ 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`.
Because we're using artifact dependencies, when you run the `boot` package, the kernel is automatically pulled in and compiled as a dependency. Previously, in version 0.10 of the bootloader crate, you had to build the kernel binary first, but now, thanks to artifact dependencies, this is no longer required.
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.
Note also that we specified names for the image files. Although we used `bootimage-bios-blog_os.img` and `bootimage-uefi-blog_os.img` for compatibility, they can now be given whatever names you see fit.
## Running our Kernel
@@ -652,7 +547,7 @@ 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
format=raw,file=bootimage-bios-blog_os.img
```
As a result, you should see a window open that looks like this:
@@ -685,7 +580,7 @@ After downloading it, we can then run our UEFI disk image using the following co
```
qemu-system-x86_64 -drive \
format=raw,file=target/x86_64-blog_os/debug/bootimage-uefi-blog_os.img \
format=raw,file=bootimage-uefi-blog_os.img \
-bios /path/to/OVMF_pure-efi.fd,
```
@@ -733,8 +628,8 @@ Since screen output won't be essential for our kernel (there are other possible
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
[`FrameBuffer`]: https://docs.rs/bootloader/0.11.0/bootloader/boot_info/struct.FrameBuffer.html
[`FrameBufferInfo`]: https://docs.rs/bootloader/0.11.0/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.

View File

@@ -270,6 +270,15 @@ pre code {
color: inherit;
background-color: transparent;
}
pre mark {
// make highlights take the full width
display: block;
// use the code color (instead of black)
color: inherit;
// override the background color (instead of black)
background-color: rgba(0, 255, 0, 0.1) !important;
}
.highlight {
margin-bottom: 1rem;
border-radius: 4px;