FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (44. 拡張Inline Assemblyのサポート)

f:id:msyksphinz:20190723231953p:plain

次にコンパイルしたいのは、以下のようなCコードだ。

  • inline_assembly2.cpp
int global_data = 200;

void test ()
{
  int add_res;
  int b = 200, c = 300;
  __asm__ __volatile__("add %0,%1,%2"
                       :"=r"(add_res)
                       :"r"(b), "r"(c)
                       );
  int lw_res;
  int *lw_p = (int *)&global_data;
  __asm__ __volatile__("lw %0, %1"
                       :"=r"(lw_res)
                       :"m"(lw_p)
                       );

  const int imm = 100;
  int addi_res;
  __asm__ __volatile__("addi %0,%1,%2"
                       :"=r"(addi_res) // e=4
                       :"r"(lw_res),"i"(imm)
                       );

}

インラインアセンブリだ。組み込みの業界ではよく使う手法だ。上記のaddiでは、

この表現をサポートするためには、どのようにすればよいのだろうか。最終的にはAsmPrinterを使用してMCInstを生成するとして、それまでにこれらのオペランドの特殊形式を検査し、該当するメモリアドレス、レジスタ、即値を当てはめていく必要がある。

アセンブリ表記の改良を行うためには、AsmPrinterクラスの機能を使用する。ここでは、AsmPrinterを拡張して、これらの拡張インラインアセンブリを処理できるようにする。

追加するのは、以下のコードだ。以下のコードをオーバライドし、MYRISCVX用に改造する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXAsmPrinter.h
// インラインアセンブリのオペランドを表示する。  
bool PrintAsmOperand(const MachineInstr *MI, unsigned OpNo,
                       unsigned AsmVariant, const char *ExtraCode,
                       raw_ostream &O) override;
// メモリアドレスのオペランド
bool PrintAsmMemoryOperand(const MachineInstr *MI, unsigned OpNum,
                             unsigned AsmVariant, const char *ExtraCode,
                             raw_ostream &O) override;
// オペランドのプリント
void printOperand(const MachineInstr *MI, int opNum, raw_ostream &O);
  • PrintAsmOperand() : この関数はAsmPrinter::EmitFunctionBody()AsmPrinter::EmitInlineAsm()の中で呼ばれる。オーバーライドしていなければ通常のPrintAsmOperand()が呼ばれるが、今回はMYRISCVXAsmPrinter.cpp内のPrintAsmOperand()が呼び出される。
// Print out an operand for an inline asm expression.
bool MYRISCVXAsmPrinter::PrintAsmOperand(const MachineInstr *MI, unsigned OpNum,
                                         unsigned AsmVariant,const char *ExtraCode,
                                         raw_ostream &O) {

  // Does this asm operand have a single letter operand modifier?
  if (ExtraCode && ExtraCode[0]) {
    if (ExtraCode[1] != 0) return true; // Unknown modifier.
...
  }

  printOperand(MI, OpNum, O);
  return false;
}

PrintAsmOperand()の先頭では、インラインアセンブリの方言を処理している(xxx)。最終的にprintOperand()オペランドを印刷する処理の本体だ。

printOperand()は以下のように実装されている。

void MYRISCVXAsmPrinter::printOperand(const MachineInstr *MI, int opNum,
                                      raw_ostream &O) {
  const MachineOperand &MO = MI->getOperand(opNum);
  bool closeP = false;

  if (MO.getTargetFlags())
    closeP = true;

  switch(MO.getTargetFlags()) {
    case MYRISCVXII::MO_GPREL    : O << "%gp_rel("   ; break;
    case MYRISCVXII::MO_GOT_CALL : O << "%call16("   ; break;
    case MYRISCVXII::MO_GOT      : O << "%got("      ; break;
    case MYRISCVXII::MO_ABS_HI   : O << "%hi("       ; break;
    case MYRISCVXII::MO_ABS_LO   : O << "%lo("       ; break;
    case MYRISCVXII::MO_GOT_HI20 : O << "%got_hi20(" ; break;
    case MYRISCVXII::MO_GOT_LO12 : O << "%got_lo12(" ; break;
  }

  switch (MO.getType()) {
    case MachineOperand::MO_Register:
      O << StringRef(MYRISCVXInstPrinter::getRegisterName(MO.getReg())).lower();
      break;

    case MachineOperand::MO_Immediate:
      O << MO.getImm();
      break;

    case MachineOperand::MO_MachineBasicBlock:
      O << *MO.getMBB()->getSymbol();
      return;

    case MachineOperand::MO_GlobalAddress:
      O << *getSymbol(MO.getGlobal());
      break;

    case MachineOperand::MO_BlockAddress: {
      MCSymbol *BA = GetBlockAddressSymbol(MO.getBlockAddress());
      O << BA->getName();
      break;
    }

    case MachineOperand::MO_ExternalSymbol:
      O << *GetExternalSymbolSymbol(MO.getSymbolName());
      break;

    case MachineOperand::MO_JumpTableIndex:
      O << MAI->getPrivateGlobalPrefix() << "JTI" << getFunctionNumber()
        << '_' << MO.getIndex();
      break;

    case MachineOperand::MO_ConstantPoolIndex:
      O << MAI->getPrivateGlobalPrefix() << "CPI"
        << getFunctionNumber() << "_" << MO.getIndex();
      if (MO.getOffset())
        O << "+" << MO.getOffset();
      break;

    default:
      llvm_unreachable("<unknown operand type>");
  }

  if (closeP) O << ")";
}

MachineOperandの形式に応じて出力形式を作っていく。MachineOperand::MO_Register, MachineOperand::MO_Immediate, MachineOperand::MO_MachineBasicBlockあたりを見れば、何をしているのか分かると思う。

bool MYRISCVXAsmPrinter::PrintAsmMemoryOperand(const MachineInstr *MI,
                                               unsigned OpNum, unsigned AsmVariant,
                                               const char *ExtraCode,
                                               raw_ostream &O) {

  int Offset = 0;
  // Currently we are expecting either no ExtraCode or 'D'
  if (ExtraCode) {
    return true; // Unknown modifier.
  }

  const MachineOperand &MO = MI->getOperand(OpNum);
  assert(MO.isReg() && "unexpected inline asm memory operand");
  O << Offset << "(" << MYRISCVXInstPrinter::getRegisterName(MO.getReg()) << ")";

  return false;
}
  • MYRISCVXTargetLowering::getConstraintType () : ターゲットの制約を取得する。

    • デフォルトでは、最初から組み込まれているTargetLowering::getConstraintType(Constraint)が呼ばれる。

    • しかし、それ以外に特殊なケースについてswitch文が組み込まれている。

    • これはどこから来ているかというと、MIPSの規定として以下の特殊なイントリンジックの表記が使えるとある。 https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html#Machine-Constraints

    • このなかで、

      • c : A register suitable for use in an indirect jump. This will always be $25 for -mabicalls.
      • R : An address that can be used in a non-macro load or store.
      • cの場合はC_RegisterClass,Rの場合はC_Memory`を返す。
    • 例えば、以下のようにインラインアセンブリを使用してみる。

    • ```cpp int foo = 10; const int bar = 15;

      asm volatile("add %0,%1,%2" :"=r"(foo) // 5 :"c"(foo), "c"(bar) ); ```

    • asm addi 10, zero, 15 sw 10, 4(2) lw 5, 8(2) addi 5, 10, 0 #APP add $11,$5,$5 #NO_APP

    • このように、アーキテクチャ独自のインラインアセンブリの記法を導入することができる。

  • MYRISCXVTargetLowering::getSingleConstraintMatchWeight () :

  • std::pair<unsigned, const TargetRegisterClass *> Cpu0TargetLowering::getRegForInlineAsmConstraint()

    • constraintに応じて、レジスタを取得する。つまり、今回のケースだとrcの場合にMYRISCVX::GPRRegClassを返す。cの場合には、優先的にt0 (=x5)が使用されるようになる。
  • void Cpu0TargetLowering::LowerAsmOperandForConstraint()

    • オペランドの文字解析を行いる。通常はTargetLowering::LowerAsmOperandForConstraintを使用しるが、MIPSの特殊なレジスタ記法 :
      • I, J, K, L, N, O, P
    • の場合に特殊な処理を行いる。
  • bool Cpu0TargetLowering::isLegalAddressingMode()

    • アドレッシングモードが正しく記述されているかどうかをチェックする。

上記の実装を追加した結果、以下のようにインラインアセンブリを含んだソースコードコンパイルできるようになった。

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch11_2.cpp -emit-llvm
./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=obj ch11_2.bc -o -
_Z16inlineasm_globalv:
        .frame  $8,16,$1
        .mask   0x00000100,-4
        .set    noreorder
        .cpload $t9
        .set    nomacro
# %bb.0:                                # %entry
        lui     10, %hi(_gp_disp)
        addi    10, 10, %lo(_gp_disp)
        addi    2, 2, -16
        sw      8, 12(2)                # 4-byte Folded Spill
        move    8, 2
        lui     10, %got_hi(g)
        add     10, 10, 3
        lw      10, %got_lo(g)(10)
        addi    11, 10, 8
        #APP
        lw $11,0($11)
        #NO_APP
        sw      11, 8(2)
        lw      11, 8(2)
        #APP
        addi $11,$11,1
        #NO_APP
        sw      11, 4(2)
        lw      10, 4(2)
        move    2, 8
        lw      8, 12(2)                # 4-byte Folded Reload
        addi    2, 2, 16
        jalr    1