Install and Configure Rust Nightly Toolchain for OS Development
Mục tiêu: Learn why the Rust nightly toolchain is essential for operating system development and how to install and configure it for a project using rustup.
Welcome to your journey of building an operating system kernel in Rust! Before we can start writing the code that will interact directly with the hardware, we need to set up a specialized development environment. The very first piece of this environment is the Rust nightly toolchain.
Why the Nightly Toolchain is Essential
In the Rust ecosystem, there are three primary “release channels” or “toolchains”: Stable, Beta, and Nightly.
- Stable: This is the most reliable version of Rust, released every six weeks. It’s recommended for most production-ready applications due to its stability.
- Beta: This channel contains features that are planned for the next stable release, allowing for testing before they are finalized.
- Nightly: This is a build from the latest development branch of Rust, compiled every night. It is the most unstable but also contains experimental features that have not yet been stabilized.
For operating system development, we absolutely require the nightly toolchain. This is because creating a “bare-metal” application—one that runs without any underlying OS—requires features that the Rust team considers experimental. These include:
- Access to unstable features: Certain low-level functionalities, like inline assembly (
asm!), specific compiler intrinsics, and the ability to build Rust’s core libraries for a custom target, are only available on nightly. - Custom Target Specifications: We will be compiling our code for a target that isn’t your typical desktop OS, and nightly provides the flexibility needed to do this effectively.
- Building without the Standard Library (
no_std): Whileno_stdis available on stable, many of the supporting features that make it practical for OS development are gated behind nightly.
By using the nightly toolchain, we unlock the full power of Rust for systems programming.
Installing Nightly with rustup
The official and most effective way to manage Rust installations is through a command-line tool called rustup. It allows you to install and switch between different toolchains effortlessly. If you don’t have Rust installed at all, you can get rustup by following the instructions on the official rust-lang.org website.
Once you have rustup installed, open your terminal or command prompt and run the following command to install the latest nightly toolchain:
rustup toolchain install nightly
This command will:
- Contact the Rust distribution servers.
- Download the latest nightly compiler, standard library, and tools (like Cargo and rustfmt) for your specific operating system and architecture.
- Install it alongside any other toolchains you may have (like
stable).
Verifying the Installation
To make sure the nightly toolchain was installed successfully, you can ask rustup to list all the toolchains it currently manages on your system.
rustup toolchain list
The output should look something like this, confirming that nightly is now available:
stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu
Setting a Project-Specific Override
For this project, we will always need to use the nightly toolchain. Instead of manually specifying it for every command, we can tell rustup to automatically use nightly whenever we are working inside our project’s directory.
Navigate in your terminal to where you plan to create the kernel project. Once there, run this command:
rustup override set nightly
This simple command is very powerful. It creates a file named rust-toolchain in the current directory containing the word nightly. From now on, any rustc or cargo command executed within this directory (or any of its subdirectories) will automatically use the nightly toolchain. This is a clean, non-intrusive way to manage project-specific requirements without changing your system’s global default toolchain.
You have now successfully configured the foundational component of our OS development environment! With the nightly toolchain in place, we are ready to proceed with setting up the other necessary tools.
Our next task is to install QEMU, the emulator that will act as a virtual computer, allowing us to boot and test our kernel without needing physical hardware.
Further Reading
To deepen your understanding of the concepts we’ve covered in this task, here are some excellent resources:
- Rust Release Channels: A detailed explanation of Stable, Beta, and Nightly in the official Rust Forge book.
- The
rustupBook: The official guide to usingrustupfor advanced toolchain management can be found in TherustupBook. - The Unstable Book: For a comprehensive list of all the experimental, nightly-only features in Rust, refer to The Unstable Book.
Install QEMU for Kernel Development
Mục tiêu: Set up the QEMU machine emulator and virtualizer on Linux, macOS, or Windows. This is a crucial step for safely and efficiently testing a custom bare-metal kernel without needing physical hardware.
Having successfully set up the Rust nightly toolchain, we’ve prepared our compiler for the specialized task of building a kernel. But where will this kernel run? We are writing code for “bare metal,” meaning it’s designed to run directly on computer hardware without an underlying operating system like Windows, macOS, or Linux. Testing this on a physical machine would be slow, cumbersome, and potentially risky. This is where an emulator comes in. Our next crucial step is to install QEMU, a powerful and versatile machine emulator and virtualizer.
Why QEMU is Essential for Kernel Development
An emulator is a piece of software that enables one computer system (the host, your development machine) to behave like another computer system (the guest). QEMU (Quick EMUlator) is an open-source emulator that can simulate a wide variety of hardware architectures, including the x86_64 architecture we are targeting for our kernel.
For OS development, using an emulator like QEMU provides several key advantages over testing on real hardware:
- Safety: A bug in kernel code can easily cause a physical machine to freeze, crash, or even (in rare cases of misconfiguring hardware) cause damage. Inside an emulator, the worst a bug can do is crash the virtual machine, leaving your host system completely unharmed.
- Speed: Rebooting a physical machine takes time. With QEMU, you can boot your kernel, test a feature, and shut it down in a matter of seconds. This drastically speeds up the development cycle.
- Debugging: QEMU integrates with debuggers like GDB, allowing you to step through your kernel code line by line, inspect memory, and examine CPU registers. This is an incredibly powerful tool for diagnosing complex bugs.
- Convenience: You don’t need a spare physical computer to test your OS. Everything happens in a window on your development machine.
The specific QEMU executable we will use is qemu-system-x86_64. The system part indicates that we are emulating a full computer system (including CPU, memory, and peripherals like a graphics card and keyboard), and x86_64 specifies the 64-bit architecture we’re building for.
Installing QEMU
The installation process for QEMU varies depending on your operating system. Follow the instructions for your specific platform below.
On Linux
Most Linux distributions provide QEMU in their official package repositories. Open your terminal and use the appropriate command for your distro:
- Debian/Ubuntu/Mint:
sh sudo apt-get update sudo apt-get install qemu-system-x86 - Fedora/CentOS/RHEL:
sh sudo dnf install qemu-system-x86 - Arch Linux:
sh sudo pacman -S qemu-system-x86
On macOS
The easiest way to install QEMU on macOS is by using the Homebrew package manager. If you have Homebrew installed, run the following command in your terminal:
brew install qemu
On Windows
For Windows, you can download the official installer from the QEMU website.
- Navigate to the QEMU for Windows download page.
- Download the latest installer, which will be a file named something like
qemu-w64-setup-YYYYMMDD.exe. - Run the installer. You can accept the default settings.
- Crucially, you must add the QEMU installation directory to your system’s
PATHenvironment variable. By default, this isC:\Program Files\qemu.- Press the
Windowskey, type “Edit the system environment variables,” and open it. - In the “System Properties” window, click the “Environment Variables…” button.
- In the “System variables” section, find and select the
Pathvariable, then click “Edit…”. - Click “New” and add the path to your QEMU installation folder (e.g.,
C:\Program Files\qemu). - Click “OK” on all open windows to save the changes.
- You may need to close and reopen your terminal (Command Prompt, PowerShell, or WSL) for the change to take effect.
- Press the
Verifying the Installation
After following the steps for your OS, you should verify that QEMU was installed correctly and is accessible from your terminal. Open a new terminal window and run:
qemu-system-x86_64 --version
If the installation was successful, you will see output detailing the QEMU version, like this (your version number may differ):
QEMU emulator version 8.1.2
Copyright (c) 2003-2023 Fabrice Bellard and the QEMU Project developers
If you see this message, congratulations! You now have a virtual machine ready to boot your kernel. We have our specialized compiler (Rust nightly) and a virtual computer (QEMU). The next step is to install bootimage, a Cargo tool that will bridge the gap between them, automating the process of compiling our kernel and running it in QEMU.
Further Reading
To gain a deeper appreciation for the technology we’ve just installed, you can explore these topics:
- The Official QEMU Documentation: The definitive source for all QEMU features and commands. QEMU Documentation.
- Computer Emulation on Wikipedia: A great high-level overview of what emulation is and how it works. Wikipedia: Emulator.
- Virtualization vs. Emulation: An article explaining the subtle but important differences between these two related concepts. Red Hat: What is the difference between emulation and virtualization?.
Install the bootimage Crate for Rust OS Development
Mục tiêu: Install the bootimage cargo subcommand, a crucial tool that packages a Rust kernel with a bootloader to create a bootable disk image for QEMU.
With the Rust nightly toolchain ready to compile our low-level code and QEMU standing by to emulate a computer for us, we face a crucial question: How do we bridge the gap between our compiled kernel and the virtual machine? A standard executable file generated by cargo build can’t just be “run” on bare metal. A computer needs a specific set of instructions to start up, initialize its hardware, and load an operating system. This is the job of a bootloader, and our next step is to install a tool that handles this for us: bootimage.
The Missing Link: From Compiled Code to a Bootable Disk
When a computer powers on, its firmware (either the legacy BIOS or the modern UEFI) performs a Power-On Self-Test (POST) and then looks for a bootable device. It loads a small, special program—the bootloader—from a specific location on that device (like the Master Boot Record or an EFI System Partition).
The bootloader’s responsibilities are critical and complex:
- CPU Mode Switching: It must switch the CPU from its initial state (e.g., 16-bit real mode on BIOS systems) into the 64-bit “long mode” that our kernel requires.
- Memory Mapping: It queries the firmware for a map of available physical memory.
- Loading the Kernel: It finds our compiled kernel on the disk, loads it into memory, and finally, jumps to the kernel’s entry point, handing over control.
Writing a bootloader is a formidable task in itself, often requiring extensive assembly programming. To allow us to focus on writing our kernel in Rust, we will use the bootimage tool. bootimage is a Cargo subcommand that automates the entire process: it compiles our kernel, downloads a pre-built compatible bootloader, and combines them into a single bootable disk image that QEMU can run. It effectively acts as the glue between our Rust project and the QEMU emulator.
Installing bootimage
The bootimage tool is distributed as a crate on crates.io, the official Rust package registry. We can install it globally using Cargo’s built-in installation command. Open your terminal and run the following:
cargo install bootimage
Let’s break down what this command does:
cargo install: This is a standard Cargo command that fetches a crate’s source code fromcrates.io.- It then compiles that crate into an executable binary.
- Finally, it places the compiled executable into a central location (
~/.cargo/bin/by default on Linux/macOS and%USERPROFILE%\.cargo\bin\on Windows). Because the Rust installer adds this directory to your system’sPATHenvironment variable, the new command becomes available everywhere in your terminal.
Verifying the Installation
To ensure that bootimage was installed correctly and is accessible from your command line, you can check its version.
cargo bootimage --version
If the installation was successful, you will see output similar to this (the version number may vary):
bootimage 0.10.3
Note: bootimage is a cargo subcommand, so we invoke it via cargo bootimage.
Seeing this output confirms that you have successfully installed the final piece of our core development toolchain. We now have:
- A compiler (
rustcon the nightly channel) to build our kernel. - An emulator (
qemu-system-x86_64) to run our kernel. - A boot tool (
bootimage) to package our kernel and launch it in the emulator.
With our complete toolchain in place, we are finally ready to create the Rust project that will become our operating system kernel. The next task is to use Cargo to generate the boilerplate for our new binary crate.
Further Reading
To understand the tools and concepts involved in this step more deeply, check out these resources:
- The
bootimageCrate: The official page oncrates.io, with links to its repository and documentation. crates.io: bootimage - The
bootloaderCrate: This is the underlying bootloader thatbootimageuses. Reading its documentation can provide insight into how the boot process works. GitHub: rust-osdev/bootloader cargo installdocumentation: The official documentation for the command in The Cargo Book. The Cargo Book:cargo install- OSDev.org Wiki on the Boot Process: A comprehensive resource for anyone interested in the low-level details of how a computer starts. OSDev Wiki: Boot Sequence
Create a New Rust Kernel Project with Cargo
Mục tiêu: Use the cargo new command to scaffold a new binary Rust project that will serve as the foundation for a bare-metal operating system kernel. This task covers the command, its options, and the initial project structure generated by Cargo.
Having assembled our essential toolchain—the Rust nightly compiler, the QEMU emulator, and the bootimage utility—we have all the external pieces in place. We are now perfectly positioned to create the actual project that will house our kernel’s source code. This is where we move from setting up tools to creating the tangible foundation of our operating system.
Introducing Cargo: Rust’s Build System and Package Manager
Before we run the command, it’s important to understand the tool we’re about to use: Cargo. Cargo is the official build system and package manager for Rust, and it is central to the development workflow. It handles a multitude of tasks that would otherwise be complex and error-prone:
- Project Scaffolding: It creates new projects with a standardized directory structure.
- Building Code: It invokes the Rust compiler (
rustc) with the correct flags to compile your project. - Dependency Management: It downloads, compiles, and links any external libraries (called “crates” in Rust) your project depends on.
- Running Tests: It provides a built-in framework for writing and running unit and integration tests.
- And much more: It also runs benchmarks, generates documentation, and publishes your packages to the community repository,
crates.io.
For our project, Cargo will be the primary interface we use to build, run, and manage our kernel.
Creating Our Kernel Project
Now, let’s create our project. Navigate in your terminal to the directory where you want your project to live. If you previously ran rustup override set nightly, make sure you are in that same directory so that our new project folder inherits the nightly toolchain setting.
Run the following command:
cargo new os_kernel --bin
This command instructs Cargo to perform a simple but crucial action. Let’s break it down:
cargo new: This is the command to create a new Rust project.os_kernel: This is the name we are giving our project. Cargo will create a new directory with this name. This also becomes the default name of our “crate” (the package of code we are building).--bin: This flag is very important. It tells Cargo to create a binary project, as opposed to a library project (--lib). A binary project is one that compiles to a standalone executable. Even though our kernel isn’t a typical application you’d run on Windows or Linux, it is the final, runnable artifact for our target hardware, so we structure it as a binary.
Exploring the Project Structure
After running the command, you will see a new directory named os_kernel. If you look inside, you’ll find that Cargo has generated a few files and folders for us:
os_kernel/
├── .gitignore
├── Cargo.toml
└── src/
└── main.rs
This is the standard layout for a Rust binary project. Let’s examine each part:
os_kernel/: This is the root directory of our project, also known as the “package root”..gitignore: Cargo thoughtfully includes a default.gitignorefile. This file lists files and directories that the Git version control system should ignore, such as thetargetdirectory where compiled files are placed. This is a best practice that keeps your repository clean.-
Cargo.toml: This is the manifest file, and it’s the heart of a Cargo project. Written in the TOML (Tom’s Obvious, Minimal Language) format, it contains all the metadata Cargo needs to compile your project. If you open it, you’ll see something like this:```toml [package] name = “os_kernel” version = “0.1.0” edition = “2021”
See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
`` This file defines the project's name, version, and the Rust edition to use. The[dependencies]` section is where we will later add the external crates our kernel will rely on. -
src/main.rs: This is the main source file for our binary crate and is considered the “crate root”. Cargo has generated a simple “Hello, world!” program for us here:rust fn main() { println!("Hello, world!"); }Important: This code relies on Rust’s standard library (std) for features like theprintln!macro, which handles printing to the console. Since our kernel will run on bare metal without any underlying OS, we will not have access to the standard library. Therefore, we will be completely replacing the contents of this file in a future step. For now, it serves as a valid placeholder.
You have now successfully created the skeleton of our OS project. It’s a standard Rust program at this point, but we are about to transform it into something that can run on bare metal.
Our next task is to begin this transformation by defining a custom target. This involves creating a JSON file that describes the specific architecture and environment we are building for (x86_64 with no underlying OS), which will tell the Rust compiler exactly how to produce a binary suitable for our needs.
Further Reading
- The Cargo Book: Creating a New Package: The official documentation on the
cargo newcommand. - The Cargo Manifest Format: A detailed reference for all the possible keys and values in the
Cargo.tomlfile. - The Rust Programming Language, Chapter 1.2: A beginner-friendly look at the “Hello, World!” program and the role of Cargo.
Create a Custom Rust Build Target for a Bare-Metal Kernel
Mục tiêu: Define a custom build target in a JSON file to configure the Rust compiler for building a freestanding, bare-metal x86_64 executable. This involves specifying compiler options like the LLVM target, linker, panic strategy, and disabling the red zone, which are essential for operating system development.
Having used cargo new to create our project’s directory structure, we have a standard Rust binary project. However, by default, Cargo and the Rust compiler (rustc) are configured to build an executable for your operating system (e.g., Windows, macOS, or Linux). This is a problem because we aren’t building an application to run on an existing OS; we are building the OS itself.
Our goal is to create a “freestanding” or “bare-metal” executable. To do this, we must explicitly tell the compiler about the environment we are targeting. This is accomplished by defining a custom target.
The Concept of a Build Target
In the world of compilers, a target is a specification that describes the environment for which the code is being compiled. This is often represented as a “target triple” in the format architecture-vendor-os-abi. For example, a common target on a 64-bit Windows machine is x86_64-pc-windows-msvc. Let’s break that down:
x86_64: The CPU architecture.pc: The vendor (in this case, “personal computer”).windows: The underlying operating system.msvc: The Application Binary Interface (ABI), which defines conventions for things like function calls. This one specifies compatibility with the Microsoft Visual C++ toolchain.
A build for this target would assume the presence of the Windows OS, link against its libraries, and produce a .exe file in the standard Windows format. This is precisely what we don’t want.
Our target is a 64-bit x86_64 CPU with no underlying OS. For this, we use the x86_64-unknown-none target. While Rust has a built-in target with this name, creating our own custom target specification file gives us fine-grained control over crucial compiler settings, ensuring our kernel is built exactly as we need it.
Creating the Target Specification File
We will define our custom target in a JSON file. Create a new file in the root directory of your os_kernel project (at the same level as your Cargo.toml file) and name it x86_64-os_kernel.json. Giving it a unique name like this prevents any potential clashes with built-in targets.
Add the following content to your newly created x86_64-os_kernel.json file:
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-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"
}
This file may seem intimidating, but each line serves a specific and important purpose. Let’s go through it key by key.
"llvm-target": "x86_64-unknown-none": This tells Rust’s compiler backend, LLVM, what kind of machine code to generate. We’re targeting a generic 64-bit system with no OS."data-layout": "...": This is a complex string that defines low-level properties like how data types are aligned in memory. The default value forx86_64is correct for us, but it’s good practice to specify it explicitly."arch": "x86_64": The CPU architecture family."target-endian": "little": Specifies the byte ordering. x86 CPUs are “little-endian,” meaning the least significant byte is stored at the lowest memory address."target-pointer-width": "64": We are building a 64-bit kernel, so memory addresses (pointers) are 64 bits wide."os": "none": This is crucial. It tells the compiler that there is no underlying operating system, which prevents it from trying to link against any OS-specific libraries."linker-flavor": "ld.lld"and"linker": "rust-lld": These instructrustcto useLLD, the LLVM linker, to link our compiled code into a final executable. LLD is a modern, cross-platform linker that comes with Rust and is perfect for bare-metal targets."panic-strategy": "abort": When a Rust program panics, it can either “unwind” the stack (running destructors for all live objects) or simply abort the program. Unwinding requires OS support that we don’t have, so our only option is to abort."disable-redzone": true: This is a critical setting for OS development. The x86-64 ABI includes a “red zone,” which is a 128-byte area on the stack below the current stack pointer that a function can use without explicitly moving the stack pointer. This is a performance optimization, but it’s disastrous for interrupt handling. An interrupt can occur at any time, and the interrupt handler would overwrite this red zone, corrupting the data of the interrupted function. Disabling it ensures our interrupt handlers will be safe."features": "-mmx,-sse,+soft-float": This string controls which CPU features are enabled. We disablemmxandsseinstructions because they use special, large registers. If we used them, we would need to save and restore the state of these registers on every interrupt, which adds complexity we want to avoid for now.+soft-floattells the compiler to emulate floating-point operations using integer instructions, avoiding the need for a hardware Floating Point Unit (FPU) and its complex state.
With this file in place, you have created a detailed blueprint that tells the Rust compiler exactly how to build a binary for our bare-metal kernel environment. We’ve taken a significant step from building a standard application to building a truly freestanding executable.
The next logical step is to configure Cargo to use this new target specification by default for our project. We will do this by creating a special configuration file inside a .cargo directory.
Further Reading
- Custom Targets in
rustc: The official documentation on defining custom targets. - A Deeper Dive into the Red Zone: An excellent blog post explaining why the red zone is problematic for kernels.
- LLVM Target Triples: A high-level explanation of LLVM’s target triple concept.
Automate Rust Kernel Build with Cargo Configuration
Mục tiêu: Create a project-specific Cargo configuration file (.cargo/config.toml) to set a custom bare-metal target as the default and use the unstable build-std feature to compile the core and compiler\_builtins libraries for the new target.
In our previous step, we meticulously crafted the x86_64-os_kernel.json file. This file serves as a detailed blueprint, instructing the Rust compiler on the precise architecture, features, and constraints of our bare-metal environment. We’ve defined what we’re building for, but we haven’t yet told Cargo to use this blueprint by default. Constantly typing --target x86_64-os_kernel.json for every command would be tedious and error-prone.
Our current task is to automate this process by creating a project-specific Cargo configuration file. This file will not only set our custom target as the default but also unlock a powerful nightly feature essential for compiling Rust’s core libraries for our unique environment.
Understanding Cargo’s Configuration
Cargo is highly configurable. While it has global settings, it also allows for per-project configuration through a special file: .cargo/config.toml. When you run any cargo command, it searches for this file in the current directory and parent directories. This allows us to define specific behaviors for our project without affecting other Rust projects on our system.
For our kernel, we need to configure two key things:
- The default build target.
- The recompilation of core libraries for our custom target.
Creating the Configuration File
First, we need to create the directory and the file itself. In the root of your os_kernel project (the same directory that contains Cargo.toml), follow these steps:
- Create a new directory named
.cargo. The leading dot is important, as it often signifies a configuration directory. - Inside the new
.cargodirectory, create a new file namedconfig.toml.
Your project structure should now look like this:
os_kernel/
├── .cargo/
│ └── config.toml
├── .gitignore
├── Cargo.toml
├── src/
│ └── main.rs
└── x86_64-os_kernel.json
Now, open the newly created .cargo/config.toml and add the following content:
[build]
target = "x86_64-os_kernel.json"
[unstable]
build-std = ["core", "compiler_builtins"]
Let’s dissect this file line by line to understand its profound impact on our build process.
The [build] Section
target = "x86_64-os_kernel.json": This is the most direct part of the configuration. Under the[build]table, we are setting thetargetkey. The value is the name of our custom target file, which is located in the project’s root. From now on, whenever you runcargo build,cargo run, orcargo checkwithin this project, Cargo will automatically behave as if you had manually added the--target x86_64-os_kernel.jsonflag. This makes our custom target the default for this project.
The [unstable] Section and build-std
This section is where the magic happens, and it’s a primary reason we require the nightly toolchain.
- What is
build-std? Normally, when you compile a Rust program, Cargo uses pre-compiled versions of Rust’s standard (std) and core (core) libraries for your target. However, no such pre-compiled libraries exist for our completely customx86_64-os_kerneltarget. Thebuild-stdfeature is an unstable (nightly-only) flag that tells Cargo: “Don’t look for pre-compiled libraries. Instead, download their source code and compile them from scratch specifically for the target I’ve provided.” -
build-std = ["core", "compiler_builtins"]: This line specifies which libraries to recompile.core: The core library is the absolute foundation of Rust. It contains fundamental types likeOption<T>,Result<T, E>, iterators, and modules for primitive types. Crucially, it has no dependency on an underlying operating system, making it the bedrock of any#![no_std]application. By recompiling it for our target, we make all this essential functionality available to our kernel.compiler_builtins: The Rust compiler often generates calls to basic helper functions for low-level operations like memory manipulation (memcpy,memset) or complex arithmetic that isn’t directly supported by a single CPU instruction. Thecompiler_builtinscrate provides implementations for these functions. Without it, our kernel would fail to link because these fundamental helper functions would be missing.
By setting this configuration, we’ve instructed Cargo to build a self-contained ecosystem for our kernel, ensuring that even the most basic parts of Rust are perfectly tailored to our bare-metal environment.
Verifying Our Progress
With this configuration file in place, we can test that Cargo is using it correctly. Open your terminal in the project root and run the build command:
cargo build
This command will now trigger a much more complex process than before:
- Cargo reads
.cargo/config.toml. - It sees the default target is
x86_64-os_kernel.json. - It sees the
build-stdflag and begins by downloading and compiling thecoreandcompiler_builtinscrates for our target. - Finally, it attempts to compile our
src/main.rs.
The build will fail, and this is not only expected but is a sign of success! The error message will look something like this:
error: linking with `rust-lld` failed: exit status: 1
|
= note: "rust-lld" "-flavor" "ld.lld" "--error-limit" "0" "-Z" "nostartfiles" "-o" "target/x86_64-os_kernel/debug/os_kernel" ...
= note: rust-lld: error: undefined symbol: main
>>> referenced by crt0.c
>>> /home/runner/work/rust/rust/src/bootstrap/lib/rust-installer/../crt0.c:28
>>> crt0-a8d6b8a2e5d1871a.o:(main)
This error message is excellent news. It means Cargo successfully compiled the core libraries and our crate for the x8-64-os_kernel target. The failure happens at the very end, during the “linking” phase. The linker is complaining that it can’t find a main function, which is the standard entry point for programs hosted on an OS. Our bare-metal kernel won’t have a main function; it will have a different entry point. We will fix this in the next step, but for now, this error confirms that our target and build configurations are working perfectly.
We are now on the cusp of having a fully configured build system. The final piece of the setup puzzle is to tell Cargo how to run our kernel using bootimage and QEMU. This will be handled by modifying our Cargo.toml file in the next task.
Further Reading
- Cargo Configuration: The official documentation in The Cargo Book, detailing all the possible configuration keys.
- The
build-stdCargo Feature: The unstable book’s documentation on this powerful feature. std,core, andalloc: A great blog post explaining the different levels of Rust’s standard libraries.
Configure Cargo to Run a Bare-Metal Rust Kernel
Mục tiêu: Configure the cargo run command to execute a bare-metal kernel by setting bootimage as a custom runner in .cargo/config.toml. This automates creating a bootable disk image and launching it in the QEMU emulator.
We’ve successfully configured Cargo to build our kernel for a custom bare-metal target by creating the .cargo/config.toml file. This means the cargo build command now knows exactly what to create. However, the development cycle isn’t just about building; it’s about running and testing. Our next task is to teach Cargo’s run command how to execute our kernel.
Since our kernel is not a standard application for your host operating system, simply running the compiled binary is impossible. Instead, “running” our kernel involves a multi-step process: bundling it with a bootloader to create a bootable disk image, and then launching that image in the QEMU emulator. Fortunately, the bootimage tool we installed is designed to handle this entire sequence. We just need to tell Cargo to use it.
Integrating bootimage with cargo run
The standard cargo run command is a convenient shortcut that first builds your code and then executes the resulting binary. Cargo allows us to customize this second step by specifying a “runner” executable for a given target. This is the perfect mechanism for our needs. We can designate bootimage as our runner, effectively hijacking the execution step to perform the specialized launch sequence our kernel requires.
We will add this configuration to the same .cargo/config.toml file we created in the previous step, keeping all our project-specific build and run settings neatly in one place.
Open your .cargo/config.toml file. It currently contains the [build] and [unstable] sections. We will now add a new section to define the runner for our specific target.
Your existing .cargo/config.toml file:
[build]
target = "x86_64-os_kernel.json"
[unstable]
build-std = ["core", "compiler_builtins"]
Append the following highlighted section to the end of the file:
[build]
target = "x86_64-os_kernel.json"
[unstable]
build-std = ["core", "compiler_builtins"]
[target.x86_64-os_kernel.json]
runner = ["bootimage", "runner"]
Deconstructing the Configuration
Let’s break down the new lines we’ve just added.
[target.x86_64-os_kernel.json]: This is a TOML table header that creates a configuration section specifically for our custom target. Any settings inside this block will only apply when Cargo is building forx86_64-os_kernel.json. This is incredibly powerful, as it ensures these settings don’t interfere with normal builds if you were to add other targets later.runner = ["bootimage", "runner"]: This is the key instruction. We are setting therunnerfor our target.- The value is an array of strings:
["bootimage", "runner"]. This tells Cargo that when it needs to “run” the compiled binary, it should execute the commandbootimage runnerand pass the path to the compiled kernel binary as the last argument. Using an array is slightly more robust than a single string ("bootimage runner") as it avoids potential issues with shell command parsing, especially on different operating systems. - The
bootimage runnercommand is a special mode of thebootimagetool. It takes the path to your compiled kernel, bundles it with its pre-built bootloader, creates a bootable disk image, and automatically launches it using QEMU with sensible default settings.
- The value is an array of strings:
The Complete Workflow
With this final configuration piece in place, our entire development workflow is now integrated into standard Cargo commands:
- You run
cargo runin your terminal. - Cargo reads
.cargo/config.toml. - It sees the default
targetisx86_64-os_kernel.jsonand begins a cross-compilation. - It sees the
build-stdflag and compiles thecoreandcompiler_builtinslibraries for our target. - It compiles our own kernel code from
src/main.rs. - Instead of executing the final binary directly, it sees the
runnerconfiguration. - It invokes
bootimage runner path/to/kernel/binary. bootimagecreates the bootable disk image and launchesqemu-system-x86_64with that image.
You can try it now! Run cargo run from your project’s root directory.
cargo run
Just like at the end of the last task, the command will still fail with a linker error about a missing main symbol. This is still the correct and expected outcome! We haven’t written any valid kernel code yet. However, this test confirms that our entire toolchain is now configured. Cargo is successfully compiling the code and attempting to hand it off to the bootimage runner.
Congratulations! You have completed the most challenging part of the setup. You now have a fully configured development environment for building and running a bare-metal operating system kernel in Rust. All the complex steps of cross-compilation, linking, and emulation are now abstracted behind the simple and familiar cargo run command.
Our next step is a very exciting one: we will finally start writing the kernel itself. We will begin by removing the standard library dependencies and creating our own entry point, paving the way to print our first “Hello, World!” directly to the screen from our own OS.
Further Reading
- Cargo Configuration -
runner: The official documentation for therunnerkey in The Cargo Book. - The
bootimagecargo runIntegration: The official documentation from thebootimagedevelopers explaining this feature.