mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Format edition-3 markdown as 'one sentence per line'
This makes the diffs nicer when we change something in the future.
This commit is contained in:
@@ -15,13 +15,18 @@ icon = '''
|
||||
'''
|
||||
+++
|
||||
|
||||
The first step in creating our own operating system kernel is to create a [bare metal] Rust executable that does not depend on an underlying operating system. For that we need to disable most of Rust's standard library and adjust various compilation settings. The result is a minimal operating system kernel that forms the base for the following posts of this series.
|
||||
The first step in creating our own operating system kernel is to create a [bare metal] Rust executable that does not depend on an underlying operating system.
|
||||
For that we need to disable most of Rust's standard library and adjust various compilation settings.
|
||||
The result is a minimal operating system kernel that forms the base for the following posts of this series.
|
||||
|
||||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||||
|
||||
<!-- more -->
|
||||
|
||||
This blog is openly developed on [GitHub]. If you have any problems or questions, please open an issue there. You can also leave comments [at the bottom]. The complete source code for this post can be found in the [`post-01`][post branch] branch.
|
||||
This blog is openly developed on [GitHub].
|
||||
If you have any problems or questions, please open an issue there.
|
||||
You can also leave comments [at the bottom].
|
||||
The complete source code for this post can be found in the [`post-01`][post branch] branch.
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
@@ -31,9 +36,13 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
<!-- toc -->
|
||||
|
||||
## Introduction
|
||||
To write an operating system kernel, we need code that does not depend on any operating system features. This means that we can't use threads, files, heap memory, the network, random numbers, standard output, or any other features requiring OS abstractions or specific hardware. Which makes sense, since we're trying to write our own OS and our own drivers.
|
||||
To write an operating system kernel, we need code that does not depend on any operating system features.
|
||||
This means that we can't use threads, files, heap memory, the network, random numbers, standard output, or any other features requiring OS abstractions or specific hardware.
|
||||
Which makes sense, since we're trying to write our own OS and our own drivers.
|
||||
|
||||
While this means that we can't use most of the [Rust standard library], there are still a lot of Rust features that we _can_ use. For example, we can use [iterators], [closures], [pattern matching], [option] and [result], [string formatting], and of course the [ownership system]. These features make it possible to write a kernel in a very expressive, high level way without worrying about [undefined behavior] or [memory safety].
|
||||
While this means that we can't use most of the [Rust standard library], there are still a lot of Rust features that we _can_ use.
|
||||
For example, we can use [iterators], [closures], [pattern matching], [option] and [result], [string formatting], and of course the [ownership system].
|
||||
These features make it possible to write a kernel in a very expressive, high level way without worrying about [undefined behavior] or [memory safety].
|
||||
|
||||
[option]: https://doc.rust-lang.org/core/option/
|
||||
[result]:https://doc.rust-lang.org/core/result/
|
||||
@@ -46,21 +55,29 @@ While this means that we can't use most of the [Rust standard library], there ar
|
||||
[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
|
||||
[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
|
||||
|
||||
In order to create a minimal OS kernel in Rust, we start by creating an executable that can be run without an underlying operating system. Such an executable is often called a “freestanding” or “bare-metal” executable. We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel.
|
||||
In order to create a minimal OS kernel in Rust, we start by creating an executable that can be run without an underlying operating system.
|
||||
Such an executable is often called a “freestanding” or “bare-metal” executable.
|
||||
We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel.
|
||||
|
||||
## Disabling the Standard Library
|
||||
By default, all Rust crates link the [standard library], which depends on the operating system for features such as threads, files, or networking. It also depends on the C standard library `libc`, which closely interacts with OS services. Since our plan is to write an operating system, we cannot use any OS-dependent libraries. So we have to disable the automatic inclusion of the standard library, which we can do through the [`no_std` attribute].
|
||||
By default, all Rust crates link the [standard library], which depends on the operating system for features such as threads, files, or networking.
|
||||
It also depends on the C standard library `libc`, which closely interacts with OS services.
|
||||
Since our plan is to write an operating system, we cannot use any OS-dependent libraries.
|
||||
So we have to disable the automatic inclusion of the standard library, which we can do through the [`no_std` attribute].
|
||||
|
||||
[standard library]: https://doc.rust-lang.org/std/
|
||||
[`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||||
|
||||
We start by creating a new cargo application project. The easiest way to do this is through the command line:
|
||||
We start by creating a new cargo application project.
|
||||
The easiest way to do this is through the command line:
|
||||
|
||||
```
|
||||
cargo new blog_os --bin --edition 2018
|
||||
```
|
||||
|
||||
I named the project `blog_os`, but of course you can choose your own name. The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2018` flag specifies that we want to use the [2018 edition] of Rust for our crate. When we run the command, cargo creates the following directory structure for us:
|
||||
I named the project `blog_os`, but of course you can choose your own name.
|
||||
The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2018` flag specifies that we want to use the [2018 edition] of Rust for our crate.
|
||||
When we run the command, cargo creates the following directory structure for us:
|
||||
|
||||
[2018 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
@@ -71,13 +88,16 @@ blog_os
|
||||
└── main.rs
|
||||
```
|
||||
|
||||
The `Cargo.toml` contains the crate configuration, for example the crate name, the author, the [semantic version] number, and dependencies. The `src/main.rs` file contains the root module of our crate and our `main` function. You can compile your crate through `cargo build` and then run the compiled `blog_os` binary in the `target/debug` subfolder.
|
||||
The `Cargo.toml` contains the crate configuration, for example the crate name, the author, the [semantic version] number, and dependencies.
|
||||
The `src/main.rs` file contains the root module of our crate and our `main` function.
|
||||
You can compile your crate through `cargo build` and then run the compiled `blog_os` binary in the `target/debug` subfolder.
|
||||
|
||||
[semantic version]: https://semver.org/
|
||||
|
||||
### The `no_std` Attribute
|
||||
|
||||
Right now our crate implicitly links the standard library. Let's try to disable this by adding the [`no_std` attribute]:
|
||||
Right now our crate implicitly links the standard library.
|
||||
Let's try to disable this by adding the [`no_std` attribute]:
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
@@ -99,7 +119,9 @@ error: cannot find macro `println!` in this scope
|
||||
| ^^^^^^^
|
||||
```
|
||||
|
||||
The reason for this error is that the [`println` macro] is part of the standard library, which we no longer include. So we can no longer print things. This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system.
|
||||
The reason for this error is that the [`println` macro] is part of the standard library, which we no longer include.
|
||||
So we can no longer print things.
|
||||
This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system.
|
||||
|
||||
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
|
||||
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||||
@@ -124,7 +146,8 @@ Now the compiler is missing a `#[panic_handler]` function and a _language item_.
|
||||
|
||||
### Panic Implementation
|
||||
|
||||
The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs. The standard library provides its own panic handler function, but in a `no_std` environment we need to define it ourselves:
|
||||
The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs.
|
||||
The standard library provides its own panic handler function, but in a `no_std` environment we need to define it ourselves:
|
||||
|
||||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||||
|
||||
@@ -140,7 +163,9 @@ fn panic(_info: &PanicInfo) -> ! {
|
||||
}
|
||||
```
|
||||
|
||||
The [`PanicInfo` parameter][PanicInfo] contains the file and line where the panic happened and the optional panic message. The function should never return, so it is marked as a [diverging function] by returning the [“never” type] `!`. There is not much we can do in this function for now, so we just loop indefinitely.
|
||||
The [`PanicInfo` parameter][PanicInfo] contains the file and line where the panic happened and the optional panic message.
|
||||
The function should never return, so it is marked as a [diverging function] by returning the [“never” type] `!`.
|
||||
There is not much we can do in this function for now, so we just loop indefinitely.
|
||||
|
||||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||||
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
|
||||
@@ -155,14 +180,21 @@ error: language item required, but not found: `eh_personality`
|
||||
|
||||
### The `eh_personality` Language Item
|
||||
|
||||
Language items are special functions and types that are required internally by the compiler. For example, the [`Copy`] trait is a language item that tells the compiler which types have [_copy semantics_][`Copy`]. When we look at the [implementation][copy code], we see it has the special `#[lang = "copy"]` attribute that defines it as a language item.
|
||||
Language items are special functions and types that are required internally by the compiler.
|
||||
For example, the [`Copy`] trait is a language item that tells the compiler which types have [_copy semantics_][`Copy`].
|
||||
When we look at the [implementation][copy code], we see it has the special `#[lang = "copy"]` attribute that defines it as a language item.
|
||||
|
||||
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
|
||||
[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299
|
||||
|
||||
While providing custom implementations of language items is possible, it should only be done as a last resort. The reason is that language items are highly unstable implementation details and not even type checked (so the compiler doesn't even check if a function has the right argument types). Fortunately, there is a more stable way to fix the above language item error.
|
||||
While providing custom implementations of language items is possible, it should only be done as a last resort.
|
||||
The reason is that language items are highly unstable implementation details and not even type checked (so the compiler doesn't even check if a function has the right argument types).
|
||||
Fortunately, there is a more stable way to fix the above language item error.
|
||||
|
||||
The [`eh_personality` language item] marks a function that is used for implementing [stack unwinding]. By default, Rust uses unwinding to run the destructors of all live stack variables in case of a [panic]. This ensures that all used memory is freed and allows the parent thread to catch the panic and continue execution. Unwinding, however, is a complicated process and requires some OS specific libraries (e.g. [libunwind] on Linux or [structured exception handling] on Windows), so we don't want to use it for our operating system.
|
||||
The [`eh_personality` language item] marks a function that is used for implementing [stack unwinding].
|
||||
By default, Rust uses unwinding to run the destructors of all live stack variables in case of a [panic].
|
||||
This ensures that all used memory is freed and allows the parent thread to catch the panic and continue execution.
|
||||
Unwinding, however, is a complicated process and requires some OS specific libraries (e.g. [libunwind] on Linux or [structured exception handling] on Windows), so we don't want to use it for our operating system.
|
||||
|
||||
[`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
@@ -171,7 +203,9 @@ The [`eh_personality` language item] marks a function that is used for implement
|
||||
|
||||
#### Disabling Unwinding
|
||||
|
||||
There are other use cases as well for which unwinding is undesirable, so Rust provides an option to [abort on panic] instead. This disables the generation of unwinding symbol information and thus considerably reduces binary size. There are multiple ways to disable unwinding, the easiest is to add the following lines to our `Cargo.toml`:
|
||||
There are other use cases as well for which unwinding is undesirable, so Rust provides an option to [abort on panic] instead.
|
||||
This disables the generation of unwinding symbol information and thus considerably reduces binary size.
|
||||
There are multiple ways to disable unwinding, the easiest is to add the following lines to our `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[profile.dev]
|
||||
@@ -181,11 +215,13 @@ panic = "abort"
|
||||
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.
|
||||
This sets the panic strategy to `abort` for both the `dev` profile (used for `cargo build`) and the `release` profile (used for `cargo build --release`).
|
||||
Now the `eh_personality` language item should no longer be required.
|
||||
|
||||
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
|
||||
|
||||
Now we fixed both of the above errors. However, if we try to compile it now, another error occurs:
|
||||
Now we fixed both of the above errors.
|
||||
However, if we try to compile it now, another error occurs:
|
||||
|
||||
```
|
||||
> cargo build
|
||||
@@ -196,17 +232,25 @@ Our program is missing the `start` language item, which defines the entry point.
|
||||
|
||||
### The `start` Language Item
|
||||
|
||||
One might think that the `main` function is the first function called when a program is run. However, most languages have a [runtime system], which is responsible for things such as garbage collection (e.g. in Java) or software threads (e.g. goroutines in Go). This runtime needs to be called before `main`, since it needs to initialize itself.
|
||||
One might think that the `main` function is the first function called when a program is run.
|
||||
However, most languages have a [runtime system], which is responsible for things such as garbage collection (e.g. in Java) or software threads (e.g. goroutines in Go).
|
||||
This runtime needs to be called before `main`, since it needs to initialize itself.
|
||||
|
||||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||||
|
||||
In a typical Rust binary that links the standard library, execution starts in a C runtime library called [`crt0`] (“C runtime zero”), which sets up the environment for a C application. This includes creating a [call stack] and placing the command line arguments in the right CPU registers. The C runtime then invokes the [entry point of the Rust runtime][rt::lang_start], which is marked by the `start` language item. Rust only has a very minimal runtime, which takes care of some small things such as setting up stack overflow guards or printing a backtrace on panic. The runtime then finally calls the `main` function.
|
||||
In a typical Rust binary that links the standard library, execution starts in a C runtime library called [`crt0`] (“C runtime zero”), which sets up the environment for a C application.
|
||||
This includes creating a [call stack] and placing the command line arguments in the right CPU registers.
|
||||
The C runtime then invokes the [entry point of the Rust runtime][rt::lang_start], which is marked by the `start` language item.
|
||||
Rust only has a very minimal runtime, which takes care of some small things such as setting up stack overflow guards or printing a backtrace on panic.
|
||||
The runtime then finally calls the `main` function.
|
||||
|
||||
[`crt0`]: https://en.wikipedia.org/wiki/Crt0
|
||||
[call stack]: https://en.wikipedia.org/wiki/Call_stack
|
||||
[rt::lang_start]: hhttps://github.com/rust-lang/rust/blob/0d97f7a96877a96015d70ece41ad08bb7af12377/library/std/src/rt.rs#L59-L70
|
||||
|
||||
Our freestanding executable does not have access to the Rust runtime and `crt0`, so we need to define our own entry point. Implementing the `start` language item wouldn't help, since it would still require `crt0`. Instead, we need to overwrite the `crt0` entry point directly.
|
||||
Our freestanding executable does not have access to the Rust runtime and `crt0`, so we need to define our own entry point.
|
||||
Implementing the `start` language item wouldn't help, since it would still require `crt0`.
|
||||
Instead, we need to overwrite the `crt0` entry point directly.
|
||||
|
||||
#### Overwriting the Entry Point
|
||||
To tell the Rust compiler that we don't want to use the normal entry point chain, we add the `#![no_main]` attribute.
|
||||
@@ -224,7 +268,9 @@ fn panic(_info: &PanicInfo) -> ! {
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
You might notice that we removed the `main` function.
|
||||
The reason is that a `main` doesn't make sense without an underlying runtime that calls it.
|
||||
Instead, we are now overwriting the operating system entry point with our own `_start` function:
|
||||
|
||||
```rust
|
||||
#[no_mangle]
|
||||
@@ -233,9 +279,16 @@ pub extern "C" fn _start() -> ! {
|
||||
}
|
||||
```
|
||||
|
||||
By using the `#[no_mangle]` attribute we disable the [name mangling] to ensure that the Rust compiler really outputs a function with the name `_start`. Without the attribute, the compiler would generate some cryptic `_ZN3blog_os4_start7hb173fedf945531caE` symbol to give every function an unique name. The reason for naming the function `_start` is that this is the default entry point name for most systems.
|
||||
By using the `#[no_mangle]` attribute we disable the [name mangling] to ensure that the Rust compiler really outputs a function with the name `_start`.
|
||||
Without the attribute, the compiler would generate some cryptic `_ZN3blog_os4_start7hb173fedf945531caE` symbol to give every function an unique name.
|
||||
The reason for naming the function `_start` is that this is the default entry point name for most systems.
|
||||
|
||||
We mark the function as `extern "C"` to tell the compiler that it should use the [C calling convention] for this function (instead of the unspecified Rust calling convention). The `!` return type means that the function is diverging, i.e. not allowed to ever return. This is required because the entry point is not called by any function, but invoked directly by the operating system or bootloader. So instead of returning, the entry point should e.g. invoke the [`exit` system call] of the operating system. In our case, shutting down the machine could be a reasonable action, since there's nothing left to do if a freestanding binary returns. For now, we fulfill the requirement by looping endlessly.
|
||||
We mark the function as `extern "C"` to tell the compiler that it should use the [C calling convention] for this function (instead of the unspecified Rust calling convention).
|
||||
The `!` return type means that the function is diverging, i.e. not allowed to ever return.
|
||||
This is required because the entry point is not called by any function, but invoked directly by the operating system or bootloader.
|
||||
So instead of returning, the entry point should e.g. invoke the [`exit` system call] of the operating system.
|
||||
In our case, shutting down the machine could be a reasonable action, since there's nothing left to do if a freestanding binary returns.
|
||||
For now, we fulfill the requirement by looping endlessly.
|
||||
|
||||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||||
@@ -245,17 +298,26 @@ When we run `cargo build` now, we get an ugly _linker_ error.
|
||||
|
||||
## Linker Errors
|
||||
|
||||
The [linker] is a program that combines the generated code into an executable. Since the executable format differs between Linux, Windows, and macOS, each system has its own linker that throws a different error. The fundamental cause of the errors is the same: the default configuration of the linker assumes that our program depends on the C runtime, which it does not.
|
||||
The [linker] is a program that combines the generated code into an executable.
|
||||
Since the executable format differs between Linux, Windows, and macOS, each system has its own linker that throws a different error.
|
||||
The fundamental cause of the errors is the same: the default configuration of the linker assumes that our program depends on the C runtime, which it does not.
|
||||
|
||||
To solve the errors, we need to tell the linker that we want to build for a bare-metal target, where no underlying operating system or C runtime exist. As an alternative, it is also possible to disable the linking of the C runtime by passing a certain set of arguments to the linker.
|
||||
To solve the errors, we need to tell the linker that we want to build for a bare-metal target, where no underlying operating system or C runtime exist.
|
||||
As an alternative, it is also possible to disable the linking of the C runtime by passing a certain set of arguments to the linker.
|
||||
|
||||
### Linker Arguments
|
||||
|
||||
Linkers are very complex programs with a lot of configuration options. Each of the major operating systems (Linux, Windows, macOS) has its own linker implementation with different options, but all of them provide a way to disable the linking of the C runtime. By using these options, it is possible to create a freestanding executable that still runs on top of an existing operating system.
|
||||
Linkers are very complex programs with a lot of configuration options.
|
||||
Each of the major operating systems (Linux, Windows, macOS) has its own linker implementation with different options, but all of them provide a way to disable the linking of the C runtime.
|
||||
By using these options, it is possible to create a freestanding executable that still runs on top of an existing operating system.
|
||||
|
||||
_This is not what we want for our kernel, so this section is only provided for completeness. Feel free to skip this section if you like._
|
||||
_This is not what we want for our kernel, so this section is only provided for completeness.
|
||||
Feel free to skip this section if you like._
|
||||
|
||||
In the subsections below, we explain the required linker arguments for each operating system. It's worth noting that creating a freestanding executable this way is probably not a good idea. The reason is that our executable still expects various things, for example that a stack is initialized when the `_start` function is called. Without the C runtime, some of these requirements might not be fulfilled, which might cause our program to fail, e.g. by causing a segmentation fault.
|
||||
In the subsections below, we explain the required linker arguments for each operating system.
|
||||
It's worth noting that creating a freestanding executable this way is probably not a good idea.
|
||||
The reason is that our executable still expects various things, for example that a stack is initialized when the `_start` function is called.
|
||||
Without the C runtime, some of these requirements might not be fulfilled, which might cause our program to fail, e.g. by causing a segmentation fault.
|
||||
|
||||
If you want to create a minimal binary that runs on top of an existing operating system, including `libc` and setting the `#[start]` attribute as described [here](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) is probably a better idea.
|
||||
|
||||
@@ -280,9 +342,14 @@ error: linking with `cc` failed: exit code: 1
|
||||
collect2: error: ld returned 1 exit status
|
||||
```
|
||||
|
||||
The problem is that the linker includes the startup routine of the C runtime by default, which is also called `_start`. It requires some symbols of the C standard library `libc` that we don't include due to the `no_std` attribute, therefore the linker can't resolve these references. To solve this, we can tell the linker that it should not link the C startup routine by passing the `-nostartfiles` flag.
|
||||
The problem is that the linker includes the startup routine of the C runtime by default, which is also called `_start`.
|
||||
It requires some symbols of the C standard library `libc` that we don't include due to the `no_std` attribute, therefore the linker can't resolve these references.
|
||||
To solve this, we can tell the linker that it should not link the C startup routine by passing the `-nostartfiles` flag.
|
||||
|
||||
One way to pass linker attributes via cargo is the `cargo rustc` command. The command behaves exactly like `cargo build`, but allows to pass options to `rustc`, the underlying Rust compiler. `rustc` has the `-C link-arg` flag, which passes an argument to the linker. Combined, our new build command looks like this:
|
||||
One way to pass linker attributes via cargo is the `cargo rustc` command.
|
||||
The command behaves exactly like `cargo build`, but allows to pass options to `rustc`, the underlying Rust compiler.
|
||||
`rustc` has the `-C link-arg` flag, which passes an argument to the linker.
|
||||
Combined, our new build command looks like this:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-arg=-nostartfiles
|
||||
@@ -308,7 +375,10 @@ error: linking with `link.exe` failed: exit code: 1561
|
||||
= note: LINK : fatal error LNK1561: entry point must be defined
|
||||
```
|
||||
|
||||
The "entry point must be defined" error means that the linker can't find the entry point. On Windows, the default entry point name [depends on the used subsystem][windows-subsystems]. For the `CONSOLE` subsystem the linker looks for a function named `mainCRTStartup` and for the `WINDOWS` subsystem it looks for a function named `WinMainCRTStartup`. To override the default and tell the linker to look for our `_start` function instead, we can pass an `/ENTRY` argument to the linker:
|
||||
The "entry point must be defined" error means that the linker can't find the entry point.
|
||||
On Windows, the default entry point name [depends on the used subsystem][windows-subsystems].
|
||||
For the `CONSOLE` subsystem the linker looks for a function named `mainCRTStartup` and for the `WINDOWS` subsystem it looks for a function named `WinMainCRTStartup`.
|
||||
To override the default and tell the linker to look for our `_start` function instead, we can pass an `/ENTRY` argument to the linker:
|
||||
|
||||
[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol
|
||||
|
||||
@@ -328,13 +398,16 @@ error: linking with `link.exe` failed: exit code: 1221
|
||||
defined
|
||||
```
|
||||
|
||||
This error occurs because Windows executables can use different [subsystems][windows-subsystems]. For normal programs they are inferred depending on the entry point name: If the entry point is named `main`, the `CONSOLE` subsystem is used, and if the entry point is named `WinMain`, the `WINDOWS` subsystem is used. Since our `_start` function has a different name, we need to specify the subsystem explicitly:
|
||||
This error occurs because Windows executables can use different [subsystems][windows-subsystems].
|
||||
For normal programs they are inferred depending on the entry point name: If the entry point is named `main`, the `CONSOLE` subsystem is used, and if the entry point is named `WinMain`, the `WINDOWS` subsystem is used.
|
||||
Since our `_start` function has a different name, we need to specify the subsystem explicitly:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
|
||||
```
|
||||
|
||||
We use the `CONSOLE` subsystem here, but the `WINDOWS` subsystem would work too. Instead of passing `-C link-arg` multiple times, we use `-C link-args` which takes a space separated list of arguments.
|
||||
We use the `CONSOLE` subsystem here, but the `WINDOWS` subsystem would work too.
|
||||
Instead of passing `-C link-arg` multiple times, we use `-C link-args` which takes a space separated list of arguments.
|
||||
|
||||
With this command, our executable should build successfully on Windows.
|
||||
</details>
|
||||
@@ -351,17 +424,20 @@ On macOS, the following linker error occurs (shortened):
|
||||
error: linking with `cc` failed: exit code: 1
|
||||
|
|
||||
= note: "cc" […]
|
||||
= note: ld: entry point (_main) undefined. for architecture x86_64
|
||||
= note: ld: entry point (_main) undefined.
|
||||
for architecture x86_64
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
This error message tells us that the linker can't find an entry point function with the default name `main` (for some reason all functions are prefixed with a `_` on macOS). To set the entry point to our `_start` function, we pass the `-e` linker argument:
|
||||
This error message tells us that the linker can't find an entry point function with the default name `main` (for some reason all functions are prefixed with a `_` on macOS).
|
||||
To set the entry point to our `_start` function, we pass the `-e` linker argument:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start"
|
||||
```
|
||||
|
||||
The `-e` flag specifies the name of the entry point function. Since all functions have an additional `_` prefix on macOS, we need to set the entry point to `__start` instead of `_start`.
|
||||
The `-e` flag specifies the name of the entry point function.
|
||||
Since all functions have an additional `_` prefix on macOS, we need to set the entry point to `__start` instead of `_start`.
|
||||
|
||||
Now the following linker error occurs:
|
||||
|
||||
@@ -374,7 +450,8 @@ error: linking with `cc` failed: exit code: 1
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
macOS [does not officially support statically linked binaries] and requires programs to link the `libSystem` library by default. To override this and link a static binary, we pass the `-static` flag to the linker:
|
||||
macOS [does not officially support statically linked binaries] and requires programs to link the `libSystem` library by default.
|
||||
To override this and link a static binary, we pass the `-static` flag to the linker:
|
||||
|
||||
[does not officially support statically linked binaries]: https://developer.apple.com/library/archive/qa/qa1118/_index.html
|
||||
|
||||
@@ -392,7 +469,8 @@ error: linking with `cc` failed: exit code: 1
|
||||
clang: error: linker command failed with exit code 1 […]
|
||||
```
|
||||
|
||||
This error occurs because programs on macOS link to `crt0` (“C runtime zero”) by default. This is similar to the error we had on Linux and can be also solved by adding the `-nostartfiles` linker argument:
|
||||
This error occurs because programs on macOS link to `crt0` (“C runtime zero”) by default.
|
||||
This is similar to the error we had on Linux and can be also solved by adding the `-nostartfiles` linker argument:
|
||||
|
||||
```
|
||||
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
@@ -403,9 +481,12 @@ Now our program should build successfully on macOS.
|
||||
|
||||
### Building for a Bare Metal Target
|
||||
|
||||
By default Rust tries to build an executable that is able to run in your current system environment. For example, if you're using Windows and an `x86_64` CPU, Rust tries to build a `.exe` Windows executable that uses `x86_64` instructions. This environment is called your "host" system.
|
||||
By default Rust tries to build an executable that is able to run in your current system environment.
|
||||
For example, if you're using Windows and an `x86_64` CPU, Rust tries to build a `.exe` Windows executable that uses `x86_64` instructions.
|
||||
This environment is called your "host" system.
|
||||
|
||||
To describe different environments, Rust uses a string called [_target triple_]. You can see the target triple for your host system by running `rustc --version --verbose`:
|
||||
To describe different environments, Rust uses a string called [_target triple_].
|
||||
You can see the target triple for your host system by running `rustc --version --verbose`:
|
||||
|
||||
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
|
||||
|
||||
@@ -418,13 +499,17 @@ host: x86_64-unknown-linux-gnu
|
||||
release: 1.49.0
|
||||
```
|
||||
|
||||
The above output is from a `x86_64` Linux system. We see that the `host` triple is `x86_64-unknown-linux-gnu`, which includes the CPU architecture (`x86_64`), the vendor (`unknown`), the operating system (`linux`), and the [ABI] (`gnu`).
|
||||
The above output is from a `x86_64` Linux system.
|
||||
We see that the `host` triple is `x86_64-unknown-linux-gnu`, which includes the CPU architecture (`x86_64`), the vendor (`unknown`), the operating system (`linux`), and the [ABI] (`gnu`).
|
||||
|
||||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||||
|
||||
By compiling for our host triple, the Rust compiler and the linker assume that there is an underlying operating system such as Linux or Windows that uses the C runtime by default, which causes the linker errors. So to avoid the linker errors, we can compile for a different environment with no underlying operating system.
|
||||
By compiling for our host triple, the Rust compiler and the linker assume that there is an underlying operating system such as Linux or Windows that uses the C runtime by default, which causes the linker errors.
|
||||
So to avoid the linker errors, we can compile for a different environment with no underlying operating system.
|
||||
|
||||
An example for such a bare metal environment is the `thumbv7em-none-eabihf` target triple, which describes an [embedded] [ARM] system. The details are not important, all that matters is that the target triple has no underlying operating system, which is indicated by the `none` in the target triple. To be able to compile for this target, we need to add it in rustup:
|
||||
An example for such a bare metal environment is the `thumbv7em-none-eabihf` target triple, which describes an [embedded] [ARM] system.
|
||||
The details are not important, all that matters is that the target triple has no underlying operating system, which is indicated by the `none` in the target triple.
|
||||
To be able to compile for this target, we need to add it in rustup:
|
||||
|
||||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||||
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
|
||||
@@ -433,23 +518,29 @@ An example for such a bare metal environment is the `thumbv7em-none-eabihf` targ
|
||||
rustup target add thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
This downloads a pre-compiled copy of the `core` library for the target. Afterwards we can build our freestanding executable for the target:
|
||||
This downloads a pre-compiled copy of the `core` library for the target.
|
||||
Afterwards we can build our freestanding executable for the target:
|
||||
|
||||
```
|
||||
cargo build --target thumbv7em-none-eabihf
|
||||
```
|
||||
|
||||
By passing a `--target` argument we [cross compile] our executable for a bare metal target system. Since the target system has no operating system, the linker does not try to link the C runtime and our build succeeds without any linker errors.
|
||||
By passing a `--target` argument we [cross compile] our executable for a bare metal target system.
|
||||
Since the target system has no operating system, the linker does not try to link the C runtime and our build succeeds without any linker errors.
|
||||
|
||||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||||
|
||||
## Kernel Target
|
||||
|
||||
We just saw that we can compile our executable for a embedded ARM system by passing a `--target` argument. Rust supports [many different target triples][platform-support], including `arm-linux-androideabi` for Android or [`wasm32-unknown-unknown` for WebAssembly](https://www.hellorust.com/setup/wasm-target/).
|
||||
We just saw that we can compile our executable for a embedded ARM system by passing a `--target` argument.
|
||||
Rust supports [many different target triples][platform-support], including `arm-linux-androideabi` for Android or [`wasm32-unknown-unknown` for WebAssembly](https://www.hellorust.com/setup/wasm-target/).
|
||||
|
||||
[platform-support]: https://doc.rust-lang.org/nightly/rustc/platform-support.html
|
||||
|
||||
In order to create an operating system kernel, we need to choose a target that describes the environment on a bare-metal `x86_64` system. This requires some special configuration parameters (e.g. no underlying OS), so none of the officially supported target triples fit. Fortunately, Rust allows us to define [our own target][custom-targets] through a JSON file. For example, a JSON file that describes the `x86_64-unknown-linux-gnu` target looks like this:
|
||||
In order to create an operating system kernel, we need to choose a target that describes the environment on a bare-metal `x86_64` system.
|
||||
This requires some special configuration parameters (e.g. no underlying OS), so none of the officially supported target triples fit.
|
||||
Fortunately, Rust allows us to define [our own target][custom-targets] through a JSON file.
|
||||
For example, a JSON file that describes the `x86_64-unknown-linux-gnu` target looks like this:
|
||||
|
||||
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
|
||||
|
||||
@@ -469,14 +560,20 @@ In order to create an operating system kernel, we need to choose a target that d
|
||||
}
|
||||
```
|
||||
|
||||
Most fields are required by LLVM to generate code for that platform. For example, the [`data-layout`] field defines the size of various integer, floating point, and pointer types. Then there are fields that Rust uses for conditional compilation, such as `target-pointer-width`. The third kind of fields define how the crate should be built. For example, the `pre-link-args` field specifies arguments passed to the [linker]. For a full list of available fields and their meaning, check out the docs for Rust's internal [`Target`] and [`TargetOptions`] types.
|
||||
Most fields are required by LLVM to generate code for that platform.
|
||||
For example, the [`data-layout`] field defines the size of various integer, floating point, and pointer types.
|
||||
Then there are fields that Rust uses for conditional compilation, such as `target-pointer-width`.
|
||||
The third kind of fields define how the crate should be built.
|
||||
For example, the `pre-link-args` field specifies arguments passed to the [linker].
|
||||
For a full list of available fields and their meaning, check out the docs for Rust's internal [`Target`] and [`TargetOptions`] types.
|
||||
|
||||
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
|
||||
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
|
||||
[`Target`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.Target.html
|
||||
[`TargetOptions`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_target/spec/struct.TargetOptions.html
|
||||
|
||||
We also target `x86_64` systems with our kernel, so our target specification will look very similar to the one above. Let's start by creating a `x86_64-blog_os.json` file (choose any name you like) with the common content:
|
||||
We also target `x86_64` systems with our kernel, so our target specification will look very similar to the one above.
|
||||
Let's start by creating a `x86_64-blog_os.json` file (choose any name you like) with the common content:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -512,7 +609,8 @@ We add the following build-related entries:
|
||||
"panic-strategy": "abort",
|
||||
```
|
||||
|
||||
This setting specifies that the target doesn't support [stack unwinding] on panic, so instead the program should abort directly. This has the same effect as the `panic = "abort"` option in our Cargo.toml, so we can remove it from there.
|
||||
This setting specifies that the target doesn't support [stack unwinding] on panic, so instead the program should abort directly.
|
||||
This has the same effect as the `panic = "abort"` option in our Cargo.toml, so we can remove it from there.
|
||||
|
||||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||||
|
||||
@@ -522,7 +620,9 @@ We add the following build-related entries:
|
||||
"disable-redzone": true,
|
||||
```
|
||||
|
||||
We're writing a kernel, so we'll need to handle interrupts at some point. To do that safely, we have to disable a certain stack pointer optimization called the _“red zone”_, because it would cause stack corruptions otherwise. For more information, see our separate post about [disabling the red zone].
|
||||
We're writing a kernel, so we'll need to handle interrupts at some point.
|
||||
To do that safely, we have to disable a certain stack pointer optimization called the _“red zone”_, because it would cause stack corruptions otherwise.
|
||||
For more information, see our separate post about [disabling the red zone].
|
||||
|
||||
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
@@ -532,13 +632,21 @@ We add the following build-related entries:
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
```
|
||||
|
||||
The `features` field enables/disables target features. We disable the `mmx` and `sse` features by prefixing them with a minus and enable the `soft-float` feature by prefixing it with a plus. Note that there must be no spaces between different flags, otherwise LLVM fails to interpret the features string.
|
||||
The `features` field enables/disables target features.
|
||||
We disable the `mmx` and `sse` features by prefixing them with a minus and enable the `soft-float` feature by prefixing it with a plus.
|
||||
Note that there must be no spaces between different flags, otherwise LLVM fails to interpret the features string.
|
||||
|
||||
The `mmx` and `sse` features determine support for [Single Instruction Multiple Data (SIMD)] instructions, which can often speed up programs significantly. However, using the large SIMD registers in OS kernels leads to performance problems. The reason is that the kernel needs to restore all registers to their original state before continuing an interrupted program. This means that the kernel has to save the complete SIMD state to main memory on each system call or hardware interrupt. Since the SIMD state is very large (512–1600 bytes) and interrupts can occur very often, these additional save/restore operations considerably harm performance. To avoid this, we disable SIMD for our kernel (not for applications running on top!).
|
||||
The `mmx` and `sse` features determine support for [Single Instruction Multiple Data (SIMD)] instructions, which can often speed up programs significantly.
|
||||
However, using the large SIMD registers in OS kernels leads to performance problems.
|
||||
The reason is that the kernel needs to restore all registers to their original state before continuing an interrupted program.
|
||||
This means that the kernel has to save the complete SIMD state to main memory on each system call or hardware interrupt.
|
||||
Since the SIMD state is very large (512–1600 bytes) and interrupts can occur very often, these additional save/restore operations considerably harm performance.
|
||||
To avoid this, we disable SIMD for our kernel (not for applications running on top!).
|
||||
|
||||
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
|
||||
|
||||
A problem with disabling SIMD is that floating point operations on `x86_64` require SIMD registers by default. To solve this problem, we add the `soft-float` feature, which emulates all floating point operations through software functions based on normal integers.
|
||||
A problem with disabling SIMD is that floating point operations on `x86_64` require SIMD registers by default.
|
||||
To solve this problem, we add the `soft-float` feature, which emulates all floating point operations through software functions based on normal integers.
|
||||
|
||||
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
|
||||
@@ -572,20 +680,32 @@ To build our kernel for our new custom target we pass the path to the JSON file
|
||||
error[E0463]: can't find crate for `core`
|
||||
```
|
||||
|
||||
It fails! The error tells us that the Rust compiler no longer finds the [`core` library]. This library contains basic Rust types such as `Result`, `Option`, and iterators, and is implicitly linked to all `no_std` crates.
|
||||
It fails! The error tells us that the Rust compiler no longer finds the [`core` library].
|
||||
This library contains basic Rust types such as `Result`, `Option`, and iterators, and is implicitly linked to all `no_std` crates.
|
||||
|
||||
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
|
||||
|
||||
The problem is that the core library is distributed together with the Rust compiler as a precompiled library. These precompiled versions are available through `rustup` for all officially supported targets. We already saw this above, when we [built our kernel for the `thumbv7em-none-eabihf` target](#building-for-a-bare-metal-target). For our custom target, however, we need to build the `core` library ourselves.
|
||||
The problem is that the core library is distributed together with the Rust compiler as a precompiled library.
|
||||
These precompiled versions are available through `rustup` for all officially supported targets.
|
||||
We already saw this above, when we [built our kernel for the `thumbv7em-none-eabihf` target](#building-for-a-bare-metal-target).
|
||||
For our custom target, however, we need to build the `core` library ourselves.
|
||||
|
||||
While `cargo` has built-in support for building the `core` library, this feature is still considered [_unstable_][cargo-unstable]. Unstable features are only available in the "nightly" release channel of Rust, not on normal stable releases. So in order to build the `core` library, we need to install a nightly version of Rust first.
|
||||
While `cargo` has built-in support for building the `core` library, this feature is still considered [_unstable_][cargo-unstable].
|
||||
Unstable features are only available in the "nightly" release channel of Rust, not on normal stable releases.
|
||||
So in order to build the `core` library, we need to install a nightly version of Rust first.
|
||||
|
||||
[cargo-unstable]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html
|
||||
|
||||
### Installing Rust Nightly
|
||||
Rust has three release channels: _stable_, _beta_, and _nightly_. The Rust Book explains the difference between these channels really well, so take a minute and [check it out](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Apart from the availability of unstable features, there is not really a difference between nightly and stable releases. Every 6 weeks, the current nightly is released on the beta channel and the current beta is released as stable. Since we will need some unstable features for our operating system (such as building `core`), we need to install a nightly version of Rust.
|
||||
Rust has three release channels: _stable_, _beta_, and _nightly_.
|
||||
The Rust Book explains the difference between these channels really well, so take a minute and [check it out](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains).
|
||||
Apart from the availability of unstable features, there is not really a difference between nightly and stable releases.
|
||||
Every 6 weeks, the current nightly is released on the beta channel and the current beta is released as stable.
|
||||
Since we will need some unstable features for our operating system (such as building `core`), we need to install a nightly version of Rust.
|
||||
|
||||
The recommend tool to manage Rust installations is [rustup]. It allows you to install nightly, beta, and stable compilers side-by-side and makes it easy to update them. With rustup you can use a nightly compiler for the current directory by running:
|
||||
The recommend tool to manage Rust installations is [rustup].
|
||||
It allows you to install nightly, beta, and stable compilers side-by-side and makes it easy to update them.
|
||||
With rustup you can use a nightly compiler for the current directory by running:
|
||||
|
||||
```
|
||||
rustup override set nightly
|
||||
@@ -598,7 +718,8 @@ Alternatively, you can add a file called **`rust-toolchain`** to the project's r
|
||||
channel = "nightly"
|
||||
```
|
||||
|
||||
After doing one of these things, both the `cargo` and `rustc` command should use a nightly version of Rust when invoked from within the current directory. You can verify that you have a nightly version installed and active by running `rustc --version`: The version number should contain `-nightly` at the end, for example:
|
||||
After doing one of these things, both the `cargo` and `rustc` command should use a nightly version of Rust when invoked from within the current directory.
|
||||
You can verify that you have a nightly version installed and active by running `rustc --version`: The version number should contain `-nightly` at the end, for example:
|
||||
|
||||
[rustup]: https://www.rustup.rs/
|
||||
|
||||
@@ -608,18 +729,24 @@ rustc 1.51.0-nightly (04caa632d 2021-01-30)
|
||||
|
||||
<div class="note">
|
||||
|
||||
Note that this version number is just an example, your version should be newer. This post and the rest of the blog is regularly updated to always compile on the newest nightly version. So if something doesn't work try updating to the latest nightly by running `rustup update nightly`.
|
||||
Note that this version number is just an example, your version should be newer.
|
||||
This post and the rest of the blog is regularly updated to always compile on the newest nightly version.
|
||||
So if something doesn't work try updating to the latest nightly by running `rustup update nightly`.
|
||||
|
||||
</div>
|
||||
|
||||
In addition to building `core`, using a nightly compiler allows us to opt-in to [various experimental features] by using so-called _feature flags_ at the top of our file. For example, we could enable the experimental [`asm!` macro] for inline assembly by adding `#![feature(asm)]` to the top of our `main.rs`. Note that such experimental features are completely unstable, which means that future Rust versions might change or remove them without prior warning. For this reason we will only use them if absolutely necessary.
|
||||
In addition to building `core`, using a nightly compiler allows us to opt-in to [various experimental features] by using so-called _feature flags_ at the top of our file.
|
||||
For example, we could enable the experimental [`asm!` macro] for inline assembly by adding `#![feature(asm)]` to the top of our `main.rs`.
|
||||
Note that such experimental features are completely unstable, which means that future Rust versions might change or remove them without prior warning.
|
||||
For this reason we will only use them if absolutely necessary.
|
||||
|
||||
[various experimental features]: https://doc.rust-lang.org/unstable-book/the-unstable-book.html
|
||||
[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
### The `build-std` Option
|
||||
|
||||
Now that we switched to nightly Rust, we are able use the [`build-std` feature] of cargo. It allows to build `core` and other standard library crates on demand, instead of using the precompiled versions shipped with the Rust installation.
|
||||
Now that we switched to nightly Rust, we are able use the [`build-std` feature] of cargo.
|
||||
It allows to build `core` and other standard library crates on demand, instead of using the precompiled versions shipped with the Rust installation.
|
||||
|
||||
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
|
||||
|
||||
@@ -633,7 +760,9 @@ unable to build with the standard library, try:
|
||||
rustup component add rust-src
|
||||
```
|
||||
|
||||
It still fails. The problem is that cargo needs a copy of the Rust source code in order to recompile the `core` crate. The error message helpfully suggest to provide such a copy by installing the `rust-src` component.
|
||||
It still fails.
|
||||
The problem is that cargo needs a copy of the Rust source code in order to recompile the `core` crate.
|
||||
The error message helpfully suggest to provide such a copy by installing the `rust-src` component.
|
||||
|
||||
Instead of running the suggested `rustup component add rust-src` command, we an also record the dependency on the `rust-src` component in our `rust-toolchain` file:
|
||||
|
||||
@@ -645,7 +774,8 @@ channel = "nightly"
|
||||
components = ["rust-src"]
|
||||
```
|
||||
|
||||
This way, `rustup` will automatically download the required components so that no manual steps are necessary. You can also automatically install components such as [`rustfmt`] or [`clippy`] this way if you like to use them:
|
||||
This way, `rustup` will automatically download the required components so that no manual steps are necessary.
|
||||
You can also automatically install components such as [`rustfmt`] or [`clippy`] this way if you like to use them:
|
||||
|
||||
|
||||
```toml
|
||||
@@ -674,13 +804,24 @@ We see that `cargo build` now builds the `core`, `compiler_builtins` (a dependen
|
||||
|
||||
### Memory-Related Intrinsics
|
||||
|
||||
The Rust compiler assumes that a certain set of built-in functions is available for all systems. Most of these functions are provided by the `compiler_builtins` crate that we just built. However, there are some memory-related functions in that crate that are not enabled by default because they are normally provided by the C library on the system. These functions include `memset`, which sets all bytes in a memory block to a given value, `memcpy`, which copies one memory block to another, and `memcmp`, which compares two memory blocks. While we didn't need any of these functions to compile our kernel right now, they will be required as soon as we add some more code to it (e.g. when copying structs around).
|
||||
The Rust compiler assumes that a certain set of built-in functions is available for all systems.
|
||||
Most of these functions are provided by the `compiler_builtins` crate that we just built.
|
||||
However, there are some memory-related functions in that crate that are not enabled by default because they are normally provided by the C library on the system.
|
||||
These functions include `memset`, which sets all bytes in a memory block to a given value, `memcpy`, which copies one memory block to another, and `memcmp`, which compares two memory blocks.
|
||||
While we didn't need any of these functions to compile our kernel right now, they will be required as soon as we add some more code to it (e.g. when copying structs around).
|
||||
|
||||
Since we can't link to the C library of the operating system, we need an alternative way to provide these functions to the compiler. One possible approach for this could be to implement our own `memset` etc. functions and apply the `#[no_mangle]` attribute to them (to avoid the automatic renaming during compilation). However, this is dangerous since the slightest mistake in the implementation of these functions could lead to bugs and undefined behavior. For example, you might get an endless recursion when implementing `memcpy` using a `for` loop because `for` loops implicitly call the [`IntoIterator::into_iter`] trait method, which might call `memcpy` again. So it's a good idea to reuse existing well-tested implementations instead of creating your own.
|
||||
Since we can't link to the C library of the operating system, we need an alternative way to provide these functions to the compiler.
|
||||
One possible approach for this could be to implement our own `memset` etc.
|
||||
functions and apply the `#[no_mangle]` attribute to them (to avoid the automatic renaming during compilation).
|
||||
However, this is dangerous since the slightest mistake in the implementation of these functions could lead to bugs and undefined behavior.
|
||||
For example, you might get an endless recursion when implementing `memcpy` using a `for` loop because `for` loops implicitly call the [`IntoIterator::into_iter`] trait method, which might call `memcpy` again.
|
||||
So it's a good idea to reuse existing well-tested implementations instead of creating your own.
|
||||
|
||||
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
|
||||
|
||||
Fortunately, the `compiler_builtins` crate already contains implementations for all the needed functions, they are just disabled by default to not collide with the implementations from the C library. We can enable them by passing an additional `-Z build-std-features=compiler-builtins-mem` flag to `cargo`. Like the `build-std` flag, this [`build-std-features`] flag is still unstable, so it might change in the future.
|
||||
Fortunately, the `compiler_builtins` crate already contains implementations for all the needed functions, they are just disabled by default to not collide with the implementations from the C library.
|
||||
We can enable them by passing an additional `-Z build-std-features=compiler-builtins-mem` flag to `cargo`.
|
||||
Like the `build-std` flag, this [`build-std-features`] flag is still unstable, so it might change in the future.
|
||||
|
||||
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
|
||||
|
||||
@@ -691,36 +832,51 @@ cargo build --target x86_64-blog_os.json -Z build-std=core \
|
||||
-Z build-std-features=compiler-builtins-mem
|
||||
```
|
||||
|
||||
Behind the scenes, the new flag enables the [`mem` feature] of the `compiler_builtins` crate. The effect of this is that the `#[no_mangle]` attribute is applied to the [`memcpy` etc. implementations] of the crate, which makes them available to the linker. It's worth noting that these functions are already optimized using [inline assembly] on `x86_64`, so their performance should be much better than a custom loop-based implementation.
|
||||
Behind the scenes, the new flag enables the [`mem` feature] of the `compiler_builtins` crate.
|
||||
The effect of this is that the `#[no_mangle]` attribute is applied to the [`memcpy` etc.
|
||||
implementations] of the crate, which makes them available to the linker.
|
||||
It's worth noting that these functions are already optimized using [inline assembly] on `x86_64`, so their performance should be much better than a custom loop-based implementation.
|
||||
|
||||
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
|
||||
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
[`memcpy` etc.
|
||||
implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
[inline assembly]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
|
||||
|
||||
With the additional `compiler-builtins-mem` flag, our kernel now has valid implementations for all compiler-required functions, so it will continue to compile even if our code gets more complex.
|
||||
|
||||
## A Shorter Build Command
|
||||
|
||||
Our build command is quite long now, so it's a bit cumbersome to type and difficult to remember. So let's try to shorten it!
|
||||
Our build command is quite long now, so it's a bit cumbersome to type and difficult to remember.
|
||||
So let's try to shorten it!
|
||||
|
||||
### Setting Defaults
|
||||
|
||||
Since we want to always pass these flags to our build command, it would make sense to set them as default. Unfortunately, Cargo currently only supports changing the default build command through [`.cargo/config.toml`] configuration files. The problem with these files is that they are applied based on the current working directory, not based on the compiled project. This leads to [various problems][cargo-config-problems], for example that the settings also apply to all crates in subdirectories. These problems make `.cargo/config.toml` files unsuitable for our use case, since the code in the next post would be broken this way.
|
||||
Since we want to always pass these flags to our build command, it would make sense to set them as default.
|
||||
Unfortunately, Cargo currently only supports changing the default build command through [`.cargo/config.toml`] configuration files.
|
||||
The problem with these files is that they are applied based on the current working directory, not based on the compiled project.
|
||||
This leads to [various problems][cargo-config-problems], for example that the settings also apply to all crates in subdirectories.
|
||||
These problems make `.cargo/config.toml` files unsuitable for our use case, since the code in the next post would be broken this way.
|
||||
|
||||
[`.cargo/config.toml`]: https://doc.rust-lang.org/cargo/reference/config.html
|
||||
[cargo-config-problems]: https://internals.rust-lang.org/t/problems-of-cargo-config-files-and-possible-solutions/12987
|
||||
|
||||
To fix these problems, I proposed to [move some `.cargo/config.toml` settings to `Cargo.toml`][internals-proposal] to make them crate-specific. This would allow us to set proper defaults for our kernel too. So let's hope that it is implemented soon :). Until then, we can use _aliases_ to shorten our build command.
|
||||
To fix these problems, I proposed to [move some `.cargo/config.toml` settings to `Cargo.toml`][internals-proposal] to make them crate-specific.
|
||||
This would allow us to set proper defaults for our kernel too.
|
||||
So let's hope that it is implemented soon :).
|
||||
Until then, we can use _aliases_ to shorten our build command.
|
||||
|
||||
[internals-proposal]: https://internals.rust-lang.org/t/proposal-move-some-cargo-config-settings-to-cargo-toml/13336
|
||||
|
||||
### Aliases
|
||||
|
||||
Cargo allows to define custom [command aliases], for example `cargo br` for `cargo build --release`. While these aliases are defined in a `.cargo/config.toml` file too, they apply only to the command-line invocation and don't affect the normal build process of other crates. Thus, we can use them without problems.
|
||||
Cargo allows to define custom [command aliases], for example `cargo br` for `cargo build --release`.
|
||||
While these aliases are defined in a `.cargo/config.toml` file too, they apply only to the command-line invocation and don't affect the normal build process of other crates.
|
||||
Thus, we can use them without problems.
|
||||
|
||||
[command aliases]: https://doc.rust-lang.org/cargo/reference/config.html#alias
|
||||
|
||||
To shorten our build command using an alias, we first need to create a directory named `.cargo` in the crate's root (i.e. next to the `Cargo.toml`). In that directory, we create a new file named `config.toml` with the following content:
|
||||
To shorten our build command using an alias, we first need to create a directory named `.cargo` in the crate's root (i.e. next to the `Cargo.toml`).
|
||||
In that directory, we create a new file named `config.toml` with the following content:
|
||||
|
||||
```toml
|
||||
[alias]
|
||||
@@ -728,15 +884,19 @@ kbuild = """build --target x86_64-blog_os.json -Z build-std=core \
|
||||
-Z build-std-features=compiler-builtins-mem"""
|
||||
```
|
||||
|
||||
This defines a new `kbuild` command (for "kernel build") that expands to the long build command of our kernel. Now we can build our kernel by running just:
|
||||
This defines a new `kbuild` command (for "kernel build") that expands to the long build command of our kernel.
|
||||
Now we can build our kernel by running just:
|
||||
|
||||
```
|
||||
cargo kbuild
|
||||
```
|
||||
|
||||
The name of the alias doesn't matter, so you can also name the alias `kb` if you like it even shorter. Note that overriding the built-in `build` command is not possible.
|
||||
The name of the alias doesn't matter, so you can also name the alias `kb` if you like it even shorter.
|
||||
Note that overriding the built-in `build` command is not possible.
|
||||
|
||||
One drawback of the alias approach is that you need to define a separate alias for every cargo subcommand (e.g. [`cargo check`], [`cargo doc`], or [`cargo clippy`][`clippy`]), which you want to use. You also need to adjust your IDE (e.g. [rust-analyzer]) to use a non-standard build/check command. So this approach is clearly just a workaround until proper package-specific defaults are implemented in Cargo.
|
||||
One drawback of the alias approach is that you need to define a separate alias for every cargo subcommand (e.g. [`cargo check`], [`cargo doc`], or [`cargo clippy`][`clippy`]), which you want to use.
|
||||
You also need to adjust your IDE (e.g. [rust-analyzer]) to use a non-standard build/check command.
|
||||
So this approach is clearly just a workaround until proper package-specific defaults are implemented in Cargo.
|
||||
|
||||
[`cargo check`]: https://doc.rust-lang.org/cargo/commands/cargo-check.html
|
||||
[`cargo doc`]: https://doc.rust-lang.org/cargo/commands/cargo-doc.html
|
||||
@@ -744,6 +904,7 @@ One drawback of the alias approach is that you need to define a separate alias f
|
||||
|
||||
## What's next?
|
||||
|
||||
In the [next post], we will learn how to turn our minimal kernel in a bootable disk image, which can then be started in the [QEMU] virtual machine and on real hardware. For this, we'll explore the boot process of `x86_64` systems and learn about the differences between UEFI and the legacy BIOS firmware.
|
||||
In the [next post], we will learn how to turn our minimal kernel in a bootable disk image, which can then be started in the [QEMU] virtual machine and on real hardware.
|
||||
For this, we'll explore the boot process of `x86_64` systems and learn about the differences between UEFI and the legacy BIOS firmware.
|
||||
|
||||
[next post]: @/edition-3/posts/02-booting/index.md
|
||||
|
||||
Reference in New Issue
Block a user