Merge pull request #836 from phil-opp/build-std

Update blog to use `build-std` feature instead of cargo-xbuild
This commit is contained in:
Philipp Oppermann
2020-07-17 13:10:16 +02:00
committed by GitHub
10 changed files with 129 additions and 80 deletions

View File

@@ -423,10 +423,10 @@ Now our program should build successfully on macOS.
#### Unifying the Build Commands
Right now we have different build commands depending on the host platform, which is not ideal. To avoid this, we can create a file named `.cargo/config` that contains the platform specific arguments:
Right now we have different build commands depending on the host platform, which is not ideal. To avoid this, we can create a file named `.cargo/config.toml` that contains the platform specific arguments:
```toml
# in .cargo/config
# in .cargo/config.toml
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-nostartfiles"]
@@ -438,7 +438,7 @@ rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]
```
The `rustflags` key contains arguments that are automatically added to every invocation of `rustc`. For more information on the `.cargo/config` file check out the [official documentation](https://doc.rust-lang.org/cargo/reference/config.html).
The `rustflags` key contains arguments that are automatically added to every invocation of `rustc`. For more information on the `.cargo/config.toml` file check out the [official documentation](https://doc.rust-lang.org/cargo/reference/config.html).
Now our program should be buildable on all three platforms with a simple `cargo build`.

View File

@@ -251,50 +251,86 @@ It fails! The error tells us that the Rust compiler no longer finds the [`core`
The problem is that the core library is distributed together with the Rust compiler as a _precompiled_ library. So it is only valid for supported host triples (e.g., `x86_64-unknown-linux-gnu`) but not for our custom target. If we want to compile code for other targets, we need to recompile `core` for these targets first.
#### Cargo xbuild
That's where [`cargo xbuild`] comes in. It is a wrapper for `cargo build` that automatically cross-compiles `core` and other built-in libraries. We can install it by executing:
#### The `build-std` Option
[`cargo xbuild`]: https://github.com/rust-osdev/cargo-xbuild
That's where the [`build-std` feature] of cargo comes in. It allows to recompile `core` and other standard library crates on demand, instead of using the precompiled versions shipped with the Rust installation. This feature is very new and still not finished, so it is marked as "unstable" and only available on [nightly Rust compilers].
```
cargo install cargo-xbuild
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust compilers]: #installing-rust-nightly
To use the feature, we need to create a [cargo configuration] file at `.cargo/config.toml` with the following content:
```toml
# in .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
```
The command depends on the rust source code, which we can install with `rustup component add rust-src`.
This tells cargo that it should recompile the `core` and `compiler_builtins` libraries. The latter is required because it is a dependency of `core`. In order to recompile these libraries, cargo needs access to the rust source code, which we can install with `rustup component add rust-src`.
Now we can rerun the above command with `xbuild` instead of `build`:
<div class="note">
**Note:** The `unstable.build-std` configuration key requires at least the Rust nightly from 2020-07-15. Right now, the `rustfmt` component is [not available](https://rust-lang.github.io/rustup-components-history/) on these recent nightlies, so you might need to use `rustup update nightly --force` to update your nightly, which skips the `rustfmt` component if it's not available.
</div>
After setting the `unstable.build-std` configuration key and installing the `rust-src` component, we can rerun the our build command:
```
> cargo xbuild --target x86_64-blog_os.json
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling compiler_builtins v0.1.5
Compiling rustc-std-workspace-core v1.0.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling alloc v0.0.0 (/tmp/xargo.PB7fj9KZJhAI)
Finished release [optimized + debuginfo] target(s) in 45.18s
Compiling blog_os v0.1.0 (file:///…/blog_os)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
We see that `cargo xbuild` cross-compiles the `core`, `compiler_builtin`, and `alloc` libraries for our new custom target. Since these libraries use a lot of unstable features internally, this only works with a [nightly Rust compiler]. Afterwards, `cargo xbuild` successfully compiles our `blog_os` crate.
We see that `cargo build` now recompiles the `core`, `rustc-std-workspace-core` (another dependency of `core`), and `compiler_builtin` libraries for our new custom target. Since these libraries use a lot of unstable features internally, this only works with a [nightly Rust compiler].
[nightly Rust compiler]: #installing-rust-nightly
#### The `rlibc` Crate
Now we are able to build our kernel for a bare metal target. However, our `_start` entry point, which will be called by the boot loader, is still empty. So let's output something to screen from it.
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 recompiled. However, there are some 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.
### Set a Default Target
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. So it's a good idea to provide implementations for these functions now to avoid linker errors later. While there is no way to enable the implementations of the `compiler_builtins` crate yet (see the [tracking issue](https://github.com/rust-lang/wg-cargo-std-aware/issues/15)), there is a good alternative: the [`rlibc`] crate.
To avoid passing the `--target` parameter on every invocation of `cargo xbuild`, we can override the default target. To do this, we create a [cargo configuration] file at `.cargo/config` with the following content:
[`rlibc`]: https://docs.rs/rlibc/1.0.0/rlibc/
To include the crate, we need to add it as a dependency in our `Cargo.toml` file:
```toml
# in Cargo.toml
[dependencies]
rlibc = "1.0.0"
```
For normal crates, this would be enough. However, since we never use any functions of `rlibc` directly, we need to explicitly instruct the Rust compiler to link the crate. We can do so by adding the following to our `main.rs`:
```rust
// in main.rs
extern crate rlibc;
```
With this change, our kernel has valid implementations for all required functions, so it will continue to compile even if our code gets more complex.
#### Set a Default Target
To avoid passing the `--target` parameter on every invocation of `cargo build`, we can override the default target. To do this, we add the following to our [cargo configuration] file at `.cargo/config.toml`:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
This tells `cargo` to use our `x86_64-blog_os.json` target when no explicit `--target` argument is passed. This means that we can now build our kernel with a simple `cargo xbuild`. For more information on cargo configuration options, check out the [official documentation][cargo configuration].
This tells `cargo` to use our `x86_64-blog_os.json` target when no explicit `--target` argument is passed. This means that we can now build our kernel with a simple `cargo build`. For more information on cargo configuration options, check out the [official documentation][cargo configuration].
We are now able to build our kernel for a bare metal target with a simple `cargo build`. However, our `_start` entry point, which will be called by the boot loader, is still empty. It's time that we output something to screen from it.
### Printing to Screen
The easiest way to print text to the screen at this stage is the [VGA text buffer]. It is a special memory area mapped to the VGA hardware that contains the contents displayed on screen. It normally consists of 25 lines that each contain 80 character cells. Each character cell displays an ASCII character with some foreground and background colors. The screen output looks like this:
@@ -363,7 +399,7 @@ Instead of writing our own bootloader, which is a project on its own, we use the
# in Cargo.toml
[dependencies]
bootloader = "0.9.3"
bootloader = "0.9.8"
```
Adding the bootloader as dependency is not enough to actually create a bootable disk image. The problem is that we need to link our kernel with the bootloader after compilation, but cargo has no support for [post-build scripts].
@@ -384,7 +420,7 @@ After installing `bootimage` and adding the `llvm-tools-preview` component, we c
> cargo bootimage
```
We see that the tool recompiles our kernel using `cargo xbuild`, so it will automatically pick up any changes you make. Afterwards it compiles the bootloader, which might take a while. Like all crate dependencies it is only built once and then cached, so subsequent builds will be much faster. Finally, `bootimage` combines the bootloader and your kernel to a bootable disk image.
We see that the tool recompiles our kernel using `cargo build`, so it will automatically pick up any changes you make. Afterwards it compiles the bootloader, which might take a while. Like all crate dependencies it is only built once and then cached, so subsequent builds will be much faster. Finally, `bootimage` combines the bootloader and your kernel to a bootable disk image.
After executing the command, you should see a bootable disk image named `bootimage-blog_os.bin` in your `target/x86_64-blog_os/debug` directory. You can boot it in a virtual machine or copy it to an USB drive to boot it on real hardware. (Note that this is not a CD image, which have a different format, so burning it to a CD doesn't work).
@@ -434,7 +470,7 @@ After writing the image to the USB stick, you can run it on real hardware by boo
To make it easier to run our kernel in QEMU, we can set the `runner` configuration key for cargo:
```toml
# in .cargo/config
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
@@ -446,7 +482,7 @@ The `bootimage runner` command is specifically designed to be usable as a `runne
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
Now we can use `cargo xrun` to compile our kernel and boot it in QEMU. Like `xbuild`, the `xrun` subcommand builds the sysroot crates before invoking the actual cargo command. The subcommand is also provided by `cargo-xbuild`, so you don't need to install an additional tool.
Now we can use `cargo run` to compile our kernel and boot it in QEMU.
## What's next?

View File

@@ -22,7 +22,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
## Requirements
This post replaces the (now deprecated) [_Unit Testing_] and [_Integration Tests_] posts. It assumes that you have followed the [_A Minimal Rust Kernel_] post after 2019-04-27. Mainly, it requires that you have a `.cargo/config` file that [sets a default target] and [defines a runner executable].
This post replaces the (now deprecated) [_Unit Testing_] and [_Integration Tests_] posts. It assumes that you have followed the [_A Minimal Rust Kernel_] post after 2019-04-27. Mainly, it requires that you have a `.cargo/config.toml` file that [sets a default target] and [defines a runner executable].
[_Unit Testing_]: @/second-edition/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/second-edition/posts/deprecated/05-integration-tests/index.md
@@ -40,10 +40,10 @@ Unfortunately it's a bit more complicated for `no_std` applications such as our
[`test`]: https://doc.rust-lang.org/test/index.html
We can see this when we try to run `cargo xtest` in our project:
We can see this when we try to run `cargo test` in our project:
```
> cargo xtest
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
@@ -85,7 +85,7 @@ Our runner just prints a short debug message and then calls each test function i
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
When we run `cargo xtest` now, we see that it now succeeds. However, we still see our "Hello World" instead of the message from our `test_runner`. The reason is that our `_start` function is still used as entry point. The custom test frameworks feature generates a `main` function that calls `test_runner`, but this function is ignored because we use the `#[no_main]` attribute and provide our own entry point.
When we run `cargo test` now, we see that it now succeeds. However, we still see our "Hello World" instead of the message from our `test_runner`. The reason is that our `_start` function is still used as entry point. The custom test frameworks feature generates a `main` function that calls `test_runner`, but this function is ignored because we use the `#[no_main]` attribute and provide our own entry point.
To fix this, we first need to change the name of the generated function to something different than `main` through the `reexport_test_harness_main` attribute. Then we can call the renamed function from our `_start` function:
@@ -107,7 +107,7 @@ pub extern "C" fn _start() -> ! {
We set the name of the test framework entry function to `test_main` and call it from our `_start` entry point. We use [conditional compilation] to add the call to `test_main` only in test contexts because the function is not generated on a normal run.
When we now execute `cargo xtest`, we see the "Running 0 tests" message from our `test_runner` on the screen. We are now ready to create our first test function:
When we now execute `cargo test`, we see the "Running 0 tests" message from our `test_runner` on the screen. We are now ready to create our first test function:
```rust
// in src/main.rs
@@ -120,17 +120,17 @@ fn trivial_assertion() {
}
```
When we run `cargo xtest` now, we see the following output:
When we run `cargo test` now, we see the following output:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png)
The `tests` slice passed to our `test_runner` function now contains a reference to the `trivial_assertion` function. From the `trivial assertion... [ok]` output on the screen we see that the test was called and that it succeeded.
After executing the tests, our `test_runner` returns to the `test_main` function, which in turn returns to our `_start` entry point function. At the end of `_start`, we enter an endless loop because the entry point function is not allowed to return. This is a problem, because we want `cargo xtest` to exit after running all tests.
After executing the tests, our `test_runner` returns to the `test_main` function, which in turn returns to our `_start` entry point function. At the end of `_start`, we enter an endless loop because the entry point function is not allowed to return. This is a problem, because we want `cargo test` to exit after running all tests.
## Exiting QEMU
Right now we have an endless loop at the end of our `_start` function and need to close QEMU manually on each execution of `cargo xtest`. This is unfortunate because we also want to run `cargo xtest` in scripts without user interaction. The clean solution to this would be to implement a proper way to shutdown our OS. Unfortunately this is relatively complex, because it requires implementing support for either the [APM] or [ACPI] power management standard.
Right now we have an endless loop at the end of our `_start` function and need to close QEMU manually on each execution of `cargo test`. This is unfortunate because we also want to run `cargo test` in scripts without user interaction. The clean solution to this would be to implement a proper way to shutdown our OS. Unfortunately this is relatively complex, because it requires implementing support for either the [APM] or [ACPI] power management standard.
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
@@ -144,7 +144,7 @@ Luckily, there is an escape hatch: QEMU supports a special `isa-debug-exit` devi
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
The `bootimage runner` appends the `test-args` to the default QEMU command for all test executables. For a normal `cargo xrun`, the arguments are ignored.
The `bootimage runner` appends the `test-args` to the default QEMU command for all test executables. For a normal `cargo run`, the arguments are ignored.
Together with the device name (`isa-debug-exit`), we pass the two parameters `iobase` and `iosize` that specify the _I/O port_ through which the device can be reached from our kernel.
@@ -218,10 +218,10 @@ fn test_runner(tests: &[&dyn Fn()]) {
}
```
When we run `cargo xtest` now, we see that QEMU immediately closes after executing the tests. The problem is that `cargo test` interprets the test as failed even though we passed our `Success` exit code:
When we run `cargo test` now, we see that QEMU immediately closes after executing the tests. The problem is that `cargo test` interprets the test as failed even though we passed our `Success` exit code:
```
> cargo xtest
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
@@ -245,7 +245,7 @@ test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
With this configuration, `bootimage` maps our success exit code to exit code 0, so that `cargo xtest` correctly recognizes the success case and does not count the test as failed.
With this configuration, `bootimage` maps our success exit code to exit code 0, so that `cargo test` correctly recognizes the success case and does not count the test as failed.
Our test runner now automatically closes QEMU and correctly reports the test results out. We still see the QEMU window open for a very short time, but it does not suffice to read the results. It would be nice if we could print the test results to the console instead, so that we can still see them after QEMU exited.
@@ -371,10 +371,10 @@ test-args = [
]
```
When we run `cargo xtest` now, we see the test output directly in the console:
When we run `cargo test` now, we see the test output directly in the console:
```
> cargo xtest
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
@@ -424,7 +424,7 @@ For our test panic handler, we use `serial_println` instead of `println` and the
Now QEMU also exits for failed tests and prints a useful error message on the console:
```
> cargo xtest
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
@@ -462,16 +462,16 @@ Now QEMU runs completely in the background and no window is opened anymore. This
### Timeouts
Since `cargo xtest` waits until the test runner exits, a test that never returns can block the test runner forever. That's unfortunate, but not a big problem in practice since it's normally easy to avoid endless loops. In our case, however, endless loops can occur in various situations:
Since `cargo test` waits until the test runner exits, a test that never returns can block the test runner forever. That's unfortunate, but not a big problem in practice since it's normally easy to avoid endless loops. In our case, however, endless loops can occur in various situations:
- The bootloader fails to load our kernel, which causes the system to reboot endlessly.
- The BIOS/UEFI firmware fails to load the bootloader, which causes the same endless rebooting.
- The CPU enters a `loop {}` statement at the end of some of our functions, for example because the QEMU exit device doesn't work properly.
- The hardware causes a system reset, for example when a CPU exception is not caught (explained in a future post).
Since endless loops can occur in so many situations, the `bootimage` tool sets a timeout of 5 minutes for each test executable by default. If the test does not finish in this time, it is marked as failed and a "Timed Out" error is printed to the console. This feature ensures that tests that are stuck in an endless loop don't block `cargo xtest` forever.
Since endless loops can occur in so many situations, the `bootimage` tool sets a timeout of 5 minutes for each test executable by default. If the test does not finish in this time, it is marked as failed and a "Timed Out" error is printed to the console. This feature ensures that tests that are stuck in an endless loop don't block `cargo test` forever.
You can try it yourself by adding a `loop {}` statement in the `trivial_assertion` test. When you run `cargo xtest`, you see that the test is marked as timed out after 5 minutes. The timeout duration is [configurable][bootimage config] through a `test-timeout` key in the Cargo.toml:
You can try it yourself by adding a `loop {}` statement in the `trivial_assertion` test. When you run `cargo test`, you see that the test is marked as timed out after 5 minutes. The timeout duration is [configurable][bootimage config] through a `test-timeout` key in the Cargo.toml:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
@@ -561,7 +561,7 @@ fn trivial_assertion() {
}
```
The `cargo xtest` output now looks like this:
The `cargo test` output now looks like this:
```
Running 1 tests
@@ -666,7 +666,7 @@ We use the [`unimplemented`] macro that always panics as a placeholder for the `
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
If you run `cargo xtest` at this stage, you will get an endless loop because the panic handler loops endlessly. You need to use the `Ctrl+c` keyboard shortcut for exiting QEMU.
If you run `cargo test` at this stage, you will get an endless loop because the panic handler loops endlessly. You need to use the `Ctrl+c` keyboard shortcut for exiting QEMU.
### Create a Library
@@ -676,11 +676,13 @@ To make the required functions available to our integration test, we need to spl
// src/lib.rs
#![no_std]
extern crate rlibc;
```
Like the `main.rs`, the `lib.rs` is a special file that is automatically recognized by cargo. The library is a separate compilation unit, so we need to specify the `#![no_std]` attribute again.
Like the `main.rs`, the `lib.rs` is a special file that is automatically recognized by cargo. The library is a separate compilation unit, so we need to specify the `#![no_std]` attribute and the `extern crate rlibc` statement again.
To make our library work with `cargo xtest`, we need to also add the test functions and attributes:
To make our library work with `cargo test`, we need to also add the test functions and attributes:
```rust
// in src/lib.rs
@@ -722,7 +724,7 @@ pub fn test_panic_handler(info: &PanicInfo) -> ! {
loop {}
}
/// Entry point for `cargo xtest`
/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
@@ -817,7 +819,7 @@ fn panic(info: &PanicInfo) -> ! {
The library is usable like a normal external crate. It is called like our crate, which is `blog_os` in our case. The above code uses the `blog_os::test_runner` function in the `test_runner` attribute and the `blog_os::test_panic_handler` function in our `cfg(test)` panic handler. It also imports the `println` macro to make it available to our `_start` and `panic` functions.
At this point, `cargo xrun` and `cargo xtest` should work again. Of course, `cargo xtest` still loops endlessly (you can exit with `ctrl+c`). Let's fix this by using the required library functions in our integration test.
At this point, `cargo run` and `cargo test` should work again. Of course, `cargo test` still loops endlessly (you can exit with `ctrl+c`). Let's fix this by using the required library functions in our integration test.
### Completing the Integration Test
@@ -836,7 +838,7 @@ fn panic(info: &PanicInfo) -> ! {
Instead of reimplementing the test runner, we use the `test_runner` function from our library. For our `panic` handler, we call the `blog_os::test_panic_handler` function like we did in our `main.rs`.
Now `cargo xtest` exits normally again. When you run it, you see that it builds and runs the tests for our `lib.rs`, `main.rs`, and `basic_boot.rs` separately after each other. For the `main.rs` and the `basic_boot` integration test, it reports "Running 0 tests" since these files don't have any functions annotated with `#[test_case]`.
Now `cargo test` exits normally again. When you run it, you see that it builds and runs the tests for our `lib.rs`, `main.rs`, and `basic_boot.rs` separately after each other. For the `main.rs` and the `basic_boot` integration test, it reports "Running 0 tests" since these files don't have any functions annotated with `#[test_case]`.
We can now add tests to our `basic_boot.rs`. For example, we can test that `println` works without panicking, like we did in the vga buffer tests:
@@ -851,7 +853,7 @@ fn test_println() {
}
```
When we run `cargo xtest` now, we see that it finds and executes the test function.
When we run `cargo test` now, we see that it finds and executes the test function.
The test might seem a bit useless right now since it's almost identical to one of the VGA buffer tests. However, in the future the `_start` functions of our `main.rs` and `lib.rs` might grow and call various initialization routines before running the `test_main` function, so that the two tests are executed in very different environments.
@@ -939,7 +941,7 @@ fn should_fail() {
The test uses the `assert_eq` to assert that `0` and `1` are equal. This of course fails, so that our test panics as desired. Note that we need to manually print the function name using `serial_print!` here because we don't use the `Testable` trait.
When we run the test through `cargo xtest --test should_panic` we see that it is successful because the test panicked as expected. When we comment out the assertion and run the test again, we see that it indeed fails with the _"test did not panic"_ message.
When we run the test through `cargo test --test should_panic` we see that it is successful because the test panicked as expected. When we comment out the assertion and run the test again, we see that it indeed fails with the _"test did not panic"_ message.
A significant drawback of this approach is that it only works for a single test function. With multiple `#[test_case]` functions, only the first function is executed because the execution cannot continue after the panic handler has been called. I currently don't know of a good way to solve this problem, so let me know if you have an idea!
@@ -991,7 +993,7 @@ fn panic(_info: &PanicInfo) -> ! {
}
```
We now call the `should_fail` function directly from our `_start` function and exit with a failure exit code if it returns. When we run `cargo xtest --test should_panic` now, we see that the test behaves exactly as before.
We now call the `should_fail` function directly from our `_start` function and exit with a failure exit code if it returns. When we run `cargo test --test should_panic` now, we see that the test behaves exactly as before.
Apart from creating `should_panic` tests, disabling the `harness` attribute can also be useful for complex integration tests, for example when the individual test functions have side effects and need to be run in a specified order.

View File

@@ -406,7 +406,7 @@ pub extern "C" fn _start() -> ! {
}
```
When we run it in QEMU now (using `cargo xrun`), we see the following:
When we run it in QEMU now (using `cargo run`), we see the following:
![QEMU printing `EXCEPTION: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png)
@@ -421,7 +421,7 @@ Let's create a test that ensures that the above continues to work. First, we upd
```rust
// in src/lib.rs
/// Entry point for `cargo xtest`
/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
@@ -431,7 +431,7 @@ pub extern "C" fn _start() -> ! {
}
```
Remember, this `_start` function is used when running `cargo xtest --lib`, since Rust's tests the `lib.rs` completely independent of the `main.rs`. We need to call `init` here to set up an IDT before running the tests.
Remember, this `_start` function is used when running `cargo test --lib`, since Rust's tests the `lib.rs` completely independent of the `main.rs`. We need to call `init` here to set up an IDT before running the tests.
Now we can create a `test_breakpoint_exception` test:
@@ -447,7 +447,7 @@ fn test_breakpoint_exception() {
The test invokes the `int3` function to trigger a breakpoint exception. By checking that the execution continues afterwards, we verify that our breakpoint handler is working correctly.
You can try this new test by running `cargo xtest` (all tests) or `cargo xtest --lib` (only tests of `lib.rs` and its modules). You should see the following in the output:
You can try this new test by running `cargo test` (all tests) or `cargo test --lib` (only tests of `lib.rs` and its modules). You should see the following in the output:
```
blog_os::interrupts::test_breakpoint_exception... [ok]

View File

@@ -444,7 +444,7 @@ harness = false
[without a test harness]: @/second-edition/posts/04-testing/index.md#no-harness-tests
Now `cargo xtest --test stack_overflow` should compile successfully. The test fails of course, since the `unimplemented` macro panics.
Now `cargo test --test stack_overflow` should compile successfully. The test fails of course, since the `unimplemented` macro panics.
### Implementing `_start`
@@ -537,7 +537,7 @@ extern "x86-interrupt" fn test_double_fault_handler(
When the double fault handler is called, we exit QEMU with a success exit code, which marks the test as passed. Since integration tests are completely separate executables, we need to set `#![feature(abi_x86_interrupt)]` attribute again at the top of our test file.
Now we can run our test through `cargo xtest --test stack_overflow` (or `cargo xtest` to run all tests). As expected, we see the `stack_overflow... [ok]` output in the console. Try to comment out the `set_stack_index` line: it should cause the test to fail.
Now we can run our test through `cargo test --test stack_overflow` (or `cargo test` to run all tests). As expected, we see the `stack_overflow... [ok]` output in the console. Try to comment out the `set_stack_index` line: it should cause the test to fail.
## Summary
In this post we learned what a double fault is and under which conditions it occurs. We added a basic double fault handler that prints an error message and added an integration test for it.

View File

@@ -127,7 +127,7 @@ We use the [`initialize`] function to perform the PIC initialization. Like the `
[`initialize`]: https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html#method.initialize
If all goes well we should continue to see the "It did not crash" message when executing `cargo xrun`.
If all goes well we should continue to see the "It did not crash" message when executing `cargo run`.
## Enabling Interrupts
@@ -144,7 +144,7 @@ pub fn init() {
}
```
The `interrupts::enable` function of the `x86_64` crate executes the special `sti` instruction (“set interrupts”) to enable external interrupts. When we try `cargo xrun` now, we see that a double fault occurs:
The `interrupts::enable` function of the `x86_64` crate executes the special `sti` instruction (“set interrupts”) to enable external interrupts. When we try `cargo run` now, we see that a double fault occurs:
![QEMU printing `EXCEPTION: DOUBLE FAULT` because of hardware timer](qemu-hardware-timer-double-fault.png)
@@ -240,7 +240,7 @@ The `notify_end_of_interrupt` figures out whether the primary or secondary PIC s
We need to be careful to use the correct interrupt vector number, otherwise we could accidentally delete an important unsent interrupt or cause our system to hang. This is the reason that the function is unsafe.
When we now execute `cargo xrun` we see dots periodically appearing on the screen:
When we now execute `cargo run` we see dots periodically appearing on the screen:
![QEMU printing consecutive dots showing the hardware timer](qemu-hardware-timer-dots.gif)
@@ -359,10 +359,10 @@ Note that disabling interrupts shouldn't be a general solution. The problem is t
## Fixing a Race Condition
If you run `cargo xtest` you might see the `test_println_output` test failing:
If you run `cargo test` you might see the `test_println_output` test failing:
```
> cargo xtest --lib
> cargo test --lib
[…]
Running 4 tests
test_breakpoint_exception...[ok]
@@ -425,7 +425,7 @@ We performed the following changes:
[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html
With the above changes, `cargo xtest` now deterministically succeeds again.
With the above changes, `cargo test` now deterministically succeeds again.
This was a very harmless race condition that only caused a test failure. As you can imagine, other race conditions can be much more difficult to debug due to their non-deterministic nature. Luckily, Rust prevents us from data races, which are the most serious class of race conditions since they can cause all kinds of undefined behavior, including system crashes and silent memory corruptions.
@@ -479,7 +479,7 @@ Let's update our `lib.rs` as well:
```rust
// in src/lib.rs
/// Entry point for `cargo xtest`
/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {

View File

@@ -340,7 +340,7 @@ use bootloader::{entry_point, BootInfo};
#[cfg(test)]
entry_point!(test_kernel_main);
/// Entry point for `cargo xtest`
/// Entry point for `cargo test`
#[cfg(test)]
fn test_kernel_main(_boot_info: &'static BootInfo) -> ! {
// like before

View File

@@ -204,7 +204,18 @@ The first step in implementing a heap allocator is to add a dependency on the bu
extern crate alloc;
```
Contrary to normal dependencies, we don't need to modify the `Cargo.toml`. The reason is that the `alloc` crate ships with the Rust compiler as part of the standard library, so we just need to enable it. This is what this `extern crate` statement does. (Historically, all dependencies needed an `extern crate` statement, which is now optional).
Contrary to normal dependencies, we don't need to modify the `Cargo.toml`. The reason is that the `alloc` crate ships with the Rust compiler as part of the standard library, so the compiler already knows about the crate. By adding this `extern crate` statement, we specify that the compiler should try to include it. (Historically, all dependencies needed an `extern crate` statement, which is now optional).
Since we are compiling for a custom target, we can't use the precompiled version of `alloc` that is shipped with the Rust installation. Instead, we have to tell cargo to recompile the crate from source. We can do that, by adding it to the `unstable.build-std` array in our `.cargo/config.toml` file:
```toml
# in .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins", "alloc"]
````
Now the compiler will recompile and include the `alloc` crate in our kernel.
The reason that the `alloc` crate is disabled by default in `#[no_std]` crates is that it has additional requirements. We can see these requirements as errors when we try to compile our project now:
@@ -762,7 +773,7 @@ This test ensures that the allocator reuses freed memory for subsequent allocati
Let's run our new integration test:
```
> cargo xtest --test heap_allocation
> cargo test --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
@@ -770,7 +781,7 @@ large_vec... [ok]
many_boxes... [ok]
```
All three tests succeeded! You can also invoke `cargo xtest` (without `--test` argument) to run all unit and integration tests.
All three tests succeeded! You can also invoke `cargo test` (without `--test` argument) to run all unit and integration tests.
## Summary

View File

@@ -373,7 +373,7 @@ Now our kernel uses our bump allocator! Everything should still work, including
[`heap_allocation` tests]: @/second-edition/posts/10-heap-allocation/index.md#adding-a-test
```
> cargo xtest --test heap_allocation
> cargo test --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
@@ -416,7 +416,7 @@ Like the `many_boxes` test, this test creates a large number of allocations to p
When we try run our new test, we see that it indeed fails:
```
> cargo xtest --test heap_allocation
> cargo test --test heap_allocation
Running 4 tests
simple_allocation... [ok]
large_vec... [ok]
@@ -791,7 +791,7 @@ Since the `init` function behaves the same for the bump and linked list allocato
When we now run our `heap_allocation` tests again, we see that all tests pass now, including the `many_boxes_long_lived` test that failed with the bump allocator:
```
> cargo xtest --test heap_allocation
> cargo test --test heap_allocation
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
@@ -1158,7 +1158,7 @@ Since the `init` function behaves the same for all allocators we implemented, we
When we now run our `heap_allocation` tests again, all tests should still pass:
```
> cargo xtest --test heap_allocation
> cargo test --test heap_allocation
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]

View File

@@ -1153,7 +1153,7 @@ extern "x86-interrupt" fn keyboard_interrupt_handler(
We removed all the keyboard handling code from this function and instead added a call to the `add_scancode` function. The rest of the function stays the same as before.
As expected, keypresses are no longer printed to the screen when we run our project using `cargo xrun` now. Instead, we see the warning that the scancode queue is uninitialized for every keystroke.
As expected, keypresses are no longer printed to the screen when we run our project using `cargo run` now. Instead, we see the warning that the scancode queue is uninitialized for every keystroke.
#### Scancode Stream
@@ -1393,7 +1393,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
}
```
When we execute `cargo xrun` now, we see that keyboard input works again:
When we execute `cargo run` now, we see that keyboard input works again:
![QEMU printing ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif)
@@ -1709,7 +1709,7 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
We only need to change the import and the type name. Since our `run` function is marked as diverging, the compiler knows that it never returns so that we no longer need a call to `hlt_loop` at the end of our `kernel_main` function.
When we run our kernel using `cargo xrun` now, we see that keyboard input still works:
When we run our kernel using `cargo run` now, we see that keyboard input still works:
![QEMU printing ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif)
@@ -1783,7 +1783,7 @@ impl Executor {
To avoid race conditions, we disable interrupts before checking whether the `task_queue` is empty. If it is, we use the [`enable_interrupts_and_hlt`] function to enable interrupts and put the CPU to sleep as a single atomic operation. In case the queue is no longer empty, it means that an interrupt woke a task after `run_ready_tasks` returned. In that case, we enable interrupts again and directly continue execution without executing `hlt`.
Now our executor properly puts the CPU to sleep when there is nothing to do. We can see that the QEMU process has a much lower CPU utilization when we run our kernel using `cargo xrun` again.
Now our executor properly puts the CPU to sleep when there is nothing to do. We can see that the QEMU process has a much lower CPU utilization when we run our kernel using `cargo run` again.
#### Possible Extensions