Linux "Hello world" in no_std
Rust
The Rust-glibc example used glibc, which provided Rust with OS APIs. In this section, the goal is to write the same "Hello world" program without any libc. It must rely only on RISC-V Linux ABI, reimplementing the required OS API as we need it, just like the GNU assembly "Hello world".
This section is inspired by Embeddonomicon. It takes the Linux process sandbox as a kind of an "embedded" environment, with complete control over memory, without any external code, with only Linux ABI as its "hardware". A custom runtime will be grown as we go.
Tools and references
Ensure that rustup is installed.
Make sure that a rustc target riscv64gc-unknown-none-elf
is installed:
$ rustup target add riscv64gc-unknown-none-elf
Unlike riscv64gc-unknown-linux-gnu
, it assumes a "baremetal" environment and does not try to link
any libraries.
cargo-binutils
is not strictly necessary, but it's nice to have cargo objdump
and cargo nm
:
$ cargo install cargo-binutils
Documentation:
- Embeddonomicon shows how to bring up a custom Rust runtime in a new environment.
- Rust inline assembly is the definitive reference manual for the topic.
The minimal no_std
binary
$ cargo init --name hello-nostd
Set the default target and runner in .cargo/config.toml
:
[build]
target = "riscv64gc-unknown-none-elf" # build for this target by default
[target.riscv64gc-unknown-none-elf] # configuration for this target
runner = "qemu-riscv64" # for `cargo run` to work on x86_64
Set src/main.rs
to be #![no_main]
and #![no_std]
.
A no_std
environment still requires at least two basic runtime mechanisms, both related to
Rust panicking:
-
what to do when unwinding the stack on panic. This is implemented by a
#[lang = "eh_personality"]
function or just by waiving it off inCargo.toml
:[profile.dev] panic = "abort" [profile.release] panic = "abort"
Although:
riscv64gc-unknown-none-elf
assumes"panic-strategy": "abort"
by default. -
a
#[panic_handler]
function to execute when panic happened and the stack was unwound successfully:src/main.rs
:#![no_main] #![no_std] #[panic_handler] fn panic_handler(_panic: &core::panic::PanicInfo) -> ! { loop {} // for now, just hang to satisfy the typechecker. }
#![no_main]
means that we should also remove fn main()
. We'll get back to it later.
The binary that can be built at this stage does not actually contain any executable code.
$ cargo run
'cargo run' terminated by signal SIGSEGV (Address boundary error)
$ cargo objdump --release -- -d | rustfilt
hello-nostd: file format elf64-littleriscv
A minimal executable that exits successfully
The output of cargo rustc -- -Z unstable-options --print target-spec-json
suggests that
riscv64-unknown-none-elf
uses rust.lld
as its default linker.
I did not dig into details, but I guessed that its default linker script uses a _start
symbol
as its entrypoint.
Reading the Rust inline assembly guide and translating the knowledge from the assembly "Hello world", we get this:
#![allow(unused)] #![no_main] #![no_std] #![feature(start)] // to enable #[start] fn main() { use core::arch::asm; // to use asm!() #[panic_handler] fn panic_handler(_panic: &core::panic::PanicInfo) -> ! { loop {} } #[no_mangle] // for linker to be able to see `_start` #[start] pub unsafe extern "C" // everything about this function is unsafe! fn _start() -> ! { // does not return asm!( "ecall", in("a7") 93, // __NR_exit in("a0") 0, // status code 0 options(noreturn), ) // `noreturn` assigns this block the return type `!` } }
Writing to stdout
#![allow(unused)] fn main() { ... fn _start() -> ! { static HELLO: &[u8] = b"Hello world!\n"; asm!( "ecall", in("a7") 64, // __NR_write in("a0") 1, // STDOUT_FILENO in("a1") HELLO.as_ptr().addr(), // #![feature(strict_provenance)] in("a2") HELLO.len(), options(readonly), // expect no changes to memory ); ... } }
Tidying up: linux-rt
and its linker script
TODO
Troubleshooting
Getting the JSON spec of the current rustc target (requires nightly):
cargo rustc -- -Z unstable-options --print target-spec-json
cargo rustc -- -Z unstable-options --print target-spec-json
$ cargo rustc -- -Z unstable-options --print target-spec-json
Compiling hello-nostd v0.1.0 (/home/user/code/learn/eval/rvemu/riscv/hello-nostd)
{
"arch": "riscv64",
"code-model": "medium",
"cpu": "generic-rv64",
"data-layout": "e-m:e-p:64:64-i64:64-i128:128-n64-S128",
"eh-frame-header": false,
"emit-debug-gdb-scripts": false,
"features": "+m,+a,+f,+d,+c",
"is-builtin": true,
"linker": "rust-lld",
"linker-flavor": "ld.lld",
"llvm-abiname": "lp64d",
"llvm-target": "riscv64",
"max-atomic-width": 64,
"panic-strategy": "abort",
"relocation-model": "static",
"target-pointer-width": "64"
}