Rust no_std Programs: Entry Points & Panics
A no_std program is a Rust program with no standard library runtime. The program must name its own first step and answer its own panics.
No main either
Programs give up the ready-made entry point too. This second line says so:
#![no_main]Your own first step
The entry point is a plain function with a fixed name. On this Linux host the name is _start. The no_mangle mark keeps the name exact:
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
// your program lives here
}A panic needs a handler
Nobody prints the panic message now. You write the one function that receives every panic:
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
// report if you can, then stop the program
}Panics stop; they do not unwind
Unwinding walks the stack and tidies up as it goes. That machinery lives in std. Bare programs switch it off in Cargo.toml:
// Cargo.toml
// [profile.release]
// panic = "abort"One symbol to stand in
Code that can panic leaves the linker asking for one unwinding symbol. Nothing will ever call it, since panics now abort. An empty namesake ends the argument:
#[unsafe(no_mangle)]
extern "C" fn rust_eh_personality() {}The chip costume
On ARM Cortex-M chips, the cortex-m-rt crate offers a mark instead. You will meet this shape in the wild, so learn to recognise it. This course cannot run chip code:
#[entry]
fn main() -> ! {
loop {}
}Example
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[link(name = "c")]
unsafe extern "C" {
fn write(fd: i32, buf: *const u8, count: usize) -> isize;
fn exit(code: i32) -> !;
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
let msg = b"panic: giving up\n";
unsafe {
write(2, msg.as_ptr(), msg.len());
exit(101)
}
}
fn read_sensor(slot: usize) -> u32 {
let readings = [10, 20];
readings[slot] // panics when slot is past the end
}
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let slot = core::hint::black_box(5);
let _value = read_sensor(slot);
unsafe { exit(0) }
}
// With panic = "abort" nothing ever unwinds, but core still names this
// symbol; an empty stand-in satisfies the linker.
#[unsafe(no_mangle)]
extern "C" fn rust_eh_personality() {}Key points
- Use #![no_main] to say the binary brings its own entry point.
- The usual main is gone
- The entry point never returns.
- It ends in a loop, a wait, or an exit.
- Its return type is written !, a type that means this never comes back
- Every no_std binary needs a #[panic_handler].
- It receives a PanicInfo.
- It must never return
- Set panic = "abort" in Cargo.toml.
- A panic then stops the program at once.
- Unwinding needs std machinery, so it is not available
- Real chips differ in costume only.
- A runtime crate for the chip, like cortex-m-rt, prepares memory first.
- Then it calls the function you marked
- Our host demo passes -nostartfiles to the linker.
- The C runtime owns a _start of its own.
- With no runtime underneath, the binary itself supplies the entry point and the panic answer.
- No stage is built before the code starts.
In this example, _start asks read_sensor for slot 5. The array holds two readings, so the index panics. There is no std to print the news. The panic handler takes over. It writes its line to the error output, file descriptor 2. Then it exits with code 101. Nothing reaches the normal output. The last function is the linker stand-in from the snippet above. On a real device there is no exit call either. The handler might blink a light, send a message down a wire, or simply halt the chip.
Note
Two entry points cannot share one name
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Your no_std program panics on a device with no screen. What happens?