FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V Getting Started GuideのLinuxビルドを試す

f:id:msyksphinz:20200503214338p:plain

RISC-V Getting Started Guideというページがある。目的ベースで、QEMULinuxなどのオペレーティングシステムを動かすための方法や、様々なツールキットを動かすための情報がまとめられている。

risc-v-getting-started-guide.readthedocs.io

ここに"Running 64- and 32-bit RISC-V Linux on QEMU" という項目がある。これまでFreedom-u-sdkのツールキットを使うしか方法が無いのかと思っていたが、どうやら別の方法を使ってQEMULinuxをブートできるらしい。やってみよう。

risc-v-getting-started-guide.readthedocs.io

前提としてriscv64-unknown-elf-gccriscv64-unknown-linux-gnu-gccがビルドできる状態であること。 フローの中にはricsv-gnu-toolchainをダウンロードしてこれらのビルドを行うステップも含まれているが、ネットワーク帯域が必要だしビルドも時間がかかるので、私はすでにビルドしてある自前のツールを使って省略。

Getting the sourcesから始める。

# すでにビルドしてあるものを使用するためriscv-gnu-toolchainは使用しない
# git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
git clone https://github.com/qemu/qemu
git clone https://github.com/torvalds/linux
git clone https://github.com/riscv/riscv-pk
git clone https://github.com/michaeljclark/busybear-linux

まずはQEMUのビルドから。これはフローに乗るだけで問題なくビルドできた。sudo make installがしたくないので、prefix=を設定してホーム領域中にインストールしておく。

cd qemu
git checkout v3.0.0
./configure --target-list=riscv64-softmmu --prefix=${HOME}/riscv-linux-toolkit
make -j $(nproc)
make install

次はLinuxのビルドとなる。これもフローに則って行う。

cd linux
git checkout v4.19-rc3
cp ../busybear-linux/conf/linux.config .config
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- olddefconfig

カーネルのコンフィグレーションは特に変更せずに特に設定は適用されていた。

make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- nconfig

.configで以下のパラメータが設定されていることを確認する。

ARCH_RV64I
CMODEL_MEDANY
CONFIG_SIFIVE_PLIC

ビルドを実行する。

make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- vmlinux -j $(nproc)

次はBBL(Berkeley Boot Loader)のビルドを行う。BBLはブートローダなのだが、ブートローダによるセットアップが終わった後にvmlinuxを起動するという要領だ。

cd riscv-pk
mkdir build && cd build
../configure --enable-logo --host=riscv64-unknown-elf --with-payload=../../linux/vmlinux
make -j $(nproc)

最後にファイルシステムの用意だ。Busybear Linuxのビルドを行う。

cd busybear-linux
make -j $(nproc)

あれ、途中でビルドが止まってしまった。

  KSYM    .tmp_kallsyms2.o
  LD      vmlinux
  SYSMAP  System.map
make[1]: Leaving directory '/home/msyksphinz/work/riscv/riscv-linux-kit/busybear-linux/build/linux-5.0'
./scripts/build.sh: line 84: ../../src/riscv-pk/configure: No such file or directory
make: *** [Makefile:10: busybear.bin] Error 127
zsh: exit 2     make -j$(nproc)
DESKTOP-P42Q0NR:busybear-linux msyksphinz$

色々調べると、なんだ、busybear-linuxにはひそかにsubmodulesが含まれているじゃないか。

git submodule update --init --recusive

再度ビルドに挑戦する。

In file included from /home/msyksphinz/riscv64-ctng-linux/riscv64-unknown-linux-gnu/sysroot/usr/include/features.h:474,
                 from /home/msyksphinz/riscv64-ctng-linux/riscv64-unknown-linux-gnu/sysroot/usr/include/sys/stat.h:25,
                 from ../../src/riscv-pk/pk/file.h:6,
                 from ../../src/riscv-pk/pk/file.c:3:
/home/msyksphinz/riscv64-ctng-linux/riscv64-unknown-linux-gnu/sysroot/usr/include/gnu/stubs.h:8:11: fatal error: gnu/stubs-lp64.h: No such file or directory
 # include <gnu/stubs-lp64.h>
           ^~~~~~~~~~~~~~~~~~

次はこんな感じのエラーで怒られた。gnu/stubs-lp64.hが無い? 渡しのriscv64-unknown-linux-gnuはcrostools-ngでビルドしたものだ。これだと問題が発生するのだろうか?

以下のIssueを見ながら、とりあえず既存のstubs-lp64d.hstubs-lp64.hにコピーした。

sudo cp ./riscv64-unknown-linux-gnu/sysroot/usr/include/gnu/stubs-lp64d.h ./riscv64-unknown-linux-gnu/sysroot/usr/include/gnu/stubs-lp64.h

さらに進めると今度は謎のエラーだ。なんだこれは。。。

riscv64-unknown-linux-gnu-ranlib libsoftfloat.a
riscv64-unknown-linux-gnu-gcc -Wl,--build-id=none -nostartfiles -nostdlib -static  -march=rv64gc -mabi=lp64 -o pk pk.o -L.  -lpk  -lmachine  -lsoftfloat  -lutil -lgcc -T ../../src/riscv-pk/pk/pk.lds
riscv64-unknown-linux-gnu-gcc -Wl,--build-id=none -nostartfiles -nostdlib -static  -march=rv64gc -mabi=lp64 -o bbl bbl.o -L.  -lbbl  -ldummy_payload  -lmachine  -lsoftfloat  -lutil -lgcc -T ../../src/risc
v-pk/bbl/bbl.lds
make[1]: Leaving directory '/home/msyksphinz/work/riscv/riscv-linux-kit/busybear-linux/build/riscv-pk'
[sudo] password for msyksphinz:
env: 'Files/PuTTY:/mnt/c/Program': No such file or directory
make: *** [Makefile:10: busybear.bin] Error 127

これはどうもWSL特有の問題らしい。PATHに変な文字列が入っているのがおかしいようなので、PATHに通すディレクトリを限定して再度試行する。

100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 0.0597549 s, 1.8 GB/s
mke2fs 1.44.4 (18-Aug-2018)
Creating filesystem with 102400 1k blocks and 25688 inodes
Filesystem UUID: e992d615-9ab7-470d-b78e-6b980fffb641
Superblock backups stored on blocks:
        8193, 24577, 40961, 57345, 73729

Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

mount: mnt: mount failed: Operation not permitted.
make: *** [Makefile:10: busybear.bin] Error 1
zsh: exit 2     make -j$(nproc)

最後はエラーで止まったが、目標のbusybear.binは生成されたようなのでさっそくQEMUで実行してみよう。

~/riscv-linux-kit/bin/qemu-system-riscv64 -nographic -machine virt \
     -kernel riscv-pk/build/bbl -append "root=/dev/vda ro console=ttyS0" \
     -drive file=busybear-linux/busybear.bin,format=raw,id=hd0 \
     -device virtio-blk-device,drive=hd0
bbl loader
              vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
                  vvvvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrrrr    vvvvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrrrrr      vvvvvvvvvvvvvvvvvvvvvv
rrrrrrrrrrrrr       vvvvvvvvvvvvvvvvvvvvvv
rr                vvvvvvvvvvvvvvvvvvvvvv
rr            vvvvvvvvvvvvvvvvvvvvvvvv      rr
rrrr      vvvvvvvvvvvvvvvvvvvvvvvvvv      rrrr
rrrrrr      vvvvvvvvvvvvvvvvvvvvvv      rrrrrr
rrrrrrrr      vvvvvvvvvvvvvvvvvv      rrrrrrrr
rrrrrrrrrr      vvvvvvvvvvvvvv      rrrrrrrrrr
rrrrrrrrrrrr      vvvvvvvvvv      rrrrrrrrrrrr
rrrrrrrrrrrrrr      vvvvvv      rrrrrrrrrrrrrr
rrrrrrrrrrrrrrrr      vv      rrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrr          rrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrr      rrrrrrrrrrrrrrrrrrrr
rrrrrrrrrrrrrrrrrrrrrr  rrrrrrrrrrrrrrrrrrrrrr

       INSTRUCTION SETS WANT TO BE FREE
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80200000
[    0.000000] Linux version 4.19.0-rc3 (msyksphinz@DESKTOP-P42Q0NR) (gcc version 8.3.0 (crosstool-NG 1.24.0)) #1 SMP Sun May 3 19:13:43 JST 2020
[    0.000000] bootconsole [early0] enabled
[    0.000000] Initial ramdisk at: 0x(____ptrval____) (512 bytes)
[    0.000000] Zone ranges:
[    0.000000]   DMA32    empty
[    0.000000]   Normal   [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080200000-0x0000000087ffffff]
[    0.000000] software IO TLB: mapped [mem 0x83e3d000-0x87e3d000] (64MB)
[    0.000000] elf_hwcap is 0x112d
[    0.000000] percpu: Embedded 16 pages/cpu @(____ptrval____) s25496 r8192 d31848 u65536
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 31815
[    0.000000] Kernel command line: root=/dev/vda ro console=ttyS0

ブートは始まったようだが、しばらくすると止まってしまった。うーん、やはりWSLでビルドしたのが良くないのだろうか...?

[    0.592000] Run /bin/init as init process
[    0.592000] Run /bin/sh as init process
[    0.592000] Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst for guidance.
[    0.592000] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 4.19.0-rc3 #1
[    0.592000] Call Trace:
[    0.592000] [<ffffffe00002825c>] walk_stackframe+0x0/0xa0
[    0.592000] [<ffffffe0000283bc>] show_stack+0x2a/0x34
[    0.592000] [<ffffffe0003376f4>] dump_stack+0x62/0x7c
[    0.592000] [<ffffffe00002c31a>] panic+0xd2/0x1e8
[    0.592000] [<ffffffe0003489d8>] kernel_init+0xe4/0xf0
[    0.592000] [<ffffffe0000272ee>] ret_from_exception+0x0/0xc

カーネルパニックを起こしてしまった。これが問題なのかが分からないので、純粋なUbuntuの環境を作成して試行してみよう。

Verilatorの使い方(3. Lintとして活用する)

Verilatorは論理シミュレータだけではなく、Lintとしても活用することができる。例えばこれまで使用してきたcounter_4bit.vをLintに掛けてみよう。

$ verilator --lint-only -Wall counter_4bit.v
%Warning-DECLFILENAME: counter_4bit.v:1:8: Filename 'counter_4bit' does not match MODULE name: 'counter'
    1 | module counter
      |        ^~~~~~~
                       ... Use "/* verilator lint_off DECLFILENAME */" and lint_on around source to disable this message.
%Error: Exiting due to 1 warning(s)

いきなりエラー通知が出てきた。どうやらファイル名と中のモジュール名が一致しないようだ。確かにこれはVerilog記述の一般常識から外れている。。。エラーが消えるように修正する。

diff --git a/counter_4bit.v b/counter_4bit.v
index 9cbe73c..27e98e7 100644
--- a/counter_4bit.v
+++ b/counter_4bit.v
@@ -1,4 +1,4 @@
-module counter
+module counter_4bit
   (
    input logic        clk,
    input logic        reset_n,
@@ -14,4 +14,4 @@ always_ff @(posedge clk, negedge reset_n) begin
     end
   end
 end
-endmodule // counter
+endmodule // counter_4bit

これでLintを通すとエラーが無くなった。問題なさそうだ。

RISC-Vにおける32ビットよりも大きな定数の生成方法

ふと気になって、RISC-Vの32ビットよりも大きな定数の作り方を調べることにした。これはRISC-VのISAの話というよりもコンパイラの話だ。

例えば以下のようなプログラムをGCCコンパイルするとどのように定数を生成するのかチェックしてみる。

  • long_value.c
int long_value()
{
  long long_const = 0x0123456789abcdefULL;
  return 0;
}
riscv64-unknown-elf-gcc -c long_value.c -S -o -
long_value:
        lui     a5,%hi(.LC0)
        ld      a5,%lo(.LC0)(a5)
        addi    sp,sp,-16
        li      a0,0
        sd      a5,8(sp)
        addi    sp,sp,16
        jr      ra
        .size   long_value, .-long_value
        .section        .srodata.cst8,"aM",@progbits,8
        .align  3
.LC0:
        .dword  81985529216486895

ラベルとして定数を宣言してそこに配置してきた。ぬ、その手があったか。

これではつまらないのでもう少し見ていきたい。LLVMだとどうか。

./bin/clang -O3 long_value.c -c -target riscv64-unknown-elf -S -o -
long_value:                             # @long_value
# %bb.0:                                # %entry
        addi    sp, sp, -16
        lui     a0, 146
        addiw   a0, a0, -1493
        slli    a0, a0, 12
        addi    a0, a0, 965
        slli    a0, a0, 13
        addi    a0, a0, -1347
        slli    a0, a0, 12
        addi    a0, a0, -529
        sd      a0, 8(sp)
        mv      a0, zero
        addi    sp, sp, 16
        ret

なんだか摩訶不思議な命令が生成された。これを読み解いていくカギは、LLVMRISC-V実装における定数生成アルゴリズムにある。

0x0123456789abcdefという64ビット数をレジスタに格納するために必要なことを考える。

  1. 下位の12ビットと上位の50ビットを分割して考える。上位の50ビットを生成できれば、下位の12ビットはADDI命令を使って結合することで生成64ビット数を生成できる。
  2. 上位の50ビットも下位の12ビットと上位の38ビットに分割して考える。上位の38ビットを生成できれば、12ビットシフトした後にADDI命令で下位の12ビットと結合して50ビットの値を生成できる。
  3. これを繰り返す。つまり再帰的に上位から順に値を作ってはシフトして加算する。
f:id:msyksphinz:20200503112316p:plain

この方式はRISC-V向けのRISCVISelDAGToDAGクラスを参考にした。

もしRV64向けに64ビットの定数を生成する場合はselectImm()にジャンプし、定数を手動で生成する。

  • llvm/lib/Target/RISCV/RISCVISelDAGToDAG.cpp
void RISCVDAGToDAGISel::Select(SDNode *Node) {
  // If we have a custom node, we have already selected.
  if (Node->isMachineOpcode()) {
    LLVM_DEBUG(dbgs() << "== "; Node->dump(CurDAG); dbgs() << "\n");
    Node->setNodeId(-1);
    return;
  }
...
  switch (Opcode) {
  case ISD::Constant: {
    auto ConstNode = cast<ConstantSDNode>(Node);
    if (VT == XLenVT && ConstNode->isNullValue()) {
      SDValue New = CurDAG->getCopyFromReg(CurDAG->getEntryNode(), SDLoc(Node),
                                           RISCV::X0, XLenVT);
      ReplaceNode(Node, New.getNode());
      return;
    }
    int64_t Imm = ConstNode->getSExtValue();
    if (XLenVT == MVT::i64) {
      ReplaceNode(Node, selectImm(CurDAG, SDLoc(Node), Imm, XLenVT));
      return;
    }
    break;
  }

selectImm()の実装は以下となる。

  • llvm/lib/Target/RISCV/RISCVISelDAGToDAG.cpp
static SDNode *selectImm(SelectionDAG *CurDAG, const SDLoc &DL, int64_t Imm,
                         MVT XLenVT) {
  RISCVMatInt::InstSeq Seq;
  RISCVMatInt::generateInstSeq(Imm, XLenVT == MVT::i64, Seq);

  SDNode *Result = nullptr;
  SDValue SrcReg = CurDAG->getRegister(RISCV::X0, XLenVT);
  for (RISCVMatInt::Inst &Inst : Seq) {
    SDValue SDImm = CurDAG->getTargetConstant(Inst.Imm, DL, XLenVT);
    if (Inst.Opc == RISCV::LUI)
      Result = CurDAG->getMachineNode(RISCV::LUI, DL, XLenVT, SDImm);
    else
      Result = CurDAG->getMachineNode(Inst.Opc, DL, XLenVT, SrcReg, SDImm);

    // Only the first instruction has X0 as its source.
    SrcReg = SDValue(Result, 0);
  }

  return Result;
}

RISCVMatInt::generationIntSeqに64ビットの定数を生成するためのアセンブリ命令の出力アルゴリズムが含まれている。

1つずつ読み解いていく。まず32ビットに収まる値の範囲内であれば何も考えずに値を生成する。

  • llvm/lib/Target/RISCV/Utils/RISCVMatInt.cpp
void generateInstSeq(int64_t Val, bool IsRV64, InstSeq &Res) {
  if (isInt<32>(Val)) {
    // Depending on the active bits in the immediate Value v, the following
    // instruction sequences are emitted:
    //
    // v == 0                        : ADDI
    // v[0,12) != 0 && v[12,32) == 0 : ADDI
    // v[0,12) == 0 && v[12,32) != 0 : LUI
    // v[0,32) != 0                  : LUI+ADDI(W)
    int64_t Hi20 = ((Val + 0x800) >> 12) & 0xFFFFF;
    int64_t Lo12 = SignExtend64<12>(Val);

    if (Hi20)
      Res.push_back(Inst(RISCV::LUI, Hi20));

    if (Lo12 || Hi20 == 0) {
      unsigned AddiOpc = (IsRV64 && Hi20) ? RISCV::ADDIW : RISCV::ADDI;
      Res.push_back(Inst(AddiOpc, Lo12));
    }
    return;
  }

32ビットよりも大きな値を作りたいときは下位12ビットと上位12ビットを切り離す。

  int64_t Lo12 = SignExtend64<12>(Val);
  int64_t Hi52 = ((uint64_t)Val + 0x800ull) >> 12;
  int ShiftAmount = 12 + findFirstSet((uint64_t)Hi52);
  Hi52 = SignExtend64(Hi52 >> (ShiftAmount - 12), 64 - ShiftAmount);

上位ビットの定数生成を行うために、上位ビットの値を引数としてgenerateInstSeq()再帰的に呼び出すことで命令を生成する。

  Hi52 = SignExtend64(Hi52 >> (ShiftAmount - 12), 64 - ShiftAmount);

  generateInstSeq(Hi52, IsRV64, Res);

生成した上位ビットは、シフト命令SLLIを使って上位にシフトする。さらにADDI命令を生成して下位12ビットを結合する。

この実装ではもう少し賢い方法を取っている。上位ビットHi52の下位ビットに0が並んでいる場合、これを省略してより高いビット位置から上位定数の生成を開始している。これにより再帰的呼び出しの回数を減らし、定数生成のステップを減らそうといる。

f:id:msyksphinz:20200503112934p:plain

生成された命令に対して、コメントを付加してみると以下のようになる。

long_value:                             # @long_value

# %bb.0:                                # %entry
        addi    x2, x2, -8
        lui     x10, 146                # 0x0000_0000_0009_2000
        addi    x10, x10, -1493         # 0x0000_0000_0009_1a2b
        slli    x10, x10, 12            # 0x0000_0000_91a2_b000
        addi    x10, x10, 965           # 0x0000_0000_91a2_b3c5
        slli    x10, x10, 13            # 0x0000_1234_5678_a000
        addi    x10, x10, -1347         # 0x0000_1234_5678_9abd
        slli    x10, x10, 12            # 0x0123_4567_89ab_d000
        addi    x10, x10, -529          # 0x0123_4567_89ab_cdef
        sd      x10, 0(x2)
        addi    x10, zero, 0
        addi    x2, x2, 8
        ret
       

Verilatorの使い方(2. 波形ダンプの方法)

Verilatorの使い方。基本的なデジタル回路のシミュレーション方法が分かったら、次は波形をダンプする方法だ。

Verilatorを使ってRTLシミュレーションをすべてフリーのツールで完結させたい場合、波形をダンプするためには主に以下の2種類の方法がある。

  • vcdをダンプしてGTKWaveで観察する。
  • fstをダンプしてGTKWaveで観察する。

VCDは"Value Change Dump"の略称で波形情報を含んでいる。これはバイナリ圧縮されていないので中身を目視で観察できる一方、ファイルサイズが大きい。

一方でfstは"Fast Signal Trace"の略称で同様に波形情報を格納している。これはバイナリ圧縮されておりファイルサイズがVCDと比較して小さい。

どちらを使っても良いが、ここでは両方の方法を解説したいと思う。個人的にはfstを使う方が好みだ。このご時世VCDファイルの中身を読むことなどあり得ない。

VCDをダンプする場合

コンパイル

Verilatorにて回路をコンパイルする際、以下のオプションを追加する。

--trace --trace-params --trace-structs --trace-underscore

最初の--traceが必須で、以降のオプションは任意。これらのオプションはSystemVerilogの情報をどこまで出力するかを決定する。パラメータ、構造体の情報などは出力しておかないとデバッグ時に意外と面倒なことが多い。

$ verilator --cc --exe  --trace --trace-params --trace-structs --trace-underscore \
    counter_4bit.v -exe tb_counter_4bit.cpp

テストベンチで波形ダンプをONにする

次に行うべきはテストベンチ内で波形ダンプをONにする。有償シミュレータでも、テストベンチ内で波形ダンプのON/OFFを有効化する記述があるかと思う($fsdbDumpOn$fsdbDumpOffなど)。それと似たような記述をC++で記述したテストベンチ内で行う。

VCDをダンプする場合、前回示したtb_counter_4bit.cppに以下の記述を加える。

#include <verilated_vcd_c.h>  # VCD出力用のインクルードファイルを追加

  // Instantiate DUT
  Vcounter_4bit *dut = new Vcounter_4bit();

  // -- ここから
  // Trace DUMP ON
  Verilated::traceEverOn(true);
  VerilatedVcdC* tfp = new VerilatedVcdC;

  dut->trace(tfp, 100);  // Trace 100 levels of hierarchy
  tfp->open("simx.vcd");
  // -- ここまで

これは波形ダンプを有効化し、波形ファイル名をsimx.vcdとしてファイルディスクリプタをオープンしている。これによりsimv.vcdに波形が書き出される仕組みだ。

これだけでは波形は生成されず、dut->eval()毎に波形ダンプ用の関数を呼び出す必要がある。

...
  // Reset Time
  while (time_counter < 100) {
    dut->eval();
    tfp->dump(time_counter);  // 波形ダンプ用の記述を追加

    time_counter++;
  }
...
  int cycle = 0;
  while (time_counter < 500 && !Verilated::gotFinish()) {
    if ((time_counter % 5) == 0) {
...
    // Evaluate DUT
    dut->eval();
    tfp->dump(time_counter);  // 波形ダンプ用の記述を追加
...
  dut->final();
  tfp->close(); 

ここまででコンパイルして実行してみよう。VCDファイルが生成されるはずだ。

$ verilator --cc --exe  --trace --trace-params --trace-structs --trace-underscore \
    counter_4bit.v -exe tb_counter_4bit.cpp
$ make -C obj_dir -f Vcounter_4bit.mk
$ ./obj_dir/Vcounter_4bit
$ ls -lt
-rw-rw-rw- 1 msyksphinz msyksphinz  321 May  1 11:53 counter_4bit.v
-rw-rw-rw- 1 msyksphinz msyksphinz  248 May  2 11:33 Makefile
-rw-rw-rw- 1 msyksphinz msyksphinz 1313 May  2 11:51 tb_counter_4bit.cpp
drwxrwxrwx 1 msyksphinz msyksphinz 4096 May  2 11:52 obj_dir
-rw-rw-rw- 1 msyksphinz msyksphinz 3196 May  2 11:52 simx.vcd

VCDファイルを開いてみよう。GTKwaveを使用する。

$ gtkwave simx.vcd &

波形を確認できた。

f:id:msyksphinz:20200502121400p:plain

FSTをダンプする場合

コンパイル

FSTでダンプする場合はオプションを少し変更するだけだ。

--trace-fst --trace-params --trace-structs --trace-underscore

--trace--trace-fstに変更する。これだけで問題ない。

$ verilator --cc --exe  --trace-fst --trace-params --trace-structs --trace-underscore \
    counter_4bit.v -exe tb_counter_4bit.cpp

テストベンチでFST波形ダンプをONにする

FST波形ダンプの場合もVCD波形ダンプとほとんど変わらない。セットアップ用のコードは以下のように変更する。

#include <verilated_fst_c.h>  # FST出力用のインクルードファイルを追加

  // Instantiate DUT
  Vcounter_4bit *dut = new Vcounter_4bit();

  // -- ここから
  // Trace DUMP ON
  Verilated::traceEverOn(true);
  VerilatedFstC* tfp = new VerilatedFstC;

  dut->trace(tfp, 100);  // Trace 100 levels of hierarchy
  tfp->open("simx.fst");
  // -- ここまで

波形ダンプの方法はVCDの場合と全く変わらない。

...
  // Reset Time
  while (time_counter < 100) {
    dut->eval();
    tfp->dump(time_counter);  // 波形ダンプ用の記述を追加

    time_counter++;
  }
...
  int cycle = 0;
  while (time_counter < 500 && !Verilated::gotFinish()) {
    if ((time_counter % 5) == 0) {
...
    // Evaluate DUT
    dut->eval();
    tfp->dump(time_counter);  // 波形ダンプ用の記述を追加
...
  dut->final();
  tfp->close(); 

ここまででコンパイルしてシミュレーションを実行する。

$ verilator --cc --exe  --trace-fst --trace-params --trace-structs --trace-underscore \
    counter_4bit.v -exe tb_counter_4bit.cpp
$ make -C obj_dir -f Vcounter_4bit.mk
$ ./obj_dir/Vcounter_4bit
$ ls -lt
-rw-rw-rw- 1 msyksphinz msyksphinz  675 May  2 12:03 simx.fst
drwxrwxrwx 1 msyksphinz msyksphinz 4096 May  2 12:03 obj_dir
-rw-rw-rw- 1 msyksphinz msyksphinz 1313 May  2 12:02 tb_counter_4bit.cpp
-rw-rw-rw- 1 msyksphinz msyksphinz  252 May  2 12:02 Makefile
-rw-rw-rw- 1 msyksphinz msyksphinz  321 May  1 11:53 counter_4bit.v

simx.fstをGTKwaveで開いてみよう。

$ gtkwave simx.fst &

波形が出力できていることが確認できた。

f:id:msyksphinz:20200502121425p:plain

freedom-u-sdkのLinuxを立ち上げながらLinuxのブートプロセスを学ぶ(8. start_kernel(void)の処理)

私の開発したRISC-VシミュレータはLinuxを立ち上げることができる。シミュレータのデバッグ時には相当中身を読み込んだのだが、きちんと文章化していない挙句、大昔のプロジェクトなのでもう忘れかけている。

Linuxのブートの方法から各種プロセスの取り扱いまで、思い出しながらRISC-Vシミュレータを動かしていき、ちゃんと文章化しておきたいと思った。

start_kernel(void)の処理

  • freedom-u-sdk/linux/init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    set_task_stack_end_magic(&init_task);
    smp_setup_processor_id();
    debug_objects_early_init();
   
    cgroup_init_early();

    local_irq_disable();
    early_boot_irqs_disabled = true;
...

set_task_stack_end_magicはスタックポインタの準備で、スタックの最終アドレスにMAGICコードを埋め込む。これはスタック破壊を検出するためのコードだ。

void set_task_stack_end_magic(struct task_struct *tsk)
{
    unsigned long *stackend;

    stackend = end_of_stack(tsk);
    *stackend = STACK_END_MAGIC;    /* for overflow detection */
}

次のsmp_setup_processor_id()は特に何もしない。必要ならばオーバライドして使うようだ。

void __init __weak smp_setup_processor_id(void)
{
}

local_irq_disable()割り込み禁止を有効化する。

  • freedom-u-sdk/linux/arch/riscv/include/asm/irqflags.h
/* unconditionally disable interrupts */
static inline void arch_local_irq_disable(void)
{
    csr_clear(sstatus, SR_SIE);
}

さて、次は必要なセットアップ処理を行う。

  • freedom-u-sdk/linux/init/main.c (start_kernel()の続き)
 /*
    * Interrupts are still disabled. Do necessary setups, then
    * enable them.
    */
    boot_cpu_init();
    page_address_init();
    pr_notice("%s", linux_banner);
    setup_arch(&command_line);

boot_cpu_init(void)はブート用のプロセッサ(通常はCPU0)を有効化する。これは大体最初から有効化されている。

  • freedom-u-sdk/linux/kernel/cpu.c
/*
 * Activate the first processor.
 */
void __init boot_cpu_init(void)
{
    int cpu = smp_processor_id();

    /* Mark the boot cpu "present", "online" etc for SMP and UP case */
    set_cpu_online(cpu, true);
    set_cpu_active(cpu, true);
    set_cpu_present(cpu, true);
    set_cpu_possible(cpu, true);

#ifdef CONFIG_SMP
    __boot_cpu_id = cpu;
#endif
}

ここでsmp_processor_id()によりブート対象とするCPUのIDを入手するのだが、smp_processor_id()RISC-Vアーキテクチャでは以下のように変換されている。

  • freedom-u-sdk/linux/arch/riscv/include/asm/smp.h
/*
 * This is particularly ugly: it appears we can't actually get the definition
 * of task_struct here, but we need access to the CPU this task is running on.
 * Instead of using C we're using asm-offsets.h to get the current processor
 * ID.
 */
#define raw_smp_processor_id() (*((int*)((char*)get_current() + TASK_TI_CPU)))
  • freedom-u-sdk/linux/arch/riscv/include/asm/current.h
/*
 * This only works because "struct thread_info" is at offset 0 from "struct
 * task_struct".  This constraint seems to be necessary on other architectures
 * as well, but __switch_to enforces it.  We can't check TASK_TI here because
 * <asm/asm-offsets.h> includes this, and I can't get the definition of "struct
 * task_struct" here due to some header ordering problems.
 */
static __always_inline struct task_struct *get_current(void)
{
    register struct task_struct *tp __asm__("tp");
    return tp;
}

さらにここで指定したCPUをオンラインに設定する。といってもLinux上のグローバル変数にビットパタンを書き込むだけである。

  • freedom-u-sdk/linux/include/linux/cpumask.h
static inline void
set_cpu_online(unsigned int cpu, bool online)
{
    if (online)
        cpumask_set_cpu(cpu, &__cpu_online_mask);
    else
        cpumask_clear_cpu(cpu, &__cpu_online_mask);
}

次のpage_address_init()では特に何もしない。

  • freedom-u-sdk/linux/include/linux/mm.h
#if defined(WANT_PAGE_VIRTUAL)
static inline void *page_address(const struct page *page)
{
    return page->virtual;
}
static inline void set_page_address(struct page *page, void *address)
{
    page->virtual = address;
}
#define page_address_init()  do { } while(0)
#endif

次のpr_notice("%s", linux_banner)ではLinuxのバージョン番号などの表示を行うだけである。実際にはprintkが呼ばれている。

  • freedom-u-sdk/linux/init/version.c
/* FIXED STRINGS! Don't touch! */
const char linux_banner[] =
    "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
    LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";

次のsetup_arch(char **cmdline_p)アーキテクチャ毎のセットアップを行う関数である。

  • freedom-u-sdk/linux/arch/riscv/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
#if defined(CONFIG_EARLY_PRINTK)
       if (likely(early_console == NULL)) {
               early_console = &riscv_sbi_early_console_dev;
               register_console(early_console);
       }
#endif
    *cmdline_p = boot_command_line;

    parse_early_param();

    init_mm.start_code = (unsigned long) _stext;
    init_mm.end_code   = (unsigned long) _etext;
    init_mm.end_data   = (unsigned long) _edata;
    init_mm.brk        = (unsigned long) _end;

    setup_bootmem();
    paging_init();
    unflatten_device_tree();

    swiotlb_init(1);

#ifdef CONFIG_SMP
    setup_smp();
#endif

#ifdef CONFIG_DUMMY_CONSOLE
    conswitchp = &dummy_con;
#endif

    riscv_fill_hwcap();
}

Verilatorの使い方(1. Verilatorの考え方と基本的なシミュレーション実行方法)

f:id:msyksphinz:20200501120616p:plain

Verilatorについて全く知らない人が、どのように使えば良いのかきちんとした文章が世の中に存在していない気がするので、少しまとめてみることにした。VerilatorはフリーでオープンソースVerilogシミュレーションシステムなので、うまく活用すれば強力な武器になる。高額なEDAツールを使わずとも家でハードウェア開発ができるようになる強力なツールだ。

www.veripool.org

RTLシミュレータと言えば、有償のものも含めれば代表的なものは以下のようなものであろうか:

  • Synopsys VCS : 有償。
  • Cadence Xcelium:有償。
  • Mentor QuestaSim:有償

と、ほとんどのツールが有償であるが、Verilatorは無償かつオープンソースである。かつ性能も高いので、オープンソースプロジェクトなどで使うのにはちょうど良い。

予め説明しておくが、Verilatorは良いことばかりではない。なぜ高性能なのか?どういう制約があるのか?昔私のブログで紹介していた。

  • 高速Verilogシミュレータ"Verilator"で「出来ないこと」

msyksphinz.hatenablog.com

個人的には、以下が猛烈に大きい。

全ての遅延記述 (#) は無視される。

event系のイベント (waitなど) はサポートされない。

Unknownステートはサポートされない

前提条件:VerilatorはVerilogをそのままシミュレーションするわけではない

上記の制約も含め、Verilatorの基本的な考え方を説明する。

Verilatorは他の多くのRTLシミュレーションシステムと同様にコンパイル型である。つまり、

という2段階構成を踏む。この際に気をつけなければならないのが上記の制約で、遅延記述やイベント系の構文は全くサポートされない。つまり、テストベンチ系のコードは全くコンパイルすることができないのだ。ほとんどデジタル回路に直結するVerilog記述しか受け付けることができないと考えて良い。

有償のシミュレータと考え方を比較してみる。有償シミュレータはテストすべき回路記述部分(DUT:Design Under Test)とそれを取り囲む形でテストベンチが構成される。DUTとテストベンチは両方Verilogとして記述されている。

しかしVerilatorの場合は違う。DUTはVerilogとして記述されるが、それ以外のテストベンチは殆どの記述が受け付けられないので、C言語を使ってテストベンチを構成する必要がある。これはVerilatorがDUTをC言語プログラムとして変換するため、これをリンクする形でC言語のテストベンチプログラムを用意するという形になる。

f:id:msyksphinz:20200501120052p:plain

カウンタ回路を作ってVerilatorでシミュレーションする

具体的な例を考えて行こう。以下のようなVerilogで記述した回路を考える。en信号でカウントアップする簡単なカウンタだ。

  • counter_4bit.v
module counter
  (
   input logic        clk,
   input logic        reset_n,
   input logic        en,
   output logic [3:0] cnt
   );
always_ff @(posedge clk, negedge reset_n) begin
  if (!reset_n) begin
    cnt <= 4'h0;
  end else begin
    if (en) begin
      cnt <= cnt + 4'h1;
    end
  end
end
endmodule // counter

Verilogでテストベンチを書くのは簡単だが、ここではVerilatorを使う。このカウンタをシミュレーションするVerilator環境を作ってみよう。

使用するVerilatorのバージョンは以下となる。

$ verilator --version
Verilator 4.032 2020-04-04 rev UNKNOWN_REV

まずはこのDUTをコンパイルしてみよう。

$ verilator -cc counter_4bit.v

obj_dirというディレクトリが作成されている。

$ ls -1 obj_dir
Vcounter_4bit.cpp
Vcounter_4bit.h
Vcounter_4bit.mk
Vcounter_4bit__Syms.cpp
Vcounter_4bit__Syms.h
Vcounter_4bit__ver.d
Vcounter_4bit__verFiles.dat
Vcounter_4bit_classes.mk

これらがcounter_4bit.vから生成されたファイル群だ。C言語のコードに変換されていることが分かる。これをシミュレーションするための環境を作っていく。

C++を使ってcounter_4bitのテスト環境を作っていく

次はcounter_4bitのテストベンチ環境を作っていく。説明したとおり、テストベンチはC/C++で記述する必要があり、これが有償シミュレータとの大きな違いだ。このテストベンチの構成方法について、ざっくりとしたテンプレートから示していこうと思う。

// 必要なファイルのインクルード
#include <iostream>
#include <verilated.h>
#include "Vcounter_4bit.h"

int main(int argc, char **argv) {
    // Verilatorのコマンド解析
    // DUTモジュールのインスタンス化
    // DUTモジュールのインタフェースの初期化
    
    // while文 {
    //     クロックを1サイクルずつ進めていく記述
    //     DUTを評価(回路を実行する)記述
    //     DUTインタフェースを評価する処理
    // }
    
    // 終了
}

この流れを見ると、Verilogで記述しているテストベンチと流れは大して変わらないことが分かる。Verilogで記述しているテストベンチを、C/C++で書き直しているだけである。ただし書き直しているといってもVerilog記述のように自然に並列記述をすることはできないため、並列なテスト動作を記述するためには少し工夫する必要がある。

また、while文の内部についても少し考慮が必要だ。DUT内の論理回路の実行は、具体的にはdut->eval();という記述で実行されるが、これはdutが最小時間単位で動いていることを意味する。つまりVerilogで言うtimescale記述のようなものだと考えて良い。クロックとの関係性で言えば、例えば10回分eval();を進めたところで外部クロック信号をトグルする、というような記述を行う。以下のようなイメージだ。

dut->clk = 0;
while (true) {
    dut->eval();
    if (time_counter % 5 == 0) {        // 5 eval()に1回clkをtoggleする。(つまり周期は10 eval()分)
        dut->clk = !dut->clk;
    } 
    time_counter ++;
}

ではさっそくテストベンチを記述する。以下のようになった。

#include <iostream>
#include <verilated.h>
#include "Vcounter_4bit.h"

int time_counter = 0;

int main(int argc, char** argv) {

  Verilated::commandArgs(argc, argv);

  // Instantiate DUT
  Vcounter_4bit *dut = new Vcounter_4bit();

  // Format
  dut->reset_n = 0;
  dut->clk = 0;
  dut->en = 0;

  // Reset Time
  while (time_counter < 100) {
    dut->eval();
    time_counter++;
  }
  // Release reset
  dut->reset_n = 1;

  int cycle = 0;
  while (time_counter < 500) {
    if ((time_counter % 5) == 0) {
      dut->clk = !dut->clk; // Toggle clock
    }
    if ((time_counter % 10) == 0) {
      // Cycle Count
      cycle ++;
    }

    if (cycle % 5 == 0) {
      dut->en = 1;   // Assert En
    } else {
      dut->en = 0;   // Deassert En
    }

    // Evaluate DUT
    dut->eval();

    time_counter++;
  }

  // std::cout << "Final Counter Value = " << dut->cnt << '\n';
  printf("Final Counter Value = %d\n", dut->cnt);

  dut->final();
}

上記で説明したとおりであり、

  1. dutとしてcounter_4bitモジュールをインスタンス化する。
  2. reset_n / clk / en を初期化する。
  3. 100単位時間ほどリセット状態を保持する。
  4. リセットをリリースし、シミュレーション開始する。
  5. 5単位時間に1回clkをトグルする。
  6. 5サイクルに1回en信号を有効化する。単位時間毎にAssetを変更してしまうと一瞬でDeassertされてしまうので注意。
  7. 毎単位時間にdut->eval()を呼び出しDUTを評価する。
  8. 所定時間シミュレーションを実行すると終了し、最後に結果dut->cntを出力する。

ではこれをシミュレーション環境を準備してみよう。以下のように実行する。

$ verilator --cc counter_4bit.v -exe tb_counter_4bit.cpp
$ cd obj_dir
$ make -C obj_dir -f Vcounter_4bit.mk

obj_dir/Vcounter_4bitというバイナリが生成されていることが分かる。これがシミュレーション本体だ。ではさっそく実行してみよう。

$ ./obj_dir/Vcounter_4bit
Final Counter Value = 8

問題なくカウンタが動作していることが分かった。ここまででVerilatorの基礎は完了だ。以降では、波形のダンプ方法、様々なVerilatorの機能について紹介していく。

RISC-Vのコードモデルについて(2. コードモデルとRelaxationの挙動まとめ)

RISC-Vのコードモデルについていろいろ調べる機会があったのでまとめておく。

参考にしたのは、

www.sifive.com

  • RISC-V Large Code Model Software Workaround

https://sifive.cdn.prismic.io/sifive%2F15d1d90e-f60e-42a2-b6bf-c805c6c73b0d_riscv-large-code-model-workaround.pdf


さて、ここまでGCCのオプションによって生成される命令が異なることが分かったが、どのように使い分ければいいのか。重要なのはこの2つのアドレッシングモードが、アドレスとして指定することのできる範囲が異なるということである。それぞれのコードモデルにおいてアクセス可能な最小のアドレスと最大のアドレスを計算してみる。

  • medlowコードモデル
    • 最小アドレス:LUI x1, 0x80000により0x8000_0000となる。つまり、現在のPCから-2GBまでの距離。
    • 最大アドレス:LUI x1, 0x7ffffによりx1に0x8000_0000を格納しLW -1(x1)にアクセスすることで+2GB-1Bの場所までアクセスすることができる。
  • medanyコードモデル
    • 最小アドレス:AUIPC x1, 0x80000によりPC+0x8000_0000となる。つまり、現在のPCから-2GBまでの距離。
    • 最大アドレス:AUIPC x1, 0x7ffffによりPC+0x7ffff_f000を格納し、LW -1(x1)にアクセスすることで現在のPCから最大で+2GB-1Bの距離までアクセスできる。

このように、コードモデルに応じてアクセスできる距離が異なる。medanyでは、現在のコードからの距離に応じてアクセスできる範囲が異なる、というのがミソだ。

関数呼び出しにおけるリロケーション情報の挿入

ABIに関する仕様書を再び読み解いていく前に、関数呼び出しについてもチェックしておく。以下のようなプログラムをコンパイルして動作をチェックしておこう。

  • func_call_main.c
extern void func_call();

int main()
{
  func_call();
}
  • func_call.c
int global_v;

void func_call()
{
  global_v = global_v + 1;
}
$ riscv64-unknown-elf-gcc -c -o func_call_main.o func_call_main.c
$ riscv64-unknown-elf-gcc -c -o func_call.o func_call.c
$ riscv64-unknown-elf-ld -o func_call.riscv func_call_main.o func_call.o

func_call_main.cでは外部で定義されている関数としてfunc_call()を呼び出すが、func_call_main.cコンパイルする時点でこの関数のアドレスは分かっていない。ではどのようにしてリロケーション情報を挿入しているのか。

$ riscv64-unknown-elf-objdump -D -r func_call_main.o
0000000000000000 <main>:
   0:   1141                    addi    sp,sp,-16
   2:   e406                    sd      ra,8(sp)
   4:   e022                    sd      s0,0(sp)
   6:   0800                    addi    s0,sp,16
   8:   00000097                auipc   ra,0x0
                        8: R_RISCV_CALL func_call
                        8: R_RISCV_RELAX        *ABS*
   c:   000080e7                jalr    ra
  10:   4781                    li      a5,0
  12:   853e                    mv      a0,a5
  14:   60a2                    ld      ra,8(sp)
  16:   6402                    ld      s0,0(sp)
  18:   0141                    addi    sp,sp,16
  1a:   8082                    ret

R_RISCV_CALLR_RISCV_RELAXというリロケーション情報が挿入されることが分かった。これはPC相対ジャンプを行うときに挿入される。ABIのマニュアルを読むと、staticモードの場合はR_RISCV_CALLが挿入され、picモードの場合はR_RISCV_CALL_PLTが挿入されることが分かる。

Enum ELF Reloc Type Description Details
18 R_RISCV_CALL PC-relative call MACRO call,tail (auipc+jalr pair)
19 R_RISCV_CALL_PLT PC-relative call (PLT) MACRO call,tail (auipc+jalr pair) PIC

The pseudoinstruction:

    call symbol

expands to the following assembly and relocation:

    auipc ra, 0           # R_RISCV_CALL (symbol), R_RISCV_RELAX (symbol)
    jalr  ra, ra, 0

and when -fpic is enabled it expands to:

    auipc ra, 0           # R_RISCV_CALL_PLT (symbol), R_RISCV_RELAX (symbol)
    jalr  ra, ra, 0

では同じファイル内に存在している関数に対して、リロケーション情報を挿入する必要があるか?

extern void func_call();

extern void func_call2();
extern int global_v2;

int main()
{
  func_call();
  func_call2();
}


void func_call2()
{
  global_v2 = global_v2 + 1;
}

結果、同様に挿入された。同じファイルの内部であろうがなかろうが、関数ジャンプに対してはリロケーション情報は必ず挿入される。

0000000000000000 <main>:
   0:   1141                    addi    sp,sp,-16
   2:   e406                    sd      ra,8(sp)
   4:   e022                    sd      s0,0(sp)
   6:   0800                    addi    s0,sp,16
   8:   00000097                auipc   ra,0x0
                        8: R_RISCV_CALL func_call
                        8: R_RISCV_RELAX        *ABS*
   c:   000080e7                jalr    ra
  10:   00000097                auipc   ra,0x0
                        10: R_RISCV_CALL        func_call2
                        10: R_RISCV_RELAX       *ABS*
  14:   000080e7                jalr    ra
  18:   4781                    li      a5,0
  1a:   853e                    mv      a0,a5
  1c:   60a2                    ld      ra,8(sp)
  1e:   6402                    ld      s0,0(sp)
  20:   0141                    addi    sp,sp,16
  22:   8082                    ret

グローバル変数のアクセスに関するリロケーション情報の挿入

次はグローバル変数のアクセスについて纏めておく。前回紹介したコードモデルと、ロード命令、ストア命令についてリロケーション情報をまとめておく。

  • medlowコードモデルの場合、絶対アドレスを用いてアドレスが計算される。

    • アドレスの計算:LUI + ADDIを使ってアドレスの計算が行われる。

      • リロケーションシンボル:R_RISCV_HI20 + R_RISCV_LO12_I

        asm 0: 00000537 lui a0,0x0 0: R_RISCV_HI20 global_v2 0: R_RISCV_RELAX *ABS* 4: 00050513 mv a0,a0 4: R_RISCV_LO12_I global_v2 4: R_RISCV_RELAX *ABS*

    • メモリロード:LUI + LW命令を使ってメモリロードが実行される。

      • リロケーションシンボル:R_RISCV_HI20 + R_RISCV_LO12_I (最後の_IはI-Typeの命令を使用することを意味する)

        asm 4: R_RISCV_LO12_I global_v2 4: R_RISCV_RELAX *ABS* 8: 00078513 mv a0,a5 8: R_RISCV_LO12_I global_v2 8: R_RISCV_RELAX *ABS*

    • メモリストア:LUI+SW命令を使ってメモリストアが実行される。

      • リロケーションシンボル:R_RISCV_HI20 + R_RISCV_LO12_S (最後の_SはS-Typeの命令を使用することを意味する)
  • medanyコードモデルの場合、PC相対アドレスを用いてアドレスが計算される。

    • アドレスの計算:AUIPC + ADDIを使ってアドレスの計算が行われる。

      • リロケーションシンボル:R_RISCV_PCREL_HI + R_RISCV_PCREL_LO12_I

      asm 0: 00000517 auipc a0,0x0 0: R_RISCV_PCREL_HI20 global_v2 0: R_RISCV_RELAX *ABS* 4: 00050513 mv a0,a0 4: R_RISCV_PCREL_LO12_I .L0 4: R_RISCV_RELAX *ABS*

    • メモリロード:AUIPC + LWを使ってメモリロードが実行される。

      • リロケーションシンボル:R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_I (最後の_IはI-Typeの命令を使用することを意味する)
    • メモリストア:AUIPC+SW命令を使ってメモリストアが実行される。

      • リロケーションシンボル:R_RISCV_PCREL_HI20 + R_RISCV_PCREL_LO12_S (最後の_SはS-Typeの命令を使用することを意味する)

諸々合わせて表を作っておこう。-fpicを付けたときとそうでないときの場合分けも行った。riscv64-unknown-elf-gccを使ってチェックを行った。

どうもmedanyの時はR_RISCV_PCREL_LO12_Sが上手く効いてくれないなあ。何か制約があるのだろうか。

f:id:msyksphinz:20200427010552p:plain