Don’t panic Link to heading
Trying to build the project with
> cargo build
error[E0463]: can't find crate for `std`
|
= note: the `aarch64-unknown-none` target may not support the standard library
= note: `std` is required by `tutnova` because it does not declare `#![no_std]`
error: cannot find macro `println` in this scope
--> src/main.rs:2:5
|
2 | println!("Hello, world!");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
For more information about this error, try `rustc --explain E0463`.
error: could not compile `tutnova` (bin "tutnova") due to 3 previous errors
throws 3 errors we have to address.
As the third error says we are missing a panic handler. A normal rust program can just exit, but what happens when rust panics in an bare metal environment?
In our src/main.rs
we need a way for our kernel to catch a panic. Here we can use the convenient #[panic_handler]
.
So we add a short function that loops infinitely:
#[panic_handler]
fn panic(_panic: &PanicInfo) -> ! {
loop {}
}
Where is my std Link to heading
Surprise we also can’t use the rust standard library. std
requires OS functionality that doesn’t exist because no other code is run before ours(except the firmware).
That means we have to disable the standard library by adding following lines at the top of the main.rs
:
#![no_main]
#![no_std]
Main Link to heading
Our main function will also not work because it also relies on OS magic to happen so we can delete the function and replace it with:
#[no_mangle]
#[link_section = ".text._start"]
pub unsafe extern "C" fn _start() {
asm!("ldr x0, =0x8004000", "mov sp, x0");
loop {}
}
Okay this was a lot lets go through the lines and understand what happens:
#[no_mangle]
- Compilers love to change names of functions to convey more information but as the first function we want the name to be consistent.
#[link_section = ".text._start"]
- Last session we created our linker file that defines the layout of our memory. Here we defined that our .text._start
shall come before any other code so we can be sure it starts with our main function.
.text : {
KEEP(*(.text._start))
*(.text .text.*)
}
extern "C"
- This tells the compiler to use the C-lang Application Binary Interfaces(ABI) instead of the rust ABI. Rust ABI has a different behavior in some circumstances and may cause undefined behavior. So we want the first function to be compiled like C would have done it.
asm!("ldr x0, =0x8004000", "mov sp, x0");
- As you may know each program has stack and heap memory. But how does the chip know where in memory the stack data has to go? This is what this assembly fixes. We write into the x0
register what address shall be the start of our stack and move that value from the x0
register into a special purpose register called stack pointer(sp)
.
In our case the code starts at 0x80000
. Stack pointer starts at 0x8004000
growing down, so it could collide with the code when writing too much data to the stack. It’s always possible to change it and we have to keep that in mind.
We have about 0x8004000-0x80000 bytes = 8MB
of stack capability here. To ensure efficient memory access the address of the stack pointer shall be a multiple of 16 because we use a 64bit
architecture.
Thats it, our first “Hello World”. A bit underwhelming but functional.
Complete main.rs Link to heading
//src/main.rs
#![no_main]
#![no_std]
use core::{arch::asm, panic::PanicInfo};
#[panic_handler]
fn panic(_panic: &PanicInfo) -> ! {
loop {}
}
#[no_mangle]
#[link_section = ".text._start"]
pub unsafe extern "C" fn _start() {
// Set the stack pointer
asm!("ldr x0, =0x8004000", "mov sp, x0");
loop {}
}
Let’s Build Link to heading
We should be able to build now:
> cargo build --release
Still failing?: Depending on the package edition in
Cargo.toml
-link_section
andno_mangle
may requireunsafe
.
First Hardware Test Link to heading
Finally we can test our code on hardware(not really). We will be able to to deploy it but there is no way to check if it works. But let’s try it anyway.
- Get a SD card that you don’t need anymore
- Remove all partitions and create one named
boot
asfat32
. - Download
bootcode.bin
,start.elf
form the official repo and copy the files to the SD card. - Create a
config.txt
fill it with:
kernel=kernel8.img
arm_64bit=1
enable_uart=1
- Move the
config.txt
to the Raspberry PIs SD card - Install
LLVM
- Now we have to convert our
elf
file to aimg
file. Theelf
file probably has the same name as the project and has no ending (e.g.target/aarch64-unknown-none/release/tutnova
)
> llvm-objcopy -O binary target/aarch64-unknown-none/release/YOUR_ELF_NAME kernel8.img
- The generated
kernel8.img
can also be copied to the SD card - Take the SD card plug it in and watch it do… nothing
It should blink once and do nothing afterwards. If there are other blinking patterns there might be an issue.
I was a bit quick. Let me explain more in detail what just happened.
The files you downloaded is the firmware of the Raspberry PI. There is a start4.elf
which is used for newer Raspberry PIs.
The bootcode.bin
initializes the SDRAM and load the start.elf
. The start.elf
will initialize the hardware and load your kernel.
The config we created defines that we want to use kernel8.img
as our kernel and that our code is built for 64bit
architecture.
enable_uart
will be more important for future steps.
A ELF file
contains more information about the binary but a Raspberry PI doesn’t need that information, thats why we convert it to a raw binary blob via the llvm-objcopy
command.