@@ -82,7 +82,7 @@ We name the project `kernel` here, but of course you can choose your own name.
The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2021` flag specifies that we want to use the [2021 edition] of Rust for our crate.
The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2021` flag specifies that we want to use the [2021 edition] of Rust for our crate.
When we run the command, cargo creates the following directory structure for us:
When we run the command, cargo creates the following directory structure for us:
To download and set up the `x86_64-unknown-none` target, we use the following `rustup` command:
```
```
rustup target add x86_64-unknown-none
rustup target add x86_64-unknown-none
@@ -334,7 +350,12 @@ Afterwards, we can [cross compile] our executable for a bare metal environment b
error: requires `start` lang_item
error: requires `start` lang_item
```
```
We still get the error about a missing `start` language item because we're still depending on the Rust runtime. To remove that dependency, we can use the `#[no_main]` attribute.
We still get the error about a missing `start` language item because the custom target only removed the dependency on the C library.
To remove the dependency on the Rust runtime as well, we can use the `#[no_main]` attribute.
Before that, we can do a small cleanup.
The `x86_64-unknown-none` target defaults to `panic = "abort"`, so the we can remove the `profile.dev` and `profile.release` tables from our `Cargo.toml` again.
@@ -284,7 +284,7 @@ There are a few notable things:
- 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.
- 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.
- 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.
- There is a new [`BootInfo`] argument, which the bootloader fills with various system information. We will use this argument later. For now, we prefix it with an underscore to avoid an "unused variable" warning.
- 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.
- 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.
@@ -326,46 +326,393 @@ This means that we can now look into how to create a bootable disk image from ou
Now that our kernel is compatible with the `bootloader` crate, we can turn it into a bootable disk image.
Now that our kernel is compatible with the `bootloader` crate, we can turn it into a bootable disk image.
To do that, we need to create a disk image file with an [MBR] or [GPT] partition table and create a new [FAT][FAT file system] boot partition there.
To do that, we need to create a disk image file with an [MBR] or [GPT] partition table and create a new [FAT][FAT file system] boot partition there.
Then we can copy our compiled kernel and the compiled bootloader implementation there.
Then we copy our compiled kernel and the compiled bootloader to this boot partition.
While we could perform these steps manually using platform-specific tools (e.g. [`mkfs`] on Linux), this would not be cumbersome and fragile.
While we could perform these steps manually using platform-specific tools (e.g. [`mkfs`] on Linux), this would be cumbersome to use and difficult to set up.
Fortunately, the `bootloader` crate provides a [`DiskImageBuilder`] to construct both BIOS and UEFI disk images in a simple way.
Fortunately, the `bootloader` crate provides a cross-platform [`DiskImageBuilder`] type to construct BIOS and UEFI disk images.
It works on Windows, macOS, and Linux without any additional dependencies.
We just need to pass path to our kernel executable and then call [`create_bios_image`] and/or [`create_uefi_image`] withour desired target path.
We just need to pass path to our kernel executable and then call `create_bios_image` and/or `create_uefi_image` with our desired target path.
Creating a cargo workspace is easy. We first create a new subfolder named `kernel` and move our existing `Cargo.toml` file and `src` folder there.
We keep the `Cargo.lock` file and the `target` folder in the outer level, `cargo` will update them automatically.
The folder structure should look like this now:
```bash ,hl_lines=3-6
.
├── Cargo.lock
├── kernel
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
```
Next, we create a new `blog_os` crate at the root using `cargo init`:
```bash
❯ cargo init --name blog_os
```
You can of course choose any name you like for the crate.
The command creates a new `src/main.rs` at the root with a main function printing "Hello, world!".
It also creates a new `Cargo.toml` file at the root.
The directory structure now looks like this:
```bash,hl_lines=3 8-9
.
├── Cargo.lock
├── Cargo.toml
├── kernel
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── src
│ └── main.rs
└── target
```
The final step is to add the workspace configuration to the `Cargo.toml` at the root:
```toml ,hl_lines=8-9
# in top-level Cargo.toml
[package]
name = "blog_os"
version = "0.1.0"
edition = "2021"
[workspace]
members = ["kernel"]
[dependencies]
```
That's it!
Now our `blog_os` and `kernel` crates live in the same workspace.
To ensure that everything works as intended, we can run `cargo tree` to list all the packages in the workspace:
```bash
❯ cargo tree --workspace
blog_os v0.1.0 (/.../os)
kernel v0.1.0 (/.../os/kernel)
└── bootloader_api v0.11.3
```
We see that both the `blog_os` and the `kernel` crates are listed, which means that `cargo` recognizes that they're both part of the same workspace.
<div class="note">
If you're getting a _"profiles for the non root package will be ignored"_ warning here, you probably still have a manual `panic = "abort"` override specified in your `kernel/Cargo.toml`.
This override is no longer needed since we compile our kernel for the `x86_64-unknown-none` target, which uses `panic = "abort"` by default.
So to fix this warning, just remove the `profile.dev` and `profile.release` tables from your `kernel/Cargo.toml` file.
</div>
We now have a simple cargo workspace and a new `blog_os` crate at the root.
But what do we need that new crate for?
#### Adding an Artifact Dependency
#### Adding an Artifact Dependency
TODO
The reason that we added the new `blog_os` crate is that we want to do something with our _compiled_ kernel.
`Cargo` provides an useful feature for this, called [_artifact dependencies_].
The basic idea is that crates can depend on compiled artifacts (e.g. executables) of other crates.
This is especially useful for artifacts that need to be compiled for a specific target, such as our OS kernel.
Rustup automatically reads the `rust-toolchain.toml` file and sets up the requested Rust version when running a `cargo` or `rustc` command in this folder, or a subfolder.
We specify that the `kernel` crate lives in the `kernel` subdirectory through the `path` key.
The `artifact = "bin"` key specifies that we're interested in the compiled kernel binary (this makes the dependency an artifact dependency).
Finally, we use the `target` key to specify that our kernel binary should be compiled for the `x86_64-unknown-none` target.
Now `cargo` will automatically build our kernel before building our `blog_os` crate.
We can see this when building the `blog_os` crate using `cargo build`:
```
❯ cargo build
Compiling bootloader_api v0.11.3
Compiling kernel v0.1.0 (/.../os/kernel)
Compiling blog_os v0.1.0 (/.../os)
Finished dev [unoptimized + debuginfo] target(s) in 0.51s
```
The `blog_os` crate should be built for our host system, so we don't specify a `--target` argument.
Cargo uses the same profile for compiling the `blog_os` and `kernel` crates, so `cargo build --release` will also build the `kernel` binary with optimizations enabled.
Now that we have set up an artifact dependency on our kernel, we can finally create the bootable disk image.
#### Using the `DiskImageBuilder`
The last step to create the bootable disk image is to invoke the [`DiskImageBuilder`] of the `bootloader` crate.
For that, we first add a dependency on the `bootloader` crate to our `blog_os` crate:
Now we need to decide where we want to place the disk images.
This is entirely up to.
In the following, we will place the images next to `blog_os` executable, which will be under `target/debug` (for development builds) or `target/release` (for optimized builds):
```rust ,hl_lines=2 4 9-10 12
use bootloader::DiskImageBuilder;
use std::{env, error::Error, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
// set by cargo for the kernel artifact dependency
let kernel_path = PathBuf::from(env!("CARGO_BIN_FILE_KERNEL"));
let disk_builder = DiskImageBuilder::new(kernel_path);
// place the disk image files under target/debug or target/release
let target_dir = env::current_exe()?;
Ok(())
}
```
We use the [`std::env::current_exe`] function to get the path to the `blog_os` executable.
This function can (rarely) fail, so we add some basic error handling to our `main` function.
For that, we change the return value of the function to a [`Result`] with a dynamic error type (a [_trait object_] of the [`Error`] trait).
This allows us to use the [`?` operator] to exit with an error code on error.
// colors with transparency that work for both light and dark mode
// colors with transparency that work for both light and dark mode
--background-color-note:#00ff001a;
--background-color-note:#6666cc1a;
--border-color-note:#00ff0050;
--border-color-note:#6666cc50;
}
}
@mixin set-colors-dark{
@mixin set-colors-dark{
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.