We register a single handler function for a [divide by zero error] \(index 0). Like the name says, this exception occurs when dividing a number by 0. Thus we have an easy way to test our new exception handler.
[divide by zero error]: https://wiki.osdev.org/Exceptions#Divide-by-zero_Error
[divide by zero error]: https://wiki.osdev.org/Exceptions#Division_Error
The code from this post (and all following) is [automatically tested](https://travis-ci.org/phil-opp/blog_os) every day and should always work for the newest nightly. If it doesn't, please [file an issue](https://github.com/phil-opp/blog_os/issues).
## Creating a Cargo project
[Cargo] is Rust's excellent package manager. Normally you would call `cargo new` when you want to create a new project folder. We can't use it because our folder already exists, so we need to do it manually. Fortunately we only need to add a cargo configuration file named `Cargo.toml`:
To write an operating system kernel, we need code that does not depend on any operating system features.
So we can't use threads, files, heap memory, the network, random numbers, standard output, or any other features requiring OS abstractions or specific hardware.
Which makes sense, since we're trying to write our own OS and our own drivers.
Kernels are the heart of an operating system.
They provide all the fundamental building blocks that are required for building higher-level programs.
Typical building blocks are threads, files, heap memory, timers, or sockets.
Other important tasks of a kernel are the isolation of different programs and the multiplexing of resources.
While this means that we can't use most of the [Rust standard library], there are still a lot of Rust features that we _can_ use.
When writing an operating system kernel, we need to provide all of these building blocks ourselves.
This means that we can't use most of the [Rust standard library].
However, there are still a lot of Rust features that we _can_ use.
For example, we can use [iterators], [closures], [pattern matching], [`Option`] and [`Result`], [string formatting], and of course the [ownership system].
These features make it possible to write a kernel in a very expressive, high level way without worrying about [undefined behavior] or [memory safety].
These features make it possible to write a kernel in a very expressive, high level way and worry less about [undefined behavior] or [memory safety].
In order to create a minimal OS kernel in Rust, we start by creating an executable that can be run without an underlying operating system.
In this post, we create a minimal OS kernel that can be run without an underlying operating system.
Such an executable is often called a “freestanding” or “bare-metal” executable.
We then make this executable compatible with the early-boot environment of the `x86_64` architecture so that we can boot it as an operating system kernel.
@@ -99,7 +102,7 @@ You can compile your crate through `cargo build` and then run the compiled `kern
Right now our crate implicitly links the standard library.
Let's try to disable this by adding the [`no_std` attribute]:
```rust
```rust,hl_lines=3
// main.rs
#![no_std]
@@ -109,7 +112,7 @@ fn main() {
}
```
When we try to build it now (by running `cargo build`), the following error occurs:
When we try to build it now (by running `cargo build`), the following errors occur:
```
error: cannot find macro `println!` in this scope
@@ -124,8 +127,7 @@ error: language item required, but not found: `eh_personality`
[...]
```
There are multiple errors.
The reason for the first one is that the [`println` macro] is part of the standard library, which we no longer include.
Thereason for the first error is that the [`println` macro] is part of the standard library, which we no longer include.
So we can no longer print things.
This makes sense, since `println` writes to [standard output], which is a special file descriptor provided by the operating system.
@@ -134,7 +136,7 @@ This makes sense, since `println` writes to [standard output], which is a specia
So let's remove the printing and try again with an empty main function:
```rust
```rust,hl_lines=5
// main.rs
#![no_std]
@@ -155,15 +157,19 @@ The `println` error is gone, but the compiler is still missing a `#[panic_handle
### Panic Implementation
The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs.
The standard library provides its own panic handler function, but in a `no_std` environment we need to define it ourselves:
The standard library provides its own panic handler function, but in a `no_std` environment we need to define one ourselves:
This sets the panic strategy to `abort` for both the `dev` profile (used for `cargo build`) and the `release` profile (used for `cargo build --release`).
Now the `eh_personality` language item should no longer be required.
Now we fixed both of the above errors.
However, if we try to compile it now, another error occurs:
When we try to compile our kernel now, a new error occurs:
```
❯ cargo build
@@ -281,13 +285,13 @@ You can see the target triple for your host system by running `rustc --version -
If you're on a UNIX system, you might already have the `nm`, `objdump`, and `strip` tools installed.
Otherwise, you can use the LLVM binutils shipped by `rustup` through the [`cargo-binutils`] crate.
If you're on a UNIX system, you might already have the above tools installed.
Otherwise (and on Windows), you can use the LLVM binutils shipped by `rustup` through the [`cargo-binutils`] crate.
To install it, run **`cargo install cargo-binutils`** and **`rustup component add llvm-tools-preview`**.
Afterwards, you can run the tools through `rust-nm`, `rust-objdump`, and `rust-strip`.
@@ -422,26 +431,26 @@ To verify that it is properly exposed in the executable, we can run `nm` to list
```
❯ rust-nm target/x86_64-unknown-none/debug/kernel
0000000000002218 d _DYNAMIC
0000000000001210 T _start
0000000000201120 T _start
```
If we comment out the `_start` function or if we remove the `#[no_mangle]` attribute, the `_start` symbol is no longer there after recompiling:
```
❯ rust-nm target/x86_64-unknown-none/debug/kernel
0000000000002218 d _DYNAMIC
```
This way we can ensure that we set the `_start` function correctly.
### `objdump`
The `objdump` tool can inspect different parts of executables that use the [ELF file format].
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
On x86, there are two firmware standards: the “Basic Input/Output System“ (**[BIOS]**) and the newer “Unified Extensible Firmware Interface” (**[UEFI]**).
The BIOS standard is old and outdated, but simple and well-supported on any x86 machine since the 1980s.
UEFI, in contrast, is more modern and has much more features, but also more complex.
The BIOS standard is outdated and not standardized, but relatively simple and supported on almost any x86 machine since the 1980s.
UEFI, in contrast, is more modern and has much more features, but also more complex and only runs on fairly recent hardware (built since ~2012).
@@ -116,8 +116,10 @@ Since it is not possible to do all that within the available 446 bytes, most boo
Writing a BIOS bootloader is cumbersome as it requires assembly language and a lot of non insightful steps like _“write this magic value to this processor register”_.
Therefore we don't cover bootloader creation in this post and instead use the existing [`bootloader`] crate to make our kernel bootable.
If you are interested in building your own BIOS bootloader: Stay tuned, a set of posts on this topic is already planned! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built.
-->
(If you are interested in building your own BIOS bootloader, you can look through the [BIOS source code] of the `bootloader` crate on GitHub, which is mostly written in Rust and has only about 50 lines of assembly code.)
@@ -152,9 +154,7 @@ Thus, malware should be prevented from compromising the early boot process.
#### Issues & Criticism
While most of the UEFI specification sounds like a good idea, there are also many issues with the standard.
The main issue for most people is the fear that the _secure boot_ mechanism can be used to [lock users into the Windows operating system][uefi-secure-boot-lock-in] and thus prevent the installation of alternative operating systems such as Linux.
The main issue for most people is the fear that the _secure boot_ mechanism could be used to lock users into a specific operating system (e.g. Windows) and thus prevent the installation of alternative operating systems.
Another point of criticism is that the large number of features make the UEFI firmware very complex, which increases the chance that there are some bugs in the firmware implementation itself.
This can lead to security problems because the firmware has complete control over the hardware.
@@ -174,7 +174,7 @@ The UEFI boot process works in the following way:
These partitions must be formatted with the [FAT file system] and assigned a special ID that indicates them as EFI system partition.
The UEFI standard understands both the [MBR] and [GPT] partition table formats for this, at least theoretically.
In practice, some UEFI implementations seem to [directly switch to BIOS-style booting when an MBR partition table is used][mbr-csm], so it is recommended to only use the GPT format with UEFI.
- If the firmware finds a EFI system partition, it looks for an executable file named `efi\boot\bootx64.efi` (on x86_64 systems) in it.
- If the firmware finds an EFI system partition, it looks for an executable file named `efi\boot\bootx64.efi` (on x86_64 systems).
This executable must use the [Portable Executable (PE)] format, which is common in the Windows world.
- It then loads the executable from disk to memory, sets up the execution environment (CPU state, page tables, etc.) in a standardized way, and finally jumps to the entry point of the loaded executable.
@@ -218,13 +218,13 @@ The reference implementation is [GNU GRUB], which is the most popular bootloader
To make a kernel Multiboot compliant, one just needs to insert a so-called [Multiboot header] at the beginning of the kernel file.
This makes it very easy to boot an OS in GRUB.
However, GRUB and the Multiboot standard have some problems too:
However, GRUB and the Multiboot standard have some issues too:
- The standard is designed to make the bootloader simple instead of the kernel.
For example, the kernel needs to be linked with an [adjusted default page size], because GRUB can't find the Multiboot header otherwise.
Another example is that the [boot information], which is passed to the kernel, contains lots of architecturedependent structures instead of providing clean abstractions.
Another example is that the [boot information], which is passed to the kernel, contains lots of architecture-dependent structures instead of providing clean abstractions.
- The standard supports only the 32-bit protected mode on BIOS systems.
This means that you still have to do the CPU configuration to switch to the 64-bit long mode.
- For UEFI systems, the standard provides very little added value as it simply exposes the normal UEFI interface to kernels.
@@ -236,7 +236,7 @@ This makes development on Windows or Mac more difficult.
Because of these drawbacks we decided to not use GRUB or the Multiboot standard for this series.
However, we plan to add Multiboot support to our [`bootloader`] crate, so that it becomes possible to load your kernel on a GRUB system too.
However, we might add Multiboot support to our [`bootloader`] crate at some point, so that it becomes possible to load your kernel on a GRUB system too.
If you're interested in writing a Multiboot compliant kernel, check out the [first edition] of this blog series.
[first edition]: @/edition-1/_index.md
@@ -246,109 +246,89 @@ If you're interested in writing a Multiboot compliant kernel, check out the [fir
We now know that most operating system kernels are loaded by bootloaders, which are small programs that initialize the hardware to reasonable defaults, load the kernel from disk, and provide it with some fundamental information about the underlying system.
In this section, we will learn how to combine the [minimal kernel] we created in the previous post with the `bootloader` crate in order to create a bootable disk image.
### The `bootloader` Crate
Since bootloaders quite complex on their own, we won't create our own bootloader here (but we are planning a separate series of posts on this).
Instead, we will boot our kernel using the [`bootloader`] crate.
This crate supports both BIOS and UEFI booting, provides all the necessary system information we need, and creates a reasonable default execution environment for our kernel.
The [`bootloader`] crate supports both BIOS and UEFI booting on `x86_64` and creates a reasonable default execution environment for our kernel.
This way, we can focus on the actual kernel design in the following posts instead of spending a lot of time on system initialization.
For normal Rust crates, this step would be all that's needed for adding them as a dependency.
However, the `bootloader` crate is a bit special.
The problem is that it needs access to our kernel _after compilation_ in order to create a bootable disk image.
However, cargo has no support for automatically running code after a successful build, so we need some manual build code for this.
(There is a proposal for [post-build scripts] that would solve this issue, but it is not clear yet whether the Cargo team wants to add such a feature.)
Now we need to replace our custom `_start` entry point function with [`bootloader_api::entry_point`] macro. This macro instructs the compiler to create a special `.bootloader-config` section with encoded configuration options in the resulting executable, which is later read by the bootloader implementation.
We will take a closer look at the `entry_point` macro and the different configuration options later. For now, we just use the default setup:
Before we look into the bootable disk image creation, we update need to update our `_start` entry point to be compatible with the `bootloader` crate.
As we already mentioned above, bootloaders commonly pass additional system information when invoking the kernel, such as the amount of available memory.
The `bootloader` crate also follows this convention, so we need to update our `_start` entry point to expect an additional argument.
```rust,hl_lines=3 6-8
// in main.rs
The [`bootloader` documentation][`BootInfo`] specifies that a kernel entrypoint should have the following signature:
<summary><h5>About <code>extern "C"</code> and <code>!</code></h5></summary>
The [`extern "C"`] qualifier specifies that the function should use the same [ABI] and [calling convention] as C code.
It is common to use this qualifier when communicating across different executables because C has a stable ABI that is guaranteed to never change.
Normal Rust functions, on the other hand, don't have a stable ABI, so they might change it the future (e.g. to optimize performance) and thus shouldn't be used across different executables.
While we could simply add the additional argument to our `_start` function, it would result in very fragile code.
The problem is that because the `_start` function is called externally from the bootloader, no checking of the function signature occurs.
So no compilation error occurs, even if the function signature completely changed after updating to a newer `bootloader` version.
At runtime, however, the code would fail or introduce undefined behavior.
To avoid these issues and make sure that the entry point function has always the correct signature, the `bootloader` crate provides an [`entry_point`] macro that provides a type-checked way to define a Rust function as the entry point.
This way, the function signature is checked at compile time so that no runtime error can occur.
We no longer need to use `extern "C"` or `no_mangle` for our entry point, as the macro defines the actual lower-level `_start` entry point for us.
The `kernel_main` function is now a completely normal Rust function, so we can choose an arbitrary name for it.
Since the signature of the function is enforced by the macro, a compilation error occurs when it e.g. has the wrong argument type.
There are a few notable things:
After adjusting our entry point for the `bootloader` crate, we can now look into how to create a bootable disk image from our kernel.
- The `kernel_main` function is just a normal Rust function with an arbitrary name. No `#[no_mangle]` attribute is needed anymore since the `entry_point` macro handles this internally.
- Like before, our entry point function is [diverging], i.e. it must never return. We ensure this by looping endlessly.
- There is a new [`BootInfo`] argument, which the bootloader fills with various system information. We will use this argument later.
- The `entry_point` macro verifies that the `kernel_main` function has the correct arguments and return type, otherwise a compile error will occur. This is important because undefined behavior might occur when the function signature does not match the bootloader's expectations.
To verify that the `entry_point` macro worked as expected, we can use the `objdump` tool as [described in the previous post][objdump-prev]. First, we recompile using `cargo build --target x86_64-unknown-none`, then we inspect the section headers using `objdump` or `rust-objdump`:
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
Sections:
Idx Name Size VMA Type
0 00000000 0000000000000000
1 .bootloader-config 0000007c 0000000000200120 DATA
2 .text 00000075 00000000002011a0 TEXT
3 .debug_abbrev 000001c8 0000000000000000 DEBUG
4 .debug_info 00000b56 0000000000000000 DEBUG
5 .debug_aranges 00000090 0000000000000000 DEBUG
6 .debug_ranges 00000040 0000000000000000 DEBUG
7 .debug_str 00000997 0000000000000000 DEBUG
8 .debug_pubnames 0000014c 0000000000000000 DEBUG
9 .debug_pubtypes 00000548 0000000000000000 DEBUG
10 .debug_frame 000000b0 0000000000000000 DEBUG
11 .debug_line 0000012c 0000000000000000 DEBUG
12 .comment 00000013 0000000000000000
13 .symtab 000000a8 0000000000000000
14 .shstrtab 000000b8 0000000000000000
15 .strtab 000000cd 0000000000000000
```
We see that there is indeed a new `.bootloader-config` section of size `0x7c` in our kernel executable. This means that we can now look into how to create a bootable disk image from our kernel.
### Creating a Disk Image
The [docs of the `bootloader` crate][`bootloader` docs] describes how to create a bootable disk image for a kernel.
The [docs of the `bootloader` crate][`bootloader` docs] describe how to create a bootable disk image for a kernel.
The first step is to find the directory where cargo placed the source code of the `bootloader` dependency.
Then, a special build command needs to be executed in that directory, passing the paths to the kernel binary and its `Cargo.toml` as arguments.
This will result in multiple disk image files as output, which can be used to boot the kernel on BIOS and UEFI systems.
The first step in creating the bootable disk image is to to locate where cargo put the source code of the `bootloader` dependency.
For that we can use cargo's [`cargo metadata`] subcommand, which outputs all kinds of information about a cargo project as a JSON object.
Among other things, it contains the manifest path (i.e. the path to the `Cargo.toml`) of all dependencies, including the `bootloader` crate.
The first step in creating the bootable disk image is to enable support for [artifact dependencies](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) from inside your kernel's `.cargo/config.toml` because we're going to need that support later:
After this, you need to add an artifact dependency on your kernel from inside the boot crate. This tells the bootloader crate where the source code to your kernel resides:
Finally, you need to add a dependency on the main `bootloader` crate. Previous versions used `bootloader_locator` instead, but now, thanks to artifact dependencies, that is no longer necessary.
```toml
# in boot/Cargo.toml
[dependencies]
bootloader = "0.11.0"
```
We can see how this works by printing the Cargo-generated environment variable pointing to the absolute path of the kernel binary
let kernel_binary = Path::new(env!("CARGO_BIN_FILE_KERNEL_kernel"));
dbg!(kernel_binary);
}
```
The `locate_bootloader` function takes the name of the bootloader dependency as argument to allow alternative bootloader crates that are named differently.
Since the function might fail, we use the [`unwrap`] method to [panic] on an error.
Panicking is ok here because the `boot` crate is only part of our build process.
If everything succeeds, the [`dbg!`] macro should print the path to the `bootloader` source code.
Note that we need to run the `boot` binary from the root directory of our workspace, not from within the `boot` directory.
Otherwise the `locate_bootloader` function would operate on the `boot/Cargo.toml`, where it won't find a bootloader dependency.
The `CARGO_BIN_FILE_KERNEL_kernel` environment variable is defined by Cargo as the absolute path to the binary file created after compiling an artifact dependency — and in this case, the binary file it points to is your kernel's binary. This makes it very easy to begin the process of boot image creation, as explained in detail below.
It worked! We see that the bootloader source code lives somewhere in the `.cargo` directory in our user directory.
By querying the source code for the exact bootloader version that our kernel is using, we ensure that the bootloader and the kernel use the exact same version of the `BootInfo` type.
It worked! We see that the kernel binary lives somewhere in the dependency tree of our `boot` crate.
By depending on the kernel as a binary dependency of `boot`, we ensure that the bootloader and the kernel use the exact same version of the `BootInfo` type.
This is important because the `BootInfo` type is not stable yet, so undefined behavior can occur when when using different `BootInfo` versions.
#### Running the Build Command
#### Building a Boot Image
The next step is to run the build command of the bootloader.
From the [`bootloader` docs] we learn that the crate requires the following build command:
The next step is to actually build the boot image.
From the [`bootloader` docs] we learn that the crate defines two completely unique bootloader objects: `BiosBoot` for BIOS and `UefiBoot` for UEFI. To keep it simple, we will support both, although it's possible to choose which to exclusively support later to keep your workflow streamlined as your kernel becomes more complex.
In addition, the docs recommend to use the `--target-dir` and `--out-dir` arguments when building the bootloader as a dependency to override where cargo places the compilation artifacts.
Let's try to invoke that command from our `main` function.
For that we use the [`process::Command`] type of the standard library, which allows us to spawn new processes and wait for their results:
if let Err(e) = uefi.create_disk_image(&uefi_path) {
eprintln!("{:#?}", &e);
exit(1)
}
// run the command
letexit_status=build_cmd.status().unwrap();
if!exit_status.success(){
panic!("bootloader build failed");
// attempt to create BIOS boot image
if let Err(e) = bios.create_disk_image(&bios_path) {
eprintln!("{:#?}", &e);
exit(1)
}
}
```
We use the [`Command::new`] function to create a new [`process::Command`].
Instead of hardcoding the command name "cargo", we use the [`CARGO` environment variable] that cargo sets when compiling the `boot` crate.
This way, we ensure that we use the exact same cargo version for compiling the `bootloader` crate, which is important when using non-standard cargo versions, e.g. through rustup's [toolchain override shorthands].
Since the environment variable is set at compile time, we use the compiler-builtin [`env!`] macro to retrieve its value.
We use both the `UefiBoot` and `BiosBoot` types to create disk images for the BIOS and UEFI implementations, respectively. By using the `if let` syntax, we can exit the build gracefully whenever an error occurs.
After creating the `UefiBoot` and `BiosBoot` types using the `CARGO_BIN_FILE_KERNEL_kernel` environment variable that we went over previously as the constructor argument for both, we now are ready for the next step.
After creating the `Command` type, we pass all the required arguments by calling the [`Command::arg`] method.
Most of the paths are still set to [`todo!()`] as a placeholder and will be filled out in a moment.
Since the build command needs to be run inside the source directory of the `bootloader` crate, we use the [`Command::current_dir`] method to set the working directory accordingly.
We can determine the `bootloader_dir` path from the `bootloader_manifest` path by using the [`Path::parent`] method.
Since not all paths have a parent directory (e.g. the path `/` has not), the `parent()` call can fail.
However, this should never happen for the `bootloader_manifest` path, so we use the [`Option::unwrap`] method that panics on `None`.
We still need to fill in the paths we marked as `todo!` above.
We start with the path to the kernel binary:
We still need to fill in the paths we marked as `todo!` above. Like with the kernel binary, we can also use the `env!()` builtin for this, since another environment variable can also be used as a reference point for determining the filenames for the disk images:
```rust
// in `main` in boot/src/main.rs
usestd::path::Path;
// we know that the kernel lives in the parent directory of the `boot` crate
let kernel_dir = Path::new(env!("CARGO_MANIFEST_DIR")).manifest_dir.parent().unwrap();
By default, cargo places our compiled kernel executable in a subdirectory of the `target` folder.
The `x86_64_blog_os` is the name of our target JSON file and the `debug` indicates that this was a build with debug information and without optimizations.
For now we simply hardcode the path to keep things simple, but we will make it more flexible later in this post.
Since we're going to need an absolute path, we use the [`Path::canonicalize`] method to get the full path to the file.
We use [`unwrap`] to panic if the file doesn't exist.
// we know that the kernel lives in the parent directory
letkernel_dir=manifest_dir.parent().unwrap();
letkernel_manifest=kernel_dir.join("Cargo.toml");
// use the same target folder for building the bootloader
lettarget_dir=kernel_dir.join("target");
// place the resulting disk image next to our kernel binary
letout_dir=kernel_binary.parent().unwrap();
// use the above as a target folder in which to place both the BIOS and UEFI disk images
let bios_image = kernel_dir.join("bootimage-bios-blog_os.img");
let uefi_image = kernel_dir.join("bootimage-uefi-blog_os.img");
```
The [`CARGO_MANIFEST_DIR`] environment variable always points to the `boot` directory, even if the crate is built from a different directory (e.g. via cargo's `--manifest-path` argument).
This gives use a good starting point for creating the paths we care about since we know that our kernel lives in the [parent][`Path::parent`] directory.
After that can finally use our `boot` crate to create some bootable disk images from our kernel:
```bash
> cargo kbuild
> cargo run --package boot
```
We first compile our kernel through `cargo kbuild` to ensure that the kernel binary is up to date.
Then we run our `boot` crate through `cargo run --package boot`, which takes the kernel binary and builds the bootloader around it.
The result are some disk image files named `bootimage-*` next to our kernel binary inside `target/x86_64-blog_os/debug`.
Because we're using artifact dependencies, when you run the `boot` package, the kernel is automatically pulled in and compiled as a dependency. Previously, in version 0.10 of the bootloader crate, you had to build the kernel binary first, but now, thanks to artifact dependencies, this is no longer required.
Note that the command will only work from the root directory of our project.
This is because we hardcoded the `kernel_binary` path in our `main` function.
We will fix this later in the post, but first it is time to actually run our kernel!
From the [`bootloader` docs], we learn that the bootloader the following disk images:
- A BIOS boot image named `bootimage-bios-<bin_name>.img`.
- Multiple images suitable for UEFI booting
- An EFI executable named `bootimage-uefi-<bin_name>.efi`.
- A FAT partition image named `bootimage-uefi-<bin_name>.fat`, which contains the EFI executable under `efi\boot\bootx64.efi`.
- A GPT disk image named `bootimage-uefi-<bin_name>.img`, which contains the FAT image as EFI system partition.
In general, the `.img` files are the ones that you want to copy to an USB stick in order to boot from it.
The other files are useful for booting the kernel in virtual machines such as [QEMU].
The `<bin_name>` placeholder is the binary name of the kernel, i.e. `blog_os` or the crate name you chose.
Note also that we specified names for the image files. Although we used `bootimage-bios-blog_os.img` and `bootimage-uefi-blog_os.img` for compatibility, they can now be given whatever names you see fit.
## Running our Kernel
@@ -652,7 +547,7 @@ Then you can run the BIOS disk image of our kernel through the following command
@@ -733,8 +628,8 @@ Since screen output won't be essential for our kernel (there are other possible
The [`FrameBuffer`] type provides two methods: The `info` method returns a [`FrameBufferInfo`] instance with all kinds of information about the framebuffer format, including the pixel type and the screen resolution.
The `buffer` method returns the actual framebuffer content in form of a mutable byte [slice].
// override the background color (instead of black)
background-color:rgba(0,255,0,0.1)!important;
}
.highlight{
margin-bottom:1rem;
border-radius:4px;
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.