FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (13. 命令のプリントに関するメソッド群)

命令のプリント、つまりアセンブリ命令をファイルに出力する処理は、基本的にMYRISCVXInstrInfo.tdに記述した命令の定義に基づいて生成される。

LLVMではPrintInstruction()というメソッドがその役割を担います。 このメソッドはMYRISCVXInstPrinterクラスに記述されています。 したがって、PrintInstruction()に関連するメソッドはすべてMYRISCVXInstPrinterに記述することになります。

  • build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenAsmWriter.inc
/// printInstruction - This method is automatically generated by tablegen
/// from the instruction set description.
void MYRISCVXInstPrinter::printInstruction(const MCInst *MI, raw_ostream &O) {
  static const char AsmStrs[] = {
...
}

ここでサポートメソッドとして追加しなければならないことは、以下の2つ。

  • printOperand() : オペランドを出力する。
  • printMemOperand() : メモリオペランドを出力する。メモリオペランドimm(reg)の形式で出力するので、printOperand()を2回呼び出している。

  • llvm-myriscvx/lib/Target/MYRISCVX/InstPrinter/MYRISCVXInstPrinter.cpp

void MYRISCVXInstPrinter::printOperand(const MCInst *MI, unsigned OpNo,
                                       raw_ostream &O) {
  const MCOperand &Op = MI->getOperand(OpNo);
  if (Op.isReg()) {
    printRegName(O, Op.getReg());
    return;
  }

  if (Op.isImm()) {
    O << Op.getImm();
    return;
  }

  assert(Op.isExpr() && "unknown operand kind in printOperand");
  Op.getExpr()->print(O, &MAI, true);
}

void MYRISCVXInstPrinter::
printMemOperand(const MCInst *MI, int opNum, raw_ostream &O) {
  // Load/Store memory operands -- imm($reg)
  // If PIC target the target is loaded as the
  // pattern ld $t9,%call16($gp)
  printOperand(MI, opNum+1, O);
  O << "(";
  printOperand(MI, opNum, O);
  O << ")";
}
f:id:msyksphinz:20190526224919p:plain
printInstruction()により命令がプリントされる仕組み。

このprintInstruction()などの命令プリントメソッドとMYRISCVXTargetMachineとの関連付けは、MCTargetDesc/MYRISCVXMCTargetDesc.cppで行われている。

  • llvm-myriscvx/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
// MYRISCVXInstPrinterメソッドを返す。
static MCInstPrinter *createMYRISCVXMCInstPrinter(const Triple &T,
                                                  unsigned SyntaxVariant,
                                                  const MCAsmInfo &MAI,
                                                  const MCInstrInfo &MII,
                                                  const MCRegisterInfo &MRI) {
  return new MYRISCVXInstPrinter(MAI, MII, MRI);
}

extern "C" void LLVMInitializeMYRISCVXTargetMC() {
  for (Target *T : {&getTheMYRISCVX32Target(), &getTheMYRISCVX64Target()}) {
    // Register the MC asm info.
    RegisterMCAsmInfoFn X(*T, createMYRISCVXMCAsmInfo);

    // Register the MC instruction info.
    TargetRegistry::RegisterMCInstrInfo(*T, createMYRISCVXMCInstrInfo);

    // Register the MC register info.
    TargetRegistry::RegisterMCRegInfo(*T, createMYRISCVXMCRegisterInfo);

    // Register the MC subtarget info.
    TargetRegistry::RegisterMCSubtargetInfo(*T,
                                            createMYRISCVXMCSubtargetInfo);
    // Register the MC instruction analyzer.
    TargetRegistry::RegisterMCInstrAnalysis(*T, createMYRISCVXMCInstrAnalysis);
    
    // createMYRISCVXMCInstPrinterクラスを通じて、MYRISCVXInstPrintクラスを登録する。
    TargetRegistry::RegisterMCInstPrinter(*T,
                                          createMYRISCVXMCInstPrinter);
  }

}

LLVMのバックエンドを作るための第一歩 (12. llcのターゲット・CPU・アトリビュートの関係)

llcLLVM IRからアセンブリ言語を生成する場合、バックエンドのターゲットとして大きく分けて以下の3つを指定する。

./bin/llc -march=myriscvx32 -mcpu=simple32 -mattr=-64bit
f:id:msyksphinz:20190526124548p:plain
コマンドラインから設定されるターゲットの情報と内部変数の関係

それぞれが意味を持っている。

  • -march まず、-marchによってターゲットアーキテクチャを指定する。これはTripleで登録したmyriscvx32myriscvx64を指定しなければならない。

  • -mattr

-mattrでは、そのアーキテクチャの機能を記述する。例えば、RISC-Vだと拡張命令セットとしてM,A,F,Dなどの拡張命令セットをサポートしている。 どの命令セットを拡張するか、このattrで指定することができる。これはLLVMの中でFeatureとして設定される。 MYRISCVXでは、Featureとして以下を追加した。

  • lib/Target/MYRISCVX/MYRISCVX.td
def FeatureMExt : SubtargetFeature<"m", "is_enable_M", "true",
    "'M' (Integer Multiplication and Division) support">;

def FeatureAExt : SubtargetFeature<"a", "is_enable_A", "true",
    "'A' (Atomic) support">;

def FeatureFExt : SubtargetFeature<"f", "is_enable_F", "true",
    "'F' (Singple Precision Floating Point) support">;

def FeatureDExt : SubtargetFeature<"d", "is_enable_D", "true",
    "'D' (Double Precision Floating Point) support">;

def FeatureCExt : SubtargetFeature<"c", "is_enable_C", "true",
    "'C' (Compressed Instruction) support">;

def FeatureRV64 : SubtargetFeature<"64bit", "HasRV64", "true", "RV64 support">;

サブターゲットの記述には要素が4つ並んでいる。

  • Featureの名前
  • Featureの属性(実装時に追加される変数の名前)
  • Featureの値(属性に設定される値)
  • Featureの説明。

この記述を加えることによって、tdファイルから生成されるMYRISCVXGenSubtargetInfo.incにはSubtargetFeatureに基づく変数制御が追加される。

  • build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.inc
void llvm::MYRISCVXSubtarget::ParseSubtargetFeatures(StringRef CPU, StringRef FS) {
  LLVM_DEBUG(dbgs() << "\nFeatures:" << FS);
  LLVM_DEBUG(dbgs() << "\nCPU:" << CPU << "\n\n");
  InitMCProcessorInfo(CPU, FS);
  const FeatureBitset& Bits = getFeatureBits();
  if (Bits[MYRISCVX::FeatureAExt]) is_enable_A = true;
  if (Bits[MYRISCVX::FeatureCExt]) is_enable_C = true;
  if (Bits[MYRISCVX::FeatureDExt]) is_enable_D = true;
  if (Bits[MYRISCVX::FeatureFExt]) is_enable_F = true;
  if (Bits[MYRISCVX::FeatureMExt]) is_enable_M = true;
  if (Bits[MYRISCVX::FeatureRV64]) HasRV64 = true;
}

したがって、MYRISCVXSubtargetクラスには、これらのメソッドを追加する必要がある。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXSubtarget.h
class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
  virtual void anchor();

 public:

 protected:
  bool is_enable_M = false;
  bool is_enable_A = false;
  bool is_enable_F = false;
  bool is_enable_D = false;
  bool is_enable_C = false;

  bool HasRV64 = false;
  • -mcpu ターゲットアーキテクチャの中で、さらに特定のCPUを指定するために記述する。 特定のCPUを定義することによって、さまざまなFeatureをまとめて1つのCPU情報として記述することができる。 ここでは、以下の2つを定義した。

  • simple32 : 32ビットのシンプルなMYRISCVX実装。M/A/F/Dの拡張命令は実装していない。

  • rocket64 : 64ビットのRISC-V実装。M/A/F/Dの拡張命令を実装している。

という訳でMYRISCVX.tdに以下の2つのProcessorModelを追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVX.td
def : ProcessorModel<"simple32", NoSchedModel, []>;
def : ProcessorModel<"rocket64", NoSchedModel, [FeatureRV64,
                                                FeatureMExt,
                                                FeatureAExt, FeatureFExt, FeatureDExt, FeatureCExt]>;

これにより、MYRISCVXGenSubtargetInfoクラスに以下の情報が追加される。

  • build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.inc
// Sorted (by key) array of values for CPU subtype.
extern const llvm::SubtargetFeatureKV MYRISCVXSubTypeKV[] = {
  { "rocket64", "Select the rocket64 processor", { MYRISCVX::FeatureRV64, MYRISCVX::FeatureMExt, MYRISCVX::FeatureAExt, MYRISCVX::FeatureFExt, MYRISCVX::FeatureDExt, MYRISCVX::FeatureCExt }, { } },
  { "simple32", "Select the simple32 processor", { }, { } },
};

-mcpuオプションを指定すると、Featureの設定されたCPUが生成される、という仕組みである。

  • llvm-myriscvx/lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
/// Select the MYRISCVX Architecture Feature for the given triple and cpu name.
/// The function will be called at command 'llvm-objdump -d' for MYRISCVX elf input.
static StringRef selectMYRISCVXArchFeature(const Triple &TT, StringRef CPU) {
  if (TT.getArch() == Triple::ArchType::myriscvx64) {
    return "+64bit";
  } else if (TT.getArch() == Triple::ArchType::myriscvx32) {
    return "-64bit";
  }
  return "";
}

static MCSubtargetInfo *createMYRISCVXMCSubtargetInfo(const Triple &TT,
                                                      StringRef CPU, StringRef FS) {
  std::string ArchFS = selectMYRISCVXArchFeature(TT, CPU);

  if (!FS.empty()) {
    if (!ArchFS.empty())
      ArchFS = ArchFS + "," + FS.str();
    else
      ArchFS = FS;
  }
  // createMYRISCVXMCSubtargetInfoImpl defined in MYRISCVXGenSubtargetInfo.inc
  return createMYRISCVXMCSubtargetInfoImpl(TT, CPU, ArchFS);
}
f:id:msyksphinz:20190526124652p:plain
コマンドラインから設定されるターゲットの情報と内部クラスの関係

LLVMのバックエンドを作るための第一歩 (11. LLVMが独自ターゲットマシンを認識する仕組み)

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

MYRISCVXTargetMachineはターゲットマシンを定義するファイルである。

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

f:id:msyksphinz:20190525161248p:plain
  • 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);
};

これらのターゲットマシンをLLVM側に認識させるために、LLVMInitializeMYRISCVXTarget()という関数を作る。この関数の名前ルールは決まっており、llcで名前が決まっている。

  • lib/Target/MYRISCVX/MYRISCVXTargetMachine.cpp
extern "C" void LLVMInitializeMYRISCVXTarget() {
  // Register the target.
  //- Little endian Target Machine

  RegisterTargetMachine<MYRISCVX32TargetMachine> X(getTheMYRISCVX32Target());
  RegisterTargetMachine<MYRISCVX64TargetMachine> Y(getTheMYRISCVX64Target());
}

LLVMInitialize(Target名)Target()という初期化用のメソッドはllvm::InitializeAllTargets()から呼ばれる。

  • include/llvm/Support/TargetSelect.h
/// InitializeAllTargets - The main program should call this function if it
/// wants access to all available target machines that LLVM is configured to
/// support, to make them available via the TargetRegistry.
///
/// It is legal for a client to make multiple calls to this function.
inline void InitializeAllTargets() {
  // FIXME: Remove this, clients should do it.
  InitializeAllTargetInfos();

#define LLVM_TARGET(TargetName) LLVMInitialize##TargetName##Target();
#include "llvm/Config/Targets.def"
}

まずは、InitializeAllTargetInfos()が呼ばれる。

namespace llvm {
  /// InitializeAllTargetInfos - The main program should call this function if
  /// it wants access to all available targets that LLVM is configured to
  /// support, to make them available via the TargetRegistry.
  ///
  /// It is legal for a client to make multiple calls to this function.
  inline void InitializeAllTargetInfos() {
#define LLVM_TARGET(TargetName) LLVMInitialize##TargetName##TargetInfo();
#include "llvm/Config/Targets.def"
  }

llvm/Config/Targets.defはCMakeにてビルド用のコンフィグレーションを作成したときに自動的に生成される。 LLVM_TARGETS_TO_BULIDおよびDLLVM_EXPERIMENTAL_TARGETS_TO_BUILDで指定したターゲットについて、初期化用の関数が呼ばれるという仕組みだ。

f:id:msyksphinz:20190525161329p:plain
CMakeのコンフィグレーションからターゲットアーキテクチャが生成される仕組み
  • build-myriscvx/include/llvm/Config/Targets.def
#ifndef LLVM_TARGET
#  error Please define the macro LLVM_TARGET(TargetName)
#endif

LLVM_TARGET(X86)
LLVM_TARGET(Mips)
LLVM_TARGET(MYRISCVX)  # これが展開されて`LLVMInitializeMYRISCVXTargetInfo()`となる。
LLVM_TARGET(RISCV)

#undef LLVM_TARGET

これによりMYRISCVXとしてはLLVMInitializeMYRISCVXTargetInfo()およびLLVMInitializeMYRISCVXTarget()が呼ばれる。

LLVMInitializeMYRISCVXTarget()を見てみるとRegisterTargetMachineにてターゲットをLLVMに登録する。 この際getTheMYRISCVX32Target()およびgetTheMYRISCVX64Target()という関数が呼ばれているが、 これはlib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cppで定義されている。getTheMYRISCX{32,64}Target()を一度呼び出すとそのインスタンスが生成され、以降は同じインスタンスが呼び出させる、という仕組みだ。

  • lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
extern "C" void LLVMInitializeMYRISCVXTarget() {
  // Register the target.
  //- Little endian Target Machine

  RegisterTargetMachine<MYRISCVX32TargetMachine> X(getTheMYRISCVX32Target());
  RegisterTargetMachine<MYRISCVX64TargetMachine> Y(getTheMYRISCVX64Target());
}
  • lib/Target/MYRISCVX/TargetInfo/MYRISCVXTargetInfo.cpp
namespace llvm {
Target &getTheMYRISCVX32Target() {
  static Target TheMYRISCVX32Target;
  return TheMYRISCVX32Target;
}

Target &getTheMYRISCVX64Target() {
  static Target TheMYRISCVX64Target;
  return TheMYRISCVX64Target;
}
}

一方でLLVMInitializeMYRISCVXTargetInfo()では以下を実行する。ターゲットアーキテクチャを登録する。

extern "C" void LLVMInitializeMYRISCVXTargetInfo() {
  RegisterTarget<Triple::myriscvx32> X(getTheMYRISCVX32Target(), "myriscvx32",
                                       "MYRISCVX (32-bit)", "MYRISCVX");
  RegisterTarget<Triple::myriscvx64> Y(getTheMYRISCVX64Target(), "myriscvx64",
                                       "MYRISCVX (64-bit)", "MYRISCVX");
}

LLVMのバックエンドを作るための第一歩 (10. サブターゲットを追加するためのSubtargetFeature)

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

前回に引き続きLLVMのバックエンドを作るために必要なファイルを読み解いていく。

MYRISCVXSubTarget.{h,cpp}

サブターゲットは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;
  }

このSubtargetFeatureではアーキテクチャのバリエーションを指定するので、今回は32ビットモードと64ビットモードのサブターゲットを追加してみる。 デフォルトでは32ビットで、オプションで64ビットに設定されるようにする。

  • lib/Target/MYRISCVX/MYRISCVX.td
//===----------------------------------------------------------------------===//
// MYRISCVX subtarget features and instruction predicates.
//===----------------------------------------------------------------------===//

def FeatureRV64 : SubtargetFeature<"64bit", "HasRV64", "true", "RV64 support">;

サブターゲットの記述には要素が4つ並んでいる。

  • Featureの名前
  • Featureの属性(実装時に追加される変数の名前)
  • Featureの値(属性に設定される値)
  • Featureの説明。

この記述を加えることによって、tdファイルから生成されるMYRISCVXGenSubtargetInfo.incにはSubtargetFeatureに基づく変数制御が追加される。

  • lib/Target/MYRISCVX/MYRISCVXGenSubtargetInfo.inc
// Sorted (by key) array of values for CPU features.
extern const llvm::SubtargetFeatureKV MYRISCVXFeatureKV[] = {
  { "64bit", "RV64 support", { MYRISCVX::FeatureRV64 }, { } },
};

// Sorted (by key) array of values for CPU subtype.
extern const llvm::SubtargetFeatureKV MYRISCVXSubTypeKV[] = {
  { "cpu-rv32", "Select the cpu-rv32 processor", { }, { } },
  { "cpu-rv64", "Select the cpu-rv64 processor", { MYRISCVX::FeatureRV64 }, { } },
};
...
// ParseSubtargetFeatures - Parses features string setting specified
// subtarget options.
void llvm::MYRISCVXSubtarget::ParseSubtargetFeatures(StringRef CPU, StringRef FS) {
  LLVM_DEBUG(dbgs() << "\nFeatures:" << FS);
  LLVM_DEBUG(dbgs() << "\nCPU:" << CPU << "\n\n");
  InitMCProcessorInfo(CPU, FS);
  const FeatureBitset& Bits = getFeatureBits();
  if (Bits[MYRISCVX::FeatureRV64]) HasRV64 = true;
}

したがって、MYRISCVXSubtargetクラスにはメンバ変数HasRV64を追加する必要がある。

  • lib/Target/MYRISCVX/MYRISCVXSubtarget.h
  class MYRISCVXSubtarget : public MYRISCVXGenSubtargetInfo {
...
        bool HasRV64 = false;

セキュリティプロセッサMorpheusの論文を読む。ランダム化のための"Churn"とは。

少し前のニュースで、ASPLOS19でRISC-Vをベースとした新しいセキュアプロセッサの発表が行われたというニュースを見た。

Morpheus: A Vulnerability-Tolerant Secure Architecture Based on Ensembles of Moving Target Defenses with Churn

dl.acm.org

セキュリティ界隈には全く詳しくないのだが、面白そうなので読んでみることにした。

結論から言うと「RISC-Vベース」というのは単なる釣りで、特にRISC-Vは関係ない。別に何のアーキテクチャでも良いのだが、サンプルとしてRISC-Vを使用した、というだけだった。

しかし、普段あまり関わりのない領域の論文を読むのは面白い。プロセッサのセキュリティ対策としてどのようなものがあるのか、勉強になった。

長いので、読むのが面倒な人向け。

  • 様々なランダム化機構を埋め込む。あと、「意味的におかしな」コードがあればそれの実行を行わない。
    • このため、「本当に意味的におかしな」プログラムがあればその性能は低下する。ランダムパタン等かな?
  • チャーンという定期的に再ランダム化させる機構により、攻撃者が時間をかけてシステムの攻撃目標を見つける(これをProbeと呼ぶ)のを防ぐ。
    • このあたり、引用文献が非常に多いので、ある程度既存な技術が混じっていることは間違いないと思う。それを上手く組み合わせ、チャーンを組み込むのがポイント。
  • RTLを実装したのかと思ったら違った。いろんなニュースでは、「マイクロアーキテクチャの発表」だとか、「チップのデモ」などと書いてあるが、論文にはその言及はなかった。
    • だいたい、ダイ写真やプロセスの情報も出てこない。
  • プログラムにセマンティクスを入れる必要があるため、コンパイラに手を入れる必要がある。
    • これはマイナスポイント。よっぽど特殊な、軍事向けなどの特殊なチップとしてなら使えそう。
  • 性能低下は、どの程度暗号化を行うかに依存する。

Morpheusは、システムの攻撃を防ぐための機能を搭載したプロセッサ。ここでは、大きく分けて以下の3つの機能を解説する。

  • Domain Tagging : セマンティックと命令を元にデータの意味をタグ付けする。
  • Pointer Displacement : 命令とデータの場所をランダムに分離し、攻撃を防ぐ。
  • Domain Encryption : Tag付けしたDomainを暗号化する。
  • Churn (チャーン、と読む) : 語源は「かくはん」。定期的にランダム値を切替、攻撃者のシステム解析を遅らせる。

1. Introduction

これまでにあるCPUによる悪意のあるコードを実行されることを防ぐ仕組みは以下の通り。

  • NX-bit : 特定のメモリ領域(に置かれたデータ)に付与する実行不可属性、またはその属性付与機能
  • ASLR : Address Space Layout Randomization

今回は、プロセッサにチャーンと呼ばれる機構を導入してセキュリティの強化を行う。 チャーン(churn)とは、システムが動作している最中に透過的に移動するターゲットプログラムの値を再ランダム化するメカニズムのこと。 基本的な考え方は、システム中にランダムな値を動的に組み込んで攻撃者がターゲットの場所を読み取りにくくするのだが、チャーンと呼ばれる定期的にランダム値を再ランダム化する機構により、よりターゲットを探索しにくくするという機構である。

Morpheusでは、セキュリティの機構として大きく分けて以下の2つを理解する必要がある。

  1. ポインタ Displacement : アドレス空間中のコードとデータをランダムかつ独立に配置する。
  2. ドメイン暗号化 : コード、コードポインタ、データポインタを強力な暗号化によりランダム化する。

上記の2つを組み合わせて、 - ensembles of moving target defenses(EMTDs) を導入する。これは、上記のセキュリティ機構を組み合わせて、未確定な要素をランダム化する機構。 - Morpheusアーキテクチャを導入する。RISC-Vベースのプロセッサであり、制御フロー攻撃をEMTDとチャーンを使って防御するアーキテクチャである。

Morpheusを使うと、504ビットのエントロピーと50msのチャーン期間によって平均1%の性能低下(最大7%)がSPEC06およびMiBenchベンチマークで観測された。 Morpheusにより制御フロー攻撃に対する強力な防御が可能であることを示した。これには、ROPおよびBack-Call-Site攻撃も含まれる。 これらの分析により、Morpheusアーキテクチャ潜在的な制御フロー攻撃に対して効率的な防御が行えることを示した。

2. 脆弱性の例と、それに対抗するセキュアアーキテクチャ

以下ののコードは、ROP(Return Oriented Programming)を使用した攻撃コードだ。 strcpyにより配列の境界を越えた場所にデータを書き込み、vulnerable()の戻り値を上書きすることでtarget()にジャンプできるようにする。

void target() {
  printf("You overflowed successfully, gg");
  exit(0);
}
void vulnerable(char* str1) {
  char buf[5];
  strcpy(buf, str1);
}
int main() {
  vulnerable("ffffffffffffffff\xf0\x03\x02\x01");
  printf("This only prints in normal control flow");
}

この時の特徴は、攻撃対象のコードはセマンティック的に意味を持たないコードで構成されている。 つまり、普通はデータフロー的に意味のあるフローで流れているはずが、突如としてデータフロー的に意味のない命令が挿入され、データが改ざんされる。これをとらえれば、攻撃を検知することができる、という訳だ。

例えば、上記のプログラムにおける未定義な部分は、

  • 境界を越えた配列アクセスbuf[]
  • 戻りアドレスの場所 : コードオブジェクトのアドレス

そこで、防御の方法としてMoving Target Defense(攻撃対象となる領域を移動させる)。つまり、未定義なセマンティクスに対してランダムな領域に移動を行う。これにより、攻撃者が攻撃の際に前提に置く動作を防ぐことができる。

図2. Moving Target Defenseにより、攻撃者がシステムの未定義の値を事前調査する必要が生じることを示している。通常であれば、「システムの事前調査(Probe)」→「攻撃の用意(Wepononize)」→「攻撃(Attack)」→「漏洩(Exploited)」となるところを、

  • EMTDsを導入 : 「システムの事前調査(Probe)」に非常に時間がかかる→「攻撃の用意(Wepononize)」→「攻撃(Attack)」→「漏洩(Exploited)」となり攻撃に時間がかかる。
  • EMTDs + チャーン : システムの事前調査をしてもチャーンにより定期的に再ランダム化されるため、いつまでたってもProbeが完了しない。
f:id:msyksphinz:20190521222402p:plain
本論文Figure 2.より抜粋。

例えば、上記のROPの攻撃であれば、

  • 攻撃者は、システム内でtarget()が存在するアドレスをスキャンする。
  • strcpy()を行うための所望のアドレスと文字列を算出し挿入する。

というステップが必要になる。これはASLRによりヒープ・スタックの場所をランダム化することができるが、ASLRでは一時的な障壁に過ぎない。 例えば、AnC攻撃は150秒で完全な48ビットx86_64仮想アドレスを検知することができる。 エントロピーが大きければ、システムの事前調査(Probe)に必要な時間が増加すると言う事を示している。 つまり、エントロピーを増大させることによりProbe時間を増大させ、攻撃が成功することを防ぐ、という防御手段が成り立つ。

3. 脅威モデル

本論文での脅威モデルはDoSおよびサイドチャネルアタックは対象としない。

4. Morpheus セキュアアーキテクチャ

f:id:msyksphinz:20190521222746p:plain
本論文Figure 3.より抜粋。

64ビットRISC-V命令セットをベースとする。防御対象としては、1. 命令コード、2. コードポインタ、3. データポインタとする。

MorpheusのMoving Target Defensesの機構には、大きく分けて以下の3つが存在する。

さらに、チャーンユニット(churn unit : 4.4章) をコアに追加する。4.1章では、攻撃検知器(attack detector)を搭載する。

イデアその1.ドメインタギング

CPUの動作を4つのドメインに分類する。

  • Code (C)
  • Code Pointer(CP)
  • Data Pointer(DP)
  • Other Data(D)

パイプラインはデータとともにタグを流す。最初のタグはコンパイラから生成する。このため、改造したLLVMコンパイラが必要になる。 つまり、パイプラインにデータとともにその意味(これは命令か?データか?ポインタか?)を流すわけである。 一方でマイクロアーキテクチャはタグを格納するためのストレージを持っている。

MorpheusはLLVMベースのコンパイラ拡張を使用し、実行時に正確なドメインを記録する。 コンパイラはCのソースファイルをコンパイルし、LLVM中間表現に変換し、バックエンドは各命令についてラベルを生成してメモリオブジェクトを初期化する。 この時、メモリオブジェクトの最初のタグを生成する。

  • 全てのアーキテクチャレジスタに2ビットのタグを接続する。
  • Morpheusは64ビットのalignedされたワードに対してタグを付加する。

そして命令が実行されるたびにタグを継承する。 例えば、ポインタへの算術演算命令はポインタを生成する。Attack Detectorにより、怪しいポインタの計算や型の異なる演算を検出し、チャーンのサイクル毎に例外を発生させる。 ただし、攻撃目的のプログラムでなくてもまれにこのような動作をすることがあり、その時に性能は低下する。

イデアその2. ポインタディスプレースメント防御

全ての制御フロー攻撃は、メモリオブジェクトがどこに配置されているのかという情報を必要とする。 これらの値がどこに配置されているのかをより不明確にするために、MorpheusはPointer displacementという機構を持っている。 これは、2つのランダムな分離したアドレス空間(DAS_CDAS_D)を、仮想アドレス空間VAS上に配置している。 図4に示すように、コードはDAS_C上の[tex:d{CODE}]および、DAS_D上の[tex:D{DATA}]に別々に配置される。

プログラムは、そのライフタイム中にコードを実行するとき、 [tex:d{CODE}] と [tex:d{DATA}] がランダムに決定されるのでそのアドレスを読み取られる可能性が低くなる?

f:id:msyksphinz:20190521222924p:plain
本論文 Figure 4. より抜粋。

イデアその3.ドメイン暗号化防御

Domain encryptionでは、メモリ中のコード・コードポインタ・データポインタを強力に暗号化する。 これらの要素は固有のドメイン鍵で強力に暗号化されれる。そして、コード・データ・ポインタのタグにより、どの鍵を使って暗号化されるのかが異なる。 私たちのMorpheusの実装では協力なブロック暗号化を導入した。これをQARMAと呼ぶ。

ドメイン暗号化に使用される鍵($K_C, K{CP}, K{DP}$)はマイクロアーキテクチャ上のレジスタに格納され、ソフトウェアから見ることはできない。 各プロセスは固有の鍵を持っている。最後に、カーネルが2種類の鍵を持っている。1つが特権メモリのための鍵で、もう一つはユーザ空間のための鍵である。

f:id:msyksphinz:20190521223145p:plain
本論文 Figure 5.より抜粋。

イデアその4. Moving Target Defensesのチャーン

Morpheusでは、上記で説明したドメイン暗号化とポインタディスプレースメントによる防御はCPUの実行時に再ランダム化される。 チャーンユニットはこの再ランダム化の役割を行う。 さらに、チャーンユニットはアドレス空間の安全なコンテキストスイッチングをサポートする。

チャーンの再生成が行われるときに、パイプラインはストールしフラッシュされる。

5. Morpheus アーキテクチャについて

まず、Gem5シミュレータを使ってMorpheusのプロトタイプを作成した。アーキテクチャはGem5 MinorCPU 4-stage インオーダコアとした。

Morpheusの暗号化レイヤはQARMA_7-64\sigma_1 : 3.25ns (9-cycle latency at 2.5GHz)。さらに、チャーン操作をモデル化するためにDRAMSim2を使用する。 チャーンユニットはGem5とDRAMSim2とのやり取りを行い、タグのスキャンとポインタとコードのアップテートを行う。

5.2 セキュリティ分析

ペネトレーションテストの結果 : RIPEとよばれる制御フロー攻撃のテストスイートを実行した。さらに、自作の攻撃スイートを作成してテストした。 この結果、Morpheusアーキテクチャはすべての攻撃を停止させることができた。

  • Morpheusの攻撃分析

Morpheusに実装した防御機構の効果を調査するために、以下の4種類でProbeに必要な時間を計測した。

  • \overline{EP} : Encryption無効、Pointer Displacement無効
  • E\overline{P} : Encryption有効、Pointer Displacement無効
  • \overline{E}P : Encryption無効、Pointer Displacement有効
  • EP : Encryption有効、Pointer Displacement有効

この時の結果が以下のグラフのようになる。\overline{EP}の時は一瞬でProbeが完了するが、EP になると完了するのに時間がかかるベンチが多い。そしてベンチの特性によって、効果のある防御手法が異なっている。

f:id:msyksphinz:20190521223533p:plain
本論文 Figure 7. より抜粋。
  • チャーン実装によるオーバヘッド

チャーンを定期的に実装することで攻撃者がシステム内部を解析することを防ぐが、このチャーンの定期実行時間を変化させてCPUの性能にどう影響するかを計測する。チャーンの期間が短いと、高い頻度でキャッシュのフラッシュやパイプラインのクリアが行われるためオーバヘッドが大きい。その結果が次のグラフ。

f:id:msyksphinz:20190521223627p:plain
本論文 Figure 8.より抜粋。

やはりチャーンの期間が短いと性能に対するオーバヘッドが大きい。

xSIG 2019にてRISC-Vとオープンハードウェアについてチュートリアル講演を行います

2019/05/27(月)~2019/05/28(火)に開催されるxSIGと呼ばれるワークショップにて、RISC-Vについてチュートリアル講演を行います。

xsig.hpcc.jp

xSIGというのは、「クロス・シグ」と読むらしく、「The 3rd cross-disciplinary Workshop on Computing Systems, Infrastructures, and Programming」つまり「コンピューティングシステム・インフラ・プログラミングの分野間交流ワークショップ」ということでしょうか。

昔SACSISという学会に少しだけお世話になったことがありますが、その後継のようですね。

チュートリアル講演ということで、依頼を受けてみたら90分もしゃべるものでした。 去年のEmbedded Technologyのテクニカルセッションでも90分しゃべりましたが、1時間の講義と考えるととてもつらいですね。。。 大学の先生が毎回の講義で90分もしゃべっているのを考えると本当にすごいと思います。

f:id:msyksphinz:20190518190158p:plain

ってかプログラムから基調講演・チュートリアルのページをみると、私の発表だけ異様な気がします。 他の方がすごい人なのに、一人だけ本名じゃないし、私の所だけふざけたプロフィール写真が載っているのは大変申し訳ない...

参加するためには情報処理学会のページから予約する必要があるようですが、なんと学生は無料です!これは参加するしかないね!!

という訳で、優秀な学生の皆さんや大学の先生方、お手柔らかにお願いします。

LLVMのバックエンドを作るための第一歩 (9. MCTargetDescとABIInfo)

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

前回に引き続きLLVMのバックエンドを作るために必要なファイルを読み解いていく。

MCTargetDesc/MYRISCVXMCTargetDesc.{h,cpp}

MYRISCVXMCTargeTDescでは明確なクラスを定義するわけではない。 その代わりに、これまでTarget Descriptionから生成したソースコードをincludeしていく。 MYRISCVXMCTargetDesc.hではenumなどのレジスタ番号を定義する。一方で、MYRISCVXMCTargetDesc.hは実装をincludeする。

f:id:msyksphinz:20190520003950p:plain
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.h
// Defines symbolic names for MYRISCVX registers.  This defines a mapping from
// register name to register number.
#define GET_REGINFO_ENUM
#include "MYRISCVXGenRegisterInfo.inc"

// Defines symbolic names for the MYRISCVX instructions.
#define GET_INSTRINFO_ENUM
#include "MYRISCVXGenInstrInfo.inc"

#define GET_SUBTARGETINFO_ENUM
#include "MYRISCVXGenSubtargetInfo.inc"
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXMCTargetDesc.cpp
#define GET_INSTRINFO_MC_DESC
#include "MYRISCVXGenInstrInfo.inc"

#define GET_SUBTARGETINFO_MC_DESC
#include "MYRISCVXGenSubtargetInfo.inc"

#define GET_REGINFO_MC_DESC
#include "MYRISCVXGenRegisterInfo.inc"

MCTargetDesc/MYRISCVXABIInfo.{h,cpp}`

MYRISCVXABIInfoはABIに関するクラスだ。 MYRISCVXについては、1つのABIとしてLP32を定義した。 このABIを使用するための各種メソッドを定義していく。

  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXABIInfo.h
  class MYRISCVXABIInfo {
   public:
    enum class ABI { Unknown, LP32 };

   protected:
    ABI ThisABI;

   public:
    MYRISCVXABIInfo(ABI ThisABI) : ThisABI(ThisABI) {}

    static MYRISCVXABIInfo Unknown() { return MYRISCVXABIInfo(ABI::Unknown); }
    static MYRISCVXABIInfo LP32() { return MYRISCVXABIInfo(ABI::LP32); }
    static MYRISCVXABIInfo computeTargetABI();

    bool IsKnown() const { return ThisABI != ABI::Unknown; }
    bool IsLP32 () const { return ThisABI == ABI::LP32; }
    ABI GetEnumValue() const { return ThisABI; }

    /// The registers to use for byval arguments.
    ArrayRef<MCPhysReg> GetByValArgRegs() const;

    /// The registers to use for the variable argument list.
    ArrayRef<MCPhysReg> GetVarArgRegs() const;

    /// Obtain the size of the area allocated by the callee for arguments.
    /// CallingConv::FastCall affects the value for O32.
    unsigned GetCalleeAllocdArgSizeInBytes(CallingConv::ID CC) const;

    /// Ordering of ABI's
    /// MYRISCVXGenSubtargetInfo.inc will use this to resolve conflicts when given
    /// multiple ABI options.
    bool operator<(const MYRISCVXABIInfo Other) const {
      return ThisABI < Other.GetEnumValue();
    }

    unsigned GetStackPtr() const;
    unsigned GetFramePtr() const;
    unsigned GetNullPtr() const;

    unsigned GetEhDataReg(unsigned I) const;
    int EhDataRegSize() const;
  };