FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(3. AsmPrinterとInstPrinterの役割)

MYRISCVXAsmPrinterLLVM IR変換された命令の出力を司る。最終的な詳細の出力は後述するMYRISCVXInstPrinterクラスが実行するのだが、AsmPrinterはより高位なアセンブリ出力用のフレームワークとしての役割を果たす。

  • EmitInstruction() 具体的なアセンブリ命令を出力する際に呼ばれる。この関数を呼ぶと、後述するInstPrinterなどの処理が呼ばれてアセンブリ命令が出力される。
//@EmitInstruction {
//- EmitInstruction() must exists or will have run time error.
void MYRISCVXAsmPrinter::EmitInstruction(const MachineInstr *MI) {
...
  do {

    if (I->isPseudo())
      llvm_unreachable("Pseudo opcode found in EmitInstruction()");

    MCInst TmpInst0;
    MCInstLowering.Lower(&*I, TmpInst0);
    OutStreamer->EmitInstruction(TmpInst0, getSubtargetInfo());
  } while ((++I != E) && I->isInsideBundle()); // Delay slot check
}
//@EmitInstruction }
  • printSavedRegsBitmask()はサポート用の関数だ。関数呼び出しの際などに、どのレジスタを関数側が保存する役割を持っているのか(Callee Saved Registers)をアセンブリファイルに出力する役割を持っている。
void MYRISCVXAsmPrinter::printSavedRegsBitmask(raw_ostream &O) {
  // CPU and FPU Saved Registers Bitmasks
  unsigned CPUBitmask = 0;
  int CPUTopSavedRegOff;
...
  // Set CPU Bitmask.
  for (; i != e; ++i) {
    unsigned Reg = CSI[i].getReg();
    unsigned RegNum = TRI->getEncodingValue(Reg);
    CPUBitmask |= (1 << RegNum);
  }
...
  // Print CPUBitmask
  O << "\t.mask \t"; printHex32(CPUBitmask, O);
  O << ',' << CPUTopSavedRegOff << '\n';
}

その他にもいろいろ定義すが、すべてアセンブリ出力時のサポート関数だ。

InstPrinterの追加

commit:cd6a10b5d2c] Add MYRISCVX/InstPrinter

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

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

  • build-myriscvx80/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-myriscvx80/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 << ")";
}

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

実際には、LLVM IRから命令が変換され命令が出力できる状態になると、MYRISCVXAsmPrinter::EmitInstruction()が呼ばれ、これがOutStreamer.EmitInstruction()を呼び出す。 このルーチンがオペコードやレジスタ名などをデコードし、これらの情報をMYRISCVXGenInstrInfo.incMYRISCVXGenRegisterInfo.incから呼び出してくるわけだ。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXAsmPrinter.cpp
//@EmitInstruction {
//- EmitInstruction() must exists or will have run time error.
void MYRISCVXAsmPrinter::EmitInstruction(const MachineInstr *MI) {
  //@EmitInstruction body {
  if (MI->isDebugValue()) {
    SmallString<128> Str;
    raw_svector_ostream OS(Str);

    PrintDebugValueComment(MI, OS);
    return;
  }

  //@print out instruction:
  //  Print out both ordinary instruction and boudle instruction
  MachineBasicBlock::const_instr_iterator I = MI->getIterator();
  MachineBasicBlock::const_instr_iterator E = MI->getParent()->instr_end();

  do {

    if (I->isPseudo())
      llvm_unreachable("Pseudo opcode found in EmitInstruction()");

    MCInst TmpInst0;
    MCInstLowering.Lower(&*I, TmpInst0);
    OutStreamer->EmitInstruction(TmpInst0, getSubtargetInfo());
  } while ((++I != E) && I->isInsideBundle()); // Delay slot check
}
//@EmitInstruction }
  • llvm-myriscvx80/lib/MC/MCObjectStreamer.cpp
void MCObjectStreamer::EmitInstruction(const MCInst &Inst,
                                       const MCSubtargetInfo &STI, bool) {
  getAssembler().getBackend().handleCodePaddingInstructionBegin(Inst);
  EmitInstructionImpl(Inst, STI);
  getAssembler().getBackend().handleCodePaddingInstructionEnd(Inst);
}

では、LLVM IRから命令が変換され、実際にアセンブリ命令が出力されるまでの流れを追ってみる。

f:id:msyksphinz:20190914204758p:plain
InstPrintによりアセンブリ命令が生成されるフロー。

まず、MCAsmStreamer内のEmitInstruction()が呼び出されると、命令が出力される準備が整う。 EmitInstruction()内でInstPrinter.printInst()が呼び出され、そこからターゲット固有の命令出力プロセスが流れ始める。

void MCAsmStreamer::EmitInstruction(const MCInst &Inst,
                                    const MCSubtargetInfo &STI,
                                    bool PrintSchedInfo) {
...
  if(getTargetStreamer())
    getTargetStreamer()->prettyPrintAsm(*InstPrinter, OS, Inst, STI);
  else
    InstPrinter->printInst(&Inst, OS, "", STI);   // printInstが呼び出される。
...

まず、MYRISCVXInstPrinterクラスは大きく分けて2つのファイルに記述してあるところから説明する。 MYRISCVXInstPrinterクラスは、ソースコードを我々が記述するMYRISCVXInstPrinter.cppと、MYRISCVXInstrInfo.tdから自動生成されるMYRISCVXGenAsmWriter.cppに分類される。 それぞれ、

  • llvm-myriscvx80/lib/Target/MYRISCVX/InstrPrinter/MYRISCVXInstrPrinter.cpp
  • build-myriscvx80/lib/Target/MYRISCVX/MYRISCVXGenAsmWriter.inc

に格納されている。

MYRISCVXInstrPrinter.cpp内のprintInstMYRISCVXGenAsmWriter.incに生成されてあるprintInstruction()を呼び出す。 printInstruction()内では、命令のフィールドのデコードによっていくつかの共通ルーチンが呼び出される。 例えば、レジスタオペランドの表記であればprintOperand()が呼び出され、即値に関してはprintUnsigendImmが呼び出される。 これらは、MYRISCVXInstPrinter.cpp側で用意しておく必要がある。ハンドコーディングするprintOpreand()も、場合によってはprintRegName()が使用するgetRegisterName()などのMYRISCVXGenAsmWriter.incで定義されている自動生成されたルーチンを使用する必要がある。 これらを上手く組み合わせて、命令を生成する。