Linux "Hello world" in Rust with GNU libc
Let's write a Rust program that outputs "Hello world" to stdout and compile it to a RISC-V ELF binary. Bonus points: actually run it.
If you have a RISC-V Linux installation, congratulations, everything is handled by the default
configuration and toolchains (cargo init && cargo run
should just work).
The following instructions assume cross-compilation on a x86_64 host machine with Ubuntu 22.04.
Packages and tools for cross-compilation
Ensure that rustup is installed. This is needed to manage rustc targets.
Make sure that riscv64gc-unknown-linux-gnu
rustc target is installed:
rustup target add riscv64gc-unknown-linux-gnu
It seems to be the case that rustc targets do not try to bring their own GCC toolchains with them or guess what system packages provide it (which is reasonable, since each Linux distribution has its own non-standard packaging for cross-compilation toolchains and glibc; I wish it was not so).
For Ubuntu, make sure that the following packages are installed:
gcc-riscv64-linux-gnu
, the cross-compilation GCC toolchain for RISCV.libc6-riscv64-cross
for a dynamically-linked RISCV version of glibcqemu-user
to run a RISCV binary on x86_64patchelf
in case you want to run a dynamically-linked binary on x86_64
Documentation and references:
Compilation
Create a Cargo project template in an empty directory:
$ cargo init --name hello-libc
$ cat src/main.rs
fn main() {
println!("Hello world!");
}
Now, adjust .cargo/config.toml
:
[build]
target = "riscv64gc-unknown-linux-gnu" # build for this target by default
[target.riscv64gc-unknown-linux-gnu] # settings for this target
linker = "riscv64-linux-gnu-gcc" # mandatory to link the binary
runner = "qemu-riscv64" # to make `cargo run` work on x86\_64
linker = "riscv64gc-unknown-linux-gnu"
is crucial for cross-compilation, without it
rustc just tries to use whichever ld
it finds in $PATH and fails miserably. I still
don't understand why different GCC toolchains require mostly-the-same, but different
GNU linkers.
Also, you cannot use linker = "riscv64-unknown-linux-ld"
directly, since it will not be able
to find -lgcc_s
on its own. There might be a way to tweak this, but GNU toolchain options are
pain</rant>
Now cargo build
(--release
) should produce a RISCV executable in
target/riscv64gc-unknown-linux-gnu/debug/hello-libc
.
Compiling with a statically-linked glibc
Since Rust 1.19, it is possible to link glibc
statically. To make a statically-linked executable, use a somewhat cryptically named
target-feature=+crt-static
to rustc flags in .cargo/config.toml
:
[target.riscv64gc-unknown-linux-gnu]
...
rustflags = [
"-C", "target-feature=+crt-static", # link glibc statically
]
Running it
Let's assume the following shell variables are set, to make snippets more human-friendly:
$ BIN_DIR=./target/riscv64gc-unknown-linux-gnu/debug
$ RV_SYS_DIR=/usr/riscv64-linux-gnu
The compiled binary, $BIN_DIR/hello-libc
, can be copied to a RISC-V Linux installation
and it should be able to run there (I did not verify this).
If it is statically linked, it should just work with qemu-riscv64
.
If you want to run a dynamically linked RISC-V executable on a x86_64 machine, things get complicated:
$ qemu-riscv64 $BIN_DIR/hello-libc
qemu-riscv64: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory
There is no such ELF loader ("ELF interpeter"), /lib/ld-linux-riscv64-lp64d.so.1
,
installed, but:
$ apt-file search ld-linux-riscv64-lp64d.so
libc6-riscv64-cross: /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1
[Peek under this fold to see] what does not work to fix this.
-
trying to change the executable
RPATH
does not change the hardcoded ELF interpeter path. It is not a regular shared library and always is an absolute path. -
there seem to be no way to convince the linker to use the interpreter in
$RV_SYS_DIR/lib/
. The GNU toolchain insists on hardcoding a specific ELF interpreter path it was itself configured with.
One (dirty) way to solve this is to symlink $RV_SYS_DIR/lib/ld-linux-riscv64-lp64d.so.1
into /lib/
manually and use LD_LIBRARY_PATH=$RV_SYS_DIR/lib
to override system libraries.
A better way is to patch the ELF interpreter and RPATH in (a copy of) the executable:
$ patchelf $BIN_DIR/hello-libc \
--set-interpreter $RV_SYS_DIR/lib/ld-linux-riscv64-lp64d.so.1 \
--set-rpath $RV_SYS_DIR/lib
$ qemu-riscv64 $BIN_DIR/hello-libc
Hello world!
Troubleshooting the linking
These are some tricks I found useful to understand what was (not) going on:
-
troubleshooting link failures with
cargo build
with-vv
.The actual rustc command is still a hostile lump of text.
$ cargo build -vv Compiling hello-libc v0.1.0 (/home/user/code/lang/arch/riscv/hello-libc) Running `CARGO=/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo CARGO_BIN_NAME=hello-libc CARGO_CRATE_NAME=hello_libc CARGO_MANIFEST_DIR=/home/user/code/lang/arch/riscv/hello-libc CARGO_PKG_AUTHORS='' CARGO_PKG_DESCRIPTION='' CARGO_PKG_HOMEPAGE='' CARGO_PKG_LICENSE='' CARGO_PKG_LICENSE_FILE='' CARGO_PKG_NAME=hello-libc CARGO_PKG_REPOSITORY='' CARGO_PKG_RUST_VERSION='' CARGO_PKG_VERSION=0.1.0 CARGO_PKG_VERSION_MAJOR=0 CARGO_PKG_VERSION_MINOR=1 CARGO_PKG_VERSION_PATCH=0 CARGO_PKG_VERSION_PRE='' CARGO_PRIMARY_PACKAGE=1 LD_LIBRARY_PATH='/home/user/code/lang/arch/riscv/hello-libc/target/debug/deps:/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib' rustc --crate-name hello_libc --edition=2021 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C metadata=cac59addb6dfe60a -C extra-filename=-cac59addb6dfe60a --out-dir /home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/deps --target riscv64gc-unknown-linux-gnu -C linker=riscv64-linux-gnu-gcc -C incremental=/home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/incremental -L dependency=/home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/deps -L dependency=/home/user/code/lang/arch/riscv/hello-libc/target/debug/deps` Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Copying the command into $EDITOR and breaking into human-digestible lines helps.
Alternatively, if you're in the mood for a shell vibe from the 80s, use some `sed`
$ sed -e 's/ \(CARGO_\|LD_\|-C\|--\|-L\|rustc\|src\)/\n\1/g' < tmp/link-command.txt CARGO=/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo CARGO_BIN_NAME=hello-libc CARGO_CRATE_NAME=hello_libc CARGO_MANIFEST_DIR=/home/user/code/lang/arch/riscv/hello-libc CARGO_PKG_AUTHORS='' CARGO_PKG_DESCRIPTION='' CARGO_PKG_HOMEPAGE='' CARGO_PKG_LICENSE='' CARGO_PKG_LICENSE_FILE='' CARGO_PKG_NAME=hello-libc CARGO_PKG_REPOSITORY='' CARGO_PKG_RUST_VERSION='' CARGO_PKG_VERSION=0.1.0 CARGO_PKG_VERSION_MAJOR=0 CARGO_PKG_VERSION_MINOR=1 CARGO_PKG_VERSION_PATCH=0 CARGO_PKG_VERSION_PRE='' CARGO_PRIMARY_PACKAGE=1 LD_LIBRARY_PATH='/home/user/code/lang/arch/riscv/hello-libc/target/debug/deps:/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib:/home/user/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib' rustc --crate-name hello_libc --edition=2021 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C metadata=cac59addb6dfe60a -C extra-filename=-cac59addb6dfe60a --out-dir /home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/deps --target riscv64gc-unknown-linux-gnu -C linker=riscv64-linux-gnu-gcc -C incremental=/home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/incremental -L dependency=/home/user/code/lang/arch/riscv/hello-libc/target/riscv64gc-unknown-linux-gnu/debug/deps -L dependency=/home/user/code/lang/arch/riscv/hello-libc/target/debug/deps
Tip:
rustc -C help
is your friend. -
getting verbose output from gcc with
rustflags = ["-C", "link-arg=-v"]
in[target.riscv64gc-unknown-linux-gnu]
section of.cargo/config.toml
.rustflags = [ "-C", "link-arg=-v", # make gcc more talkative "-C", "link-arg=-Wl,--verbose", # make linker more talkative ]
-
getting the ELF interpreter via
file
:$ file $BIN_DIR/hello-libc ./target/riscv64gc-unknown-linux-gnu/debug/hello-libc: ELF 64-bit LSB pie executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /usr/riscv64-linux-gnu/lib/ld-linux-riscv64-lp64d.so.1, ...