Spike を RV32 のプログラム向けにビルドする

Spike (riscv-isa-sim) は RISC-V の命令セットシミュレータで、(比較的) 手軽に RISC-V のプログラムを試す上でとても便利なツール。

Spike はデフォルトで RV64 のプログラム向けにビルドされるが、今回は Spike を RV32 のプログラム向けにビルドする。また、合わせて必要となる RISC-V のコンパイラ (riscv-gnu-toolchain) とプロキシカーネル (riscv-pk) をビルドし、Hello world プログラムを動かす。

RISC-V の開発環境は基本的に Linux だが、macOSWindows の人向けに Docker を利用したビルド方法についても触れる。



インストール

Spike と RISC-V GNU Compiler Toolchain、RISC-V Proxy Kernel をそれぞれインストールする。

基本的には GitHub のインストール方法に書かれていることと同じだが、configure のオプションが異なるので注意。

特に、Spike の configure--with-target=riscv32-unknown-elf を指定し忘れると Spike が pk を見つけられなくなり、毎回 pk のフルパスを記述するか、pk のバイナリをカレントディレクトリにコピーしなければならなくなるので注意。

↓ 忘れるとこんな感じのエラーが出力される。

$ spike pk hello
terminate called after throwing an instance of 'std::runtime_error'
  what():  could not open pk; searched paths:
    . (current directory)
    /opt/riscv/riscv64-unknown-elf/bin/ (based on configured --prefix and --with-target)

以下は Linux (Ubuntu) の人向けのインストール方法。WORK と RISCV のディレクトリはどこでもいいので適宜置き換える。

$ sudo apt update && sudo apt upgrade -y
$ export WORK="$HOME/src"
$ export RISCV="/opt/riscv"
$ export PATH="$RISCV/bin:$PATH"
$ mkdir -p $WORK

# install riscv-gnu-toolchain
$ sudo apt install -y autoconf automake autotools-dev curl python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build git cmake libglib2.0-dev libslirp-dev
$ cd $WORK
$ git clone https://github.com/riscv/riscv-gnu-toolchain
$ cd riscv-gnu-toolchain
$ ./configure --prefix=$RISCV --with-arch=rv32gc --with-abi=ilp32
$ make -j $(nproc)

# install riscv-pk
$ cd $WORK
$ git clone https://github.com/riscv-software-src/riscv-pk.git
$ cd riscv-pk
$ mkdir build
$ cd build
$ ../configure --prefix=$RISCV --host=riscv32-unknown-elf
$ make -j $(nproc)
$ sudo make install

# install riscv-isa-sim (spike)
$ sudo apt install -y device-tree-compiler
$ cd $WORK
$ git clone https://github.com/riscv-software-src/riscv-isa-sim.git
$ cd riscv-isa-sim
$ mkdir build
$ cd build
$ ../configure --prefix=$RISCV --with-target=riscv32-unknown-elf
$ make -j $(nproc)
$ sudo make install

macOSWindowsLinux の人で Docker を使う場合の Dockerfile は以下の通り。

FROM ubuntu:latest

ENV HOME="/root"
ENV WORK="$HOME/src"
ENV RISCV="/opt/riscv"
ENV PATH="$RISCV/bin:$PATH"

RUN apt-get update && apt-get install -y \
    autoconf \
    automake \
    autotools-dev \
    bc \
    bison \
    build-essential \
    cmake \
    curl \
    device-tree-compiler \
    flex \
    gawk \
    git \
    gperf \
    libexpat-dev \
    libglib2.0-dev \
    libgmp-dev \
    libmpc-dev \
    libmpfr-dev \
    libtool \
    ninja-build \
    patchutils \
    python3 \
    python3-pip \
    texinfo \
    zlib1g-dev \
 && rm -rf /var/lib/apt/lists/*

# install riscv-gnu-toolchain
WORKDIR $WORK
RUN git clone https://github.com/riscv/riscv-gnu-toolchain
WORKDIR $WORK/riscv-gnu-toolchain
RUN ./configure --prefix=$RISCV --with-arch=rv32gc --with-abi=ilp32
RUN make -j $(nproc)
RUN make distclean

# install riscv-pk
WORKDIR $WORK
RUN git clone https://github.com/riscv-software-src/riscv-pk.git
WORKDIR $WORK/riscv-pk/build
RUN ../configure --prefix=$RISCV --host=riscv32-unknown-elf
RUN make -j $(nproc)
RUN make install
RUN make distclean

# install riscv-isa-sim (spike)
WORKDIR $WORK
RUN git clone https://github.com/riscv-software-src/riscv-isa-sim.git
WORKDIR $WORK/riscv-isa-sim/build
RUN ../configure --prefix=$RISCV --with-target=riscv32-unknown-elf
RUN make -j $(nproc)
RUN make install
RUN make distclean

WORKDIR $HOME

Docker の使い方

Docker のインストールは Install Docker Engine - Docker Docs から。あるいは macOS の人は homebrew (Docker - Homebrew Formulae) から。brew install --cask docker コマンドを実行して Docker をインストールする。

インストールが終わったら Docker Engine を起動しておく。


Dockerfile のあるディレクトリで以下のコマンドを実行すると Docker image が生成される。

$ docker build . -t spike_env

Docker image の生成が終了したら以下のコマンドで確認する。

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
spike_env    latest    2fa01e5c77eb   4 hours ago   15.2GB
ubuntu       latest    35a88802559d   3 weeks ago   78MB

コンテナを動かして中に入るためには以下のコマンドを実行する。

$ docker run -it spike_env

コンテナの外に出るためにはプロンプトに exit とタイプしてから Enter を押すか、ctrl + d を押す。

Hello world

Spike のインストールが終わったので Hello, world プログラムを動かしてみる。

hello.cHello world プログラムを記述する。

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

RISC-V GNU Compiler Toolchain で hello.cコンパイルし、実行ファイルとして hello を出力する。

$ riscv32-unknown-elf-gcc -o hello hello.c

Spike で hello を実行する。

$ spike --isa=rv32gc pk hello
Hello, world!

Spike で Hello world プログラムを動かすことができた。

ここで、pk は RISC-V Proxy Kernel のこと。Spike は Spike だけでは入出力関連のシステムコールを実行できないため、これらの処理を Proxy Kernel を通してホスト OS 側で行うようにしている。

Hello world の内側

RISC-V で Hello world プログラムを動かすことができたが、ここまでではホストの環境で Hello world を実行したときとの違いがわからない。

なので、ここからは RISC-V の Hello world プログラムの内側をのぞいてみる。

まずは Hello world プログラムのアセンブリを見てみる。

以下のコマンドを実行すると Hello world プログラムの RISC-V アセンブリhello.dump に出力される。

$ riscv32-unknown-elf-objdump -d hello > hello.dump

hello.dump はこんな感じになっている。RISC-V の仕様書 (RISC-V ISA manual - GitHub) を見てもらうと、RISC-V の命令が並んでいるなぁと思ってもらえるかも。

hello:     file format elf32-littleriscv


Disassembly of section .text:

00010094 <exit>:
   10094:       1141                    addi    sp,sp,-16
   10096:       4581                    li      a1,0
   10098:       c422                    sw      s0,8(sp)
   1009a:       c606                    sw      ra,12(sp)
   1009c:       842a                    mv      s0,a0
   1009e:       7dc000ef                jal     1087a <__call_exitprocs>
   100a2:       d481a783                lw      a5,-696(gp) # 13988 <__stdio_exit_handler>
   100a6:       c391                    beqz    a5,100aa <exit+0x16>
   100a8:       9782                    jalr    a5
   100aa:       8522                    mv      a0,s0
   100ac:       19c020ef                jal     12248 <_exit>

000100b0 <register_fini>:
   100b0:       00000793                li      a5,0
   100b4:       c791                    beqz    a5,100c0 <register_fini+0x10>
   100b6:       6549                    lui     a0,0x12
   100b8:       99450513                addi    a0,a0,-1644 # 11994 <__libc_fini_array>
   100bc:       08d0006f                j       10948 <atexit>
   100c0:       8082                    ret

000100c2 <_start>:
   100c2:       00004197                auipc   gp,0x4
   100c6:       b7e18193                addi    gp,gp,-1154 # 13c40 <__global_pointer$>
   100ca:       d4818513                addi    a0,gp,-696 # 13988 <__stdio_exit_handler>
   100ce:       07018613                addi    a2,gp,112 # 13cb0 <__BSS_END__>
   100d2:       8e09                    sub     a2,a2,a0
   100d4:       4581                    li      a1,0
   100d6:       2d61                    jal     1076e <memset>
   100d8:       00001517                auipc   a0,0x1
   100dc:       87050513                addi    a0,a0,-1936 # 10948 <atexit>
   100e0:       c519                    beqz    a0,100ee <_start+0x2c>
   100e2:       00002517                auipc   a0,0x2
   100e6:       8b250513                addi    a0,a0,-1870 # 11994 <__libc_fini_array>
   100ea:       05f000ef                jal     10948 <atexit>
   100ee:       2d19                    jal     10704 <__libc_init_array>
   100f0:       4502                    lw      a0,0(sp)
   100f2:       004c                    addi    a1,sp,4
   100f4:       4601                    li      a2,0
   100f6:       20b1                    jal     10142 <main>
   100f8:       bf71                    j       10094 <exit>

...

次に、Spike が Hello world プログラムを実行したときの命令列をながめてみる。

以下のコマンドを実行すると、Spike が実行した命令列や命令の実行結果のコミット履歴などが hello.log に出力される。

$ spike --isa=rv32gc -l --log-commits --log=hello.log pk hello

hello.log はこんな感じになっている。全部で 515,083 行。スタートアップルーチンやら pk の処理などでやたら複雑になっていそう。

core   0: 0x00001000 (0x00000297) auipc   t0, 0x0
core   0: 3 0x00001000 (0x00000297) x5  0x00001000
core   0: 0x00001004 (0x02028593) addi    a1, t0, 32
core   0: 3 0x00001004 (0x02028593) x11 0x00001020
core   0: 0x00001008 (0xf1402573) csrr    a0, mhartid
core   0: 3 0x00001008 (0xf1402573) x10 0x00000000
core   0: 0x0000100c (0x0182a283) lw      t0, 24(t0)
core   0: 3 0x0000100c (0x0182a283) x5  0x80000000 mem 0x00001018
core   0: 0x00001010 (0x00028067) jr      t0
core   0: 3 0x00001010 (0x00028067)
core   0: 0x80000000 (0x1f80006f) j       pc + 0x1f8
core   0: 3 0x80000000 (0x1f80006f)
core   0: 0x800001f8 (0x00000093) li      ra, 0
core   0: 3 0x800001f8 (0x00000093) x1  0x00000000
core   0: 0x800001fc (0x00000113) li      sp, 0
core   0: 3 0x800001fc (0x00000113) x2  0x00000000
core   0: 0x80000200 (0x00000193) li      gp, 0
core   0: 3 0x80000200 (0x00000193) x3  0x00000000
core   0: 0x80000204 (0x00000213) li      tp, 0
core   0: 3 0x80000204 (0x00000213) x4  0x00000000
core   0: 0x80000208 (0x00000293) li      t0, 0
core   0: 3 0x80000208 (0x00000293) x5  0x00000000
core   0: 0x8000020c (0x00000313) li      t1, 0
...