FPGA開発日記

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

Intel CPUの脆弱性"ZombieLoad"の論文を読んでみる

Intelがまた出した。Meltdown / Spectre系のCPUの脆弱性として新たに発表された"ZombieLoad"である。

これもまた論文が発表されている。これらの論文は、最新のCPUの技術を勉強するにあたって非常に有用なものだ。ZombieLoadの論文を読んで、どのような脆弱性であるのかを読み解いていくことにした。

参考にしたのは、"ZombieLoad: Cross-Privilege-Boundary Data Sampling" という論文だ。以下からダウンロードできる。

arxiv.org

すべてを理解できたわけではないが、ZombieLoadの本質は、Intel CPUのハードウェア的なバグであるというところであると言える。アーキテクチャ上のバグではないため、AMD、Armでは現れない。Intelの実装が単純に問題だった、ということになりそうだ。

そもそもMeltdown / Spectreの分類はどのようになっているのかというところから復習する。

  • Spectre : 分岐予測が予測に失敗する事象を悪用する
  • Meltdown : CPU例外の後の一時的な実行を悪用する

ということを思い出しておく。

この中でZombieLoadはMeldwon型の脆弱性であると言える。ロードストアユニットのバッファに入っている脆弱性をついたものだ。

ちなみに、このZombieLoad単体では攻撃対象のアドレスを制御することはできない。しかし、ZembieLoadと別の攻撃方法を組み合わせることで、より詳細な攻撃が可能になるということだ。

現代のCPUは、命令はアウトオブオーダで実行される。しかし、プログラムの順番を維持しなければならないという制約から、パイプラインの途中ではアウトオブオーダで実行されるとしても最後はインオーダで完了するための機構が備わっている。ALUやLSUの内部はアウトオブオーダで実行されるのだが、特にメモリアクセスについては順番を守る必要がある。このため、LSUの内部はメモリアクセスの順番を守るための様々なバッファが搭載されている。これらの一つに、問題があるということなのだ。

ZombieLoadのキモは、あるロード命令が例外を発生した場合、そのロード命令より前のメモリ操作に属する古い値を投機的にロードしてしまう、ということだ。

f:id:msyksphinz:20190518235016p:plain

ZombieLoadについて説明するにあたり重要なCPU内ユニットとして、メモリオーダバッファ(Memory Order Buffer)がある。Intel CPUの場合、データロードを行うユニットは2つ、データストアを行うためのユニットが1つ搭載されている。メモリオーダバッファはロードバッファとストアバッファの内部を管理し、メモリの依存関係を解決してどのメモリアクセス命令を発行してよいのかを決定する。

ZombieLoadが攻撃対象とするロードバッファは、ロード命令が発行されると、その情報を格納されるバッファだ。

  1. ロード命令が発行されると、ロードバッファにキューイングされる。
  2. ロード操作がメモリに発行され、データが返されると、対応するロードエントリを開放する。
  3. ロード命令がリタイアする。

上記の手順でロード命令は処理される。

しかしここで問題なのは、Intel CPUの場合はロード命令が複雑な複雑なマイクロアーキテクチャの状態に入った場合、そのロード命令の後にロードデータを使う命令は、ロードバッファに格納されている古いデータを読むことがある、ということが観測されたのである。ロード命令が実行中断される前に、漏洩した値が後続の命令によって投機的に使用される可能性がある、ということである。

これだけだとなかなか攻撃手法として使うことができないが、さらに従来のサイドチャネル攻撃と組み合わせることでより強力な攻撃手法として成立させることができる。

  • いったい何が原因なのか?

今回のこのZombieLoadは、他のサイドチャネル攻撃と異なり、ハードウェア的な原因が明らかになって以来ということが問題となっている。つまり、アーキテクチャの問題ではなく、Intel CPUの実装的なバグではないかということである。

この問題の原因を特定するために、以下の実験を行っている。

  1. あるページをキャッシュ不可に設定し、キャッシュからそのページをフラッシュする。
  2. そのページに対するロード命令は、すべてのキャッシュを回避されメモリに直接アクセスされ、データは直接フィルバッファに転送される。
  3. キャッシュにデータのコピーがないことを確認するために、秘匿情報をキャッシュ可能なページに書き込む。
  4. 再度と秘匿データをロードすると、キャッシュ不可能な場所からデータをロードするが、ここでリークが発生する。

  5. 攻撃のシナリオ

攻撃するタイプとしては、2種類が考えられる。

  • Variant 1: Kernel Mapping : カーネルとユーザが同じ物理ページを異なるアドレスから参照する際、異なるプロセスであっても下位のアドレスがあっているため、Faultページロードが発生した際にデータのリークが発生する。
  • Variant 2: Microcode-Assisted Page-Table Walk : ページフォルトが発生した際の、マイクロコードによるページテーブルウォークが起きる。今回のシナリオあは、物理ページに対して仮想アドレス v v_2マッピングされている場合、一方のアドレス v_2 側でアクセスビットが無効になっていたとしても、ページテーブルウォークなどのマイクロコーデでの動作が有効になるとデータ漏洩が発生する。

今回ほZombieLoadでは、Meltdownと違い特定のアドレスを漏洩させることはできず、あくまで基本はLSUのフィルバッファに格納されているデータがそのまま読めている、ということだ。しかし様々な手法を使うことで、ケーススタディでは以下の秘匿データを習得することができている。

  • AES-NI鍵データ
  • SGXでガードされた鍵データ
  • VM間の交換チャネル
  • ウェブブラウザの動作のモニタリング

などの攻撃が可能だということらしい。

LLVMのバックエンドを作るための第一歩 (8. Calling Conventionとレジスタ定義)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

MYRISCVXCallingConv.td

MYRISCVXのCalling Convention、つまり呼び出し規約について設定するTarget Descriptionファイルだ。MYRISCXVの呼び出し規約は、RISC-Vのものをそのまま使用しようと思う。

ここでは、関数呼び出しの時のCallee Savedレジスタの定義だけを入れておく。この記述は、MYRISCVXRegisterInfoの実装時に使用する。

  • lib/Target/MYRISCVX/MYRISCVXCallingConv.td
  def CSR_LP32 : CalleeSavedRegs<(add SP, FP, S1, (sequence "S%u", 2, 11))>;

MYRISCVXRegisterInfo.{h, cpp}

MYRISCVX.tdレジスタを定義した。MYRISCXVRegisterInfo.{h, cpp}は、定義したレジスタのテンプレートを使った実際の実装を記述することになる。 したがって、MYRISCVXRegisterInfoクラスは、MYRISCVXRegisterInfo.tdから自動生成されたMYRISCVXGenRegisterInfoクラスを継承している。

f:id:msyksphinz:20190518012001p:plain
MYRISCVXRegisterInfoクラスの継承関係。

lib/Target/MYRISCVX/MYRISCVXRegisterInfo.hで定義しなければならないメソッドは以下の通りだ。

  • getCalleeSavedRegs() : Callee Savedなレジスタのリストを返す。これはMYRISCVXRegisterInfo.tdで定義したのでC++に変換されている。

    • lib/Target/MYRISCVX/MYRISCVXCallingConv.td
  def CSR_LP32 : CalleeSavedRegs<(add SP, FP, S1, (sequence "S%u", 2, 11))>;
  const MCPhysReg *
  MYRISCVXRegisterInfo::getCalleeSavedRegs(const MachineFunction *MF) const {
    return CSR_LP32_SaveList;
  }
  • getCallPreservedMask() : 役割としてはgetCalleeSavedRegs()と似ている。レジジスタ割り当てに使用しない予約済みのレジスタを示する。ビットマスクとして表現される。
  const uint32_t *
  MYRISCVXRegisterInfo::getCallPreservedMask(const MachineFunction &MF,
                                             CallingConv::ID) const {
    return CSR_LP32_RegMask;
  }
  BitVector MYRISCVXRegisterInfo::
  getReservedRegs(const MachineFunction &MF) const {
    //@getReservedRegs body {
    static const uint16_t ReservedCPURegs[] = {
      MYRISCVX::ZERO, MYRISCVX::RA, MYRISCVX::FP, MYRISCVX::SP, MYRISCVX::GP, MYRISCVX::TP
    };
    BitVector Reserved(getNumRegs());
  
    // ResservedCPURRegsの配列に相当するビットに1を設定していく。
    for (unsigned I = 0; I < array_lengthof(ReservedCPURegs); ++I)
      Reserved.set(ReservedCPURegs[I]);
  
    return Reserved;
  }
  • requiresRegisterScavenging() : レジスタスカベンジャ(レジスタが足りない場合の空きレジスタの探索処理)を実行するかどうかを指定する。今回は単純にtrueを返す。

  • trackLivenessAfterRegAlloc() : レジスタ割り当て後のLiveness(レジスタの生存範囲)を記憶するかどうかを指定する。今回は単純にtrueを返す。

  • eliminateFrameIndex() : 抽象的なフレームインデックスを削除するためのコードを記述する。ここではまだ実装しない。

  void MYRISCVXRegisterInfo::
  eliminateFrameIndex(MachineBasicBlock::iterator II, int SPAdj,
                      unsigned FIOperandNum, RegScavenger *RS) const {
  }
  • getFrameRegister() : フレームレジスタ(フレームポインタ)を返す。
  unsigned MYRISCVXRegisterInfo::
  getFrameRegister(const MachineFunction &MF) const {
    return MYRISCVX::FP;
  }

ChiselのParametersとConfigによるパラメタライズの方法

Chiselで記述されたRocket-Chipのデザインを見ていると、不思議な演算子が使われているのを見たことがあるかもしれない。

f:id:msyksphinz:20190517003942p:plain
ConfigInRV32, ConfigInRV64, ConfigOutによるパラメタライズ
  • src/main/scala/system/Configs.scala
class DefaultConfig extends Config(new WithNBigCores(1) ++ new BaseConfig)

Rocket-ChipはこのようなConfigParametersをうまく活用して、非常にフレキシブルな構成を可能にしている。 ただしこの部分は非常にややこしく、理解するのに時間がかかる。Chiselにおけるパラメタリゼーションについて少し調べて見た。

ベースにしているのは、以下の資料 Advanced Chisel Parameterization だ。しかしこれはChisel2をベースにしているので少し古いかもしれない。

まずConfig(new WithNBigCores(1) ++ new BaseConfig)のような記述を実現しているのは、Chiselで作られたConfigクラスの役割だ。これはChiselデフォルトの機能ではなく、freechipsの成果物であるのでソースコードを持ってきて自分のプロジェクトに配置する必要がある。

  • src/main/scala/subsystem/Configs.scala
/* Composable partial function Configs to set individual parameters */

class WithNBigCores(n: Int) extends Config((site, here, up) => {
  case RocketTilesKey => {
    val big = RocketTileParams(
      core   = RocketCoreParams(mulDiv = Some(MulDivParams(
        mulUnroll = 8,
...

このConfigクラスの実体はRocket-Chipリポジトリ./src/main/scala/config/Config.scalaに置かれている。

github.com

abstract class View {
  final def apply[T](pname: Field[T]): T = apply(pname, this)
  final def apply[T](pname: Field[T], site: View): T = {
...
}
abstract class Parameters extends View {
  final def ++ (x: Parameters): Parameters =
    new ChainParameters(this, x)
...
}
class Config(p: Parameters) extends Parameters {
  def this(f: (View, View, View) => PartialFunction[Any,Any]) = this(Parameters(f))
....
}

Configクラスの使い方は上記のAdvanced Chisel Parameterにある程度説明がある。 例えばパラメータ化したい要素を追加するときは以下のようにする(ここではSyntax Sugarを使っている)。

以下ではInWidthというパラメータを持つコンフィグレーションを2つ用意している。

case object InWidth extends Field[Int]
class ConfigInRV64 extends Config((site, here, up) => {
  case InWidth => 64
})
class ConfigInRV32 extends Config((site, here, up) => {
  case InWidth => 32
})

一方で、もう一つOutWidthパラメータを持つコンフィグレーションを1つ定義する。この状態でConfigInRV64/ConfigInRV32ConfigOutは全く別のコンフィグレーションとして定義されている。

case object OutWidth extends Field[Int]
class ConfigOut extends Config((site, here, up) => {
  case OutWidth => 32
})

しかし、これらのConfigをベースとしたクラスどうしは++演算子を用いて接続し、新たなクラスを作成することができるという特徴がある。

class DefaultConfig32 extends Config(new ConfigRV32 ++ new ConfigOut)
class DefaultConfig64 extends Config(new ConfigRV64 ++ new ConfigOut)

これらのコンフィグレーションは、パラメータクラスに変換して、モジュールのパラメータとして渡すことができる。

  val tile_parameters = (new DefaultConfig32).toInstance
  chisel3.Driver.execute(args, () => new TestModule()(tile_parameters))

TestModuleは以下のように定義されている。implicit p: Parameterstile_parametersが渡される。

class TestModule (implicit p: Parameters) extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(p(InWidth).W))
    val success = Output(UInt(p(OutWidth).W))
  })

  io.success := io.in
}

p = ParametersDefaultConifg32を設定した状態でVerilogを生成すると、inポートが32ビット、outポートが32ビットとなる。

module TestModule( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in, // @[:@6.4]
  output [31:0] io_success // @[:@6.4]
);
  assign io_success = io_in; // @[param_test.scala 27:14:@11.4]
endmodule

一方で、DefaultConfig64をパラメータに渡してVerilogを生成するとinポートが64ビット、outポートが64ビットとなる。

module TestModule( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [63:0] io_in, // @[:@6.4]
  output [31:0] io_success // @[:@6.4]
);
  assign io_success = io_in[31:0]; // @[param_test.scala 27:14:@11.4]
endmodule

このように、パラメタライズしたい項目に合わせて定数値を切り替えたり、パラメータの有無でモジュールのON/OFFを切り替えるということが柔軟に行うことができるようになる。

LLVMのバックエンドを作るための第一歩 (7. ターゲットとサブターゲット)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

MYRISCVXTargetMachine.{h,cpp}

その名の通りターゲットマシンを定義するファイルだ。MYRISCVXTargetMachineLLVMTargetMachineを継承したクラスで、ターゲットマシンのすべての情報を集約する。 このクラスには、サブターゲットの情報も含まれている。

f:id:msyksphinz:20190515233753p:plain
MYRISCVXTargetMachineクラス
  • MYRISCVXTargetMachine.h
namespace llvm {

class MYRISCVXTargetMachine : public LLVMTargetMachine {
  std::unique_ptr<TargetLoweringObjectFile> TLOF;
  // Selected ABI
  MYRISCVXABIInfo ABI;
  MYRISCVXSubtarget DefaultSubtarget;
...

また、MYRISCVXTargetMachineを継承したクラスとして、リトルエンディアンの実装であるMYRISCVXelTargetMachineが定義される。 MIPSなどのバイエンディアンアーキテクチャでは、リトルエンディアンとビックエンディアンの両方が定義されるが、RISC-Vではリトルエンディアンしか存在しないので1種類しか不要なのだが、ここでは32ビット版と64ビット版のターゲットマシンを作りたいので、MYRISCVX32TargetMachineMYRISCVX64TargetMachineを定義する。

  • MYRISCVXTargetMachine.h
/// MYRISCVX32TargetMachine - MYRISCVX32 little endian target machine.
///
class MYRISCVX32TargetMachine : public MYRISCVXTargetMachine {
  virtual void anchor();
 public:
  MYRISCVX32TargetMachine(const Target &T, const Triple &TT, StringRef CPU,
                          StringRef FS, const TargetOptions &Options,
                          Optional<Reloc::Model> RM,
                          Optional<CodeModel::Model> CM,
                          CodeGenOpt::Level OL, bool JIT);
};

/// MYRISCVX64TargetMachine - MYRISCVX64 little endian target machine.
///
class MYRISCVX64TargetMachine : public MYRISCVXTargetMachine {
  virtual void anchor();
 public:
  MYRISCVX64TargetMachine(const Target &T, const Triple &TT, StringRef CPU,
                          StringRef FS, const TargetOptions &Options,
                          Optional<Reloc::Model> RM,
                          Optional<CodeModel::Model> CM,
                          CodeGenOpt::Level OL, bool JIT);
};

TargetMachineには何を記述すべきなのか

MYRISCVXTargetMachine.cppには、ターゲットアーキテクチャの様々なことを記述する必要がある。

  • コンストラク MYRISCVXTargetMachine (...) : 親クラスLLVMTargetMachineを呼び出して基本的な情報の登録を行う。基本的な情報とは、ビッグエンディアン・リトルエンディアン、ABI、リロケーションモデルなどだ。 MYRISCVX32TargetMachineMYRISCVX64TargetMachineは両方ともリトルエンディアンなので、とりあえず現時点ではコンストラクタに渡す引数は同じだ。
  MYRISCVX32TargetMachine::MYRISCVX32TargetMachine(const Target &T, const Triple &TT,
                                                   StringRef CPU, StringRef FS,
                                                   const TargetOptions &Options,
                                                   Optional<Reloc::Model> RM,
                                                   Optional<CodeModel::Model> CM,
                                                   CodeGenOpt::Level OL, bool JIT)
      : MYRISCVXTargetMachine(T, TT, CPU, FS, Options, RM, CM, OL, JIT, true) {}
  
  void MYRISCVX64TargetMachine::anchor() { }
  
  MYRISCVX64TargetMachine::MYRISCVX64TargetMachine(const Target &T, const Triple &TT,
                                                   StringRef CPU, StringRef FS,
                                                   const TargetOptions &Options,
                                                   Optional<Reloc::Model> RM,
                                                   Optional<CodeModel::Model> CM,
                                                   CodeGenOpt::Level OL, bool JIT)
      : MYRISCVXTargetMachine(T, TT, CPU, FS, Options, RM, CM, OL, JIT, true) {}
  
  // MYRISCVX32TargetMachine / MYRISCVX64TargetMachineが呼び出す先のコンストラクタ
  MYRISCVXTargetMachine::MYRISCVXTargetMachine(const Target &T, const Triple &TT,
                                               StringRef CPU, StringRef FS,
                                               const TargetOptions &Options,
                                               Optional<Reloc::Model> RM,
                                               Optional<CodeModel::Model> CM,
                                               CodeGenOpt::Level OL, bool JIT,
                                               bool isLittle)
      : LLVMTargetMachine(T, computeDataLayout(TT, CPU, Options, isLittle), TT,
                          CPU, FS, Options, getEffectiveRelocModel(JIT, RM),
                          getEffectiveCodeModel(CM, CodeModel::Small), OL),
        isLittle(isLittle), TLOF(make_unique<MYRISCVXTargetObjectFile>()),
        ABI(MYRISCVXABIInfo::computeTargetABI()),
        DefaultSubtarget(TT, CPU, FS, isLittle, *this) {
  }

LLVMTargetMachineクラスのインスタンス化にあたり必要な情報は、以下の関数で算出している。

  • computeDataLayout(TT, CPU, Options, isLittle) xxx節で説明した。
  • getEffectiveRelocModel(JIT, RM) : MYRISCVXの有効なリロケーションモデルを返す。リロケーションモデルには以下が定義されている。

    • include/llvm/Support/CodeGen.h cpp // Relocation model types. namespace Reloc { enum Model { Static, PIC_, DynamicNoPIC, ROPI, RWPI, ROPI_RWPI }; }
  • getEffectiveCodeModel(CM, CodeModel::Small) : MYRISCVXのコードモデルを設定する。コードモデルというのは命令生成に置いてメモリアドレスの計算の方法を示し、現在のPCに対してコードとデータの相対的な関係に対する制限を付け加えることができる。ここでは、Default値としてCodeModel::Smallが設定されている。

MYRISCVXSubTarget.{h,cpp}

f:id:msyksphinz:20190515233849p:plain
MYRISCVXSubtarget

サブターゲットはMYRISCVXの中でもアーキテクチャのバリエーションを付けるために使用されるもので、その名の通りサブのターゲットだ。 llcにおいてアーキテクチャ-march=myriscvx32で指定するが、サブターゲットは-mcpu=で指定する。例えば、同じMIPSアーキテクチャの中でも、ISAのバージョン違いや実装の違いによってさまざまなバリエーションがある。

  • lib/Target/Mips/Mips.td
  def FeatureNoABICalls  : SubtargetFeature<"noabicalls", "NoABICalls", "true",
                                  "Disable SVR4-style position-independent code">;
  def FeaturePTR64Bit    : SubtargetFeature<"ptr64", "IsPTR64bit", "true",
                                  "Pointers are 64-bit wide">;
  def FeatureGP64Bit     : SubtargetFeature<"gp64", "IsGP64bit", "true",
                                  "General Purpose Registers are 64-bit wide">;
  def FeatureFP64Bit     : SubtargetFeature<"fp64", "IsFP64bit", "true",
                                  "Support 64-bit FP registers">;
  ...
./bin/llc -march=mips -mcpu=help
...

Available features for this target:

  mips1                    - Mips I ISA Support [highly experimental].
  mips16                   - Mips16 mode.
  mips2                    - Mips II ISA Support [highly experimental].
  mips3                    - MIPS III ISA Support [highly experimental].
  mips32                   - Mips32 ISA Support.
...
  noabicalls               - Disable SVR4-style position-independent code.
...

このサブターゲットを定義するためのクラスMYRISCVXSubTargetMYRISCVX.tdから生成された自動生成クラスMYRISCVXGenSubtargetInfoから生成される。

RISC-Vの場合、アーキテクチャに大きなバリエーションがある訳ではないのだが、ISAのどのモジュールが使用できるのか、例えば乗除算命令は使用できるのか、Compressed命令は使用できるのか、浮動小数点命令は使用できるのかなどのバリエーションが作れそうだ。

  • lib/Target/MYRISCVX/MYRISCVXSubtarget.h
    class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
  ...
       protected:
      bool is_enable_M = false;
      bool is_enable_A = false;
      bool is_enable_F = false;
      bool is_enable_D = false;
  ...
  • lib/Target/MYRISCVX/MYRISCVXSubtarget.cpp
  MYRISCVXSubtarget &
  MYRISCVXSubtarget::initializeSubtargetDependencies(StringRef CPU, StringRef FS,
                                                     const TargetMachine &TM) {
    if (TargetTriple.getArch() == Triple::myriscvx32) {
      if (CPU.empty() || CPU == "generic") {
        CPU = "cpu-rv32";
      }
    } else if (TargetTriple.getArch() == Triple::myriscvx64) {
      if (CPU.empty() || CPU == "generic") {
        CPU = "cpu-rv64";
      }
    } else {
      errs() << "!!!Error, TargetTriple.getArch() = " << TargetTriple.getArch()
             <<  "CPU = " << CPU << "\n";
      exit(0);
    }
  
    // Parse features string.
    ParseSubtargetFeatures(CPU, FS);
    // Initialize scheduling itinerary for the specified CPU.
    InstrItins = getInstrItineraryForCPU(CPU);
  
    return *this;
  }

LLVMのバックエンドを作るための第一歩 (6. ELFとリロケーションレコード)

f:id:msyksphinz:20190425001356p:plain
LLVM Compiler Infrastructure

LLVMのバックエンドにオリジナルターゲットアーキテクチャを追加していくプロジェクト、MYRISCVXターゲットアーキテクチャを追加したら、今度はELFの情報を追加する必要がある。

MYRISCVXはRISC-Vのオリジナル実装なんで、ELFやABIはRISC-Vと全く同一で進めようと思う。

MYRISCVXのELF出力を登録する

  • include/llvm/BinaryFormat/ELF.h こちらもELFを生成するにあたり必要だ。ELFのアーキテクチャ番号は、実は取り決めがありhttp://www.sco.com/developers/gabi/latest/ch4.eheader.htmlなどを参考にすること。このページでは、EM_RISCVは243と決められている。ここでは、まだ誰も使用していない248をMYRISCVXとして使用することにする。
  diff --git a/include/llvm/BinaryFormat/ELF.h b/include/llvm/BinaryFormat/ELF.h
  index ce35d127d43..55a6c7ddb4b 100644
  --- a/include/llvm/BinaryFormat/ELF.h
  +++ b/include/llvm/BinaryFormat/ELF.h
  @@ -312,6 +312,7 @@ enum {
     EM_RISCV = 243,         // RISC-V
     EM_LANAI = 244,         // Lanai 32-bit processor
     EM_BPF = 247,           // Linux kernel bpf virtual machine
  +  EM_MYRISCVX = 248,      // MYRISCVX
   };
  
   // Object file classes.

さらに、MYRISCVX向けのe_flagsを作成する。e_flagsはプロセッサ固有のフラグを示すのに使われる。たとえば、アーキテクチャ内の固有の実装を示したり、ISAの中のどのカテゴリをサポートしているか、などを示すためにe_flagsが使用される。例えば、MIPSの例を示す。

  • include/llvm/BinaryFormat/ELF.h
  // Mips Specific e_flags
  enum : unsigned {
    EF_MIPS_NOREORDER = 0x00000001, // Don't reorder instructions
    EF_MIPS_PIC = 0x00000002,       // Position independent code
    EF_MIPS_CPIC = 0x00000004,      // Call object with Position independent code
    EF_MIPS_ABI2 = 0x00000020,      // File uses N32 ABI
    EF_MIPS_32BITMODE = 0x00000100, // Code compiled for a 64-bit machine
  ...

RISC-Vの場合はどうなっているのか?以下の資料を参考にすると、以下の項目を定義しなければならないようだ。

Bit 0 Bit 1 - 2 Bit 3 Bit 4 Bit 5 - 31
RVC Float ABI RVE TSO Reserved

詳細はリンク先に譲る。ここでは、ELF.hに以下を追加した(というか、本家のRISC-Vと一緒だ)。

  enum : unsigned {
    EF_RISCV_RVC = 0x0001,
    EF_RISCV_FLOAT_ABI = 0x0006,
    EF_RISCV_FLOAT_ABI_SOFT = 0x0000,
    EF_RISCV_FLOAT_ABI_SINGLE = 0x0002,
    EF_RISCV_FLOAT_ABI_DOUBLE = 0x0004,
    EF_RISCV_FLOAT_ABI_QUAD = 0x0006,
    EF_RISCV_RVE = 0x0008
  };

リロケーションレコード

関数呼び出しなどをコンパイルするときに、その関数が実際にどこに配置されるかが分からないため、再配置(リロケーション)のためのシンボルが挿入される。 このリロケーションのアルゴリズムアーキテクチャ毎に決められており、LLVMではリンク時にリロケーションのアルゴリズムに応じてアドレス計算の命令が挿入される。 ELFフォーマットには、リロケーションの情報も含まれており、MYRISCVXもリロケーション情報を登録しなければならない。 ここではRISC-Vと全く同様のリロケーション情報を登録した。 RISC-Vのリロケーションは、https://github.com/riscv/riscv-elf-psabi-doc/blob/master/riscv-elf.md#relocations で見ることができる。 このリロケーション情報をinclude/llvm/BinaryFormat/ELFRelocs/MYRISCVX.defに置く。

  • include/llvm/BinaryFormat/ELFRelocs/MYRISCVX.def
  #ifndef ELF_RELOC
  #error "ELF_RELOC must be defined"
  #endif
  
  ELF_RELOC(R_MYRISCVX_NONE,               0)
  ELF_RELOC(R_MYRISCVX_32,                 1)
  ELF_RELOC(R_MYRISCVX_64,                 2)
  ELF_RELOC(R_MYRISCVX_RELATIVE,           3)
  ELF_RELOC(R_MYRISCVX_COPY,               4)
  ELF_RELOC(R_MYRISCVX_JUMP_SLOT,          5)
  ELF_RELOC(R_MYRISCVX_TLS_DTPMOD32,       6)
  ELF_RELOC(R_MYRISCVX_TLS_DTPMOD64,       7)
  ELF_RELOC(R_MYRISCVX_TLS_DTPREL32,       8)
  ELF_RELOC(R_MYRISCVX_TLS_DTPREL64,       9)
  ELF_RELOC(R_MYRISCVX_TLS_TPREL32,       10)
  ELF_RELOC(R_MYRISCVX_TLS_TPREL64,       11)
  ELF_RELOC(R_MYRISCVX_BRANCH,            16)
  ELF_RELOC(R_MYRISCVX_JAL,               17)
  ELF_RELOC(R_MYRISCVX_CALL,              18)
  ELF_RELOC(R_MYRISCVX_CALL_PLT,          19)
  ELF_RELOC(R_MYRISCVX_GOT_HI20,          20)
  ELF_RELOC(R_MYRISCVX_TLS_GOT_HI20,      21)
  ELF_RELOC(R_MYRISCVX_TLS_GD_HI20,       22)
  ELF_RELOC(R_MYRISCVX_PCREL_HI20,        23)
  ELF_RELOC(R_MYRISCVX_PCREL_LO12_I,      24)
  ELF_RELOC(R_MYRISCVX_PCREL_LO12_S,      25)
  ELF_RELOC(R_MYRISCVX_HI20,              26)
  ELF_RELOC(R_MYRISCVX_LO12_I,            27)
  ELF_RELOC(R_MYRISCVX_LO12_S,            28)
  ELF_RELOC(R_MYRISCVX_TPREL_HI20,        29)
  ELF_RELOC(R_MYRISCVX_TPREL_LO12_I,      30)
  ELF_RELOC(R_MYRISCVX_TPREL_LO12_S,      31)
  ELF_RELOC(R_MYRISCVX_TPREL_ADD,         32)
  ELF_RELOC(R_MYRISCVX_ADD8,              33)
  ELF_RELOC(R_MYRISCVX_ADD16,             34)
  ELF_RELOC(R_MYRISCVX_ADD32,             35)
  ELF_RELOC(R_MYRISCVX_ADD64,             36)
  ELF_RELOC(R_MYRISCVX_SUB8,              37)
  ELF_RELOC(R_MYRISCVX_SUB16,             38)
  ELF_RELOC(R_MYRISCVX_SUB32,             39)
  ELF_RELOC(R_MYRISCVX_SUB64,             40)
  ELF_RELOC(R_MYRISCVX_GNU_VTINHERIT,     41)
  ELF_RELOC(R_MYRISCVX_GNU_VTENTRY,       42)
  ELF_RELOC(R_MYRISCVX_ALIGN,             43)
  ELF_RELOC(R_MYRISCVX_RVC_BRANCH,        44)
  ELF_RELOC(R_MYRISCVX_RVC_JUMP,          45)
  ELF_RELOC(R_MYRISCVX_RVC_LUI,           46)
  ELF_RELOC(R_MYRISCVX_GPREL_I,           47)
  ELF_RELOC(R_MYRISCVX_GPREL_S,           48)
  ELF_RELOC(R_MYRISCVX_TPREL_I,           49)
  ELF_RELOC(R_MYRISCVX_TPREL_S,           50)
  ELF_RELOC(R_MYRISCVX_RELAX,             51)
  ELF_RELOC(R_MYRISCVX_SUB6,              52)
  ELF_RELOC(R_MYRISCVX_SET6,              53)
  ELF_RELOC(R_MYRISCVX_SET8,              54)
  ELF_RELOC(R_MYRISCVX_SET16,             55)
  ELF_RELOC(R_MYRISCVX_SET32,             56)
  ELF_RELOC(R_MYRISCVX_32_PCREL,          57)

このMYRISCVXのリロケーションテーブルは、e_flagと同様、include/llvm/BinaryFormat/ELF.h上でインクルードされる。

  • include/llvm/BinaryFormat/ELF.h
    // MYRISCVX Specific e_flags
    enum : unsigned {
      EF_MYRISCVX_RVC = 0x0001,
      EF_MYRISCVX_FLOAT_ABI = 0x0006,
    ...
        
    // ELF Relocation types for MYRISCXV
    enum {
    #include "ELFRelocs/MYRISCVX.def"
    };

このリロケーション情報を使ってどのようにしてアドレス計算を行うのかは、おいおい解説していく。

RISC-VボードHiFive UnleashedでWebサーバを立ち上げてみる

HiFive Unleashd上でDebian GNU/Linuxが動いてしまえば、あとは様々なことが可能だ。 ウェブサービスを立ち上げたり、各種アプリケーションを立ち上げることも可能となる。

例として、ngnixをインストールして、簡単なウェブサーバを立ち上げてみる。 といってもDebianが動いていますのでインストールはaptコマンドを入力するだけだ。

sudo apt install -y nginx ufw net-tools
nginx -v
nginx version: nginx/1.14.2

nginxがインストールされた。

systemctl enable ufw
service nginx start

Debianに割り当てられているIPアドレスWebブラウザからアクセスすると、nginxが動作しているのが確認できた。

f:id:msyksphinz:20190514001449p:plain
HiFive Unleashed上でnginxが動いた。

ただし、様々なウェブサービスをインストールしようとしてみたのだが、まだRISC-Vに対応していないのかMySQLがインストールできなかった。 この辺りは、各種パッケージがRISC-Vに対応するのを待つか、SQLiteなどのより簡易なSQLサーバを使ってみるしかいようだ。

AWS F1インスタンス上のFireSimを実行する(10. FireSimのワークロードを作成してプログラムを動かす)

f:id:msyksphinz:20190405012417p:plain

Rocketコアをカスタマイズし、FireSimのプラットフォームに乗せ、さらにFPGA合成したAGFIまで作成した。いよいよf1インスタンス上で動作させ、ベンチマークプログラムを動かす。

このとき、前章で紹介したようにFireSim上でBuildrootを立ち上げLinuxにログインした上でベンチマークプログラムを動かしてもよいのだが、これにはいろいろ問題がある。

まず、Buildrootのファイルシステムにどうやってコンパイル済みのベンチマークプログラムを配置するのかだが、一般的には様々な方法がある。例えばwgetコマンドで外部サーバからコンパイル済みのバイナリをダウンロードするして実行することもできるかもしれないが、残念ながらFireSim上で動いているBuildrootはネットワークの設定がされておらず、外部のインターネットに接続することができなかった。

今回はLinuxを立ち上げてベンチマークを動かす方法は諦める。その代わり、FireSimが提供している「ワークロード」というシステムを使う。

「ワークロード」は、FireSimがターゲットのコンフィグレーションをFPGAに流し込み、そして指定のプログラムをRISC-Vコアに流し込んで結果を回収するための仕組み。 FireSimのベンチマークリグレッションテストも、このワークロードを使って実行されている。

ここでは、前章で作成したmatrixmulプログラムをAGFI firesim-singlecore-no-nic-matrixmulで動かして結果を回収するために、matrixmulワークロードを作成してみる。

新しいワークロードを定義するためにはdeploy/workloads/matrixmul.iniを作成する。中身はリスト[refs:firesim_workload_matrixmul.ini]のようにする。

リスト[refs:firesim_workload_matrixmul.ini] : deploy/workloads/matrixmul.ini

[runfarm]
runfarmtag=matrixmul-unifarm

f1_16xlarges=0
m4_16xlarges=0
f1_4xlarges=0
f1_2xlarges=1

runinstancemarket=ondemand
spotinterruptionbehavior=terminate
spotmaxprice=ondemand

[targetconfig]
topology=no_net_config
no_net_num_nodes=1
linklatency=6405
switchinglatency=10
netbandwidth=200
profileinterval=-1

# This references a section from config_hwconfigs.ini
# In homogeneous configurations, use this to set the hardware config deployed
# for all simulators
defaulthwconfig=firesim-singlecore-no-nic-matrixmul

[tracing]
enable=no
startcycle=0
endcycle=-1

[workload]
workloadname=matrixmul.json
terminateoncompletion=no

defaulthwconfigに、先ほど作成したfiresim-singlecore-no-nic-matrixmulを指定する。 使用するf1インスタンスはf1_2xlargeなので、f1_2xlarges=1を指定する。さらに、ワークロードとして前章で私用したベンチマークプログラムを使いたいので、ベンチマークの詳細を指定するためのmatrixmul.jsonを作成する。このファイルはworkloadnameで設定する。

次に、matixmul.jsonを作成しましょう。リスト[refs:firesim_workload_matrixmul.json]に、matrixmul.jsonを示す。

リスト[refs:firesim_workload_matrixmul.json] : deploy/workloads/matrixmul.ini

{
  "benchmark_name" : "matrixmul",
  "common_rootfs" : "dummy.ext2",
  "deliver_dir" : "matrixmul",
  "common_args" : ["--copies 1"],
  "common_files" : ["intrate.sh"],
  "common_outputs" : ["/output"],
  "common_simulation_outputs" : ["uartlog"],
  "workloads" : [
    {
      "name": "matrixmul.riscv",
      "bootbinary" : "matrixmul.riscv",
      "simulation_outputs": [],
      "outputs": []
    }
  ]
}

ワークロードとしてmatrixmul.riscvを指定した。 このワークロードを実行すると、1つのベンチマークプログラムmarixmul.riscvがFireSimに流し込まれ、その結果が回収される。

次に、matrixmulのワークロードに必要なディレクトリを作成する。 deliver_dirに指定したように、matrixmulディレクトリを作成し、そこに流したいパタンとダミーのファイルシステムを置いておく必要がある。

# firesim/deploy/workloads で作業
mkdir matrixmul
cd matrixmul
cp ../check-rtc/dummy.ext2 .
# target-design/firechip-msyksphinz/tests/ 以下でmakeを実行し、あらかじめmatrixmul.riscvを作成しておいてくこと。
ln -s ../../../target-design/firechip-msyksphinz/tests/matrixmul.riscv

これで準備は完了。FireSimを立ち上げる。

# firesim/deployディレクトリ上で作業をすること。別のディレクトリで実行するとiniとjsonファイルのパスが合わずエラーとなった。
cd firesim/deploy
workloads/run-workload.sh workloads/matrixmul.ini --withlaunch

f1インスタンスが立ち上がり、FireSimのデザインが流し込まる。 シミュレーションが開始され、しばらく待っているとコンソールが戻ってくる。 ログが生成されるので確認してみる。

less /home/centos/firesim-msyksphinz/deploy/logs/[実行した日付]-launchrunfarm-B8G7C0DQNCNB0OF0.log
...
2019-05-11 02:45:59,050 [run_workload] [DEBUG]  jobs complete dict {u'matrixmul.riscv': True}
2019-05-11 02:45:59,050 [run_workload] [DEBUG]  global status: [True]
2019-05-11 02:45:59,050 [run_workload] [INFO ]  FireSim Simulation Exited Successfully. See results in:
/home/centos/firesim-msyksphinz/deploy/results-workload/2019-05-11--02-44-10-matrixmul/

さらに、uartlogを確認してみる。

less /home/centos/firesim-msyksphinz/deploy/results-workload/2019-05-11--02-44-10-matrixmul/matrixmul.riscv/uartlog
Script started on Sat 11 May 2019 02:44:13 AM UTC
AFI PCI  Vendor ID: 0x1d0f, Device ID 0xf000
Using xdma write queue: /dev/xdma0_h2c_0
Using xdma read queue: /dev/xdma0_c2h_0
UART0 is here (stdin/stdout).
command line for program 0. argc=19:
+permissive +mm_relaxFunctionalModel=0 +mm_writeMaxReqs=16 +mm_readMaxReqs=16 +mm_writeLatency=30 +mm_readLatency=30 +macaddr0=00:12:6D:00:00:02 +slotid=0 +niclog0=niclog +trace-start0=0 +trace-end0=-1 +linklatency0=6405 +netbw0=200 +profile-interval=-1 +zero-out-dram +shmemportname0=default +permissive-off matrixmul.riscv +blkdev0=dummy.ext2
random min: 0x0, random max: 0xffffffffffffffff
Zeroing out FPGA DRAM. This will take a few seconds...
Commencing simulation.
                    SW          HW
----------------------------------
 2 x 2 x 2 :        327        219
 4 x 4 x 4 :       1024        488
 8 x 8 x 8 :       7041       2527
16 x16 x16 :      54559      16118
18 x24 x28 :     154583      44362

*** PASSED *** after 3913301296 cycles
time elapsed: 25.0 s, simulation speed = 156.26 MHz
FPGA-Cycles-to-Model-Cycles Ratio (FMR): 1.00
Runs 3913301296 cycles
[PASS] FireSimNoNIC Test
SEED: 1557542653
f:id:msyksphinz:20190511142749p:plain
FireSimの実行結果確認

パタンが通り、シミュレーションが終了したのを確認できた(ちなみにデバッグモードにすると、SW実行した場合とHW実行した場合の結果が一致しているのも確認できる。