FPGA開発日記

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

CPUの新たな脆弱性 SPOILERの論文を読む

arxiv.org

GIGAZINEでも紹介された新たなCPUの脆弱性の論文"SPOILER"が発表された。GIGAZINEがこのような記事を公開するのは珍しいなと思いつつ、面白そうなので読んでみることにした。

ちなみに、筆者は例によってセキュリティの専門家ではないし、CPUアーキテクチャにしてもデスクトップクラスの本格的なものは設計経験がないので、いまいち本文から読み取れない部分があったりとか、間違っている部分があるかもしれない。


この攻撃手法も、CPUの高速化を達成するための様々な機構を悪用する手法となっている。

SPOILERが対象とするのは、ストア命令の内容をロード命令でフォワードするためのMOB(Memory Order Buffer)の機構だ。例えば以下のようなコードを書いた場合、

sw a0, 0(sp)
lw a1, 0(sp)

ストア命令は、投機実行をしてしまうとメモリの中身を誤って書き換えてしまうため投機実行はできない。しかし、ロード命令は投機実行をしてもレジスタの中身を復帰すればよいから投機実行できる。そこで、ストア命令の完了を待たずして、次のロード命令を実行するということが可能になる。

しかしこの例で示すように、ストア命令が完了する前にロード命令が実行されてしまい愚直にメモリから値を読んでくると、まだストア処理が完了していないためswが格納するデータを読み込むことができない。 そこで、ストア命令のデータが格納されている「ストアバッファ」の中身を確認して、ロード命令とストア命令が同じアドレスを参照すればそのデータをそのままロード命令にフォワードする「ストアフォワーディング」という技法が使われる。

f:id:msyksphinz:20190310170018p:plain
本論文より引用。Figure 1. MOBの構成

ところがこれは物理アドレスしか用いない場合は容易に実現できるが、仮想アドレスと物理アドレスが混在している場合はそうはいかない。アドレスが同じだと思っていても、物理アドレスが異なる場合、さらに仮想アドレスが異なっていても物理アドレスが同一である場合もある。ここで、LSUでメモリアクセスを発行していない場合にはまだ仮想アドレスで取り扱われており、物理アドレスに変換されていないことが問題となる。この場合LSUの中でどのようにストアフォワーディングを行うのだろうか。

ここで、一般的に以下のような手順を取ると考える。

  1. ルーズネット : アドレスの下位(ページオフセット)のみ比較する。ページオフセットは仮想アドレスと物理アドレスで異なることはないので、ここでアドレスが異なっていればフォワーディングする必要はない。もし一致している場合は、上位アドレスも比較しなければならないので次に進む。
  2. ファインネット : 仮想アドレスの上位ビットと、物理アドレスのタグビットを比較する。これでヒットすると、ストアフォワーディングを行う必要性が高まるが、まだ確定ではない。このステージでアドレスがヒットする場合、ロード命令の実行を止めるか、ストアフォワーディングを行う。そうでない場合は、最後のステージに進む。
  3. 物理アドレスマッチング : すべての物理アドレスのマッチングを行う。このステージで、ストアフォワーディングを行う必要があるかどうかが最終的に判定される。

この場合、物理アドレスが計算されるためにはTLBまで進んでアドレス計算を行う必要があるため、フォワーディングの可否が最終的に判別されるのはロード命令のコミットステージまで先延ばしにされてしまう。つまりここも投機的に実行されているわけだ。最終的な確認のために、変換されたアドレスを物理アドレスバッファ(PAB)に保管している。

この投機的実行を間違えるとどうなるか。ロード命令がストアフォワーディングの投機的実行を間違えたため、当然パイプラインはフラッシュされロード命令を再実行するということになる。これには、結構なサイクル数を消費してしまう。この事象を観測するという訳だ。

Spoiler Attackの実験

Spoiler Attackの実験を行うために、以下のようなアルゴリズムを使ってキャッシュの動きの調査を行っている。

f:id:msyksphinz:20190310170127p:plain
本論文より引用。アルゴリズム1. SPOILER Attackの基本アルゴリズム

あまりきちんと理解しているわけではないが、キャッシュのページ毎にストア命令を発行し、その直後にx番目のページに対するロード命令を実行している。先ほどの、アドレス変換によるアドレス衝突のアルゴリズムに則るならば、

  1. ルーズネット : 常にページの先頭にアクセスしているので、アドレスが一致する。したがってファインネットに移動する。
  2. ファインネット : 物理アドレスのタグビットと仮想アドレスの上位アドレスを比較する。

このときにこのプログラムを実行するのに必要なサイクル数と、発生したイベントをイベントカウンタから取得する。その結果が以下のグラフとなっている。

f:id:msyksphinz:20190310170254p:plain
本論文より引用。図.4. 黒色のグラフでは、ページ番号に応じてレイテンシが大きくなる現象が観測で来ている。HPCイベントでは、`Cycle_Activity:Stalls_Ldm_Pending`(青色のグラフ) と `Ld_Blocks_Partial:Address_Alias`(赤色のグラフ)で反応が見られる。

つまり、ページアクセスに対するエイリアシングが発生しているかどうかは、上記の2つのイベントカウンタを観測すれば計測できるということが分かる。このグラフを見てわかることは、256ページ毎にサイクル数が変動しているということ。256ページというのは256x4kByte=1MBということになる。1MB毎にエイリアシングが生じているということが分かる。イベントにおける:

  • Cycle_Activity:Stalls_Ldm_Pending : ロード命令において、ストールにより命令が実行されなかったサイクル数を記録する。
  • Ld_Blocks_Partial:Address_Alias : ルーズネットが4Kバイトのエイリアスを解決した際に、偽の依存性の数を示す。

上記のグラフをもう一度見直すと、1MBのエイリアシングによりロード命令が遅延すると、当然のごとくCycle_Activity:Stalls_Ldm_Pendingが増加していく。一方で、Ld_Blocks_Partial:Address_Aliasが減少していくのは、xxx

もう少し詳細に見ていくと、1MBのエイリアシングが発生するアドレスにストアが発生した場合は、サイクル数が大きくなりピークに達する(図6)。そこから先の次のページにアクセスする場合は少しずつサイクル数が減少していき、ステップ上にサイクル数は減っていく。これはIntelの特許に記載されているアルゴリズム"Carry chain algorithm"と一致する。ちなみに、このような現象はARMプロセッサやAMDのプロセッサでは発生しない。

f:id:msyksphinz:20190310170324p:plain
本論文より引用。いくつかのページに対して、サイクル数の変化がみられる。
図7では、エイリアシングによりメモリアクセスに必要なサイクル数のヒストグラムを示している。
f:id:msyksphinz:20190310170423p:plain
本論文より引用。ストア命令とロード命令のアドレスによりサイクル数が異なる現象をヒストグラムで表現する。
  • ストア命令とロード命令の間にエイリアシングが発生しない(つまり、オフセットアドレスからずれたストア命令とロード命令の関係) : 30サイクルでロード命令が完了する。
  • ここで面白いのが、同一ページオフセットを持っているが実際のアドレスが異なるストア命令が複数存在し、さらにオフセットと全く異なるロード命令を発行すると、ロード命令には100サイクルが消費される。つまり、直接ロード・ストア命令で4kエイリアシングが発生しなくても、ロード命令のアクセスサイクル数は減少する。
  • 4Kバイトのエイリアシングが発生した場合(つまり同一オフセットでのロード命令とメモリ命令が存在した場合)、ロード命令のアクセスサイクルは120サイクルに増加する。
  • さらに1MBのエイリアシングが発生する場合は、ロード命令は1200サイクル以上消費される。

Javascriptを用いたSPOILER攻撃

Javascriptに移植したSPOILERのコードを実行すると、ネイティブで実行したものと似たような傾向が得られた。つまり、これはブラウザ経由からSPOILERの攻撃も有効であるということを意味する。ネイティブと比較して多少ノイズが混じっているが、明らかな特徴を検出できることはネイティブの実装と同一である。

f:id:msyksphinz:20190310170526p:plain
本論文より引用。JavascriptでSPOILERを実装し実行した結果。ネイティブ版とほぼ同様の結果が得られる。

RowHammerにSPOILERを活用する

RowHammer攻撃というのは、DRAMに対して実行する攻撃手法で、メモリセルの電荷が漏れる事象を活用し、メモリアドレスで指定した場所以外の隣接したアドレスのデータを取得する攻撃手法だ。例えば取得したいアドレスがあるとしてそのデータセルの隣接するアドレスに対して何度もアクセスを繰り返すと、攻撃対象のデータが漏れ出してデータを取得できる可能性がある。

この攻撃を成立させるためには、攻撃対象となるセルの隣接するバンクを取得しなければならないのだが、これにSPOILERを使用する。SPOILERを使用すれば、攻撃対象の物理アドレスの1MB協会までのアドレスを知ることができるため、これにより対象となるバンクを取得し、RowHammerの攻撃をより効率化することができる。

回避策

今のところ有効な回避策はなく、ストア命令がバッファに入っている間にロード命令を投機的に実行することを禁止にすると性能に重大な問題を引き起こしてしまう。ソフトウェア的にはストアとロードの間にフェンスを入れることが有効ではあるが、それをすべてのソフトウェアに適用させることはできない。

ハードウェア的にも性能低下を避けるためには、投機的実行の手法を考え直す必要がある。性能を低下させずにこの問題を解決するためには、何らかの方法を考える必要があるものと思われる。

RustでRISC-V命令セットシミュレータを作ろう (3. 乗除算命令の実装とWrapping処理)

f:id:msyksphinz:20190224185310p:plain

Rustで作るRISC-Vシミュレータ。基本的な形が出来上がったので、次に命令を追加してテストパタンをパスさせていく。 次に追加するのは乗除算命令だ。RV32には以下のテストパタンが用意されているので、それぞれ実装していく。

  • rv32um-x-mulhsu
  • rv32um-x-mulh
  • rv32um-x-rem
  • rv32um-x-mulhu
  • rv32um-x-mul
  • rv32um-x-remu
  • rv32um-x-div
  • rv32um-x-divu

github.com

それぞれで実装していくうえで気を付けなければならないのはWrappingだ。いろんな実装を見ていると、Wrappingした演算を実現するためにはもう少しきれいな記法があることが分かった。

             RiscvInst::ADD => {
                 let rs1_data = self.read_reg(rs1);
                 let rs2_data = self.read_reg(rs2);
-                let reg_data:XlenType = (Wrapping(rs1_data) + Wrapping(rs2_data)).0;
+                let reg_data:XlenType = rs1_data.wrapping_add(rs2_data);
                 self.write_reg(rd, reg_data);
             }

これでテストパタンを書き直す。MUL,DIV,REMについてもWrappingした演算が定義されているのでとても便利だ。

  • DIV
                let reg_data: XlenType = rs1_data.wrapping_mul(rs2_data);
  • MUL
                    reg_data = rs1_data.wrapping_div(rs2_data);
  • REM
                    reg_data = rs1_data.wrapping_rem(rs2_data);

これでテストパタンを実行した。無事にPASSすることが確認できたぞ。

PASS : ./riscv-tests/isa/rv32ui-p-sll.bin
PASS : ./riscv-tests/isa/rv32ui-p-jal.bin
PASS : ./riscv-tests/isa/rv32ui-p-lhu.bin
PASS : ./riscv-tests/isa/rv32ui-p-slti.bin
PASS : ./riscv-tests/isa/rv32ui-p-sra.bin
PASS : ./riscv-tests/isa/rv32ui-p-sh.bin
PASS : ./riscv-tests/isa/rv32ui-p-blt.bin
PASS : ./riscv-tests/isa/rv32ui-p-and.bin
PASS : ./riscv-tests/isa/rv32ui-p-andi.bin
PASS : ./riscv-tests/isa/rv32ui-p-xor.bin
PASS : ./riscv-tests/isa/rv32ui-p-sltu.bin
PASS : ./riscv-tests/isa/rv32ui-p-bge.bin
PASS : ./riscv-tests/isa/rv32ui-p-jalr.bin
PASS : ./riscv-tests/isa/rv32ui-p-bne.bin
PASS : ./riscv-tests/isa/rv32ui-p-srai.bin
PASS : ./riscv-tests/isa/rv32ui-p-lw.bin
PASS : ./riscv-tests/isa/rv32ui-p-bltu.bin
PASS : ./riscv-tests/isa/rv32ui-p-lbu.bin
PASS : ./riscv-tests/isa/rv32ui-p-beq.bin
PASS : ./riscv-tests/isa/rv32ui-p-ori.bin
PASS : ./riscv-tests/isa/rv32ui-p-lui.bin
PASS : ./riscv-tests/isa/rv32ui-p-sltiu.bin
PASS : ./riscv-tests/isa/rv32ui-p-srli.bin
PASS : ./riscv-tests/isa/rv32ui-p-add.bin
PASS : ./riscv-tests/isa/rv32ui-p-slt.bin
PASS : ./riscv-tests/isa/rv32ui-p-lh.bin
PASS : ./riscv-tests/isa/rv32ui-p-fence_i.bin
PASS : ./riscv-tests/isa/rv32ui-p-slli.bin
PASS : ./riscv-tests/isa/rv32ui-p-xori.bin
PASS : ./riscv-tests/isa/rv32ui-p-addi.bin
PASS : ./riscv-tests/isa/rv32ui-p-bgeu.bin
PASS : ./riscv-tests/isa/rv32ui-p-srl.bin
PASS : ./riscv-tests/isa/rv32ui-p-sw.bin
PASS : ./riscv-tests/isa/rv32ui-p-simple.bin
PASS : ./riscv-tests/isa/rv32ui-p-auipc.bin
PASS : ./riscv-tests/isa/rv32ui-p-sb.bin
PASS : ./riscv-tests/isa/rv32ui-p-or.bin
PASS : ./riscv-tests/isa/rv32ui-p-lb.bin
PASS : ./riscv-tests/isa/rv32ui-p-sub.bin
PASS : ./riscv-tests/isa/rv32um-p-mulhsu.bin
PASS : ./riscv-tests/isa/rv32um-p-mulh.bin
PASS : ./riscv-tests/isa/rv32um-p-rem.bin
PASS : ./riscv-tests/isa/rv32um-p-mulhu.bin
PASS : ./riscv-tests/isa/rv32um-p-mul.bin
PASS : ./riscv-tests/isa/rv32um-p-remu.bin
PASS : ./riscv-tests/isa/rv32um-p-div.bin
PASS : ./riscv-tests/isa/rv32um-p-divu.bin

オリジナルLLVM Backendを追加しよう (17. 32-bitコアでの64bit整数演算の命令出力実装)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

32bit環境でのlong longの乗算についての実装を行っている。ISDで言うと所の、SMUL_LOHIとUMUL_LOHIの実装だ。

SMUL_LOHI/UMUL_LOHI - Multiply two integers of type iN, producing a signed/unsigned value of type i[2*N], and return the full value as two results, each of type iN.

これを実装したくて、いろいろ調査していたのだが、ビルドして実行してもどうしても正しく命令を生成できない状況が続いていた。 よく考えると、別にMIPSの方式をまねる必要はなく、RISC-VにはHIレジスタもLOレジスタも存在しないのだから、直接レジスタをコピーしてしまえば良いのではなかろうか。

そこで、以下のように実装を行った。

diff --git a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
index 2a2c7ab1e26..e825e1ce8bc 100644
--- a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.cpp
@@ -162,3 +162,16 @@ loadRegFromStack(MachineBasicBlock &MBB, MachineBasicBlock::iterator I,
   BuildMI(MBB, I, DL, get(Opc), DestReg).addFrameIndex(FI).addImm(Offset)
       .addMemOperand(MMO);
 }
+
+
+void MYRISCVXSEInstrInfo::copyPhysReg(MachineBasicBlock &MBB,
+                                      MachineBasicBlock::iterator MBBI,
+                                      const DebugLoc &DL, unsigned DstReg,
+                                      unsigned SrcReg, bool KillSrc) const {
+  if (MYRISCVX::GPRRegClass.contains(DstReg, SrcReg)) {
+    BuildMI(MBB, MBBI, DL, get(MYRISCVX::ADDI), DstReg)
+        .addReg(SrcReg, getKillRegState(KillSrc))
+        .addImm(0);
+    return;
+  }
+}
diff --git a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.h b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.h
index c8df6dafa72..6c0dd18b6c8 100644
--- a/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.h
+++ b/lib/Target/MYRISCVX/MYRISCVXSEInstrInfo.h
@@ -55,6 +55,10 @@ namespace llvm {
                           const TargetRegisterInfo *TRI,
                           int64_t Offset) const override;

+    void copyPhysReg(MachineBasicBlock &MBB, MachineBasicBlock::iterator MBBI,
+                     const DebugLoc &DL, unsigned DstReg, unsigned SrcReg,
+                     bool KillSrc) const override;
+
    private:
     void expandRetLR(MachineBasicBlock &MBB, MachineBasicBlock::iterator I) const;

要するに単純にcopyPhysRegを使って、必要なレジスタにデータを移すだけだ。通常ならばmuhsで自動的に推論してくれるはずなので、copyPhysRegで自由にレジスタ間を移動できるようにしておくだけである。

これで以下のようなテストプログラムを作成してビルドを行った。

  • ch7_1_longlong_mult.cpp
void test_longlong()
{

  int a1 = 0x30001000;
  int b1 = 0x40001000;

  long long f = (long long)a1 * (long long)b1;

  return;
}
./bin/clang -c -target mips-unknown-linux-gnu ../lbdex/input/ch7_1_longlong_mult.cpp -emit-llvm
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch7_1_longlong_mult.bc -o -

生成されたアセンブリコードは以下のようになった。無事にmulとmulhが生成されている!

        addi    x2, x2, -16
        lui     x10, 196609
        sw      x10, 12(x2)
        lui     x10, 262145
        sw      x10, 8(x2)
        lw      x10, 12(x2)
        lw      x11, 8(x2)
        mul     x12, x10, x11
        mulh    x10, x10, x11
        addi    x11, x2, 0
        ori     x11, x11, 4
        sw      x10, 0(x11)
        sw      x12, 0(x2)
        addi    x2, x2, 16
        ret

f:id:msyksphinz:20190307010859p:plain

オリジナルLLVM Backendを追加しよう (16. 32-bitコアでの64bit整数演算の命令出力調査)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

第7章では、long long(64-bit)の対応も含まれている。解説の中では、MIPSのようなHIレジスタとLOレジスタを使って64bitの演算を実現する方法が解説してある。

あれ、じゃあ32bit版のRISC-Vで、64bitの乗算ってどうやってLLVMに命令を出力すればいいんだ。良く分からなかったので実際にriscv32-unknown-elf-gccにコードを出力させて確認した。 以下のようなコードを書いてコンパイル

#include <stdint.h>

int64_t test_longlong(int64_t a1, int64_t b1)
{
  return (int64_t)a1 * (int64_t)b1;
}
riscv32-unknown-elf-gcc -O3 -c ../lbdex/input/ch7_1_longlong_mult.cpp
riscv32-unknown-elf-objdump -d ch7_1_longlong_mult.o | less

なるほどね。mulhuを使うのか。mulhuは、32bitと32bitの値を乗算し、上位の32bitの結果をレジスタに格納する命令だ。これを使って、部分積を取った場合の繰り上がりの計算を行っている。

Disassembly of section .text:

00000000 <_Z13test_longlongxx>:
   0:   02a686b3                mul     a3,a3,a0
   4:   02c585b3                mul     a1,a1,a2
   8:   02c537b3                mulhu   a5,a0,a2
   c:   00d585b3                add     a1,a1,a3
  10:   02c50533                mul     a0,a0,a2
  14:   00f585b3                add     a1,a1,a5
  18:   00008067                ret

図にするとこんな感じかな。イメージはできたので、LLVMのバックエンドに実装してみよう。

f:id:msyksphinz:20190307010859p:plain

2019/03/08追記。以下のような記述を追加した。とりあえずMULとMULHでしのぐことにする。

--- a/lib/Target/MYRISCVX/MYRISCVXSEISelDAGToDAG.cpp
+++ b/lib/Target/MYRISCVX/MYRISCVXSEISelDAGToDAG.cpp
@@ -70,19 +70,16 @@ void MYRISCVXSEDAGToDAGISel::selectAddESubE(unsigned MOp, SDValue InFlag,
 std::pair<SDNode *, SDNode *>
 MYRISCVXSEDAGToDAGISel::selectMULT(SDNode *N, unsigned Opc, const SDLoc &DL, EVT Ty,
                                    bool HasLo, bool HasHi) {
-  SDNode *Lo = 0, *Hi = 0;
-  SDNode *Mul = CurDAG->getMachineNode(Opc, DL, MVT::Glue, N->getOperand(0),
-                                       N->getOperand(1));
-  SDValue InFlag = SDValue(Mul, 0);
-  if (HasLo) {
-    Lo = CurDAG->getMachineNode(MYRISCVX::MFLO, DL,
-                                Ty, MVT::Glue, InFlag);
-    InFlag = SDValue(Lo, 1);
+  SDNode *MulHi = 0;
+  SDNode *MulLo = CurDAG->getMachineNode(Opc, DL, MVT::Glue,
+                                         N->getOperand(0),
+                                         N->getOperand(1));
+  if (HasHi) {
+    MulHi = CurDAG->getMachineNode(MYRISCVX::MULH, DL, MVT::Glue,
+                                   N->getOperand(0),
+                                   N->getOperand(1));
   }
-  if (HasHi)
-    Hi = CurDAG->getMachineNode(MYRISCVX::MFHI, DL,
-                                Ty, InFlag);
-  return std::make_pair(Lo, Hi);
+  return std::make_pair(MulHi, MulLo);
 }
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch7_1_longlong_mult.bc -o -

llc: /home/msyksphinz/others/riscv/llvm/llvm-myriscvx/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp:9025: void llvm::SelectionDAGISel::LowerArguments(const llvm::Function &): Assertion `InVals.size() == Ins.size() && "LowerFormalArguments didn't emit the correct number of values!"' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch7_1_longlong_mult.bc -o -
1.      Running pass 'Function Pass Manager' on module 'ch7_1_longlong_mult.bc'.
2.      Running pass 'MYRISCVX DAG->DAG Pattern Instruction Selection' on function '@_Z13test_longlongxx'
#0 0x00000000009be531 __interceptor_backtrace /home/msyksphinz/software/llvm/llvm/projects/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:4025:0
#1 0x0000000002a51293 llvm::sys::PrintStackTrace(llvm::raw_ostream&) /home/msyksphinz/others/riscv/llvm/llvm-myriscvx/lib/Support/Unix/Signals.inc:490:13

あれ?落ちてしまった。おかしいなあ。

BOOM(Berkeley Out-Of Order) コアがfpga-zynqに対応したのでFPGAで動かしてみる(2. FPGAでの動作確認)

BOOMメーリングリストで正式リリースのあった、BOOMコアのFPGA対応、ビルドが完了したのでFPGA(ZedBoard)で動かしてみる。

github.com

Windows Subsystem on Linux上で作業しているのだが、SDカードへの書き込みなどの作業は問題なく行うことができる。 DドライブにSDカードがマウントされたので、以下のQiitaの記事を参考にSDカードにデータを書き込む。

qiita.com

sudo mkdir /mnt/d
sudo mount -t drvfs D: /mnt/d

SDカードに書き込む前に、Linuxのイメージもダウンロードしておく。 README.mdを参考に、Linuxのイメージをダウンロードする。

make fetch-riscv-linux-deliver

イメージをSDカードに書き込む。

cp -r fpga-images-zedboard/* /mnt/d
cp -r deliver_output/riscv /mnt/d

最後にSDカードをunmountする。

sudo umount /mnt/d

ZedBoardを起動し、SDカードからブートする。

ARM Linuxの起動は昔に書いた以下の記事を参考にする。

RISC-V FPGA-Zynq リポジトリのU-Bootの謎 - FPGA開発日記

fatload mmc 0 0x3000000 uImage
fatload mmc 0 0x2A00000 devicetree.dtb
fatload mmc 0 0x1000000 uramdisk.image.gz
bootm 0x3000000 0x1000000 0x2A00000

Linuxがブートしたら、まずはfesvr-zynq pk helloで様子見。




次に、BOOM上でLinuxを起動させる。起動の方法は以下の通り。

root@zynq:~# mkdir /sdcard
root@zynq:~# mount /dev/mmcblk0p1 /sdcard
root@zynq:~# ./fesvr-zynq +disk=/sdcard/riscv/root.bin bbl /sdcard/riscv/vmlinux

なんだこりゃ?起動が遅いなー。こんなもんなのか?

f:id:msyksphinz:20190306005223g:plain
ZedBoardでのBOOMの起動

/proc/cpuinfo/を見るとRV64Gと書いてあるので、とりあえずはBOOMの気がする。これでよいのだろうか?

f:id:msyksphinz:20190306003013p:plain

BOOM(Berkeley Out-Of Order) コアがfpga-zynqに対応したのでFPGAで動かしてみる(1. プロジェクトのビルド)

BOOMメーリングリストで正式リリースのあった、BOOMコアのfpga-zynqリポジトリ対応、面白そうだ。

BOOMというのはRISC-Vのオープンソースアウトオブオーダプロセッサで、GitHubから入手でき、だれでも手を加えることができるプロセッサだ(ただしChiselで記述してあるので、変更は容易ではないが...)。

github.com

加えて、fpga-zynqリポジトリというのは、かつてUCBのプロジェクトチームがメインで使用していたRISC-VデザインのFPGA検証用のリポジトリで、ZedBeard, ZC707, ZYBOに対応したFPGAデザインを作成することのできるリポジトリだ。 残念ながら現在はほとんどメンテナンスが行われておらず、ほとんどがAWS F1インスタンス上で動作するFireSimに移行している。しかしZedBoardを持っている私としては、かなり貴重なFPGA検証環境として使わせてもらっている。

github.com

これまではfpga-zynqリポジトリは Rocket-Chipデザインにしか対応していなかったのだが、晴れてBOOMにも対応したらしい。さっそくデザインをダウンロードして、合成してFPGAで動かしてみたい。

ちなみに、現在はForkした別のリポジトリで管理されている。

github.com

まずはリポジトリをダウンロードして、環境設定を行っていく。

git clone https://github.com/riscv-boom/fpga-zynq.git fpga-zynq-boom
cd fpga-zynq-boom
cd zedboard
make init-submodules

make init-submodulesというのは、fpga-zynqリポジトリに必要なサブモジュールをダウンロードするものだ。

次に、少しややこしいのだがFIRRTLのバージョン互換性を保つために、Rocket-ChipのFIRRTLの処理を行う。これは何をしているのかわからないが、とりあえず必要らしい。

cd ../boom-template/rocket-chip/firrtl
sbt publishLocal

さらにもう一つ変更が必要だった。どうも以下のBuildCoreモジュールは存在しないらしい。 不要なので削ってみると、無事にVerilogを生成することができた。 これは冗長かもしれないから削るようにPRを出してみても良いかもしれない。

--- a/common/src/main/scala/Configs.scala
+++ b/common/src/main/scala/Configs.scala
@@ -5,7 +5,7 @@ import freechips.rocketchip.config.{Parameters, Config}
 import freechips.rocketchip.subsystem._
 import freechips.rocketchip.devices.tilelink.BootROMParams
 import freechips.rocketchip.rocket.{RocketCoreParams, MulDivParams, DCacheParams, ICacheParams}
-import freechips.rocketchip.tile.{RocketTileParams, BuildCore, XLen}
+import freechips.rocketchip.tile.{RocketTileParams, XLen}
 import testchipip._

 class WithBootROM extends Config((site, here, up) => {

次に、私はVivadoの最新版である2018.3しかインストールしていないので、スクリプトのバージョン番号を差し替える。

diff --git a/zedboard/src/tcl/zedboard_bd.tcl b/zedboard/src/tcl/zedboard_bd.tcl
index 7a29d77..2c222f7 100644
--- a/zedboard/src/tcl/zedboard_bd.tcl
+++ b/zedboard/src/tcl/zedboard_bd.tcl
@@ -20,7 +20,7 @@ set script_folder [_tcl::get_script_folder]
 ################################################################
 # Check if script is running in correct Vivado version.
 ################################################################
-set scripts_vivado_version 2018.2
+set scripts_vivado_version 2018.3
 set current_vivado_version [version -short]

 if { [string first $scripts_vivado_version $current_vivado_version] == -1 } {

これで準備は完了となる。ZedBoardのディレクトリに移動し、makeを実行するとすべて自動実行でVerilogの生成とFPGAの合成が始まる。

ビルドが完了するとデザインが生成されるので、VivadoのGUIを立ち上げて内容を確認してみる。

make vivado
f:id:msyksphinz:20190305003153p:plain
BOOM on fpga-zynqのImplementation結果。

合成結果も少しGUI上に表示しているが、タイミングとしては50MHzを超えている模様。あれ?昔はそんなことなかったんだけどな。 FPUがネックになって周波数が伸びないと思っていたのだけれども、改善されたのかな。 もう少し確認してみた方がよい気がする。

オリジナルLLVM Backendを追加しよう (15. int型以外の変数への対応)

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

第7章は、int型以外の各種変数型に対応させる。

github.com

diff --git a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
index 41f009b0d11..b7da82ae9f7 100644
--- a/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
+++ b/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
@@ -127,6 +133,12 @@ def store_a : AlignedStore<store>;
 // Instructions specific format
 //===----------------------------------------------------------------------===//

+class EffectiveAddress<string instr_asm, RegisterClass RC, Operand Mem> :
+  FI<0b0000011, 0b010, (outs RC:$ra), (ins Mem:$addr),
+     instr_asm, [(set RC:$ra, addr:$addr)], IIAlu> {
+}
+
+

EffectiveAddressクラスを定義している。これは良く分からないがaddi命令に対応させるようにしておけばよいのかな?

以下のプログラムでテストする。これはプログラムとしては意味のないもの、というか動かないだろうけど、とりあえずポインタの動きを見るためには良さそう。

/// start
int test_local_pointer()
{
  int b = 3;

  int* p = &b;

  return *p;
}
./bin/clang -c -target mips-unknown-linux-gnu ../lbdex/input/ch7_1_localpointer.cpp -emit-llvm
./bin/llc -march=myriscvx32 -filetype=asm ch7_1_localpointer.bc -o -
        addi    x10, x0, 3
        sw      x10, 4(x2)
        addi    x10, x2, 4
        sw      x10, 0(x2)
        lw      x10, 0(x2)
        lw      x10, 0(x10)

3という実体を作ってから、メモリに格納してから、アドレスを生成してまた実体をメモリからロードする。 一応動作としては合っているようだ。

次に、以下のコードをコンパイルしてアセンブリを生成できるようにする。charとshortが混在している環境だ。

/// start
struct Date
{
  short year;
  char month;
  char day;
  char hour;
  char minute;
  char second;
};

unsigned int b[4] = {'a', 'b', 'c', '\0'};

int test_char()
{
  unsigned char a = b[1];
  char c = (char)b[1];
  Date date1 = {2012, (char)11, (char)25, (char)9, (char)40, (char)15};
  char m = date1.month;
  char s = date1.second;

  return 0;
}

Tutorialの通りに、単純に以下のコードを追加すれば動作するのかと思った。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
defm LB  : LoadM32<0b0000011, 0b000, "lb" , sextloadi8_a >;
defm LH  : LoadM32<0b0000011, 0b001, "lh" , sextloadi16_a>;
defm LW  : LoadM32<0b0000011, 0b010, "lw" , load_a       >;
defm LBU : LoadM32<0b0000011, 0b100, "lbu", zextloadi8_a >;
defm LHU : LoadM32<0b0000011, 0b101, "lhu", zextloadi16_a>;

defm SB  : StoreM32<0b0100011, 0b000, "sb", truncstorei8_a >;
defm SH  : StoreM32<0b0100011, 0b001, "sh", truncstorei16_a>;
defm SW  : StoreM32<0b0100011, 0b010, "sw", store_a        >;

LLVMをビルドしてアセンブリをはいてみると、どうもうまくいかずにエラーとなってしまった。

LLVM ERROR: Cannot select: t45: i32,ch = load<(dereferenceable load 1 from %ir.second, align 2), anyext from i8> t44, t36, undef:i32
  t36: i32 = or FrameIndex:i32<2>, Constant:i32<6>
    t12: i32 = FrameIndex<2>
    t28: i32 = Constant<6>
  t5: i32 = undef

これらのエラーは、MIPSと比較したり、charとshortを全部intに変えてみたりして確認してみるとわかりやすい気がする。 原因を絞り込んだ結果、charとshortのメモリアクセス命令を定義しただけではだめ、ちゃんとIRから推論されるように追加する必要があった。

  • lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
@@ -413,3 +414,9 @@ class WrapperPat<SDNode node, Instruction ORiOp, RegisterClass RC>:
           (ORiOp RC:$gp, node:$in)>;

 def : WrapperPat<tglobaladdr, ORI, GPR>;
+
+def : Pat<(i32 (extloadi1  addr:$src)), (LBU addr:$src)>;
+def : Pat<(i32 (extloadi8  addr:$src)), (LBU addr:$src)>;
+def : Pat<(i32 (extloadi16 addr:$src)), (LHU addr:$src)>;
+// peepholes
+def : Pat<(store (i32 0), addr:$dst), (SW ZERO, addr:$dst)>;

これにより、上記のC言語のコードは正しく出力されるようになった。

./bin/clang -c -target mips-unknown-linux-gnu ../lbdex/input/ch7_1_char_in_struct.cpp -emit-llvm
./bin/llc -march=mips -relocation-model=pic -filetype=asm ch7_1_char_in_struct.bc -o -
# %bb.0:                                # %entry
        addi    x2, x2, -24
        lui     x10, %got_hi(b)
        add     x10, x10, x3
        lw      x10, %got_lo(b)(x10)
        addi    x10, x10, 4
        lw      x11, 0(x10)
        sb      x11, 20(x2)
        lw      x10, 0(x10)
        sb      x10, 16(x2)
        lw      x10, %got($_ZZ9test_charvE5date1)(x3)
        ori     x10, x10, %lo($_ZZ9test_charvE5date1)
        addi    x11, x10, 4
        lhu     x11, 0(x11)
        addi    x12, x10, 6
        lhu     x12, 0(x12)
        slli    x12, x12, 16
        or      x11, x12, x11
        addi    x12, x2, 8
        ori     x13, x12, 4
        sw      x11, 0(x13)
        lhu     x11, 0(x10)
        addi    x10, x10, 2
        lhu     x10, 0(x10)
        slli    x10, x10, 16
        or      x10, x10, x11
        sw      x10, 8(x2)
        ori     x10, x12, 2
        lbu     x10, 0(x10)
        sb      x10, 4(x2)
        ori     x10, x12, 6
        lbu     x10, 0(x10)
        sb      x10, 0(x2)
        addi    x10, x0, 0
        addi    x2, x2, 24
        ret