Rust
RISCV
OS自作

RustでRISC-V OS自作!はじめの一歩

RustでRISC-V OS自作!はじめの一歩

この記事は自作OS Advent Calendar 2018の4日目の記事として書かれました。

hello worldのコードは下記にあります。
riscv-rust-hello

はじめに

Rust

Rustは速度、安全性、並行性の3つのゴールにフォーカスしたシステムプログラミング言語です。

いわずと知れた至高のプログラミング言語です。
2018/10にversion 1.30がリリースされ、stable (安定版)でもRISC-Vおよびno-std(ベアメタル)プログラミングができるようになりました。

RISC-V

オープンソースのRISC CPU命令セットアーキテクチャです。

いわずと知れた至高の命令セットアーキテクチャです。
来年あたりはHypervisor拡張命令セットの盛り上がりを期待しています。

OS自作

いわずと知れた至高の戯れです。
低レイヤ好きにはたまらないですね!

つまり、本記事の趣旨は、至高のプログラミング言語で、至高の命令セットをターゲットに、至高の戯れをする!ということです。

本当のはじめに

x86でOS自作を始めるのも1つの有力な選択肢です。
その場合の利点と欠点は次のようなものと考えています。

利点

  • クロスコンパイラが不要なため、開発環境を整えやすい
  • 普段使用しているPCなどで動作するOSが作れる
  • 他の命令セットが簡単に思える

欠点

  • 歴史的経緯があり、命令セットがとにかく複雑
  • エミュレータやCPU自作に手を伸ばしにくい

これに対して、RISC-VでOS自作を始める場合、その利点欠点はx86の反対です。
開発環境もかなり整備されてきているため、RISC-VでOS自作を始めるのは悪くない選択肢であると考えています。

また、Rustを使う理由は、私が好きだからです。

対象読者

Rust、RISC-V、OS自作、3つのうちどれかの要素、および、低レイヤ技術に興味がある方を対象にしています。
著者の力不足により、低レイヤが全く分からない状態から理解できるクオリティになっていません。
もし本記事で興味をお持ちいただけた場合は、コメント欄、twitterなどでお尋ねください。

スコープ

  • 環境構築
  • qemu-riscvでベアメタルhello world
  • TIPS

具体的には、elfファイルを作成し、elfのロードはqemuに任せ、とりあえず動くものを作っていきます。
TIPSでは、Rustの便利な機能や、nightly Rustを使えば可能なことを書いていきます。

スコープ外

  • bootloader
  • OSと呼べるレベルのものを作ること

これらは今後の課題であるため、記事の中で現在記載できる内容を書きます。
今後開発を継続していく予定です。

環境構築

私の環境は下記の通りです。

Ubuntu 16.04 LTS@VirtualBox

Rust環境構築

まず、Rustの環境構築をしましょう。
Rust 1.30から、RISC-Vのクロスコンパイルが可能になりました。
そのため、stable RustでRISC-Vのアプリケーション作成が可能になりました。
ただし、実用的なアプリケーションを作るためには、アセンブリを書く必要があります。別ファイルに記述したアセンブリを、オブジェクトにする方法がないため、stableだけで開発を進めるためには、ターゲットのアセンブラが必要になります。
詳細は後述するriscv-toolchain構築で説明します。

まずは、普通にRustをインストールしましょう。選択肢はデフォルトでOKです。出力されるversionは時期によって異なります。

$ curl https://sh.rustup.rs -sSf | sh
$ rustc --version
rustc 1.30.0 (da5f414c2 2018-10-24)

次に、RISC-Vのtargetを追加します。

$ rustup target add riscv32imac-unknown-none-elf

Rustの環境構築は以上です。

riscv-toolchain構築

Rustには、インラインアセンブリ(asm)と、自由形式のアセンブリ(global_asm)があります。残念ながら、これらの機能はnightlyでしか使えません。そのため、stableでアセンブリを使う場合、当面は、外部ファイル(.s)にアセンブリを書いて、アセンブラにかけるしかありません。
ですので、stableでRISC-Vをターゲットにプログラミングするには、riscv-toolchainを構築する必要があります。

ということで、若干負けた気持ちになりながら、riscv-toolchainを構築しましょう。
git cloneとビルドに時間がかかるため、参考のリンクでも辿りながらお待ちください

RISC-V GNU Compiler Toolchain

/opt/riscvに構築することを想定して書きます。/opt/riscvに書き込み権限を付けておくか、make linuxにsudoをつけて実行して下さい。

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
$ cd riscv-gnu-toolchain
$ sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev
$ ./configure --prefix=/opt/riscv --with-arch=rv32gc --with-abi=ilp32
$ make linux

.bashrcなどに、/opt/riscv/binへのパスを追加しておきましょう。

~/.bashrc
PATH=$PATH:/opt/riscv/bin
$ riscv32-unknown-linux-gnu-gcc --version
riscv32-unknown-linux-gnu-gcc (GCC) 8.2.0

これで、riscv-toolchainの構築は終了です。

qemu-riscv環境構築

version 3.0を使用してください。2.12だと本記事内で利用するsifive_uがうまく動きませんでした。
現在Rustがターゲットとしているのは、rv32 (32-bit命令セット)ですので、時間節約のため、rv32だけをビルドします。

/opt/qemu-riscvに構築します。書き込み権限をつけておくか、make installにsudoを付けてください。

$ wget https://download.qemu.org/qemu-3.0.0.tar.xz
$ tar xf qemu-3.0.0.tar.xz
$ cd qemu-3.0.0/
$ mkdir build
$ cd build/
$ sudo apt-get install git libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev
$ ../configure --target-list=riscv32-softmmu --prefix=/opt/qemu-riscv
$ make
$ make install

.bashrcなどに、/opt/qemu-riscv/binのパスを追加します。

$ qemu-system-riscv32 --version
QEMU emulator version 3.0.0

qemu-riscvでベアメタルhello world

とくにかく動かしてみる

Hello Worldできるサンプルを用意しました。

$ git clone https://github.com/tomoyuki-nakabayashi/riscv-rust-hello.git
$ cd riscv-rust-hello/
$ env CC=riscv32-unknown-linux-gnu-gcc cargo run
    Updating crates.io index
 Downloading cc v1.0.25                                                         
   Compiling cc v1.0.25                                                         
   Compiling hello v0.1.0 (/home/nakabayashi/build/riscv-rust-hello)            
    Finished dev [unoptimized + debuginfo] target(s) in 43.96s                  
     Running `qemu-system-riscv32 -nographic -machine sifive_u -kernel target/riscv32imac-unknown-none-elf/debug/hello`
Hello from Rust!

無事に、Hello from Rust!と出力されたでしょうか?
Hello Worldできた方、おめでとうございます!もう立派なベアメタラーです!
qemuは、Ctrl+a xで終了できます。

ここからは、ソースコードの中身を見ていきましょう。

target machine sifive_u

qemu-riscvでは、5つのmachineが実装されています。

$ qemu-system-riscv32 -nographic -machine help
Supported machines are:
none                 empty machine
sifive_e             RISC-V Board compatible with SiFive E SDK
sifive_u             RISC-V Board compatible with SiFive U SDK
spike_v1.10          RISC-V Spike Board (Privileged ISA v1.10) (default)
spike_v1.9.1         RISC-V Spike Board (Privileged ISA v1.9.1)
virt                 RISC-V VirtIO Board (Privileged ISA v1.10)

今回の範囲では、ターゲットmachineはvirtでも良かったのですが、sifive_uにします。
sifiveとは、現在既に世界中で発売されているRISC-Vの評価ボードです。
実機を用意すれば動いた方が素敵ですよね!

qemu-riscv sifive_uのメモリアドレス空間は下記の通りです。

hw/riscv/sifive_u.c
static const struct MemmapEntry {
    hwaddr base;
    hwaddr size;
} sifive_u_memmap[] = {
    [SIFIVE_U_DEBUG] =    {        0x0,      0x100 },
    [SIFIVE_U_MROM] =     {     0x1000,    0x11000 },
    [SIFIVE_U_TEST] =     {   0x100000,     0x1000 },
    [SIFIVE_U_CLINT] =    {  0x2000000,    0x10000 }, /* sifive_u */
    [SIFIVE_U_CLIC] =     {  0x2000000,  0x1000000 }, /* sifive_ux */
    [SIFIVE_U_PLIC] =     {  0xc000000,  0x4000000 },
    [SIFIVE_U_UART0] =    { 0x10013000,     0x1000 },
    [SIFIVE_U_UART1] =    { 0x10023000,     0x1000 },
    [SIFIVE_U_DRAM] =     { 0x80000000,        0x0 },
    [SIFIVE_U_GEM] =      { 0x100900FC,     0x2000 },
};

hello worldする上で重要なのは、UARTとDRAMです。UARTはとりあえずUART0を使います。
それぞれ、0x10013000と0x80000000に割り当てられています。

elfバイナリのエントリーポイントが0x80000000になっていれば、後は、qemuがよきに計らって実行してくれます。詳細は後程説明します。

UARTになじみがない方もいるかと思います。
雑な説明をすると、UARTデバイスが接続されているメモリアドレスにデータをストアすると、対向側ではデータを受信します。こちらが送信するデータがアスキーコードであれば、受信側もアスキーコードで文字を受け取れるわけです。
不思議に感じるかもしれませんが、UARTの送信用バッファが、とあるメモリアドレス(0x100130000)に割り当てられており、そのアドレスに一文字ずつデータを送ると、エミュレータが受信した文字を表示してくれるわけです。これは実機でも同じです。

hello world Rustの実装

早速、世界にこんちはしましょう。
何をしているのか明確にするために、2ステップで作成していきます。

  1. Rustでライブラリを作って、riscv-toolchainでバイナリを生成する
  2. cargoのbuild scriptを使って、バイナリを生成する

Rustでライブラリを作って、riscv-toolchainでバイナリを生成する

ではまず、Rustのプロジェクトをlibrary形式で作成します。

$ cargo new --lib hello
     Created library `hello` project
$ cd hello

お好みのエディタでファイルを開きます。まずは、必要な設定をしてしまいましょう。
Cargo.tomlに次の設定を追加します。

[lib]
crate-type = ["staticlib"]

まず、libraryをstatic libraryにしておき、ライブラリオブジェクトを作成します。
さて、肝心のRustを書きましょう。src/lib.rsに次のコードを入力します。

src/lib.rs
// 標準ライブラリは使用しない
#![no_std]

// mangleしない
#[no_mangle]
pub fn __start_rust() -> ! {
    // UART0の送信バッファに1文字ずつデータをストアする
    let uart0 = 0x10013000  as *mut u8;
    for c in b"Hello from Rust!".iter() {
        unsafe {
            *uart0 = *c as u8;
        }
    }

    loop {}
}

// panic発生時のハンドラ
use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
    // 何もせず、無限ループする
    loop{}
}

// abort時のハンドラ
#[no_mangle]
pub fn abort() -> ! {
    // 何もせず、無限ループする
    loop {}
}

sifive_uのUARTは、0番レジスタがTX (送信用)レジスタになっています。
そのため、UART0がマッピングされているアドレス (0x1001_3000)にasciiコードを1文字ずつ送信すると、コンソールに文字が出力されます。
もう少し真面目に作るのであれば、UART0のステータスレジスタを見て、TXバッファがFullになっていないときのみデータを送信するようにしましょう。

さあ!ドキドキしながらビルドしましょう!

$ cargo build --target riscv32imac-unknown-none-elf

--target riscv32imac-unknown-none-elfは、RISC-Vをターゲットにクロスビルドするためのオプションです。
target/riscv32imac-unknown-none-elf/debug/libhello.aが無事、生成されたかと思います。
本当にこいつはRISC-Vバイナリなのでしょうか?
何やら簡単すぎて怪しいです。
軽い気持ちで逆アセンブルしてみましょう。

$ riscv32-unknown-linux-gnu-objdump -D target/riscv32imac-unknown-none-elf/debug/libhello.a | less
...
Disassembly of section .text.__start_rust:

00000000 <__start_rust>:
   0:   7139                    addi    sp,sp,-64
   2:   de06                    sw      ra,60(sp)
   4:   10000537                lui     a0,0x10000
   8:   c62a                    sw      a0,12(sp)
   a:   00000537                lui     a0,0x0
   e:   00052583                lw      a1,0(a0) # 0 <__start_rust>
  12:   00050513                mv      a0,a0
  16:   4148                    lw      a0,4(a0)
  18:   c42a                    sw      a0,8(sp)
  1a:   852e                    mv      a0,a1
  1c:   45a2                    lw      a1,8(sp)
  1e:   00000097                auipc   ra,0x0

__start_rustがありますね。そしてRISC-Vに特徴的な命令であるauipc命令があります。
これはどうやらRISC-Vのバイナリのようです。

ちなみに、32 bitと16 bitの命令が混在しています。
RISC-Vでは、16 bitで命令を表現するCompressed命令が定義されており、RustのターゲットもCompressed命令が含まれています。
そのため、可能な範囲で16 bit命令が使用されています。

これで、libhello.aという、Rustから生成されたライブラリを作ることができました。
ただし、まだ実行できません。ターゲットのsifive_uでは、プログラムのエントリーポイントが0x80000000から始まっている必要があります。
boot strapとリンカスクリプトを書いて、リンクしていきます。

hello world boot strap

まずはboot strapです。
少しごちゃごちゃとありますが、重要なのは、_startから始まる3命令です。

boot.s
.option norvc
// .bootセクション
.section .boot, "ax",@progbits
.global _start
.global abort

_start:
    /* Set up stack pointer. */
    lui     sp, %hi(stacks + 1024)
    ori     sp, sp, %lo(stacks + 1024)
    /* Now jump to the rust world; __start_rust.  */
    j       __start_rust

.bss

.global stacks
stacks:
    .skip 1024

spはスタックポインタです。とりあえずは、1KB あれば十分なので、stackラベルから1KBの空間を確保しておきます。
スタック領域は、メモリアドレスの下位から上位へと伸びていくため、stack + 1024をスタックポインタの初期値とします。

スタック領域とは、関数のローカル変数などを格納するためのメモリ領域です。
Rustで関数内に変数を作ると、スタック領域のメモリが使用されます。今回はHello Worldするだけなので、1KBもあれば十分事足ります。

hello world リンカスクリプト

次にリンカスクリプトです。見慣れていない方はツラいかもしれませんが、とりあえずこういうもの、ということで見てみて下さい。ちゃんと理解したい方は、リンカ・ローダ実践開発テクニックがおすすめです。

では、リンカスクリプトを見ていきましょう。少し長いですが、重要な部分は少ないです。

linker.ld
OUTPUT_ARCH("riscv")

// エントリーポイントをアセンブリで書いた`_start`に設定する
ENTRY(_start)

SECTIONS
{
    /* sifive_uのエントリーポイントアドレス */
    . = 0x80000000;

    /* text: Program code section */
    .text :
    {
        /* これで0x80000000に.bootセクションが配置される */
        *(.boot)
        *(.text .text.*)
    }

    /* rodata: Read-only data */
    .rodata :
    {
        *(.rdata .rodata .rodata.*)
    }

    /* data: Writable data */
    .data :
    {
        *(.data .data.*)
    }

    .bss :
    {
        *(.bss bss.*)
    }
}

SECTIONSのはじめを0x80000000からに設定しています。
そして、0x80000000に.bootセクション(上で書いたアセンブリのboot strap)が置かれるようにしておきます。

満を持してhello world!

Rustで作成したライブラリ(libhello.a)と、ブートストラップ(boot.s)を用いて、リンカスクリプトでリンクして、バイナリを生成します。

$ riscv32-unknown-linux-gnu-gcc -T linker.ld boot.s target/riscv32imac-unknown-none-elf/debug/libhello.a -o hello -mabi=ilp32 -nostdlib

qemuで実行します。

$ qemu-system-riscv32 -nographic -machine sifive_u -kernel hello
Hello from Rust!

やりました!
夜の住人にならず(nightlyコンパイラを使わず)、RustでRISC-Vのベアメタルプログラミングができました!

qemuが終了できなくて困っているあなた、Ctrl+a xを試してみてください。

少し、コマンドを解説しておきます。
riscv-toolchainのgccを使い、boot.sとlibhello.aからhelloバイナリを作成します。
その際、リンカスクリプトにはlinker.ldを使用します。
オプションで、mabi=ilp32-nostdlibは必須です。

mabi=ilp32は、ABIを決定するオプションです。これは、整数演算しか持たない32bit命令セットを表しています。
浮動小数点がサポートしている場合、浮動小数点レジスタを利用したABIになるため、32 bit整数とは異なるABIになります。浮動小数点をサポートする場合、mabi=ilp32dになります。

j __start_rust命令がリンクされた結果、j 0x80000010に変換されていますね。
0x80000010のアドレスを見ると、__start_rustの開始地点になっています。

helloの逆アセンブル結果
hello:     file format elf32-littleriscv


Disassembly of section .text:

80000000 <_start>:
80000000:       80000137                lui     sp,0x80000
80000004:       73416113                ori     sp,sp,1844
80000008:       0080006f                j       80000010 <__start_rust>

8000000c <abort>:
8000000c:       0000006f                j       8000000c <abort>

80000010 <__start_rust>:
80000010:       7139                    addi    sp,sp,-64
80000012:       de06                    sw      ra,60(sp)
80000014:       10013537                lui     a0,0x10013
80000018:       c62a                    sw      a0,12(sp)
8000001a:       80000537                lui     a0,0x80000
8000001e:       32c52583                lw      a1,812(a0) # 8000032c <_bss_start+0xfffffff8>
80000022:       32c50513                addi    a0,a0,812

cargoのbuild scriptを使って、バイナリを生成する

さて、皆様、せっかくcargoという素晴らしいエコシステムがあるにもかかわらず、最終的なバイナリ生成にriscv-toolchainを直叩きしているの、悔しくありません?
そこで!cargo runでRISC-V向けのバイナリを生成した上で、qemuで実行するところまで一気にできるようにしましょう。
まずは、Rustでライブラリを作成していましたが、アプリケーションを生成するようにします。

手順の簡単化のため、これまで作ったライブラリプロジェクトをアプリケーションプロジェクトに変更します。別途アプリケーションプロジェクトを作って、ライブラリプロジェクトと結合する方法の方が、本当は良いと思います。

$ mv src/lib.rs src/main.rs

cargoに設定を追加します。.cargoディレクトリを作成し、.cargo/configファイルを下記の通り、作成します。

.cargo/config
[target.riscv32imac-unknown-none-elf]
runner = "qemu-system-riscv32 -nographic -machine sifive_u -kernel"
rustflags = [
  "-C", "link-arg=-Tlinker.ld",
]

[build]
target = "riscv32imac-unknown-none-elf"

これで、バイナリを生成する際に、リンカスクリプトとして、linker.ldが使われるようになります。
build targetを指定することで、cargo buildを実行するだけで、RISC-Vのバイナリが生成されるようになります。
runnerを指定することで、cargo runとしたときに、qemuを指定のオプションで呼び出してくれます。

次に、build scriptを作成していきます。
build scriptは、cargo build時に実行されるもので、これもRustで記述します。
build scriptでccというcrateを使いします。そこで、Cargo.tomlに次の設定を追加します。

Cargo.toml
[build-dependencies]
cc = "1.0.25"

cc crateは、cargo build実行時に、CやC++の依存ファイルをビルドするためのcrateです。
build.rsファイルを下記の内容で作成します。

build.rs
extern crate cc;

use std::error::Error;
use cc::Build;

fn main() -> Result<(), Box<Error>> {
    // assemble the `boot.s` file
    Build::new()
        .file("boot.s")
        .flag("-mabi=ilp32")
        .compile("asm");

    Ok(())
}

このように記述することで、Rustコードがコンパイルされる前に、boot.sがアセンブルされ、libasm.aというアーカイブが作成されます。
Rustコードのビルドでは、このlibasm.aもリンクされます。

では、cargo runしてみましょう。

$ env CC=riscv32-unknown-linux-gnu-gcc  cargo run
   Compiling hello v0.1.0 (/home/tomoyuki/work/04.RISCV/riscv-rust-hello)
    Finished dev [unoptimized + debuginfo] target(s) in 1.69s
     Running `qemu-system-riscv32 -nographic -machine sifive_u -kernel target/riscv32imac-unknown-none-elf/debug/hello`
Hello from Rust!

無事、ビルドが実行され、qemuで実行されていますね。
cc crateは、クロスビルドツールの指定に制限があるため、環境変数で$CCにクロスビルドツールを指定しています。

TIPS

binutils

cargo経由でbinutilsを利用することができます。

$ cargo install cargo-binutils
$ rustup component add llvm-tools-preview

cargo objdumpで逆アセンブルしてみましょう。

$ cargo objdump --bin hello -- -d -no-show-raw-insn
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s

hello:  file format ELF32-riscv

Disassembly of section .reset:
_start:
80000000:       lui     sp, 524288

さらに詳しい使い方を知りたい場合は、helpを参照してください。
基本的には、llvm-objdumpの使い方と同じだと思います。

$ cargo objdump --help
cargo-objdump 0.1.4
Proxy for the `llvm-objdump` tool shipped with the Rust toolchain.

USAGE:
    cargo-objdump [FLAGS] [OPTIONS] [--] [args]...
...

riscv-toolchainなしでビルドする

nightly限定ですが、Rustのソースコード内にアセンブリを書くことができます。
これにより、外部ファイルにアセンブリを書く必要がなくなり、riscv-toolchainなしでRISC-Vのバイナリを生成できます。

// nightly限定の機能`global_asm`
#![feature(global_asm)]
#![no_std]
#![no_main]

#[no_mangle]
pub extern "C" fn __start_rust() -> ! {
    let uart = 0x1001_3000  as *mut u8;
    for c in b"Hello from Rust!".iter() {
        unsafe {
            *uart = *c as u8;
        }
    }

    loop{}
}

use core::panic::PanicInfo;
#[panic_handler]
#[no_mangle]
pub fn panic(_info: &PanicInfo) -> ! {
    loop{}
}

#[no_mangle]
pub extern "C" fn abort() -> ! {
    loop{}
}

#[cfg(target_arch = "riscv32")]
#[link_section = ".boot"]
global_asm!(r#"
_start:
    /* Set up stack pointer. */
    lui     sp, %hi(stack_end)
    ori     sp, sp, %lo(stack_end)

    /* Now jump to the rust world; __start_rust.  */
    j       __start_rust

.bss

stack_start:
    .skip 1024
stack_end:
"#);

はい、おもむろにアセンブリを書くことができます。
#[link_section]アトリビュートで配置するセクションも指定できるので、やりたい放題です。

このようにすると、build scriptなしでも

$ cargo run

で、一気にRISC-Vのバイナリが生成できます。
stableでは必要だった、環境変数CCの指定も不要です。(build scriptの中でcc crateを使用しないため)

やはりインラインアセンブラ使いたくなるので、nightlyは機能が不安定である、という認識を持ったうえで便利に使用すれば良いかと思います。

bootloader

sifive_uのメモリマップを再掲載します。

hw/riscv/sifive_u.c
static const struct MemmapEntry {
    hwaddr base;
    hwaddr size;
} sifive_u_memmap[] = {
    [SIFIVE_U_DEBUG] =    {        0x0,      0x100 },
    [SIFIVE_U_MROM] =     {     0x1000,    0x11000 },
    [SIFIVE_U_TEST] =     {   0x100000,     0x1000 },
    [SIFIVE_U_CLINT] =    {  0x2000000,    0x10000 }, /* sifive_u */
    [SIFIVE_U_CLIC] =     {  0x2000000,  0x1000000 }, /* sifive_ux */
    [SIFIVE_U_PLIC] =     {  0xc000000,  0x4000000 },
    [SIFIVE_U_UART0] =    { 0x10013000,     0x1000 },
    [SIFIVE_U_UART1] =    { 0x10023000,     0x1000 },
    [SIFIVE_U_DRAM] =     { 0x80000000,        0x0 },
    [SIFIVE_U_GEM] =      { 0x100900FC,     0x2000 },
};

アドレスマップを見ると、sifive_uの0x1000から68KB分ROM領域が用意されています。
qemuの実装を見てみると、リセット直後は、0x1000からブートし、RAM領域の0x8000_0000へジャンプするようになっています。つまり、ブートストラップが置かれているわけですね。
ここを置き換えれば、自作ブートローダーでブートできそうです。
現在調査中ですが、改造なしでは、ブートローダーを置き換えできないようです。

sifive_u.cをみると、リセットベクタがべったりとハードコーディングされています。

    /* reset vector */
    uint32_t reset_vec[8] = {
        0x00000297,                    /* 1:  auipc  t0, %pcrel_hi(dtb) */
        0x02028593,                    /*     addi   a1, t0, %pcrel_lo(1b) */
        0xf1402573,                    /*     csrr   a0, mhartid  */
#if defined(TARGET_RISCV32)
        0x0182a283,                    /*     lw     t0, 24(t0) */
#elif defined(TARGET_RISCV64)
        0x0182b283,                    /*     ld     t0, 24(t0) */
#endif
        0x00028067,                    /*     jr     t0 */
        0x00000000,
        memmap[SIFIVE_U_DRAM].base, /* start: .dword DRAM_BASE */
        0x00000000,
                                       /* dtb: */
    };

ちなみに、このリセットベクタの後には、デバイスツリーのblobがくっつけられています。
Linux立ち上げたいわけではないので、そのあたりはどうでもいいので、上書きしたいところです。
しかし、リンカスクリプトで0x1000に命令を配置すると、qemu実行時にromのリセット失敗で怒られます。

$ /opt/riscv-qemu/bin/qemu-system-riscv32 -nographic -S -machine sifive_u -kernel target/riscv32imac-unknown-none-elf/debug/rvos-rs
rom: requested regions overlap (rom mrom.reset. free=0x0000000000001378, addr=0x0000000000001000)
qemu-system-riscv32: rom check and register reset failed

qemuでFlash領域に書き込みできるmachineもあるので (ARMのlm3s6965evbとか)、そのあたりの実装と見比べながら、RAM領域へのkernel展開くらいできるようにしていきたいですね。

参考

RISC-Vで動作するOS

RISC-V bare-metal

Rust OS

Rust Embedded