mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
Merge pull request #393 from phil-opp/improvements
Wording improvements; Explain the bootimage tool
This commit is contained in:
@@ -205,7 +205,7 @@ pub extern fn rust_begin_panic(_msg: core::fmt::Arguments,
|
||||
|
||||
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.
|
||||
|
||||
The entry point convention depends on your operating system. I recommend you to read the Linux section even if you're on a different OS because it is the target we will derive to build our kernel in the next post.
|
||||
The entry point convention depends on your operating system. I recommend you to read the Linux section even if you're on a different OS because we will use this convention for our kernel.
|
||||
|
||||
#### Linux
|
||||
On Linux, the default entry point is called `_start`. The linker just looks for a function with that name and sets this function as entry point the executable. So to overwrite the entry point, we define our own `_start` function:
|
||||
|
||||
@@ -51,8 +51,9 @@ Writing a bootloader is a bit cumbersome as it requires assembly language and a
|
||||
If you are interested in building your own 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. -->
|
||||
|
||||
#### The Multiboot Standard
|
||||
To avoid that every operating system implements its own bootloader, which is only compatible with a single OS, the Free Software Foundation created an open bootloader standard called [Multiboot] in 1995. The standard defines an interface between the bootloader and operating system, so that any Multiboot compliant bootloader can load any Multiboot compliant operating system. The reference implementation is [GNU GRUB], which is the most popular bootloader for Linux systems.
|
||||
To avoid that every operating system implements its own bootloader, which is only compatible with a single OS, the [Free Software Foundation] created an open bootloader standard called [Multiboot] in 1995. The standard defines an interface between the bootloader and operating system, so that any Multiboot compliant bootloader can load any Multiboot compliant operating system. The reference implementation is [GNU GRUB], which is the most popular bootloader for Linux systems.
|
||||
|
||||
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
|
||||
[Multiboot]: https://wiki.osdev.org/Multiboot
|
||||
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
|
||||
|
||||
@@ -61,15 +62,12 @@ To make a kernel Multiboot compliant, one just needs to insert a so-called [Mult
|
||||
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
|
||||
|
||||
- They support only the 32-bit protected mode. This means that you still have to do the CPU configuration to switch to the 64-bit long mode.
|
||||
- The newest version of the standard, [Multiboot 2.0], supports kernels in the 64-bit ELF format, but requires [adjusting the default page size].
|
||||
- The development of the standard and GRUB is highly interleaved. For example, I'm not aware of another bootloader that is Multiboot compliant. As a result, one often has to look into the GRUB source code when the standard is not clear enough.
|
||||
- The standard is written for the C programming language. That means lots of C-typical structures are present, such as linked lists in the [boot information].
|
||||
- They are 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 architecture dependent structures instead of providing clean abstractions.
|
||||
- Both GRUB and the Multiboot standard are only sparsely documented.
|
||||
- GRUB needs to be installed on the host system to create a bootable disk image from the kernel file. This makes development on Windows or Mac more difficult.
|
||||
|
||||
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
|
||||
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
|
||||
[Multiboot 2.0]: https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html
|
||||
[adjusting the default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
|
||||
|
||||
Because of these drawbacks we decided to not use GRUB or the Multiboot standard. However, we plan to add Multiboot support to our [bootimage] tool, so that it's 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.
|
||||
|
||||
@@ -80,12 +78,12 @@ Because of these drawbacks we decided to not use GRUB or the Multiboot standard.
|
||||
(We don't provide UEFI support at the moment, but we would love to! If you'd like to help, please tell us in the [Github issue](https://github.com/phil-opp/blog_os/issues/349).)
|
||||
|
||||
## A Minimal Kernel
|
||||
Now that we roughly know how a computer boots, it's time to create our own minimal kernel. Our goal is to create a disk image that prints a teal “Hello World!” to the screen when booted. For that we build upon the [freestanding Rust binary] from the previous post.
|
||||
Now that we roughly know how a computer boots, it's time to create our own minimal kernel. Our goal is to create a disk image that prints a “Hello World!” to the screen when booted. For that we build upon the [freestanding Rust binary] from the previous post.
|
||||
|
||||
As you may remember, we built the freestanding binary through `cargo`, but depending on the operating system we needed different entry point names and compile flags. That's because `cargo` builds for the _host system_ by default, i.e. the system you're running on. This isn't something we want for our kernel, because a kernel that runs on top of e.g. Windows does not make much sense. Instead, we want to compile for a clearly defined _target system_.
|
||||
|
||||
### Target Specification
|
||||
Cargo supports different target systems through the `--target` parameter. The target is described by a so-called _[target triple]_, which describes the CPU architecture, the vendor, the operating system, and the [ABI]. For example, the `x86_64-unknown-linux-gnu` means a `x86_64` CPU, no clear vendor and a Linux operating system with the GNU ABI. 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/).
|
||||
Cargo supports different target systems through the `--target` parameter. The target is described by a so-called _[target triple]_, which describes the CPU architecture, the vendor, the operating system, and the [ABI]. For example, the `x86_64-unknown-linux-gnu` target triple describes a system with a `x86_64` CPU, no clear vendor and a Linux operating system with the GNU ABI. 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/).
|
||||
|
||||
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
|
||||
[ABI]: https://stackoverflow.com/a/2456882
|
||||
@@ -177,24 +175,24 @@ Our target specification file now looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"llvm-target": "x86_64-unknown-linux-gnu",
|
||||
"llvm-target": "x86_64-unknown-none",
|
||||
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
|
||||
"arch": "x86_64",
|
||||
"target-endian": "little",
|
||||
"target-pointer-width": "64",
|
||||
"target-c-int-width": "32",
|
||||
"os": "none",
|
||||
"executables": true,
|
||||
"linker-flavor": "ld",
|
||||
"linker": "ld.lld",
|
||||
"executables": true,
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
"panic-strategy": "abort",
|
||||
"disable-redzone": true,
|
||||
"panic-strategy": "abort"
|
||||
"features": "-mmx,-sse,+soft-float"
|
||||
}
|
||||
```
|
||||
|
||||
### Building our Kernel
|
||||
Compiling for our new target will use Linux conventions. I'm not quite sure why, but I assume that it's just LLVM's default. This means that we need an entry point named `_start` as described in the [previous post]:
|
||||
Compiling for our new target will use Linux conventions (I'm not quite sure why, I assume that it's just LLVM's default). This means that we need an entry point named `_start` as described in the [previous post]:
|
||||
|
||||
[previous post]: ./second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
@@ -221,7 +219,7 @@ pub fn _start() -> ! {
|
||||
}
|
||||
```
|
||||
|
||||
We can now build the kernel for our new target by passing the name of the JSON file (without the `.json` extension) as `--target`. There is currently an [open bug][custom-target-bug] for custom target specifications, so you also need to set the `RUST_TARGET_PATH` environment variable to the current directory, otherwise Rust might not be able to find your target. The full command is:
|
||||
We can now build the kernel for our new target by passing the name of the JSON file (without the `.json` extension) as `--target`. There is currently an [open bug][custom-target-bug] for custom target specifications, so you also need to set the `RUST_TARGET_PATH` environment variable to the current directory, otherwise Rust doesn't find your target. The full command is:
|
||||
|
||||
[custom-target-bug]: https://github.com/rust-lang/cargo/issues/4905
|
||||
|
||||
@@ -299,7 +297,7 @@ pub fn _start() -> ! {
|
||||
}
|
||||
```
|
||||
|
||||
First, we cast the integer `0xb8000` into a [raw pointer]. Then we [iterate] over the bytes of the [static] `HELLO` [byte string]. We use the [`enumerate`] method to additionally get a running variable `i`. In the body of the for loop, we use the [`offset`] method to write the string byte and the corresponding color byte.
|
||||
First, we cast the integer `0xb8000` into a [raw pointer]. Then we [iterate] over the bytes of the [static] `HELLO` [byte string]. We use the [`enumerate`] method to additionally get a running variable `i`. In the body of the for loop, we use the [`offset`] method to write the string byte and the corresponding color byte (`0xb` is a light cyan).
|
||||
|
||||
[iterate]: https://doc.rust-lang.org/book/second-edition/ch13-02-iterators.html
|
||||
[static]: https://doc.rust-lang.org/book/first-edition/const-and-static.html#static
|
||||
@@ -331,7 +329,19 @@ Now that we have an executable that does something perceptible, it is time to tu
|
||||
|
||||
To make things easy, we created a tool named `bootimage` that automatically downloads a bootloader and combines it with the kernel executable to create a bootable disk image. To install it, execute `cargo install bootimage` in your terminal. After installing, creating a bootimage is as easy as executing `bootimage --target x86_64-unknown-blog_os`. The tool also recompiles your kernel using `xargo`, so it will automatically pick up any changes you make.
|
||||
|
||||
You should now see a file named `bootimage.bin` in your crate root directory. This file is a bootable disk image. 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).
|
||||
After executing the command, you should see a file named `bootimage.bin` in your crate root directory. This file is a bootable disk image. 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).
|
||||
|
||||
#### How does it work?
|
||||
The `bootimage` tool performs the following steps behind the scenes:
|
||||
|
||||
- It compiles our kernel to an [ELF] file.
|
||||
- It downloads a pre-compiled bootloader release from [rust-osdev/bootloader]. The file is already a bootable image that only requires that a kernel is appended.
|
||||
- It appends the bytes of the kernel ELF file to the bootloader (without any modifications).
|
||||
|
||||
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||||
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
|
||||
|
||||
When booted, the bootloader reads and parses the appended ELF file. It then maps the program segments to virtual addresses in the page tables, zeroes the `.bss` section, and sets up a stack. Finally, it reads the entry point address (our `_start` function) and jumps to it.
|
||||
|
||||
## Booting it!
|
||||
We can now boot our kernel in a virtual machine. To boot it in [QEMU], execute the following command:
|
||||
@@ -356,7 +366,7 @@ It is also possible to write it to an USB stick and boot it on a real machine:
|
||||
> dd if=bootimage.bin of=/dev/sdX && sync
|
||||
```
|
||||
|
||||
Where `sdX` is the device name of your USB stick. It overwrites everything on that device, so be careful to choose the correct device name.
|
||||
Where `sdX` is the device name of your USB stick. **Be careful** to choose the correct device name, because everything on that device is overwritten.
|
||||
|
||||
## What's next?
|
||||
In the next post, we will explore the VGA text buffer in more detail and write a safe interface for it. We will also add support for the `println` macro.
|
||||
|
||||
Reference in New Issue
Block a user