kivantium活動日記

プログラムを使っていろいろやります

RISC-Vクロスコンパイラで生成したバイナリを自作RISC-V上で実行する

4連休の課題としてFPGAで簡単なCPUを作っているので、その進捗を記録しておきます。

RISC-V (RV32I) の作成

とりあえず今回は確実に動くCPUを作ることを目標にしました。 パイプラインなどは実装せず、フェッチ→デコード→実行→メモリ・アクセス→書き戻しの5段階にそれぞれ1クロック使って、1命令に5クロックかける設計になっています。 命令セットにはRISC-Vの一番基本的な構成であるRV32Iを採用しましたが、簡単のため特権命令や割り込み周りは省略しました。 ハードウェアには以前使ったDigilentのBasys 3を使って、Vivadoで開発しました。 kivantium.hateblo.jp

あまり工夫したところはないので実装の詳細は説明しません。ソースコードはここに置いてあります。 github.com

RISC-Vクロスコンパイラのインストール

RISC-V向けのGCCriscv-gnu-toolchainというリポジトリで公開されています。以前はriscv-toolsというリポジトリ以下で公開されていたものが移動したらしいので、古い記事を参考にするときは気をつけてください。 github.com

READMEに従ってインストールするだけですが、configureで32bit用に設定する必要があります。ソースコードだけで10GB近くあるのでディスク容量に注意してください。

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

/opt/riscv32以下にインストールされるのでPATHを通しておきます。

export PATH=/opt/riscv32/bin:$PATH

バイナリの作成

簡単な例として、フィボナッチ数列の第10番目の項を再帰で求めるプログラムを実行することにします。test.cを以下の通り作成します。main関数を抜けないようにするために最後に無限ループを実行しています。

int fib(int n) {
  if(n <= 1) return 1;
  return fib(n-1) + fib(n-2);
}

int main() {
  fib(10);
  for(;;) {}
  return 0;
}

これを自作CPUで動くようにコンパイルします。

普通にコンパイルしてしまうと未実装の命令を使った初期化ルーチンが走ってしまうので、まずはそれを無効にします。start.Sを以下の通り作成します。

.section .text.init;
.globl _start
_start:
    call main

何もせずにmainを呼び出すアセンブリになっています。

次に、命令を0番地から実行するように指定します。link.ldを以下の通り作成します。

OUTPUT_ARCH( "riscv" )
ENTRY(_start)

SECTIONS
{
  . = 0x00000000;
  .text.init : { *(.text.init) }
  .tohost : { *(.tohost) }
  .text : { *(.text) }
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}

以下のようにしてバイナリを生成します。

riscv32-unknown-elf-gcc -march=rv32i -c -o start.o start.S
riscv32-unknown-elf-gcc -march=rv32i -c -o test.o test.c
riscv32-unknown-elf-ld test.o start.o -lc -L/opt/riscv32/riscv32-unknown-elf/lib/ -Tlink.ld -nostartfiles -static -o test.elf
riscv32-unknown-elf-objcopy -O binary test.elf test.bin
hexdump -v -e '1/4 "%08x" "\n"' test.bin > test.hex

最後に出来上がるtest.hexには、CPUが実行する命令列が16進数で書かれています。

074000ef
fe010113
00112e23
(中略)
00a00513
f7dff0ef
0000006f

ELF形式のファイルに対してobjdumpを実行すると逆アセンブルした結果を見ることができます。

$ riscv32-unknown-elf-objdump -d test.elf

test.elf:     ファイル形式 elf32-littleriscv


セクション .text.init の逆アセンブル:

00000000 <_start>:
   0:   074000ef            jal ra,74 <main>

セクション .text の逆アセンブル:

00000004 <fib>:
   4:   fe010113            addi    sp,sp,-32
   8:   00112e23            sw  ra,28(sp)
   c:   00812c23            sw  s0,24(sp)
  10:   00912a23            sw  s1,20(sp)
  14:   02010413            addi    s0,sp,32
  18:   fea42623            sw  a0,-20(s0)
  1c:   fec42703            lw  a4,-20(s0)
  20:   00100793            li  a5,1
  24:   00e7c663            blt a5,a4,30 <fib+0x2c>
  28:   00100793            li  a5,1
  2c:   0300006f            j   5c <fib+0x58>
  30:   fec42783            lw  a5,-20(s0)
  34:   fff78793            addi    a5,a5,-1
  38:   00078513            mv  a0,a5
  3c:   fc9ff0ef            jal ra,4 <fib>
  40:   00050493            mv  s1,a0
  44:   fec42783            lw  a5,-20(s0)
  48:   ffe78793            addi    a5,a5,-2
  4c:   00078513            mv  a0,a5
  50:   fb5ff0ef            jal ra,4 <fib>
  54:   00050793            mv  a5,a0
  58:   00f487b3            add a5,s1,a5
  5c:   00078513            mv  a0,a5
  60:   01c12083            lw  ra,28(sp)
  64:   01812403            lw  s0,24(sp)
  68:   01412483            lw  s1,20(sp)
  6c:   02010113            addi    sp,sp,32
  70:   00008067            ret

00000074 <main>:
  74:   ff010113            addi    sp,sp,-16
  78:   00112623            sw  ra,12(sp)
  7c:   00812423            sw  s0,8(sp)
  80:   01010413            addi    s0,sp,16
  84:   00b00513            li  a0,11
  88:   f7dff0ef            jal ra,4 <fib>
  8c:   0000006f            j   8c <main+0x18>

C言語で書いた通り、再帰でフィボナッチ数を求めた後、`8c'を無限ループするプログラムになっていることが確認できます。

生成した16進数の命令列はSystemVerilogの$readmemhを利用して命令メモリに埋め込んでいます(ソースコード)。コードを変更するたびに論理合成をやり直す必要がありますが、命令列を外部から読み込ませるのは面倒なのでこうしました。

実機での動作

関数の引数と戻り値が入るa0レジスタの値を10進数で7セグLEDに表示する回路を組みました。しばらく計算した後、最終的な関数の返り値である89が表示されます。(動作を見やすくするためにクロックを10万分周しています)

今後の課題

xv6が去年からRISC-Vに対応したそうなので、xv6が動くようなCPUが作れると良いです。 OSを動かすためには割り込みなどの特権命令やスーパーバイザーモードでの仮想アドレスを実装しないといけないみたいので道のりは険しそうですが……

参考文献

ツールチェインの使い方が特に参考になりました。

github.com 上のFPGAマガジンで実装されているRISC-Vです(本誌からリンクされてなかった……)

1命令を5クロックで実行する設計はここから持ってきました(このコードではデコーダーやALUが順序回路になっていますが、自分の実装では組み合わせ回路になっています。2クロック無駄になっていますが、簡単のためです……)

mindchasers.com ツールチェインのコンパイルオプションはここを参考にしました。

広告コーナー