This is part of a series on the blog where we explore RISC-V by breaking down real programs and explaining how they work. You can view all posts in this series on the RISC-V Bytes page.
Today we are going to take a brief detour from our previous posts in this series
and look at Rust
Cross-Compilation
for RISC-V. This will be a shorter post focused on providing useful information
about how rustc
works,
as well as offering exact steps and configuration to target RISC-V when
compiling your Rust programs. There are a number of existing references for
building Rust programs for RISC-V, but I have found that many of them are
targeting a bare metal (i.e. no_std
) use case, such as running embedded code
on a microcontroller, or they don’t provide much background context on why
various configuration is being used and how required tooling is being managed.
I’ll attempt to be more comprehensive here, and will also do my best to post any
updates as the ecosystem continues to evolve. Let’s get started!
Background Link to heading
If you are already a seasoned Rust programmer feel free to skip this section.
While rustc
is the Rust compiler, most Rust projects make use of two other
tools to invoke and manage rustc
: cargo
and rustup
. rustup
is an
installer, meaning that it will handle
making sure you have the correct versions of rustc
and other language
components from
the various
channels for
toolchains you are using, as well as ensuring that the standard library is
installed for every compilation target. cargo
is the Rust package manager
and build tool, and itself can be installed
using rustup
. Throughout this post we will only be interacting with rustc
via cargo
and its related configuration. Before we move forward, make sure
that you have followed the
instructions to install rustup
.
rustc
is distributed viastable
,beta
, andnightly
channels. You may opt to consume from a channel other thanstable
if there are features required that have not been included in a formal release.
Hosts vs. Targets Link to heading
It’s worth taking a moment to explicitly distinguish toolchains and
supporting libraries, which can be confusing for folks who are not already
familiar with cross-compilation. A toolchain is the set of tools required to
build artifacts on a specific host machine. For example, I am using a 64-bit
Linux machine (or x86_64-unknown-linux-gnu
in Rust target parlance), and I
typically want to build artifacts that can run on a 64-bit Linux machine (i.e.
the same machine). I also sometimes want to build artifacts that can run on
other types of machines, which is what we are doing today. Cross-compilation
refers to any time that the host machine does not match the target machine.
x86_64-unknown-linux-gnu
and other machine type identifiers are referred to as target triples.
With this in mind, you may have guessed that since we are only ever going to be
building on my 64-bit Linux machine, we only need the x86_64-unknown-linux-gnu
toolchain. However, because we are interested in cross-compiling to a RISC-V
machine, we are going to need to add an additional target for our 64-bit Linux
toolchain. This can be summarized by the following:
- A toolchain is needed for every type of host machine.
- Supporting libraries are needed for every type of target machine.
rustc
is inherently a cross-compiler, meaning that we can use the same host toolchain to compile for many different targets as long as we have the supporting libraries. This is not true of all other compiler toolchains. For example,gcc
, as we will see later, requires a separate compiler for every host/target pair.
Rust uses a familiar three-tier hierarchy for target support, which could really be broken out into six tiers. Using the same vernacular as above, the six tiers could be translated as:
Tier 1 with Host Tools
: toolchain and target both available and guaranteed to work.Tier 1
: target available and guaranteed to work. There are no targets in this tier today.Tier 2 with Host Tools
: toolchain and target both available and guaranteed to build.Tier 2
: target available and guaranteed to build. Standard library may or may not be supported.Tier 3 with Host Tools
: toolchain and target are supported but not built, distributed, or guaranteed to work.Tier 3
: target is supported but not built, distributed, or guaranteed to work. Standard library may or may not be supported.
Setup Link to heading
Now that we know what components are required to build Rust programs on and for various machines, we can install the toolchain for our host machine and bootstrap a project. First lets check what toolchains are already present:
$ rustup toolchain list
stable-x86_64-unknown-linux-gnu (default)
rustup
has already installed the default toolchain for our machine, so we
don’t need to take any other steps on that front. We can take a look at the
components of the toolchain in
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin
:
$ ls ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin
cargo cargo-clippy cargo-fmt clippy-driver rustc rustdoc rustfmt rust-gdb rust-gdbgui rust-lldb
You’ll notice both cargo
and rustc
are present, as well as a number of other
tools, such as formatters and debuggers. When you install a toolchain, rustup
will also install required target libraries for that same type of machine:
$ ls ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/
libaddr2line-e8504b1ed73d6c6f.rlib libhashbrown-6c448d94453f4d95.rlib libprofiler_builtins-2b27848f56b860ee.rlib librustc_std_workspace_std-e2232747f29a2298.rlib
libadler-671a9f10c55c6c87.rlib liblibc-b4424726f33da388.rlib librustc_demangle-7c5cb27d99d10614.rlib libstd-4c74cbab78ec4891.rlib
liballoc-aa0bad4c4d134922.rlib libmemchr-bed369233e55d851.rlib librustc-stable_rt.asan.a libstd-4c74cbab78ec4891.so
libcfg_if-c0badcb9f7c5eab7.rlib libminiz_oxide-e35e56ad39c7e20e.rlib librustc-stable_rt.lsan.a libstd_detect-0ddec007a0883060.rlib
libcompiler_builtins-5667a4a7e2c48d47.rlib libobject-ee577127549b7793.rlib librustc-stable_rt.msan.a libtest-3d8d1f7e04ea304d.rlib
libcore-6cfcec236d576603.rlib libpanic_abort-b6371bac4bee0de9.rlib librustc-stable_rt.tsan.a libtest-3d8d1f7e04ea304d.so
libgetopts-86970f502db1b86e.rlib libpanic_unwind-0ef58120f7b95253.rlib librustc_std_workspace_alloc-22835d1ac5e3244b.rlib libunicode_width-86b36790f7c9a304.rlib
libgimli-411eeeec028606dc.rlib libproc_macro-b961a3f930b2d0eb.rlib librustc_std_workspace_core-483ad457673e0f5c.rlib libunwind-84878e033904a7a4.rlib
So we have support for building on and for x86_64-unknown-linux-gnu
machines,
but we need to add target support for RISC-V. No RISC-V targets are supported
in Tier 1, but riscv64gc-unknown-linux-gnu
is supported in Tier 2 (it also
includes host tools, which we will not be using today). Let’s go ahead and tell
rustup
to add support:
All Tier 3 RISC-V targets (
riscv32i-unknown-none-elf
,riscv32imac-unknown-none-elf
,riscv32imc-unknown-none-elf
,riscv64gc-unknown-none-elf
,riscv64imac-unknown-none-elf
) only supportno_std
development. More on this in a bit.
$ rustup target add riscv64gc-unknown-linux-gnu
info: downloading component 'rust-std' for 'riscv64gc-unknown-linux-gnu'
info: installing component 'rust-std' for 'riscv64gc-unknown-linux-gnu'
22.4 MiB / 22.4 MiB (100 %) 13.5 MiB/s in 1s ETA: 0s
You’ll notice that all rustup
does here is add library support (i.e. we are
not downloading a RISC-V toolchain). We can see now that these libraries live
under the x86_64-unknown-linux-gnu
toolchain directory in a new
riscv64gc-unknown-linux-gnu
directory:
$ ls ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/riscv64gc-unknown-linux-gnu/lib/
libaddr2line-b9f13a31d36ace4d.rlib libgimli-26f1e8e2a29c6abc.rlib libpanic_unwind-f075600c05f846f9.rlib libstd-b9a58deffd9afad7.so
libadler-e58e6ef377f5b261.rlib libhashbrown-68b1f88e6bc5c009.rlib libproc_macro-77a9b848fe88e73b.rlib libstd_detect-b0b3246e370d99f5.rlib
liballoc-08e2a49fbdb36fe7.rlib liblibc-1162f95a581d7874.rlib librustc_demangle-4132820dd26e4dd2.rlib libtest-33230b1de6d8a7e4.rlib
libcfg_if-57cb37c2109ec8d3.rlib libmemchr-4ea2a6be1c1298e6.rlib librustc_std_workspace_alloc-69bfa5a0f9a81fa5.rlib libtest-33230b1de6d8a7e4.so
libcompiler_builtins-09da015b98bbb243.rlib libminiz_oxide-3579444854cd0b3b.rlib librustc_std_workspace_core-cb5020bd1de8755e.rlib libunicode_width-c8ae7d271f61c6bd.rlib
libcore-e22fac431ac00ff6.rlib libobject-361332bd32322cb1.rlib librustc_std_workspace_std-65dffd0afcdbdc70.rlib libunwind-4d764747497457ba.rlib
libgetopts-4abf1528f3149ce7.rlib libpanic_abort-e8bba43be6b6e1d1.rlib libstd-b9a58deffd9afad7.rlib
With these components in place, let’s try to build something.
Building Link to heading
We can setup a new project using cargo
:
cargo new rusty-risc
This will give us the following directory structure:
.
└── rusty-risc
├── Cargo.toml
└── src
└── main.rs
By default, cargo
is going to give us a fairly straightforward “Hello, world!”
program in main.rs
. However, while it may look simple, this program is
actually doing quite a lot.
fn main() {
println!("Hello, world!");
}
In order for the println!
macro to show text in our
terminal, it is going to need to be able to perform I/O, which requires
interacting with the operating system via syscalls, typically accessed by
libc
. The Rust standard library handles that interaction for us, and we saw
earlier that rustup
took care of downloading it for our
riscv64gc-unknown-linux-gnu
. Let’s see how far that gets us.
As mentioned before, we are going to use cargo
to build our program. cargo
supports passing configuration via flags or a configuration file. In order to
keep track of all of our configuration, we’ll use the latter by creating a
.cargo
directory in the project and adding a config.toml
inside. Initially,
we only need to specify that we want to build for riscv64gc-unknown-linux-gnu
:
./.cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"
Now any time we invoke cargo build
, we should only build for the specified
target. However, if we give it a try, we’ll see that we immediately hit some
issues (output condensed):
$ (rusty-risc) cargo build
Compiling rusty-risc v0.1.0 (/home/dan/code/github.com/hasheddan/testing/rusty-risc)
error: linking with `cc` failed: exit status: 1
...
/usr/bin/ld: /home/dan/code/github.com/hasheddan/testing/rusty-risc/target/riscv64gc-unknown-linux-gnu/debug/deps/rusty_risc-c85960b13f0f920c.26a7wz4sk6dm01p0.rcgu.o: Relocations in generic ELF (EM: 243)
/usr/bin/ld: /home/dan/code/github.com/hasheddan/testing/rusty-risc/target/riscv64gc-unknown-linux-gnu/debug/deps/rusty_risc-c85960b13f0f920c.26a7wz4sk6dm01p0.rcgu.o: error adding symbols: file in wrong format
collect2: error: ld returned 1 exit status
There are a few things to notice here. First of all the overall error says that
we are linking with cc
. This is typically a symbolic link to the system’s C
compiler, which on my Ubuntu machine is going to be gcc
. We can double check
with the following commands:
$ (rusty-risc) which cc
/usr/bin/cc
$ (rusty-risc) readlink -f /usr/bin/cc
/usr/bin/x86_64-linux-gnu-gcc-9
Since this is an x86_64-linux
compiler and we are targeting riscv-64
, it
makes sense that this would cause problems, but why is Rust choosing to use cc
and does it choose to for every target? The answer is no and defaults can be
found in the rustc
source.
File names are links to source.
rust/compiler/rustc_target/src/spec/riscv64gc_unknown_linux_gnu.rs
use crate::spec::{CodeModel, Target, TargetOptions};
pub fn target() -> Target {
Target {
llvm_target: "riscv64-unknown-linux-gnu".to_string(),
pointer_width: 64,
data_layout: "e-m:e-p:64:64-i64:64-i128:128-n64-S128".to_string(),
arch: "riscv64".to_string(),
options: TargetOptions {
code_model: Some(CodeModel::Medium),
cpu: "generic-rv64".to_string(),
features: "+m,+a,+f,+d,+c".to_string(),
llvm_abiname: "lp64d".to_string(),
max_atomic_width: Some(64),
..super::linux_gnu_base::opts()
},
}
}
Here we can see that the TargetOptions
set a few values, then ultimately defer
to the base options for GNU/Linux, which are mostly the same a base Linux, which
consume the overall target defaults:
rust/compiler/rustc_target/src/spec/linux_gnu_base.rs
use crate::spec::TargetOptions;
pub fn opts() -> TargetOptions {
TargetOptions { env: "gnu".to_string(), ..super::linux_base::opts() }
}
rust/compiler/rustc_target/src/spec/linux_base.rs
use crate::spec::{RelroLevel, TargetOptions};
pub fn opts() -> TargetOptions {
TargetOptions {
os: "linux".to_string(),
dynamic_linking: true,
executables: true,
families: vec!["unix".to_string()],
has_rpath: true,
position_independent_executables: true,
relro_level: RelroLevel::Full,
has_thread_local: true,
crt_static_respected: true,
..Default::default()
}
}
Ultimately, if we drill down into the defaults, we’ll find that the default
linker_flavor
is in fact gcc
:
rust/compiler/rustc_target/src/spec/mod.rs#L1437
linker_flavor: LinkerFlavor::Gcc,
linker: option_env!("CFG_DEFAULT_LINKER").map(|s| s.to_string()),
So that explains why we are invoking cc
, but while we are here, its worth
noticing that linux_base.rs
also sets dynamic_linking: true
,
which is not the
default.
This is not the case for the Tier 3 riscv64gc-unknown-none-elf
target, which
consumes the default behavior to produce static binaries, but overrides the
default
linker
to be rust-lld
:
rust/compiler/rustc_target/src/spec/riscv64gc_unknown_none_elf.rs
use crate::spec::{CodeModel, LinkerFlavor, LldFlavor, PanicStrategy, RelocModel};
use crate::spec::{Target, TargetOptions};
pub fn target() -> Target {
Target {
data_layout: "e-m:e-p:64:64-i64:64-i128:128-n64-S128".to_string(),
llvm_target: "riscv64".to_string(),
pointer_width: 64,
arch: "riscv64".to_string(),
options: TargetOptions {
linker_flavor: LinkerFlavor::Lld(LldFlavor::Ld),
linker: Some("rust-lld".to_string()),
llvm_abiname: "lp64d".to_string(),
cpu: "generic-rv64".to_string(),
max_atomic_width: Some(64),
features: "+m,+a,+f,+d,+c".to_string(),
executables: true,
panic_strategy: PanicStrategy::Abort,
relocation_model: RelocModel::Static,
code_model: Some(CodeModel::Medium),
emit_debug_gdb_scripts: false,
eh_frame_header: false,
..Default::default()
},
}
}
This makes sense as the riscv64gc-unknown-none-elf
target is not expecting to
be able to target a Linux platform, and thus presumably will not need to link
libc
or load shared libraries at runtime (i.e. dynamically linked). However,
as mentioned before, this also means that we cannot use the Rust standard
library. In fact, if we use rustup
to add the riscv64gc-unknown-none-elf
target and change our build to use it, we’ll see an error indicating that we
must declare #![no_std]
:
$ rustup target add riscv64gc-unknown-none-elf
info: downloading component 'rust-std' for 'riscv64gc-unknown-none-elf'
info: installing component 'rust-std' for 'riscv64gc-unknown-none-elf'
$ rustup tarcargo build
Compiling rusty-risc v0.1.0 (/home/dan/code/github.com/hasheddan/testing/rusty-risc)
error[E0463]: can't find crate for `std`
|
= note: the `riscv64gc-unknown-none-elf` target may not support the standard library
= note: `std` is required by `rusty_risc` because it does not declare `#![no_std]`
For more information about this error, try `rustc --explain E0463`.
error: could not compile `rusty-risc` due to previous error
If we were to change our main.rs
to contain a very small #![no_std]
program,
perhaps the one in The
Embedonomicon,
we would see that it builds successfully:
./src/main.rs
#![no_main]
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
$ (rusty-risc) cargo build
Compiling rusty-risc v0.1.0 (/home/dan/code/github.com/hasheddan/testing/rusty-risc)
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Though this compiles, we would not be able to run it under a normal userspace emulator (more on this below).
This is important as we did not have to download any RISC-V toolchain (outside
of the rustup
target) to be able to build for riscv64gc-unknown-none-elf
.
You’ll frequently see bare metal RISC-V Rust projects, such as
riscv-rust-quickstart
and Stephen Marz’s fantastic
osblog
take advantage of this. In fact,
you can see where
each
of
them
switched from explicitly specifying an external linker to using Rust’s default
for riscv64gc-unknown-none-elf
and riscv32imac-unknown-none-elf
respectively. However, we want to build a program that can run on a Linux
system, so we don’t have this luxury.
Fortunately, some awesome RISC-V folks have a riscv-gnu-toolchain
repository with detailed
instructions on how you can build a cross-compiler. If you’re running Ubuntu, as
I am, they also publish periodic builds that you can download and install. As of
the writing of this post, I am using the 2022.01.17
release
for riscv64-glibc-ubuntu-20.04
.
Now that we have our toolchain, let’s swap back to our “Hello, world!” program
and change our target back to riscv64gc-unknown-linux-gnu
. We can add a
target-specific linker to override the default cc
that Rust will use:
./.cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"
[target.riscv64gc-unknown-linux-gnu]
linker = "riscv64-unknown-linux-gnu-gcc"
Alright, let’s see how that goes:
$ (rusty-risc) cargo build
Compiling rusty-risc v0.1.0 (/home/dan/code/github.com/hasheddan/testing/rusty-risc)
Finished dev [unoptimized + debuginfo] target(s) in 0.25s
Looks good! Now in order to see if this runs succesfully, we are going to need
to acquire a userspace emulator for 64-bit RISC-V. If you haven’t already
installed QEMU head back to the first RISC-V Bytes
post where we looked at
cross-platform debugging for download instructions. Once installed, we should be
able to invoke the binary produced from cargo build
directly thanks to
binfmt_misc
,
which will automatically invoke qemu-riscv64
.
$ (rusty-risc) ./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc
/lib/ld-linux-riscv64-lp64d.so.1: No such file or directory
Hmm, that doesn’t look great. If you remember earlier, the default behavior that
the riscv64gc-unknown-linux-gnu
target consumed was producing a dynamically
linked executable. We can use readelf
to see where our ELF file is requesting
ld-linux-riscv64-lp64d.so.1
as its “interpreter”:
$ (rusty-risc) riscv64-unknown-linux-gnu-readelf -l ./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc
INTERP 0x0000000000000270 0x0000000000000270 0x0000000000000270
0x0000000000000021 0x0000000000000021 R 0x1
[Requesting program interpreter: /lib/ld-linux-riscv64-lp64d.so.1]
When we installed our toolchain, it came with the ld.so
dynamic linker/loader,
which you should be able to find under sysroot/lib
in the location where you
installed your toolchain (mine is ~/opt/riscv
):
$ (rusty-risc) ls -la ~/opt/riscv/sysroot/lib/ | grep ld
-rwxr-xr-x 1 dan dan 1120424 Jan 16 20:51 ld-2.33.so
lrwxrwxrwx 1 dan dan 10 Jan 16 20:51 ld-linux-riscv64-lp64d.so.1 -> ld-2.33.so
drwxrwxr-x 2 dan dan 12288 Jan 16 20:31 ldscripts
...
Let’s try invoking it with the dynamic loader:
$ (rusty-risc) ~/opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1 ./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc
./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc: error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory
Not quite there. One of our dynamically linked libraries, libgcc_s.so
, can’t
be found. While we’re here, let’s see all of the libraries we need to load:
$ (rusty-risc) riscv64-unknown-linux-gnu-readelf -a ./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1]
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x0000000000000001 (NEEDED) Shared library: [ld-linux-riscv64-lp64d.so.1]
All of these libraries live alongside our dynamic loader in
~/opt/riscv/sysroot/lib
:
$ (rusty-risc) ls -la ~/opt/riscv/sysroot/lib/ | grep -E 'libgcc|libpthread|libdl|libc'
-rwxr-xr-x 1 dan dan 11675800 Jan 16 20:51 libc-2.33.so
-rwxr-xr-x 1 dan dan 148752 Jan 16 20:50 libcrypt-2.33.so
lrwxrwxrwx 1 dan dan 16 Jan 16 20:50 libcrypt.so.1 -> libcrypt-2.33.so
lrwxrwxrwx 1 dan dan 12 Jan 16 20:50 libc.so.6 -> libc-2.33.so
-rwxr-xr-x 1 dan dan 118136 Jan 16 20:50 libdl-2.33.so
lrwxrwxrwx 1 dan dan 13 Jan 16 20:50 libdl.so.2 -> libdl-2.33.so
-rw-r--r-- 1 dan dan 132 Jan 16 21:05 libgcc_s.so
-rw-r--r-- 1 dan dan 599912 Jan 16 21:05 libgcc_s.so.1
-rwxr-xr-x 1 dan dan 1214880 Jan 16 20:50 libpthread-2.33.so
lrwxrwxrwx 1 dan dan 18 Jan 16 20:50 libpthread.so.0 -> libpthread-2.33.so
The loader identifies shared libraries with using a cache in /etc/ld.so.cache
that is managed by ldconfig
based on the contents of /etc/ld.so.config
.
However, we can add directories at runtime that we would like to take precedence
over any in the cache using LD_LIBRARY_PATH
:
$ (rusty-risc) LD_LIBRARY_PATH=~/opt/riscv/sysroot/lib ~/opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1 --library-path ~/opt/riscv/sysroot/lib ./target/riscv64gc-unknown-linux-gnu/debug/rusty-risc
Hello, world!
The program runs successfully! We could automate this to make it possible to
invoke using cargo run
by passing LD_LIBRARY_PATH
as a flag and setting it
as the runner
:
This can be cleaned up further so we aren’t using absolute paths. Also, remember that this working is predicated on
binfmt_misc
invokingqemu-riscv64
for us automatically.
./.cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"
[target.riscv64gc-unknown-linux-gnu]
runner = "/home/dan/opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1 --library-path /home/dan/opt/riscv/sysroot/lib"
linker = "riscv64-unknown-linux-gnu-gcc"
Lastly, if we wanted to build a statically linked executable instead, we could
supply the crt-static
flag to rustc
:
./.cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"
[target.riscv64gc-unknown-linux-gnu]
rustflags = ["-C", "target-feature=+crt-static"]
linker = "riscv64-unknown-linux-gnu-gcc"
To understand how this impacts compilation, we can check the flag descriptions
in rustc
:
$ (rusty-risc) rustc -C help | grep target-features
-C target-feature=val -- target specific attributes. (`rustc --print target-features` for details). This feature is unsafe.
$ (rusty-risc) rustc --print target-features | grep crt-static
crt-static - Enables C Run-time Libraries to be statically linked.
Now let’s see if we can run without specifying any runner
at all:
$ (rusty-risc) cargo build
Compiling rusty-risc v0.1.0 (/home/dan/code/github.com/hasheddan/testing/rusty-risc)
Finished dev [unoptimized + debuginfo] target(s) in 2.40s
$ (rusty-risc) cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/riscv64gc-unknown-linux-gnu/debug/rusty-risc`
Hello, world!
Nice! We can now build both statically and dynamically linked Rust binaries for RISC-V Linux platforms.
Concluding Thoughts Link to heading
This post was a bit of a deviation from our normal content, but I am a big fan of Rust and naturally enjoy when I see more folks producing RISC-V builds. Hopefully centralizing some of this information will make it easier for folks to pick up.
As always, these posts are meant to serve as a useful resource for folks who are interested in learning more about RISC-V and low-level software in general. If I can do a better job of reaching that goal, or you have any questions or comments, please feel free to send me a message @hasheddan on Twitter!