mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
586 lines
25 KiB
Markdown
586 lines
25 KiB
Markdown
+++
|
||
title = "Minimal Kernel"
|
||
weight = 1
|
||
path = "minimal-kernel"
|
||
date = 0000-01-01
|
||
draft = true
|
||
|
||
[extra]
|
||
chapter = "Bare Bones"
|
||
icon = '''
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-binary" viewBox="0 0 16 16">
|
||
<path d="M7.05 11.885c0 1.415-.548 2.206-1.524 2.206C4.548 14.09 4 13.3 4 11.885c0-1.412.548-2.203 1.526-2.203.976 0 1.524.79 1.524 2.203zm-1.524-1.612c-.542 0-.832.563-.832 1.612 0 .088.003.173.006.252l1.559-1.143c-.126-.474-.375-.72-.733-.72zm-.732 2.508c.126.472.372.718.732.718.54 0 .83-.563.83-1.614 0-.085-.003-.17-.006-.25l-1.556 1.146zm6.061.624V14h-3v-.595h1.181V10.5h-.05l-1.136.747v-.688l1.19-.786h.69v3.633h1.125z"/>
|
||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/>
|
||
</svg>
|
||
'''
|
||
+++
|
||
|
||
The first step in creating our own operating system kernel is to create a [bare metal] Rust executable that does not depend on an underlying operating system.
|
||
For that we need to disable most of Rust's standard library and adjust various compilation settings.
|
||
The result is a minimal operating system kernel that forms the base for the following posts of this series.
|
||
|
||
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
|
||
|
||
<!-- more -->
|
||
|
||
This blog is openly developed on [GitHub].
|
||
If you have any problems or questions, please open an issue there.
|
||
You can also leave comments [at the bottom].
|
||
The complete source code for this post can be found in the [`post-3.1`][post branch] branch.
|
||
|
||
[GitHub]: https://github.com/phil-opp/blog_os
|
||
[at the bottom]: #comments
|
||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-3.1
|
||
|
||
<!-- toc -->
|
||
|
||
## Introduction
|
||
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.
|
||
|
||
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 and worry less about [undefined behavior] or [memory safety].
|
||
|
||
[`Option`]: https://doc.rust-lang.org/core/option/
|
||
[`Result`]: https://doc.rust-lang.org/core/result/
|
||
[Rust standard library]: https://doc.rust-lang.org/std/
|
||
[iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html
|
||
[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html
|
||
[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html
|
||
[string formatting]: https://doc.rust-lang.org/core/macro.write.html
|
||
[ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
|
||
[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
|
||
[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
|
||
|
||
In 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.
|
||
|
||
## Disabling the Standard Library
|
||
By default, all Rust crates link the [standard library], which depends on the operating system for features such as threads, files, or networking.
|
||
It also depends on the C standard library `libc`, which closely interacts with OS services.
|
||
Since our plan is to write an operating system, we cannot use any OS-dependent libraries.
|
||
So we have to disable the automatic inclusion of the standard library, which we can do through the [`no_std` attribute].
|
||
|
||
[standard library]: https://doc.rust-lang.org/std/
|
||
[`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||
|
||
We start by creating a new cargo application project.
|
||
The easiest way to do this is through the command line:
|
||
|
||
```
|
||
cargo new kernel --bin --edition 2021
|
||
```
|
||
|
||
We name the project `kernel` here, but of course you can choose your own name.
|
||
The `--bin` flag specifies that we want to create an executable binary (in contrast to a library) and the `--edition 2021` flag specifies that we want to use the [2021 edition] of Rust for our crate.
|
||
When we run the command, cargo creates the following directory structure for us:
|
||
|
||
[2021 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2021/index.html
|
||
|
||
```
|
||
kernel
|
||
├── Cargo.toml
|
||
└── src
|
||
└── main.rs
|
||
```
|
||
|
||
The `Cargo.toml` contains the crate configuration, for example the crate name, the [semantic version] number, and dependencies.
|
||
The `src/main.rs` file contains the root module of our crate and our `main` function.
|
||
You can compile your crate through `cargo build` and then run the compiled `kernel` binary in the `target/debug` subfolder.
|
||
|
||
[semantic version]: https://semver.org/
|
||
|
||
### The `no_std` Attribute
|
||
|
||
Right now our crate implicitly links the standard library.
|
||
Let's try to disable this by adding the [`no_std` attribute]:
|
||
|
||
```rust,hl_lines=3
|
||
// main.rs
|
||
|
||
#![no_std]
|
||
|
||
fn main() {
|
||
println!("Hello, world!");
|
||
}
|
||
```
|
||
|
||
When we try to build it now (by running `cargo build`), the following errors occur:
|
||
|
||
```
|
||
error: cannot find macro `println!` in this scope
|
||
--> src/main.rs:4:5
|
||
|
|
||
4 | println!("Hello, world!");
|
||
| ^^^^^^^
|
||
|
||
error: `#[panic_handler]` function required, but not found
|
||
|
||
error: language item required, but not found: `eh_personality`
|
||
[...]
|
||
```
|
||
|
||
The reason 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.
|
||
|
||
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
|
||
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
|
||
|
||
So let's remove the printing and try again with an empty main function:
|
||
|
||
```rust,hl_lines=5
|
||
// main.rs
|
||
|
||
#![no_std]
|
||
|
||
fn main() {}
|
||
```
|
||
|
||
```
|
||
❯ cargo build
|
||
error: `#[panic_handler]` function required, but not found
|
||
|
||
error: language item required, but not found: `eh_personality`
|
||
[...]
|
||
```
|
||
|
||
The `println` error is gone, but the compiler is still missing a `#[panic_handler]` function and a _language item_.
|
||
|
||
### Panic Implementation
|
||
|
||
The `panic_handler` attribute defines the function that the compiler should invoke when a [panic] occurs.
|
||
The standard library provides its own panic handler function, but in a `no_std` environment we need to define one ourselves:
|
||
|
||
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
|
||
|
||
```rust,hl_lines=3 9-13
|
||
// in main.rs
|
||
|
||
use core::panic::PanicInfo;
|
||
|
||
#![no_std]
|
||
|
||
fn main() {}
|
||
|
||
/// This function is called on panic.
|
||
#[panic_handler]
|
||
fn panic(_info: &PanicInfo) -> ! {
|
||
loop {}
|
||
}
|
||
```
|
||
|
||
The [`PanicInfo` parameter][PanicInfo] contains the file and line where the panic happened and the optional panic message.
|
||
The handler function should never return, so it is marked as a [diverging function] by returning the [“never” type] `!`.
|
||
There is not much we can do in this function for now, so we just loop indefinitely.
|
||
|
||
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
|
||
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
|
||
[“never” type]: https://doc.rust-lang.org/nightly/std/primitive.never.html
|
||
|
||
After defining a panic handler, only the `eh_personality` language item error remains:
|
||
|
||
```
|
||
❯ cargo build
|
||
error: language item required, but not found: `eh_personality`
|
||
|
|
||
= note: this can occur when a binary crate with `#![no_std]` is compiled for a
|
||
target where `eh_personality` is defined in the standard library
|
||
= help: you may be able to compile for a target that doesn't need `eh_personality`,
|
||
specify a target with `--target` or in `.cargo/config`
|
||
```
|
||
|
||
### Disabling Unwinding
|
||
|
||
Language items are special functions and types that are required internally by the compiler.
|
||
They are normally provided by the standard library, which we disabled using the `#![no_std]` attribute.
|
||
|
||
The [`eh_personality` language item] marks a function that is used for implementing [stack unwinding].
|
||
By default, Rust uses unwinding to run the destructors of all live stack variables in case of a [panic].
|
||
This ensures that all used memory is freed and allows the parent thread to catch the panic and continue execution.
|
||
Unwinding, however, is a complex process and requires some OS-specific libraries, such as [libunwind] on Linux or [structured exception handling] on Windows.
|
||
|
||
[`eh_personality` language item]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45
|
||
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
|
||
[libunwind]: https://www.nongnu.org/libunwind/
|
||
[structured exception handling]: https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling
|
||
|
||
While unwinding is very useful, it also has some drawbacks.
|
||
For example, it increases the size of the compiled executable because it requires additional context at runtime.
|
||
Because of these drawbacks, Rust provides an option to [abort on panic] instead.
|
||
|
||
[abort on panic]: https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html#unwinding-the-stack-or-aborting-in-response-to-a-panic
|
||
|
||
We already use a custom panic handler that never returns, so we don't need unwinding for our kernel.
|
||
By disabling it, the `eh_personality` language item won't be required anymore.
|
||
|
||
There are multiple ways to set the panic strategy, the easiest is to use [cargo profiles]:
|
||
|
||
[cargo profiles]: https://doc.rust-lang.org/cargo/reference/profiles.html
|
||
|
||
```toml,hl_lines=3-7
|
||
# in Cargo.toml
|
||
|
||
[profile.dev]
|
||
panic = "abort"
|
||
|
||
[profile.release]
|
||
panic = "abort"
|
||
```
|
||
|
||
This sets the panic strategy to `abort` for both the `dev` profile (used for `cargo build`) and the `release` profile (used for `cargo build --release`).
|
||
Now the `eh_personality` language item should no longer be required.
|
||
|
||
When we try to compile our kernel now, a new error occurs:
|
||
|
||
```
|
||
❯ cargo build
|
||
error: requires `start` lang_item
|
||
```
|
||
|
||
Our kernel is missing the `start` language item, which defines the _entry point_ of the executable.
|
||
|
||
|
||
## Setting the Entry Point
|
||
|
||
The [entry point] of a program is the function that is called when the executable is started.
|
||
One might think that the `main` function is the first function called, however, most languages have a [runtime system], which is responsible for things such as garbage collection (e.g. in Java) or software threads (e.g. goroutines in Go).
|
||
This runtime needs to be called before `main`, since it needs to initialize itself.
|
||
|
||
[entry point]: https://en.wikipedia.org/wiki/Entry_point
|
||
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
|
||
|
||
In a typical Rust binary that links the standard library, execution starts in a C runtime library called [`crt0`] (“C runtime zero”), which sets up the environment for a C application.
|
||
This includes creating a [call stack] and placing the command line arguments in the right CPU registers.
|
||
The C runtime then invokes the [entry point of the Rust runtime][rt::lang_start], which is marked by the `start` language item.
|
||
Rust only has a very minimal runtime, which takes care of some small things such as setting up stack overflow guards or printing a backtrace on panic.
|
||
The runtime then finally calls the `main` function.
|
||
|
||
[`crt0`]: https://en.wikipedia.org/wiki/Crt0
|
||
[call stack]: https://en.wikipedia.org/wiki/Call_stack
|
||
[rt::lang_start]: hhttps://github.com/rust-lang/rust/blob/0d97f7a96877a96015d70ece41ad08bb7af12377/library/std/src/rt.rs#L59-L70
|
||
|
||
Since we're building an operating system kernel that should run without any underlying operating system, we don't want our kernel to depend on any Rust or C runtime.
|
||
To remove these dependencies, we need to do two things:
|
||
|
||
1. Instruct the compiler that we want to build for a bare-metal target environment. This removes the dependency on the C library.
|
||
2. Disable the Rust main function to remove the Rust runtime.
|
||
|
||
### Bare-Metal Target
|
||
|
||
By default Rust tries to build an executable that is able to run in your current system environment.
|
||
For example, if you're using Windows and an `x86_64` CPU, Rust tries to build a `.exe` Windows executable that uses `x86_64` instructions.
|
||
This environment is called your "host" system.
|
||
|
||
To describe different environments, Rust uses a string called [_target triple_].
|
||
You can see the target triple for your host system by running `rustc --version --verbose`:
|
||
|
||
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
|
||
|
||
```
|
||
rustc 1.68.1 (8460ca823 2023-03-20)
|
||
binary: rustc
|
||
commit-hash: 8460ca823e8367a30dda430efda790588b8c84d3
|
||
commit-date: 2023-03-20
|
||
host: x86_64-unknown-linux-gnu
|
||
release: 1.68.1
|
||
LLVM version: 15.0.6
|
||
```
|
||
|
||
The above output is from a `x86_64` Linux system.
|
||
We see that the `host` triple is `x86_64-unknown-linux-gnu`, which includes the CPU architecture (`x86_64`), the vendor (`unknown`), the operating system (`linux`), and the [ABI] (`gnu`).
|
||
|
||
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
|
||
|
||
By compiling for our host triple, the Rust compiler and the linker assume that there is an underlying operating system such as Linux or Windows that uses the C runtime by default, which requires the `start` language item.
|
||
To avoid the runtimes, we can compile for a different environment with no underlying operating system.
|
||
|
||
#### The `x86_64-unknown-none` Target
|
||
|
||
Rust supports a [variety of target systems][platform-support], including some bare-metal targets.
|
||
For example, the `thumbv7em-none-eabihf` target triple can be used to compile for an [embedded] [ARM] system with a `Cortex M4F` CPU, as used in the [Rust Embedded Book].
|
||
|
||
[platform-support]: https://doc.rust-lang.org/rustc/platform-support.html
|
||
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
|
||
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
|
||
[Rust Embedded Book]: https://docs.rust-embedded.org/book/intro/index.html
|
||
|
||
Our kernel should run on a bare-metal `x86_64` system, so the suitable target triple is [`x86_64-unknown-none`].
|
||
The `-none` suffix indicates that there is no underlying operating system.
|
||
To be able to compile for this target, we need to add it using [`rustup`]:
|
||
|
||
[`x86_64-unknown-none`]: https://doc.rust-lang.org/rustc/platform-support/x86_64-unknown-none.html
|
||
[`rustup`]: https://doc.rust-lang.org/rustc/platform-support/x86_64-unknown-none.html
|
||
|
||
```
|
||
rustup target add x86_64-unknown-none
|
||
```
|
||
|
||
This downloads a pre-compiled copy of the `core` library for the target.
|
||
Afterwards, we can [cross compile] our executable for a bare metal environment by passing a `--target` argument:
|
||
|
||
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
|
||
|
||
```
|
||
❯ cargo build --target x86_64-unknown-none
|
||
Compiling kernel v0.1.0 (/<...>/kernel)
|
||
error: requires `start` lang_item
|
||
```
|
||
|
||
We still get the error about a missing `start` language item because we're still depending on the Rust runtime. To remove that dependency, we can use the `#[no_main]` attribute.
|
||
|
||
### The `#[no_main]` Attribute
|
||
|
||
To tell the Rust compiler that we don't want to use the normal entry point chain, we add the `#![no_main]` attribute.
|
||
|
||
```rust,hl_lines=4
|
||
// main.rs
|
||
|
||
#![no_std]
|
||
#![no_main]
|
||
|
||
use core::panic::PanicInfo;
|
||
|
||
/// This function is called on panic.
|
||
#[panic_handler]
|
||
fn panic(_info: &PanicInfo) -> ! {
|
||
loop {}
|
||
}
|
||
```
|
||
|
||
You might notice that we removed the `main` function.
|
||
The reason is that a `main` doesn't make sense without an underlying runtime that calls it.
|
||
Instead, we are now overwriting the operating system entry point with our own `_start` function:
|
||
|
||
```rust,hl_lines=3-6
|
||
// in main.rs
|
||
|
||
#[no_mangle]
|
||
pub extern "C" fn _start() -> ! {
|
||
loop {}
|
||
}
|
||
```
|
||
|
||
By using the `#[no_mangle]` attribute we disable the [name mangling] to ensure that the Rust compiler really outputs a function with the name `_start`.
|
||
Without the attribute, the compiler would generate some cryptic `_ZN3kernel4_start7hb173fedf945531caE` symbol to give every function an unique name.
|
||
The reason for naming the function `_start` is that this is the default entry point name for most systems.
|
||
|
||
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
|
||
|
||
We mark the function as `extern "C"` to tell the compiler that it should use the [C calling convention] for this function (instead of the unspecified Rust calling convention).
|
||
|
||
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
|
||
|
||
Like in our panic handler, the `!` return type means that the function is diverging, i.e. not allowed to ever return.
|
||
This is required because the entry point is not called by any function, but invoked directly by the operating system or bootloader.
|
||
So instead of returning, the entry point should e.g. invoke the [`exit` system call] of the operating system.
|
||
In our case, shutting down the machine could be a reasonable action, since there's nothing left to do if a freestanding binary returns.
|
||
For now, we fulfill the requirement by looping endlessly.
|
||
|
||
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
|
||
|
||
When we run `cargo build --target x86_64-unknown-none` now, it should finally compile without any errors:
|
||
|
||
```
|
||
❯ cargo build --target x86_64-unknown-none
|
||
Compiling kernel v0.1.0 (/<...>/kernel)
|
||
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
|
||
```
|
||
|
||
We successfully created a minimal bare-metal kernel executable! The compiled executable can be found at `target/x86_64-unknown-none/debug/kernel`.
|
||
There is no `.exe` extension even if you're on Windows because the `x86_64-unknown-none` target uses UNIX standards.
|
||
|
||
To build the kernel with optimizations, we can run:
|
||
|
||
```
|
||
cargo build --target x86_64-unknown-none --release
|
||
```
|
||
|
||
The compiled executable is placed at `target/x86_64-unknown-none/release/kernel` in this case.
|
||
|
||
In the next post we will cover how to turn this kernel into a bootable disk image that can be run in a virtual machine or on real hardware.
|
||
In the rest of this post, we will introduce some tools for examining our kernel executable.
|
||
These tools are very useful for debugging future issues, so it's good to know about them.
|
||
|
||
## Useful Tools
|
||
|
||
In this section, we will examine our kernel executable using the [`objdump`], [`nm`], and [`size`] tools.
|
||
|
||
[`objdump`]: https://www.man7.org/linux/man-pages/man1/objdump.1.html
|
||
[`nm`]: https://man7.org/linux/man-pages/man1/nm.1.html
|
||
[`size`]: https://man7.org/linux/man-pages/man1/size.1.html
|
||
|
||
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`.
|
||
|
||
[`cargo-binutils`]: https://github.com/rust-embedded/cargo-binutils
|
||
|
||
### `nm`
|
||
|
||
We defined a `_start` function as the entry point of our kernel.
|
||
To verify that it is properly exposed in the executable, we can run `nm` to list all the symbols defined in the executable:
|
||
|
||
```
|
||
❯ rust-nm target/x86_64-unknown-none/debug/kernel
|
||
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
|
||
```
|
||
|
||
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]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
|
||
|
||
[ELF file format]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
|
||
|
||
#### File Headers
|
||
|
||
Among other things, the ELF [file header] specifies the target architecture and the entry point address of the executable files.
|
||
To print the file header, we can use `objdump -f`:
|
||
|
||
[file header]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
|
||
|
||
```
|
||
❯ rust-objdump -f target/x86_64-unknown-none/debug/kernel
|
||
|
||
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
|
||
architecture: x86_64
|
||
start address: 0x0000000000001210
|
||
```
|
||
|
||
As expected, our kernel targets the `x86_64` CPU architecture.
|
||
The start address specifies the memory address of our `_start` function.
|
||
Here the function name `_start` becomes important.
|
||
If we rename the function to something else (e.g., `_start_here`) and recompile, we see that no start address is set in the ELF file anymore:
|
||
|
||
```bash,hl_lines=5
|
||
❯ rust-objdump -f target/x86_64-unknown-none/debug/kernel
|
||
|
||
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
|
||
architecture: x86_64
|
||
start address: 0x0000000000000000
|
||
```
|
||
|
||
#### Sections
|
||
|
||
Using `objdump -h`, we can print the various sections of our kernel executable:
|
||
|
||
```bash,hl_lines=12
|
||
❯ rust-objdump -h target/x86_64-unknown-none/debug/kernel
|
||
|
||
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
|
||
|
||
Sections:
|
||
Idx Name Size VMA Type
|
||
0 00000000 0000000000000000
|
||
1 .dynsym 00000018 00000000000001c8
|
||
2 .gnu.hash 0000001c 00000000000001e0
|
||
3 .hash 00000010 00000000000001fc
|
||
4 .dynstr 00000001 000000000000020c
|
||
5 .text 00000004 0000000000001210 TEXT
|
||
6 .dynamic 000000a0 0000000000002218
|
||
7 .debug_abbrev 0000010c 0000000000000000 DEBUG
|
||
8 .debug_info 000005ce 0000000000000000 DEBUG
|
||
9 .debug_aranges 00000040 0000000000000000 DEBUG
|
||
10 .debug_ranges 00000030 0000000000000000 DEBUG
|
||
11 .debug_str 00000492 0000000000000000 DEBUG
|
||
12 .debug_pubnames 000000bc 0000000000000000 DEBUG
|
||
13 .debug_pubtypes 0000036c 0000000000000000 DEBUG
|
||
14 .debug_frame 00000050 0000000000000000 DEBUG
|
||
15 .debug_line 00000059 0000000000000000 DEBUG
|
||
16 .comment 00000013 0000000000000000
|
||
17 .symtab 00000060 0000000000000000
|
||
18 .shstrtab 000000ce 0000000000000000
|
||
19 .strtab 00000022 0000000000000000
|
||
```
|
||
|
||
The `.text` section contains the program code, the other sections are not important right now.
|
||
The section dump is useful for debugging, for example for checking which section a pointer points to.
|
||
|
||
Most of the sections only contain debug information and are not needed for execution.
|
||
We can remove this debug information using `rust-strip`:
|
||
|
||
```
|
||
❯ rust-strip target/x86_64-unknown-none/debug/kernel
|
||
❯ rust-objdump -h target/x86_64-unknown-none/debug/kernel
|
||
|
||
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
|
||
|
||
Sections:
|
||
Idx Name Size VMA Type
|
||
0 00000000 0000000000000000
|
||
1 .dynsym 00000018 00000000000001c8
|
||
2 .gnu.hash 0000001c 00000000000001e0
|
||
3 .hash 00000010 00000000000001fc
|
||
4 .dynstr 00000001 000000000000020c
|
||
5 .text 00000004 0000000000001210 TEXT
|
||
6 .dynamic 000000a0 0000000000002218
|
||
7 .shstrtab 00000034 0000000000000000
|
||
```
|
||
|
||
#### Disassembling
|
||
|
||
Sometimes we need to check the [assembly code] that certain functions compile to.
|
||
We can use the `objdump -d` command to print the `.text` section of an executable in assembly language:
|
||
|
||
[assembly code]: https://en.wikipedia.org/wiki/X86_assembly_language
|
||
|
||
```
|
||
❯ rust-objdump -d target/x86_64-unknown-none/debug/kernel
|
||
|
||
target/x86_64-unknown-none/debug/kernel: file format elf64-x86-64
|
||
|
||
Disassembly of section .text:
|
||
|
||
0000000000001210 <_start>:
|
||
1210: eb 00 jmp 0x1212 <_start+0x2>
|
||
1212: eb fe jmp 0x1212 <_start+0x2>
|
||
```
|
||
|
||
We see that our `_start` function consists of just two [`jmp` instructions], which jump to the given address.
|
||
The first `jmp` command jumps to the second `jmp` command at address `1212`.
|
||
The second `jmp` command jumps to itself again, thereby representing the infinite loop that we've written in our `_start` function.
|
||
|
||
[`jmp` instructions]: https://www.felixcloutier.com/x86/jmp
|
||
|
||
As you probably noticed, the first `jmp` command is not really needed.
|
||
Such inefficiencies can happen in debug builds because the compiler does not optimize them.
|
||
If we disassemble the optimized release build, we see that the compiler indeed removed the unneeded `jmp`:
|
||
|
||
```
|
||
❯ cargo build --target x86_64-unknown-none --release
|
||
❯ rust-objdump -d target/x86_64-unknown-none/release/kernel
|
||
|
||
target/x86_64-unknown-none/release/kernel: file format elf64-x86-64
|
||
|
||
Disassembly of section .text:
|
||
|
||
0000000000001210 <_start>:
|
||
1210: eb fe jmp 0x1210 <_start>
|
||
```
|
||
|
||
We will use continue to use the above tools in future posts, as they're quite useful for debugging issues.
|
||
|
||
## What's next?
|
||
|
||
In the [next post], we will learn how to turn our minimal kernel in a bootable disk image, which can then be started in the [QEMU] virtual machine and on real hardware.
|
||
For this, we'll explore the boot process of `x86_64` systems and learn about the differences between UEFI and the legacy BIOS firmware.
|
||
|
||
[next post]: @/edition-3/posts/02-booting/index.md
|
||
[QEMU]: https://www.qemu.org/
|