FPGA開発日記

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

Sipeed MAIX : RISC-V 64 AI Moduleが届いた

随分前にIndieGogoで募集していた、RISC-V ASICチップ+AIモジュールキット、Sipeed MAIX が届いた。 モードと外部ディスプレイ、カメラなどの基板がセットだ。

www.indiegogo.com

とりあえずまだ時間がないのでまだ触れない... あと、ドキュメントが余りないかな。

以下のあたりだろうか?まずは組み立て方とチュートリアルを探さなければ... まずはHello World的な感じで動かしたいんですけど...

kendryte.com

f:id:msyksphinz:20190204235439p:plain
Sipeed MAIXボード

Vivado-HLSを使って高位合成でCPUを作ってみる(1. メモリのRead/Writeのモデルを作成)

Vivado-HLSの勉強をしてみたいのだけれども、何か題材をもってデザインを作ってみる方が良いと思う。

そこで、簡単なモデルでも良いのでCPUを作ってみることにした。 最終的にはRISC-Vを実行させてみたいのだけれども、まずは簡単なモデルから動かすということで、実験してみる。 Vivado-HLSはC言語だから、テストベンチなども作りやすいはずだ。

f:id:msyksphinz:20181224132702p:plain

こういった、Vivado-HLSを使ったデザイン開発には多くのメリットがあると思う。 CPUなどの性能をも取られる分野でも、

  • 性能をあまり求められないマイクロコントローラの分野でアジャイル開発の1手法として使える。
  • 機能拡張の行いやすさでメリットがある。
  • ベースとなる記述がC言語なので、いろんな場所へ応用が利きやすい。
    • 最終的に、C言語で記述された命令セットシミュレータからそのもまVerilogが生成される、というのが理想だと思う。

という訳で、簡単なモデルを作ってみた。

github.com

CPUなので、命令バスとデータバスを用意している。

void cpu_hls (const uint32_t inst_mem[1024], uint32_t data_mem[1024])

内部にCPUモデルを持っており、レジスタなどの状態を含んでいる。

...
class rv32 {
  uint32_t reg32[32];

public:
  inst_rv32_t decode_inst (uint32_t inst);

さらに、いくつかの補助関数をクラスのメンバ関数として挿入した。

  inst_rv32_t decode_inst (uint32_t inst);

  uint8_t get_rs1_addr (uint32_t inst) {
    return ((inst >> 15) & 0x1f);
  }
  uint8_t get_rs2_addr (uint32_t inst) {
    return ((inst >> 20) & 0x1f);
  }
  uint8_t get_rd_addr (uint32_t inst) {
    return ((inst >> 7) & 0x1f);
  }

  uint32_t read_reg(uint8_t addr) {
    return reg32[addr];
  }

  void write_reg(uint8_t addr, uint32_t data) {
    reg32[addr] = data;
  }

デコーダは命令を解析して、必要なOpcodeを生成するだけだ。とりあえず、ADD, LOAD, STOREだけ。

inst_rv32_t rv32::decode_inst (uint32_t inst)
{
  switch(inst & 0x3f) {
    case 0x33 : return ADD;
    case 0x03 : return LW;
    case 0x23 : return SW;
    case 0xff : return NOP;   //
    default   : return NOP;
  }
}

デコード結果に基づいて命令を実行するループを定義する。

  do {
    inst = inst_mem[addr];
    dec_inst = u_rv32.decode_inst(inst);

    uint32_t reg_data;

    uint8_t rs1 = u_rv32.get_rs1_addr (inst);
    uint8_t rs2 = u_rv32.get_rs2_addr (inst);
    uint8_t rd  = u_rv32.get_rd_addr  (inst);

    switch (dec_inst) {
      case LW  : {
        uint32_t addr = rs1 + ((inst >> 20) & 0xfff);
        reg_data = mem_access(LOAD, rs1, addr, data_mem);
        u_rv32.write_reg(rd, reg_data);
        break;
      }
      case ADD : {
        reg_data = u_rv32.read_reg(rs1) + u_rv32.read_reg(rs2);
        u_rv32.write_reg(rd, reg_data);
        break;
      }
...

まずはテストをしなければならないのだが、これだけではテストパタンも流せないので、とりあえずはVivado-HLSによるVerilogの生成を行ってみる。

directives.tclとして以下を定義した。つまり、命令バスとデータバスは1つのバスとして扱うことにする。

set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls"
set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls" inst_mem
set_directive_interface -mode s_axilite -bundle slv0 "cpu_hls" data_mem

これでVivado-HLSでVerilogファイルを生成してみる。

  • cpu_hls/normal/syn/verilog/cpu_hls.v
module cpu_hls (
        ap_clk,
        ap_rst_n,
        s_axi_slv0_AWVALID,
        s_axi_slv0_AWREADY,
        s_axi_slv0_AWADDR,
        s_axi_slv0_WVALID,
        s_axi_slv0_WREADY,
        s_axi_slv0_WDATA,
        s_axi_slv0_WSTRB,
        s_axi_slv0_ARVALID,
        s_axi_slv0_ARREADY,
        s_axi_slv0_ARADDR,
        s_axi_slv0_RVALID,
        s_axi_slv0_RREADY,
        s_axi_slv0_RDATA,
        s_axi_slv0_RRESP,
        s_axi_slv0_BVALID,
        s_axi_slv0_BREADY,
        s_axi_slv0_BRESP,
        interrupt
);
...
cpu_hls_slv0_s_axi #(
    .C_S_AXI_ADDR_WIDTH( C_S_AXI_SLV0_ADDR_WIDTH ),
    .C_S_AXI_DATA_WIDTH( C_S_AXI_SLV0_DATA_WIDTH ))
cpu_hls_slv0_s_axi_U(
    .AWVALID(s_axi_slv0_AWVALID),
    .AWREADY(s_axi_slv0_AWREADY),
    .AWADDR(s_axi_slv0_AWADDR),
    .WVALID(s_axi_slv0_WVALID),
    .WREADY(s_axi_slv0_WREADY),
...

ちゃんとバスが1つにまとめられている。

Western DigitalのRISC-VコアSweRV-EH1 (2. テストベンチを動かす)

Western DigitalからオリジナルのRISC-VコアSweRVがリリースされ、テストベンチが存在しないのでしばらく放置していたのだが、リビジョンが上がってテストベンチが公開された。

https://github.com/westerndigitalcorporation/swerv_eh1

しかし、ソースコードは公開されておらず、hexファイルしか存在してない!これではどのようにして動いているのか解析できない。

./testbench/hex/data.hex
./testbench/hex/program.hex

テストベンチの実行方法は以下だ。単純にhexをロードして動作させている。実行化完了すると"HELLO WORLD"と表示される。

make -f $RV_ROOT/tools/Makefile verilator-run
...
cp /home/msyksphinz/work/riscv/swerv_eh1/testbench/hex/*.hex .
./obj_dir/Vtb_top
Start of sim
HELLO WORLD
End of sim

手動で逆アセンブリしても良いのだが、唯一の解析手段としてトレースデータ(trace_port.csv)が出力されている。これを見てみることにする。

少し読みにくいので、文字列を揃えて読みやすくする。

diff --git a/testbench/tb_top.sv b/testbench/tb_top.sv
index a6dff4d..e5da3d1 100644
--- a/testbench/tb_top.sv
+++ b/testbench/tb_top.sv
@@ -131,8 +131,28 @@ module tb_top ( input logic core_clk, input logic reset_l);
        $write("%c", i_ahb_lsu.WriteData[7:0]);
      end

-   always @(posedge core_clk)
-     $fwrite(tp,"%b,%h,%h,%0h,%0h,3,%b,%h,%h,%b\n", rvtop.trace_rv_i_valid_ip, rvtop.trace_rv_i_address_ip[63:32], rvtop.trace_rv_i_address_ip[31:0], rvtop.trace_rv_i_insn_ip[63:32], rvtop.trace_rv_i_insn_ip[31:0],rvtop.trace_rv_i_exception_ip,rvtop.trace_rv_i_ecause_ip,rvtop.trace_rv_i_tval_ip,rvtop.trace_rv_i_interrupt_ip);
+   always @(posedge core_clk) begin
+     $fwrite(tp,"%03b,0x%08x_%08x,0x%08x_%08x,3,%03b,%01x,%08x,%03b  ",
+             rvtop.trace_rv_i_valid_ip,
+             rvtop.trace_rv_i_address_ip[63:32],
+             rvtop.trace_rv_i_address_ip[31: 0],
+             rvtop.trace_rv_i_insn_ip[63:32],
+             rvtop.trace_rv_i_insn_ip[31: 0],
+             rvtop.trace_rv_i_exception_ip,
+             rvtop.trace_rv_i_ecause_ip,
+             rvtop.trace_rv_i_tval_ip,
+             rvtop.trace_rv_i_interrupt_ip);
+   end // always @ (posedge core_clk)

    initial begin

ついでに、DASMも追加して逆アセンブルを表示できるようにする。

+     if (|rvtop.trace_rv_i_valid_ip[1:0]) begin
+       $fwrite (tp, "// ");
+       if (rvtop.trace_rv_i_valid_ip[1]) begin
+         $fwrite(tp, " | DASM(%08x)", rvtop.trace_rv_i_insn_ip[63:32]);
+       end
+       if (rvtop.trace_rv_i_valid_ip[0]) begin
+         $fwrite(tp, " | DASM(%08x)", rvtop.trace_rv_i_insn_ip[31: 0]);
+       end
+     end // else: !if(|rvtop.trace_rv_i_valid_ip[1:0])
+     $fwrite (tp, "\n");
+   end // always @ (posedge core_clk)

trace_port.csvを見てみる。さらに、先頭の3ビットが"000"のものはバブルなので除去すると以下のようになる。

$ grep -v ^000 trace_port.csv
001,0x00000000_00000000,0x00000000_b0219173,3,000,00,00000000,000  //  | DASM(b0219173)
001,0x00000000_00000004,0x00000000_ee0002b7,3,000,00,00000000,000  //  | DASM(ee0002b7)
001,0x00000000_00000008,0x00000000_0002e293,3,000,00,00000000,000  //  | DASM(0002e293)
001,0x00000000_0000000c,0x00000000_30529173,3,000,00,00000000,000  //  | DASM(30529173)
001,0x00000000_00000010,0x00000000_5d555337,3,000,00,00000000,000  //  | DASM(5d555337)
001,0x00000000_00000014,0x00000000_55536313,3,000,00,00000000,000  //  | DASM(55536313)
001,0x00000000_00000018,0x00000000_7c0310f3,3,000,00,00000000,000  //  | DASM(7c0310f3)
001,0x00000000_0000001c,0x00000000_000002b7,3,000,00,00000000,000  //  | DASM(000002b7)
001,0x00000000_00000020,0x00000000_0002e293,3,000,00,00000000,000  //  | DASM(0002e293)
001,0x00000000_00000024,0x00000000_7f829173,3,000,00,00000000,000  //  | DASM(7f829173)
001,0x00000000_00000028,0x00000000_000002b7,3,000,00,00000000,000  //  | DASM(000002b7)
001,0x00000000_0000002c,0x00000000_0002e293,3,000,00,00000000,000  //  | DASM(0002e293)
001,0x00000000_00000030,0x00000000_7f929173,3,000,00,00000000,000  //  | DASM(7f929173)
011,0x00000038_00000034,0xf0040537_00000013,3,000,00,00000000,000  //  | DASM(f0040537) | DASM(00000013)
011,0x00000040_0000003c,0x00100013_00056513,3,000,00,00000000,000  //  | DASM(00100013) | DASM(00056513)
011,0x00000048_00000044,0x04806493_000004b7,3,000,00,00000000,000  //  | DASM(04806493) | DASM(000004b7)
001,0x00000048_0000004c,0x04806493_00108093,3,000,00,00000000,000  //  | DASM(00108093)
011,0x00000054_00000050,0xd05805b7_00108093,3,000,00,00000000,000  //  | DASM(d05805b7) | DASM(00108093)
011,0x0000005c_00000058,0x000004b7_0005e593,3,000,00,00000000,000  //  | DASM(000004b7) | DASM(0005e593)
011,0x00000064_00000060,0x0095a023_0484e493,3,000,00,00000000,000  //  | DASM(0095a023) | DASM(0484e493)
001,0x00000064_00000068,0x0095a023_000004b7,3,000,00,00000000,000  //  | DASM(000004b7)
011,0x00000070_0000006c,0x0095a023_0454e493,3,000,00,00000000,000  //  | DASM(0095a023) | DASM(0454e493)
011,0x00000078_00000074,0x00748493_00000013,3,000,00,00000000,000  //  | DASM(00748493) | DASM(00000013)
001,0x00000078_0000007c,0x00748493_00000013,3,000,00,00000000,000  //  | DASM(00000013)
011,0x00000084_00000080,0x00000013_0095a023,3,000,00,00000000,000  //  | DASM(00000013) | DASM(0095a023)
011,0x0000008c_00000088,0x00000013_0095a023,3,000,00,00000000,000  //  | DASM(00000013) | DASM(0095a023)
011,0x00000094_00000090,0x00348493_00000013,3,000,00,00000000,000  //  | DASM(00348493) | DASM(00000013)

さらに逆アセンブリを追加する。

grep -v ^000 trace_port.csv | spike-dasm
001,0x00000000_00000000,0x00000000_b0219173,3,000,00,00000000,000  //  | csrrw   sp, minstret, gp
001,0x00000000_00000004,0x00000000_ee0002b7,3,000,00,00000000,000  //  | lui     t0, 0xee000
001,0x00000000_00000008,0x00000000_0002e293,3,000,00,00000000,000  //  | ori     t0, t0, 0
001,0x00000000_0000000c,0x00000000_30529173,3,000,00,00000000,000  //  | csrrw   sp, mtvec, t0
001,0x00000000_00000010,0x00000000_5d555337,3,000,00,00000000,000  //  | lui     t1, 0x5d555
001,0x00000000_00000014,0x00000000_55536313,3,000,00,00000000,000  //  | ori     t1, t1, 1365
001,0x00000000_00000018,0x00000000_7c0310f3,3,000,00,00000000,000  //  | csrrw   ra, unknown_7c0, t1
001,0x00000000_0000001c,0x00000000_000002b7,3,000,00,00000000,000  //  | lui     t0, 0x0
001,0x00000000_00000020,0x00000000_0002e293,3,000,00,00000000,000  //  | ori     t0, t0, 0
001,0x00000000_00000024,0x00000000_7f829173,3,000,00,00000000,000  //  | csrrw   sp, unknown_7f8, t0
001,0x00000000_00000028,0x00000000_000002b7,3,000,00,00000000,000  //  | lui     t0, 0x0
001,0x00000000_0000002c,0x00000000_0002e293,3,000,00,00000000,000  //  | ori     t0, t0, 0
001,0x00000000_00000030,0x00000000_7f929173,3,000,00,00000000,000  //  | csrrw   sp, unknown_7f9, t0
011,0x00000038_00000034,0xf0040537_00000013,3,000,00,00000000,000  //  | lui     a0, 0xf0040 | nop
011,0x00000040_0000003c,0x00100013_00056513,3,000,00,00000000,000  //  | li      zero, 1 | ori     a0, a0, 0
011,0x00000048_00000044,0x04806493_000004b7,3,000,00,00000000,000  //  | ori     s1, zero, 72 | lui     s1, 0x0
001,0x00000048_0000004c,0x04806493_00108093,3,000,00,00000000,000  //  | addi    ra, ra, 1
011,0x00000054_00000050,0xd05805b7_00108093,3,000,00,00000000,000  //  | lui     a1, 0xd0580 | addi    ra, ra, 1
011,0x0000005c_00000058,0x000004b7_0005e593,3,000,00,00000000,000  //  | lui     s1, 0x0 | ori     a1, a1, 0
011,0x00000064_00000060,0x0095a023_0484e493,3,000,00,00000000,000  //  | sw      s1, 0(a1) | ori     s1, s1, 72
001,0x00000064_00000068,0x0095a023_000004b7,3,000,00,00000000,000  //  | lui     s1, 0x0
011,0x00000070_0000006c,0x0095a023_0454e493,3,000,00,00000000,000  //  | sw      s1, 0(a1) | ori     s1, s1, 69
011,0x00000078_00000074,0x00748493_00000013,3,000,00,00000000,000  //  | addi    s1, s1, 7 | nop
001,0x00000078_0000007c,0x00748493_00000013,3,000,00,00000000,000  //  | nop
011,0x00000084_00000080,0x00000013_0095a023,3,000,00,00000000,000  //  | nop | sw      s1, 0(a1)
011,0x0000008c_00000088,0x00000013_0095a023,3,000,00,00000000,000  //  | nop | sw      s1, 0(a1)
  • アドレス 0xd058_0000 に文字データを書き込む。
  • H(79) → E(69) → L(69+7) → L(76) → O(79) → " "(32) → W(32+55) → O(79) → R(83) → L(76) → D(68)

という、非常にストレートで無理やり文字列を出力するプログラムだった。少し笑える。

f:id:msyksphinz:20190202222548p:plain
SweRVの実行解析結果

Vivado HLSをWindows Subsystem on Linuxで動作させるための手順

Vivado HLSについて再入門しようと思っている。 最近は環境をVirtual BoxからWindows Subsystem on Linux(WSL)に変更したので、その環境下でVivado HLSが動くようにしておきたい。

私が使うのは、基本的にGUIは立ち上げずにスクリプトだけで処理を行いたい。 そのため、これまでに作ったスクリプトがちゃんとWSL上のVivado HLSで動作するのかを確認しておいた。

使用したのは、昔作ったedge_filterのデザインだ。

github.com

実行してみると、どうもincludeファイルが読み込めずに失敗してしまう。

  • fpga_designs/vivado_hls/edge_filter/test_edge_filter.cpp
#include <stdio.h>
#include <stdint.h>
#include "./edge_filter.h"

コンパイルすると以下のようにエラーが発生する。

make
INFO: [HLS 200-10] Setting target device to 'xc7z020clg484-1'
INFO: [SIM 211-2] *************** CSIM start ***************
INFO: [SIM 211-4] CSIM will launch GCC as the compiler.
make[1]: Entering directory '/home/msyksphinz/work/fpga_designs/vivado_hls/edge_filter/edge_filter/normal/csim/build'
   Compiling ../../../../test_edge_filter.cpp in debug mode
csim.mk:77: recipe for target 'obj/test_edge_filter.o' failed
make[1]: Leaving directory '/home/msyksphinz/work/fpga_designs/vivado_hls/edge_filter/edge_filter/normal/csim/build'
In file included from ../../../../test_edge_filter.cpp:1:0:
/usr/include/stdio.h:27:36: fatal error: bits/libc-header-start.h: No such file or directory
 #include <bits/libc-header-start.h>
                                    ^
compilation terminated.
make[1]: *** [obj/test_edge_filter.o] Error 1

SDAccelなどのQ&Aを調査していると、どうもいろいろパッケージをインストールしなければならない様だ。

sudo apt-get install gcc-multilib g++-multilib

上記のパッケージをインストールすると、次に以下のエラーが発生する。

INFO: [SIM 211-2] *************** CSIM start ***************
INFO: [SIM 211-4] CSIM will launch GCC as the compiler.
make[1]: Entering directory '/home/msyksphinz/work/fpga_designs/vivado_hls/edge_filter/edge_filter/normal/csim/build'
   Compiling ../../../../test_edge_filter.cpp in debug mode
   Compiling ../../../../edge_filter.cpp in debug mode
   Generating csim.exe
Makefile.rules:399: recipe for target 'csim.exe' failed
make[1]: Leaving directory '/home/msyksphinz/work/fpga_designs/vivado_hls/edge_filter/edge_filter/normal/csim/build'
/tools/Xilinx/Vivado/2018.3/tps/lnx64/binutils-2.26/bin/ld: cannot find crt1.o: No such file or directory
/tools/Xilinx/Vivado/2018.3/tps/lnx64/binutils-2.26/bin/ld: cannot find crti.o: No such file or directory
/tools/Xilinx/Vivado/2018.3/tps/lnx64/binutils-2.26/bin/ld: cannot find -lpthread
/tools/Xilinx/Vivado/2018.3/tps/lnx64/binutils-2.26/bin/ld: cannot find -lm
collect2: error: ld returned 1 exit status
make[1]: *** [csim.exe] Error 1

今度はcrtが存在しないと怒られてしまった。これはどうしたらいいんだ?

SDAccelのQ&Aを見ていると、以下のようにシンボリックリンクを張れば良いらしい。

forums.xilinx.com

sudo ln -s /usr/lib/x86_64-linux-gnu /usr/lib64

これで動作するようになった。無事にシミュレーションとVerilogの生成に成功した。

f:id:msyksphinz:20190201010357p:plain
WSLでVivado HLSのGUIが立ち上がった。

オリジナルLLVM Backendを追加しよう (17. フレーム処理)

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

f:id:msyksphinz:20190117013715p:plain

関数呼び出しの際のフレームを生成する。これはCpu0 BackendのChapter3_5に相当するところだ。 これがうまくいくと、main()関数を呼び出すという処理が行えるようになる。

最初に実装したところだと、なぜか途中でコード生成が止まってしまいどうなっているのか分からなかった。

% ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch3.bc -o -
        .text
        .section .mdebug.abiO32
        .previous
        .file   "ch3.cpp"
'mips32r2' is not a recognized processor for this target (ignoring processor)
'+mips32r2' is not a recognized feature for this target (ignoring feature)
'-noabicalls' is not a recognized feature for this target (ignoring feature)
'mips32r2' is not a recognized processor for this target (ignoring processor)
'+mips32r2' is not a recognized feature for this target (ignoring feature)
'-noabicalls' is not a recognized feature for this target (ignoring feature)
Selecting: t7: ch = MYRISCVXISD::Ret t6, Register:i32 $a0, t6:1

Selecting: t6: ch,glue = CopyToReg t4, Register:i32 $a0, Constant:i32<0>

Selecting: t4: ch = store<(store 4 into %ir.retval)> t0, Constant:i32<0>, FrameIndex:i32<0>, undef:i32

Selecting: t5: i32 = Register $a0

Selecting: t1: i32 = Constant<0>

Selecting: t0: ch = EntryToken
(ここで無限ループに入る)

色々調べていると、emilinateFrameIndexを実装しないと途中でハングしてしまうらしい。 ハングじゃなくてちゃんと警告を出してほしいなあ。。。

eliminateFrameIndex()をちゃんと実装すると、コードが生成されるようになった。

github.com

% ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm ch3.bc -o -
        .text
        .section .mdebug.abiO32
        .previous
        .file   "ch3.cpp"
        .globl  main                    # -- Begin function main
        .p2align        2
        .type   main,@function
        .ent    main                    # @main
main:
        .frame  $x8,8,$x1
        .mask   0x00000000,0
        .set    noreorder
        .set    nomacro
# %bb.0:                                # %entry
        addi    x2, x2, -8
        addi    x10, x0, 0
        sw      x10, 4(x2)
        addi    x2, x2, 8
        ret
        .set    macro
        .set    reorder
        .end    main
$func_end0:
        .size   main, ($func_end0)-main
                                        # -- End function

どうにか生成できたぞ。

RISC-Vのアウトオブオーダコアで体験するSpectre/Meltdown (1. Variant-1を動かしてみる)

BOOM v2.2がリリースされたことに伴い、BOOMを使ったSpectre/Meltdownの攻撃手法を再現するリポジトリの存在を知った。

boom-attacks というリポジトリだ。これはアウトオブオーダプロセッサのBOOMを使い、Spectreの攻撃手法を再現しようというものである。

github.com

このリポジトリ曰く、Spectre/Meltdownのうち、

  • Variant-1 : Bound Check Bypassを再現する (condBranchMispred.c)
  • Variant-2 : Branch Target Injectionを再現する (indirBranchMispred.c)

を再現できる。実装中でまだ未完成であるが、Return Stack Buffer Attackについても実装中とのこと。

テスト環境としては、BOOM側のリポジトリとして8bb0e34を使用している(リビジョンを上げすぎると挙動が変わる可能性があるので注意)。これはまだBOOMv2 ?の実装で、RV64Gまでしかサポートしていないのでコンパイル時に注意。

github.com

boom-attacksリポジトリをcloneして、さっそくmakeしてみる。

git clone https://github.com/riscv-boom/boom-attacks.git
cd boom-attacks

使用しているBOOMのリビジョンではRVCをサポートしていないので、コンパイルオプションを変える。

diff --git a/Makefile b/Makefile
index 66498eb..3c981b2 100644
--- a/Makefile
+++ b/Makefile
@@ -16,8 +16,8 @@ DEP:=dep
 # Commands and flags
 CC:=riscv64-unknown-elf-gcc
 OBJDUMP:=riscv64-unknown-elf-objdump -S
-CFLAGS=-mcmodel=medany -l -std=gnu99 -O0 -g -fno-common -fno-builtin-printf -Wall -I$(INC) -Wno-unused-function -Wno-unused-variable
-LDFLAGS:=-static -nostdlib -nostartfiles -lgcc
+CFLAGS=-mcmodel=medany -march=rv64g -l -std=gnu99 -O0 -g -fno-common -fno-builtin-printf -Wall -I$(INC) -Wno-unused-function -Wno-unused-variable
+LDFLAGS:=-static -march=rv64g -nostdlib -nostartfiles -lgcc
 DEPFLAGS=-MT $@ -MMD -MP -MF $(DEP)/$*.d
 RM=rm -rf

これで実行してみる。まずはcondBranchMispred(Variant-1)から。

$ make output/condBranchMispred.riscv.out
cd /home/msyksphinz/work/riscv/boom-template/verisim && /home/msyksphinz/work/riscv/boom-template/verisim/simulator-boom.system-BoomConfig  +max-cycles=10000000  +verbose  output/condBranchMispred.riscv 3>&1 1>&2 2>&3 | /home/msyksphinz/riscv_boomv22/bin/spike-dasm  > output/condBranchMispred.riscv.out && [ $PIPESTATUS -eq 0 ]
m[0x0x80002d90] = want(!) =?= guess(hits,dec,char) 1.(10, 33, !) 2.(2, 5, )
m[0x0x80002d91] = want(") =?= guess(hits,dec,char) 1.(9, 34, ") 2.(2, 205,
m[0x0x80002d92] = want(#) =?= guess(hits,dec,char) 1.(10, 35, #) 2.(2, 8,)
m[0x0x80002d93] = want(T) =?= guess(hits,dec,char) 1.(8, 84, T) 2.(3, 2, )
m[0x0x80002d94] = want(h) =?= guess(hits,dec,char) 1.(8, 104, h) 2.(2, 10,
)
m[0x0x80002d95] = want(i) =?= guess(hits,dec,char) 1.(9, 105, i) 2.(3, 124, |)
m[0x0x80002d96] = want(s) =?= guess(hits,dec,char) 1.(10, 115, s) 2.(2, 8,)
m[0x0x80002d97] = want(I) =?= guess(hits,dec,char) 1.(6, 73, I) 2.(3, 44, ,)
m[0x0x80002d98] = want(s) =?= guess(hits,dec,char) 1.(7, 115, s) 2.(2, 85, U)
/home/msyksphinz/work/riscv/boom-template/Makefrag:54: recipe for target 'output/condBranchMispred.riscv.out' failed
$

ここまで進んでタイムアウトで落ちる。

この結果を見てわかるのは、

  • wantの項目がおそらく答え(!\"#ThisIsTheBabyBoomerTest)の1文字ずつを抽出していく。
  • Guessの項目によって、キャッシュアクセスのレイテンシから最も正解に近いものを選び出す。
    • 1番目のテストが「!」、2番目のテストが「\"」、3番目のテストが「#」と予測しているのでこれは正解。

実施している内容としては、まだあまり理解できていないのだが、

  • 変数resultsに答え(キャッシュのレイテンシから答えらしきもの)を格納する。

  • 最初にキャッシュをクリアする。このFlushCacheの実装がまた謎なのだが、過去の内容を上書きすることによってキャッシュの中身を追い払っているのか?RISC-Vには明確なキャッシュ操作命令が無いため、このような実装になっている?

          // run the attack on the same idx ATTACK_SAME_ROUNDS times
          for(uint64_t atkRound = 0; atkRound < ATTACK_SAME_ROUNDS; ++atkRound){
  
              // make sure array you read from is not in the cache
              flushCache((uint64_t)array2, sizeof(array2));
  • 次に攻撃対象とするインデックスを決めている。この部分については正直良く分かっていないのだが、TRAIN_TIMESが6なので、5回まではpassInIdxの値はatkRoundの値となり、最後の1回だけattackIdxとなる。これがsecretStringからarray1(いろいろと触る対象)のメモリマップ上の距離で、
      uint64_t attackIdx = (uint64_t)(secretString - (char*)array1);

ここにアクセスが発生するように仕向ける、ということなのだと思う。

つまり、BHRをだますために、最初の5回の試行は通常通りarray1へのアクセスを繰り返し、最後の1回で攻撃対象のアドレスを指すようにする。これにより騙されたBHRはまんまとsecretStringにアクセスを実施し、それをキャッシュにしまい込んでしまう、ということか。

  • Victimプロセスの中では、Spectreの論文等で見たことのあるコードが書かれている。一方でその上に配置してある意味のなさそうなアセンブラは、下の条件分岐命令の判断を遅らせるためのストール処理であり、これによりif文内のコードが投機的に実行されてSpectreの攻撃が完成する。
  void victimFunc(uint64_t idx){
      uint8_t dummy = 2;
  
      // stall array1_sz by doing div operations (operation is (array1_sz << 4) / (2*4))
      array1_sz =  array1_sz << 4;
      asm("fcvt.s.lu   fa4, %[in]\n"
          "fcvt.s.lu fa5, %[inout]\n"
          "fdiv.s    fa5, fa5, fa4\n"
          "fdiv.s    fa5, fa5, fa4\n"
          "fdiv.s    fa5, fa5, fa4\n"
          "fdiv.s    fa5, fa5, fa4\n"
          "fcvt.lu.s %[out], fa5, rtz\n"
          : [out] "=r" (array1_sz)
          : [inout] "r" (array1_sz), [in] "r" (dummy)
          : "fa4", "fa5");
  
      if (idx < array1_sz){
          dummy = array2[array1[idx] * L1_BLOCK_SZ_BYTES];
      }
  
      // bound speculation here just in case it goes over
      dummy = rdcycle();
  }
  • 最後に、L1からデータを回収して秘匿情報を読みに行くのだが、これは単純に各キャッシュラインを読んで行き、そのサイクル数を測定している。
              // read out array 2 and see the hit secret value
              // this is also assuming there is no prefetching
              for (uint64_t i = 0; i < 256; ++i){
                  start = rdcycle();
                  dummy &= array2[i * L1_BLOCK_SZ_BYTES];
                  diff = (rdcycle() - start);
                  if ( diff < CACHE_HIT_THRESHOLD ){
                      results[i] += 1;
                  }
              }

全体的な流れとしては、以下のイメージか?かなりザックリだけど。

f:id:msyksphinz:20190129122015p:plain
condBranchMispred.cの非常にザックリとした攻撃方法

マルチコアにおけるキャッシュコヒーレンシ方式の簡単なまとめ (1. キャッシュスヌープ方式)

久しぶりににヘネパタ(「コンピュータアーキテクチャ 定量的アプローチ 第5版」)を読み直していると面白い。少しマルチコアプログラミングについて確認したいことがあったので、マルチコアにおけるキャッシュコヒーレンシを保つための手法についてもう一度勉強し直した。

キャッシュコヒーレンシとは、マルチコアにおけるメモリアクセスの一貫性を保つための仕組み。例えば、複数コアにおいて1つのメモリアドレスに対して処理を行うと、各コアのキャッシュにデータが残っているため最新のデータを取得することができず、処理に矛盾が発生してしまう。

f:id:msyksphinz:20190204011647p:plain
マルチコアにおけるキャッシュ間の矛盾

コア0がある処理を行いメモリに書き込むと、その結果はコア0のキャッシュに書き込む。一方で、コア2も同様にある処理を行い同一メモリに書き込むと、そのメモリの最新の値はコア0のキャッシュの中にあるはずなのに、そのことを知らないコア2は勝手に自分のキャッシュにデータをアップデートする。すると、同じメモリアドレスのはずなのに異なる最新の値が異なるコアのキャッシュに格納されており、結果に矛盾が生じてしまう。

これを防ぐために様々な方法があるが、大きく分けて

の方法が存在する。キャッシュスヌーピングはコア数が少ない時は性能が良いが、スケーラビリティが足りない、一方でディレクトリ方式はコア数が少ない時は性能がいまいち出ないが、スケーラビリティが良くメニーコアで活用されている。

キャッシュスヌーピング方式

各コア内キャッシュは、各キャッシュラインが現在どのような状態であるのかを常に監視する。そのため、キャッシュ内で何らかのアクセスが発生すると、それを「スヌープバス」を通じて各コアにすぬーぷ情報を通知する。その方式に応じて、少しずつキャッシュスヌーピングの方式が異なる。大きく分けて以下のようなキャッシュスヌーピングの方式が存在する。

  • MSI (Modified, Share, Invalidate)
  • MOSI (Modified, Owned, Shared, Invalidate)
  • MESI (Modified, Exclusive, Shared, Invalidate)
  • MOESI (Modified, Owned, Exclusive, Shared, Invalidate)

いろいろ存在しているが、MSIがもっとも単純な方式、これの性能を上げるためにMOSI, MESI, MOESIなどの方式があると考えてよい。

MSI方式

キャッシュラインの状態は、各コアでのデータの共有状態に応じて Modified, Shared, Invalidate の3状態を取る。キャッシュの状態は、

  • Modified : データが何も入っていない状態
  • Shared : データがキャッシュに入っているが、メモリの状態と同じものである。
  • Invalidate : キャッシュ内データが無効な状態

の3種類に分けられる。

  1. まず、メモリからデータを読み出す。キャッシュにデータが格納され、それは"Shared"状態としてマークされる。
f:id:msyksphinz:20190204011721p:plain
  1. キャッシュ内のデータがアップデートされる。これによりキャッシュ内データは"Modified"状態としてマークされる。
f:id:msyksphinz:20190204011738p:plain
  1. 他のコア(コア2)から同じアドレスへのデータ読み込みが発生した場合、スヌープバスを通じて全コアにキャッシュラインの検査要求が入る。このとき、コア0のキャッシュには当該アドレスが入っており、それは"Modified"状態となっている。このデータはいったんメモリに書き戻される。その際、メモリ内のデータとコア0のキャッシュデータは一致するので、コア0内のキャッシュラインは"Shared"に変更される。
f:id:msyksphinz:20190204011756p:plain
  1. そして、コア2はメモリからアップデートされたデータをキャッシュに格納する。このコア2のキャッシュラインは"Shared"としてマークされる。
f:id:msyksphinz:20190204011819p:plain

MOSIプロトコル

MSIプロトコルの問題点は、上記の操作の際にコア2がキャッシュデータをシェアするとき、一度メモリに書き戻しを行ってから、コア2がメモリからデータを読み込むということである。この方式のメリットは、複数コアがキャッシュでデータを共有しているとき、そのデータは必ずメモリに書き戻されており、最新のデータはメモリに存在しているということだ。

しかし、データを共有するにあたり、必ず一度メモリに書き戻さなければならないというのは面倒だ。しかしメモリにデータを書き戻さないと、各コアのキャッシュに格納されている最新のデータを、一体誰がメモリに戻せばよいのかが分からなくなる。これを解決するために、キャッシュの属性に"Owned"という状態を追加する。このOwnedとなっているキャッシュラインは、そのキャッシュがデータの所有権を保持しており、メモリに対して書き戻す責任を持っている。つまり、上記のメモリアクセスの3. 4. の操作において、

  1. 他のコア(コア2)から同じアドレスのデータ読み込みが発生した場合、スヌープバスを通じて全コアにキャッシュラインの検査要求が入る。このとき、コア0のキャッシュには当該アドレスが入っており、それは"Modified"状態となっている。このデータを、メモリを介さずにコア0はコア2に通知する。コア2のキャッシュは"Shared"状態となり、コア0のキャッシュラインは"Modified"から"Owned"となる。つまり、最終的にメモリに書き戻す責任はコア0に存在する。
f:id:msyksphinz:20190204012044p:plain
f:id:msyksphinz:20190204012109p:plain

MESIプロトコル

MSIプロトコルにおいて、キャッシュの書き込みが発生した場合、スヌープバスを通じて全コアのキャッシュラインの検査が入る。しかし、もし自分のコアしかそのデータを持っていない場合、いちいちスヌープバスに問い合わせをするのは面倒となる。したがって、そのデータを自分のコアのキャッシュしか持ってないことが分かっている場合、スヌープ操作を省略することができる。このため、「自分のコアしかデータを持っていない状態」としてキャッシュラインに新たに"Exclusive"という状態を設ける。

Exclusiveの状態を検出するためには、コア0がキャッシュに新たにデータを読み込む場合、コア全体に対して渡すスヌープ検査の結果を拡張する。MSIプロトコルでは、コア0のスヌープ操作に対してコア0以外のコアが「完了」(つまり、コア0が問い合わせたデータはメモリに入っていることが保証されている)、という応答を返すのだが、MESIプロトコルでは、コア0の甥合わせに対して「そのキャッシュラインのデータは持っていない」という応答を返すことができる。この応答がすべてのコアから帰ってきた場合、コア0は当該データをキャッシュしている唯一のコアとなるので、メモリから当該データを読み込んだ後、そのキャッシュラインを"Exclusive"として良い、ということになる。

f:id:msyksphinz:20190204012535p:plain
f:id:msyksphinz:20190204012555p:plain

MOESI

これまでのプロトコルをすべて合わせて、"Modified", "Owned", "Exclusive", "Shared", "Invalidate"の5つの状態を持たせたスヌーププロトコルのことをMOESIプロトコルと呼んでいる。