Now let’s create a new rust application. I’ve chosen to call our OS “Zeno” (no particular reason, just sounds cool)
1# create a new cargo project called 'zeno'
2$ cargo new zeno
34# run our application for good luck
5$ cargo run
6Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
7Running `target/debug/zeno`
89Hello, world!
Great! the hardest part is over. Rest will be easy.
Leaving kernel dependencies behind
Next. We need to make sure we remove any connection between our rust binary to our host operating system.
Here’s why:
By default, rust and its standard library depend heavily on OS abstractions to do a lot of heavylifting for us and thus, it assumes the presence of an underlying OS and the C runtime.
When you invoke the println! macro, or allocate memory, and pretty much anything that has to do with I/O, rust makes syscalls to the host operating system to make stuff happen.
However, in our case, there is no OS. We are writing our own operating system. Which means whatever abstractions rust relies on to perform those tasks, Gone!
We cannot access them anymore so we need to get rid of any reference to them.
Step 1. Tell rust to avoid using the standard library.
We do this by adding the #![no_std] attribute at the top of our main.rs file.
The no_std attribute explicitly blocks rust from linking any parts of its standard library in our binary (which is the rust compiler’s default behavior)
Only the core library remains. Everything else, that relies on an underlying OS is gone.
The core library is platform agnostic, so we can keep it.
main.rs
1#![no_std]// 👈 Add this line
23fnmain(){4println!("Hello, world!");5}
Pretty simple right? Well not quite. Trying to compile this code quickly reveals that the rust compiler is not happy.
Although it gives pretty helpful error messages.
compiling with the no_std attribute
1Compiling zeno v0.1.0 (/path/to/zeno) 2error: cannot find macro `println` in this scope
3--> src/main.rs:4:5
4| 54| println!("Hello, world!"); 6| ^^^^^^^
7 8error: `#[panic_handler]` function required, but not found 910error: unwinding panics are not supported without std
11|12= help: using nightly cargo, use -Zbuild-std with panic="abort" to avoid unwinding13= note: since the core library is usually precompiled with panic="unwind", rebuilding your crate with panic="abort" may not be enough to fix the problem
1415error: could not compile `zeno`(bin "zeno") due to 3 previous errors
Let’s unpack this one step at a time
1error: cannot find macro `println` in this scope
This is pretty straightforward. We’ve stripped rust of all its standard library references using the #![no_std] attribute. That includes the println macro as well.
Unfortunately for us, we can’t use println! anymore. so we’ll have to get rid of those invocations for now. We’ll figure out a way to log things later.
main.rs
1#![no_std]23fnmain(){}// 👈 `println` gone
But that’s still not enough. There’s another compiler error.
1error: `#[panic_handler]` function required, but not found23error: unwinding panics are not supported without std
#[panic_handler] is used to define the behavior of panic! in #![no_std] applications.
The #[panic_handler] attribute must be applied to a function with the following signature
fn(&PanicInfo) -> !
Such function must appear once in the dependency graph of a binary / dylib / cdylib crate.
Cool. Let’s add that to our code.
main.rs
1#![no_std] 2 3usecore::panic::PanicInfo;// 👈 Add this line
4 5fnmain(){} 6 7 8// 👇 Add this function (notice the `#[panic_handler]` attribute)
9#[panic_handler]10fnpanic(_info: &PanicInfo) -> !{11loop{}12}
💡 Making rust-analyzer happy
If you are using an LSP enabled editor (most likely, you are), As soon as you add the panic function, you might notice that your editor starts yelling at you
Something along the lines of:
found duplicate lang item panic_impl the lang item is first defined in crate std (which test depends on)
This is because we’ve told our bin code to not include any main function, however, by default, cargo tries to compile tests and bench code as well, in which there’s no mention of not including the main function.
In order to fix this, we need to tell cargo to not compile them by adding this to our Cargo.toml file
Cargo.toml
1...2[[bin]]3name="zeno"# the name of our crate4test=false# tell cargo to not compile tests5bench=false# tell cargo to not compile benches6...
Additionally, another one of the default behaviors of the rust compiler is to unwind the stack whenever a program panics. This is done to allow cleanup of resources like memory, files, etc by calling Drop::drop methods on each stack frame.
However, a panic in our kernel means immediate shutdown i.e aborting the entire kernel. So we tell rust that we are no longer using unwinding panics by setting panic = "abort" in our Cargo.toml
Cargo.toml
1[profile.dev]# 👈 For dev mode2panic="abort"34[profile.release]# 👈 For release mode5panic="abort"
Lastly, the compiler says:
1error: using `fn main` requires the standard library
2|3= help: use `#![no_main]` to bypass the Rust generated entrypoint and declare a platform specific entrypoint yourself, usually with `#[no_mangle]`45error: could not compile `zeno`(bin "zeno") due to 1 previous error
Alright Rust, Fine! Let’s remove the main function, and add the #![no_main] attribute as well
main.rs
1#![no_std]2#![no_main]// 👈 Add this line
34usecore::panic::PanicInfo;56#[panic_handler]7fnpanic(_info: &PanicInfo) -> !{8loop{}9}
Seems straight forward, although a bit counter intuitive. I mean, If we get rid of main, then what? We need some kind of entry point into our OS right?
How does the hardware know where to start execution.
We get a hint of that when we try to compile. Albeit in the form of a giant, cryptic error.
I thought we told rust to get rid of all references to any system libraries, including libc
Yes. Yes we did. And to be fair, Rust did honor our request. Rust did not include any system library references. In fact the error says the same thing too, that we didn’t find any __libc_start_main symbol present
However, rustc is just a compiler
After the rust compiler is done with our code, it passes it to the linker, which then converts our generated code into an executable.
The problem is, the default linker, in this case rust-lld assumes that we are using the C runtime, which of course we do not.
Wait what!? C has a runtime??
Isn’t C one of those languages that can just run without any runtime?
Well.. yes, but not quite.
When you hear the C “runtime”, its not a runtime in the traditional sense as in, there’s no VM or an interpreter that runs your programs, instead runtime in this case basically means that the main function gets wired to the crt0 startup routines.
The crt0 routines do a bunch of things including (but not limited to) making sure that you have a valid stack to work with, initializing the heap for you so that you can dynamically allocate memory, and gathering all the arguments to your program as well as the environment variables.
1intmain(intargc,char**argv,char**envp){ 2// ..
3} 4 5// 👇 crt0 does this for you
6void_start(){ 7intargc=...; 8char**argv=...; 9char**envp=...;1011/* setup stuff */1213// 👇 Call to the main function that you wrote
14intexit_code=main(15argc,16argv,17envp18);1920/* cleanup stuff */21exit(exit_code);22}
When you start a program, it doesn’t actually jump to the main function but instead, it jumps to a symbol called _start (unless you intentionally override the default and tell the linker to emit a different e_entry ELF header)
In our case, we need to tell the linker to not expect the C runtime.
We do this by passing in additional arguments to our command