Rust Apps on Apache NuttX RTOS and QEMU RISC-V

📝 7 Apr 2024

Rust Apps on Apache NuttX RTOS and QEMU RISC-V

My mentee Rushabh Gala and I are anxiously awaiting the results of the Google Summer of Code (GSoC) Project Selection. While waiting, we explain the current steps for running barebones Rust Apps on Apache NuttX RTOS (and the challenges we faced)…

Thanks to PINE64, the sponsor of Ox64 BL808 RISC-V SBCs for our GSoC Project Testing!

Rust App for NuttX

§1 Rust App for NuttX

Below is the “Hello Rust” Demo App that’s bundled with Apache NuttX RTOS: hello_rust_main.rs

// main() function not needed
#![no_main]

// Use Rust Core Library (instead of Rust Standard Library)
#![no_std]

// Import printf() from C into Rust
extern "C" {
  pub fn printf(
    format: *const u8,  // Equivalent to `const char *`
    ...                 // Optional Arguments
  ) -> i32;             // Returns `int`
}                       // TODO: Standardise `i32` as `c_int`

(We’ll explain [no_std] in a while)

The code above imports the printf() function from C into Rust.

This is how we call it in Rust: hello_rust_main.rs

// Main Function exported by Rust to C.
// Don't mangle the Function Name.
#[no_mangle]
pub extern "C" fn hello_rust_main(
  _argc: i32,              // Equivalent to `int argc`
  _argv: *const *const u8  // Equivalent to `char **argv`
) -> i32 {                 // Returns `int`

  // Calling a C Function might have Unsafe consequences
  unsafe {
    printf(                 // Call printf() with...
      b"Hello, Rust!!\n\0"  // Byte String terminated by null
        as *const u8        // Cast as `const char *`
    );
  }

  // Exit with status 0
  0
}

Rust expects us to provide a Panic Handler. We write a simple one: hello_rust_main.rs

// Import the Panic Info for our Panic Handler
use core::panic::PanicInfo;

// Handle a Rust Panic. Needed for [no_std]
#[panic_handler]
fn panic(
  _panic: &PanicInfo<'_>  // Receives the Panic Info and Stack Trace
) -> ! {                  // Never returns

  // TODO: Print the Panic Info and Stack Trace
  // For now, we loop forever
  loop {}
}

Which doesn’t do much right now. We’ll create a proper Panic Handler during GSoC.

Build Apache NuttX RTOS for 64-bit RISC-V QEMU

§2 Build NuttX for QEMU RISC-V

How to compile our Rust App?

Follow these steps to build Apache NuttX RTOS for QEMU RISC-V (32-bit), bundled with our “Hello Rust” Demo App…

  1. Install the Build Prerequisites, skip the RISC-V Toolchain…

    “Install Prerequisites”

  2. Download the RISC-V Toolchain for riscv64-unknown-elf

    “Download Toolchain for 64-bit RISC-V”

  3. Download and configure NuttX…

    mkdir nuttx
    cd nuttx
    git clone https://github.com/apache/nuttx nuttx
    git clone https://github.com/apache/nuttx-apps apps
    
    cd nuttx
    tools/configure.sh rv-virt:nsh
    make menuconfig
    
  4. In menuconfig, browse to “Device Drivers > System Logging

    Disable this option…

    Prepend Timestamp to Syslog Message
    
  5. Browse to “Build Setup > Debug Options

    Select the following options…

    Enable Debug Features
    Enable Error Output
    Enable Warnings Output
    Enable Informational Debug Output
    Enable Debug Assertions
    Enable Debug Assertions Show Expression
    Scheduler Debug Features
    Scheduler Error Output
    Scheduler Warnings Output
    Scheduler Informational Output
    
  6. Browse to “Application Configuration > Examples

    Select “Hello Rust Example

    Select it Twice so that “<M>” changes to “<*>

    (Source Code for Hello Rust)

  7. Save and exit menuconfig.

    (See the NuttX Config)

  8. Build the NuttX Project and dump the RISC-V Disassembly to nuttx.S (for easier troubleshooting)…

    ## Add the Rust Target for RISC-V 32-bit (Soft-Float)
    rustup target add riscv32i-unknown-none-elf
    
    ## Build the NuttX Project
    make
    
    ## Dump the NuttX Disassembly to `nuttx.S`
    riscv64-unknown-elf-objdump \
      -t -S --demangle --line-numbers --wide \
      nuttx \
      >nuttx.S \
      2>&1
    

    (See the Build Log)

    This produces the NuttX ELF Image nuttx that we may boot on QEMU RISC-V Emulator. (Next Section)

  9. If the GCC Linker fails with “Can’t link soft-float modules with double-float modules”

    $ make
    LD: nuttx
    riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a
      (hello_rust_main.rs...nuttx.apps.examples.hello_rust_1.o):
      can't link soft-float modules with double-float modules
    riscv64-unknown-elf-ld: failed to merge target specific data of file
      nuttx/staging/libapps.a
      (hello_rust_main.rs...nuttx.apps.examples.hello_rust_1.o)
    

    Then we patch the ELF Header like this, and it should link correctly…

    xxd -c 1 ../apps/examples/hello_rust/*hello_rust_1.o \
      | sed 's/00000024: 00/00000024: 04/' \
      | xxd -r -c 1 - /tmp/hello_rust_1.o
    cp /tmp/hello_rust_1.o ../apps/examples/hello_rust/*hello_rust_1.o
    make
    

    (We’ll come back to this)

Rust Apps on Apache NuttX RTOS and QEMU RISC-V

§3 Run NuttX on QEMU RISC-V

We’re ready to boot NuttX on QEMU Emulator and run our Rust App!

  1. Download and install QEMU Emulator

    ## For macOS:
    brew install qemu
    
    ## For Debian and Ubuntu:
    sudo apt install qemu-system-riscv32
    
  2. Start the QEMU RISC-V Emulator (32-bit) with the NuttX ELF Image nuttx from the previous section…

    qemu-system-riscv32 \
      -semihosting \
      -M virt,aclint=on \
      -cpu rv32 \
      -smp 8 \
      -bios none \
      -kernel nuttx \
      -nographic
    
  3. NuttX is now running in the QEMU Emulator! (Pic above)

    NuttShell (NSH) NuttX-12.4.0-RC0
    nsh>
    
  4. Enter “hello_rust” to run our Rust Demo App (which will print something)

    nsh> hello_rust
    Hello, Rust!!
    

    (See the NuttX Log)

  5. Enter “help” to see the available commands…

    nsh> help
    help usage:  help [-v] [<cmd>]
    
        .           cp          exit        mkdir       rmdir       umount      
        [           cmp         expr        mkrd        set         unset       
        ?           dirname     false       mount       sleep       uptime      
        alias       dd          fdinfo      mv          source      usleep      
        unalias     df          free        pidof       test        xd          
        basename    dmesg       help        printf      time        
        break       echo        hexdump     ps          true        
        cat         env         kill        pwd         truncate    
        cd          exec        ls          rm          uname       
    
    Builtin Apps:
        hello         hello_rust    nsh           ostest        sh       
    
  6. To Exit QEMU: Press Ctrl-A then x

What about QEMU for 64-bit RISC-V?

Sorry Rust Apps won’t build correctly on NuttX for 64-bit RISC-V…

We’ll fix this in GSoC and test it on Ox64 BL808 SBC.

Console Input in Rust

§4 Console Input in Rust

We’ve done Console Output. How about Console Input?

This is how we read Console Input in Rust: hello_rust_main.rs

// main() function not needed. Use Rust Core Library.
#![no_main]
#![no_std]

// Import the Types for C Interop
use core::ffi::{ c_char, c_int, c_void };

// Import the Functions from C into Rust
extern "C" {
  pub fn printf(format: *const u8, ...) -> i32;
  pub fn puts(s: *const c_char) -> c_int;
  pub fn fgets(buf: *mut c_char, n: c_int, stream: *mut c_void) -> *mut c_char;
  pub fn lib_get_stream(fd: c_int) -> *mut c_void;
}

The code above imports the fgets() function from C into Rust.

Calling fgets() is a little more complicated: hello_rust_main.rs

// Main Function exported by Rust to C
#[no_mangle]
pub extern "C" fn hello_rust_main(_argc: i32, _argv: *const *const u8) -> i32 {

  // Receive some text from Standard Input and print it
  unsafe {

    // Standard Input comes from https://github.com/apache/nuttx/blob/master/include/stdio.h#L64-L68
    let stdin: *mut c_void =  // Equivalent to `void *`
      lib_get_stream(0);      // Init to Stream 0 (stdin)

    // Input Buffer with 256 chars (including terminating null)
    let mut buf: [c_char; 256] =  // Input Buffer is Mutable (will change)
      [0; 256];                   // Init with nulls

    // Read a line from Standard Input
    if !fgets(
      &mut buf[0],       // Input Buffer
      buf.len() as i32,  // Buffer Size
      stdin              // Standard Input
    ).is_null() {        // Catch the Input Error

      // Print the line
      printf(b"You entered...\n\0" as *const u8);
      puts(&buf[0]);
    }
  }

  // Exit with status 0
  0
}

// Omitted: Panic Handler

This gets a bit dangerous… The Input Buffer might Overflow if we’re not careful with the Parameters!

// Read a line from Standard Input
fgets(
  &mut buf[0],       // Input Buffer
  buf.len() as i32,  // Buffer Size
  stdin              // Standard Input
);

Which makes us ponder about Memory Safety: “Hmmm the fgets() buffer size… Does it include the terminating null?” (Yep it does!)

What about Rust? Does it safely handle Console Input?

Reading the Standard Input in Rust looks simpler and safer…

// Allocate an Input Buffer from Heap Memory
let mut buffer = String::new();

// Read a line from Standard Input
io::stdin().read_line(&mut buffer)?;

But this won’t work on NuttX because…

Bummer. How to do I/O safely on NuttX?

During GSoC we shall…

Rustix Project tried to provide Comprehensive Rust Wrappers for NuttX POSIX. Sadly the project has stalled. We’ll implement simpler, lighter wrappers instead.

How NuttX Compiles Rust Apps

§5 How NuttX Compiles Rust Apps

What happens when we compile our Rust App?

Watch how NuttX builds Rust Apps by calling rustc. (Instead of the usual cargo build)

## Build the NuttX Project with Tracing Enabled
$ make --trace

## Compile `hello_rust_main.rs` to `hello_rust.o`
## for Rust Target: RISC-V 32-bit (Soft-Float)
rustc \
  --edition 2021 \
  --emit obj \
  -g \
  --target riscv32i-unknown-none-elf \
  -C panic=abort \
  -O \
  hello_rust_main.rs \
  -o hello_rust_main.rs...apps.examples.hello_rust.o

## Copy `hello_rust.o` to `hello_rust_1.o` (Why?)
cp \
  hello_rust_main.rs...apps.examples.hello_rust.o \
  hello_rust_main.rs...apps.examples.hello_rust_1.o

## Omitted: Bundle `hello_rust_1.o`
## into library `staging/libapps.a`

## Link `staging/libapps.a` into `nuttx`
riscv64-unknown-elf-ld \
  --entry=__start \
  -melf32lriscv \
  --gc-sections \
  -nostdlib \
  --cref \
  -Map=nuttx/nuttx.map \
  -Tboards/risc-v/qemu-rv/rv-virt/scripts/ld.script.tmp  \
  -L staging \
  -L arch/risc-v/src/board  \
  -o nuttx \
  qemu_rv_head.o  \
  --start-group \
  -lsched \
  -ldrivers \
  -lboards \
  -lc \
  -lmm \
  -larch \
  -lm \
  -lapps \
  -lfs \
  -lbinfmt \
  -lboard riscv64-unknown-elf-toolchain-10.2.0-2020.12.8-x86_64-apple-darwin/bin/../lib/gcc/riscv64-unknown-elf/10.2.0/rv32imafdc/ilp32d/libgcc.a \
  --end-group

(See the Detailed Build Log)

(Rust Build with rustc is defined here)

(Why NuttX calls rustc instead of cargo build)

Here are the Rust Binaries produced by the NuttX Build (which will be linked into the NuttX Firmware)…

$ ls -l ../apps/examples/hello_rust     
total 112
-rw-r--r--  1   650 Jul 20  2023 Kconfig
-rw-r--r--  1  1071 Jul 20  2023 Make.defs
-rw-r--r--  1   141 Mar 17 09:44 Make.dep
-rw-r--r--  1  1492 Mar 16 20:41 Makefile
-rw-r--r--  1  3982 Mar 17 00:06 hello_rust_main.rs
-rw-r--r--  1 13168 Mar 17 09:44 hello_rust_main.rs...apps.examples.hello_rust.o
-rw-r--r--  1 18240 Mar 17 09:54 hello_rust_main.rs...apps.examples.hello_rust_1.o

(See the RISC-V Disassembly)

Now that we understand the build, let’s talk about the hiccups…

Can’t link soft-float modules with double-float modules

§6 Software vs Hardware Floating-Point

What’s this error? “Can’t link soft-float modules with double-float modules”

$ make
LD: nuttx
riscv64-unknown-elf-ld: nuttx/nuttx/staging/libapps.a
  (hello_rust_main.rs...nuttx.apps.examples.hello_rust_1.o):
  can't link soft-float modules with double-float modules

riscv64-unknown-elf-ld: failed to merge target specific data of file
  nuttx/staging/libapps.a
  (hello_rust_main.rs...nuttx.apps.examples.hello_rust_1.o)

GCC Linker failed to link the Compiled Rust Binary (hello_rust_1.o) into our NuttX Firmware because…

The two are incompatible. And the GCC Linking fails.

How to fix this?

For now we Patch the ELF Header of our Rust Object File. And NuttX Firmware will link correctly…

## Patch ELF Header from Soft-Float to Double-Float
xxd -c 1 ../apps/examples/hello_rust/*hello_rust_1.o \
  | sed 's/00000024: 00/00000024: 04/' \
  | xxd -r -c 1 - /tmp/hello_rust_1.o
cp /tmp/hello_rust_1.o ../apps/examples/hello_rust/*hello_rust_1.o
make

## NuttX links OK. Ignore these warnings: (why?)
## riscv64-unknown-elf-ld: warning: nuttx/staging/libapps.a(hello_rust_main.rs...nuttx.apps.examples.hello_rust_1.o): 
## mis-matched ISA version 2.1 for 'i' extension, the output version is 2.0

What exactly are we patching in the ELF Header?

Inside the ELF Header of an Object File: There’s a Flag (at Offset 0x24) that says whether it was compiled for…

We modified the Flag in the ELF Header so that it says Double-Float

## Before Patching: ELF Header says Software Floating-Point
$ riscv64-unknown-elf-readelf -h -A ../apps/examples/hello_rust/*hello_rust_1.o
  Flags: 0x0

## After Patching: ELF Header says Double-Precision Hardware Floating-Point
$ riscv64-unknown-elf-readelf -h -A ../apps/examples/hello_rust/*hello_rust_1.o
  Flags: 0x4, double-float ABI

And it links correctly!

(We had a similar issue with Zig Compiler)

But why Soft-Float instead of Double-Float? (Mmmm ice cream float)

Yeah patching the ELF Header is a Bad Hack! Here’s the complete analysis and proper solution…

Undefined reference to core::panicking::panic

§7 Panic is Undefined

What’s this core::panicking::panic? Why is it undefined?

$ make
riscv64-unknown-elf-ld:
  nuttx/staging/libapps.a(hello_rust_main.rs...apps.examples.hello_rust_1.o):
  in function `no symbol':
  apps/examples/hello_rust/hello_rust_main.rs:90:
  undefined reference to `core::panicking::panic'

Suppose we’re reading Console Input in our Rust App: hello_rust_main.rs

// Input Buffer with 256 chars (including terminating null)
let mut buf: [c_char; 256] =  // Input Buffer is Mutable (will change)
  [0; 256];                   // Init with nulls

// Read a line from Standard Input
fgets(
  &mut buf[0],           // Buffer
  buf.len() as i32 - 1,  // Size (cast to Signed Integer)
  stdin                  // Standard Input
);

“buf.len() - 1” might Panic and Halt. (Why?)

To implement the panic, Rust Compiler inserts a call to the Core Function core::panicking::panic.

(Which comes from the Rust Core Library)

And the Panic Function is missing somehow?

Rushabh has implemented a fix for the Undefined Panic Function…

But when we add Another Point of Panic: We see the Undefined Panic Error again (sigh)…

What’s causing this Undefined Panic Function?

According to this discussion, the Rust Core Library is compiled with Link-Time Optimisation (LTO). (Including the Panic Function)

But we’re linking it into our NuttX Firmware with GCC Linker, with LTO Disabled (by default). Which causes the Missing Panic Function.

How is this different from typical Rust Builds?

Normally we run cargo build to compile our Embedded Rust Apps. And it handles LTO correctly.

But NuttX calls rustc to compile Rust Apps, then calls GCC Linker to link into our NuttX Firmware. Which doesn’t seem to support LTO.

We’ll sort this out in GSoC!

(Why NuttX calls rustc instead of cargo build)

(Which means we can’t import Rust Crates from crates.io!)

The Embedded Rust Book

The Embedded Rust Book

§8 Standard vs Embedded Rust

What is [no_std]? Will Rust call C Standard Library, like for malloc()?

Earlier we saw [no_std] inside our Rust App.

There are 2 “flavours” of Rust, depending on the Rust Libraries that we use:

The malloc() that we mentioned: It’s called by the Rust Standard Library. (Like this)

What about Rust Drivers for NuttX Kernel?

For Kernel Dev (like Linux): We’ll use the Rust Core Library. Which doesn’t support Heap Memory and doesn’t need malloc().

But most Kernel Drivers will need Kernel Heap!

That’s why Linux Kernel supports the alloc Rust Library / Crate for Heap Memory. To implement Rust alloc, Linux Kernel calls krealloc() to allocate Kernel Heap. (Like this)

For NuttX Kernel: We’ll implement Rust alloc by calling kmm_malloc().

Anything else we need for Rust in NuttX Kernel?

Since we’re calling Rust Core Library in NuttX Kernel, we won’t touch any POSIX Application Interfaces. Thus if we need to support the Kernel Equivalent of Errno (and other Global State), we’ll have to create the Rust Library ourselves.

(See the Rust Library for Linux Kernel)

(GSoC Project Report will discuss a Simple LED Driver in Rust for NuttX Kernel)

GSoC 2024 Ideas

§9 All Things Considered

  1. Why are we doing all this?

    Yeah it’s tough work but it needs to be done because…

    — Some folks are urging us to explore Memory-Safe Programming in Rust

    — NuttX Devs among us might already be coding Rust Apps and Rust Drivers for NuttX? (We know of one Corporate User of NuttX that’s super keen on Rust)

    — Hence we’re helpfully drafting the Standards and Guidelines for folks already coding Rust in NuttX

  2. Learning Rust looks kinda hard. Any other way to write Memory-Safe Apps?

    If we’re familiar with Python: Check out the Nim Programming Language.

    Zig Programming Language is safer than C and easier to learn. But not quite Memory-Safe like Rust.

    AI Tools might be helpful for coding the difficult bits of Rust: ChatGPT, GitHub Copilot, Google Gemini, …

    (We’ll validate this during GSoC)

  3. Giving in to our AI Overlords already?

    But Rust Devs are familiar with smarty tools. Borrow Checker and Cargo Clippy are already so clever, they might as well be AI!

    And Rust Compiler is almost Sentient, always commanding us Humans: “Please do this to fix the build, you poopy nincompoop!”

    (My Biggest Wish: Someone please create a Higher-Level Dialect of Rust that will use bits of AI to compile into the present Low-Level Rust. Which might simplify Generics, Lifetimes, Box, Rc, Arc, RefCell, Fn*, dyn, async, …)

  4. Will there be Resistance to Rust Drivers inside NuttX Kernel?

    Ouch we’re trapped between a Rock and… Another Rusty Rock!

    NuttX Devs are concerned about the extra complexity that Rust Drivers add to the Kernel Build

    Rust Community is probably thinking we’re not doing enough to promote Memory-Safe Coding in NuttX Kernel

    For now we walk the Middle Way

    Lay the Groundwork for Future Integration of Rust Drivers into NuttX Kernel

    — Observe the Rust Development in Linux Kernel and Zephyr OS. Then adapt the Best Practices for NuttX Kernel.

GSoC Proposals for NuttX

§10 What’s Next

Coming This Summer: Plenty to be done for Rust Apps on Apache NuttX RTOS!

Check out the next article…

Many Thanks to my GitHub Sponsors (and the awesome NuttX Community) for supporting my work! This article wouldn’t have been possible without your support.

Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…

lupyuen.github.io/src/rust3.md

Undefined reference to core::panicking::panic

§11 Appendix: Panic is Undefined

What’s this core::panicking::panic? Why is it undefined?

$ make
riscv64-unknown-elf-ld:
  nuttx/staging/libapps.a(hello_rust_main.rs...apps.examples.hello_rust_1.o):
  in function `no symbol':
  apps/examples/hello_rust/hello_rust_main.rs:90:
  undefined reference to `core::panicking::panic'

Earlier we spoke about the Undefined Panic Function…

Which Rushabh has fixed with this patch…

But watch what happens when we add Another Point of Panic

Below is our Test Code that has Two Potential Panics: hello_rust_main.rs

  1. Buffer Length might panic. (Why?)

    // Input Buffer with 256 chars (including terminating null)
    let mut buf: [c_char; 256] =  // Input Buffer is Mutable (will change)
      [0; 256];                   // Init with nulls
    
    // Read a line from Standard Input
    fgets(
      &mut buf[0],           // Buffer
      // This might Panic! (Why?)
      buf.len() as i32 - 1,  // Unsigned Size cast to Signed Integer
      stdin                  // Standard Input
    );
  2. Divide by Zero will also panic

    // Buffer might begin with null
    // Which causes Divide by Zero
    let i = 1 / buf[0];

What happens when we compile this?

If we omit RUSTFLAGS=-O: We see Two Undefined Panic Functions…

apps/examples/hello_rust/hello_rust_main.rs:84
  buf.len() as i32 - 1,  // Might Panic (Why?)
    a0: 00000097         auipc ra,0x0
    a0: R_RISCV_CALL_PLT core::panicking::panic

apps/examples/hello_rust/hello_rust_main.rs:90
  let i = 1 / buf[0];  // Might Divide by Zero
    108: 00000097         auipc ra,0x0
    108: R_RISCV_CALL_PLT core::panicking::panic

(See the RISC-V Disassembly)

After we add RUSTFLAGS=-O: We still see One Undefined Panic Function for the divide-by-zero…

apps/examples/hello_rust/hello_rust_main.rs:90
  let i = 1 / buf[0];  // Might Divide by Zero
    d0: 00000097         auipc ra,0x0 
    d0: R_RISCV_CALL_PLT core::panicking::panic

(See the RISC-V Disassembly)

Which leads to the Undefined Panic Error again (sigh)…

$ make
riscv64-unknown-elf-ld:
  nuttx/staging/libapps.a(hello_rust_main.rs...apps.examples.hello_rust_1.o):
  in function `no symbol':
  apps/examples/hello_rust/hello_rust_main.rs:90:
  undefined reference to `core::panicking::panic'

What’s causing this Undefined Panic Function?

According to this discussion, the Rust Core Library is compiled with Link-Time Optimisation (LTO). (Including the Panic Function)

But we’re linking it into our NuttX Firmware with GCC Linker, with LTO Disabled (by default). Which causes the Missing Panic Function.

How is this different from typical Rust Builds?

Normally we run cargo build to compile our Embedded Rust Apps. And it handles LTO correctly.

But NuttX calls rustc to compile Rust Apps, then calls GCC Linker to link into our NuttX Firmware. Which doesn’t seem to support LTO.

We’ll sort this out in GSoC!

(Why NuttX calls rustc instead of cargo build)

(Which means we can’t import Rust Crates from crates.io!)

§12 Appendix: Rust Build for 64-bit RISC-V

We tested Rust Apps on QEMU for 32-bit RISC-V. What about 64-bit RISC-V?

Sorry Rust Apps won’t build correctly on NuttX for 64-bit RISC-V…

$ tools/configure.sh rv-virt:nsh64
$ make menuconfig
## TODO: Enable "Hello Rust Example"
$ make

RUSTC:  hello_rust_main.rs error: Error loading target specification: 
  Could not find specification for target "riscv64i-unknown-none-elf". 
  Run `rustc --print target-list` for a list of built-in targets

make[2]: *** [nuttx/apps/Application.mk:275: hello_rust_main.rs...nuttx.apps.examples.hello_rust.o] Error 1
make[1]: *** [Makefile:51: nuttx/apps/examples/hello_rust_all] Error 2
make: *** [tools/LibTargets.mk:232: nuttx/apps/libapps.a] Error 2

Which says that riscv64i-unknown-none-elf isn’t a valid Rust Target.

(Should be riscv64gc-unknown-none-elf instead)

We’ll fix this in GSoC and test it on Ox64 BL808 SBC.

TODO: Test on QEMU Arm32 and Arm64