Merge pull request #393 from phil-opp/improvements

Wording improvements; Explain the bootimage tool
This commit is contained in:
Philipp Oppermann
2018-02-25 16:59:56 +01:00
committed by GitHub
2 changed files with 28 additions and 18 deletions

View File

@@ -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:

View File

@@ -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.