FPGA開発日記

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

オリジナルLLVM Backendを追加しよう (28. C++のサポート)

https://cdn-ak.f.st-hatena.com/images/fotolife/m/msyksphinz/20181123/20181123225150.png

LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。

jonathan2251.github.io

C++のサポート

LLVMC++特殊な文法をサポートするためには、ポリフォーフィズムをサポートする必要がある。例えば、以下のようなプログラムを考える。

  • lbdex/input/ch12_inherit.cpp
class CPolygon { // _ZTVN10__cxxabiv117__class_type_infoE for parent class
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
    { width=a; height=b; }
    virtual int area (void) { return 0;};
    void printarea (void)
    { printf("%d\n", this->area()); }
  };

class CRectangle: public CPolygon {
  public:
    int area (void)
    { return (width * height); }
};

class CTriangle: public CPolygon {
  public:
    int area (void)
    { return (width * height / 2); }
};

class CAngle: public CPolygon {
  public:
    int area (void)
    { return (width * height / 4); }
};

int test_cpp_polymorphism() {
  CRectangle poly1;
  CTriangle poly2;
  CAngle poly3;

  CPolygon * ppoly1 = &poly1;
  CPolygon * ppoly2 = &poly2;
  CPolygon * ppoly3 = &poly3;

  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  ppoly3->printarea();

上記のC++コードでは、ベースクラスCPolygonに対して3つの派生クラスCRectangle, CTriangle, CAngleが定義されている。 この時、print_area()を呼び出した場合、それぞれの派生クラスで呼び出されるarea()メソッドが異なり、異なる結果が得られる。 C++のサポートでは、このような文法を考慮する必要がある。

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch12_inherit.cpp -emit-llvm -o - | ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm -o -

また、C++では、try - catchのような例外動作も考慮する必要がある。

  • lbdex/input/ch12_eh.cpp
class Ex1 {};
void throw_exception(int a, int b) {
  Ex1 ex1;

  if (a > b) {
    throw ex1;
  }
}

int test_try_catch() {
  try {
    throw_exception(2, 1);
  }
  catch(...) {
    return 1;
  }
  return 0;
}

このプログラムでは例外を処理するために、try - catchの構文を使っている。これをLLVMコンパイルすると、

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch12_eh.cpp -emit-llvm -o - | ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm -o -
Selecting: t4: i32,ch = load<(dereferenceable load 4 from %ir.retval)> t0, FrameIndex:i32<0>, undef:i32
Selecting: t5: i32 = Register $a0
Selecting: t0: ch = EntryToken
llc: /home/msyksphinz/work/llvm/llvm-myriscvx/lib/CodeGen/LiveVariables.cpp:133: void llvm::LiveVariables::HandleVirtRegUse(unsigned int, llvm::MachineBasicBlock*, llvm::MachineInstr&): Assertion `MRI->getVRegDef(reg) && "Register use before def!"' failed.
Stack dump:
0.      Program arguments: ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm -o -
1.      Running pass 'Function Pass Manager' on module '<stdin>'.
2.      Running pass 'Live Variable Analysis' on function '@_Z14test_try_catchv'
^C

llcが正しくIRを処理することができなかった。LLVM IRの逆アセンブルを生成して確認してみる。

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch12_eh.cpp -emit-llvm -o - | ./bin/llvm-dis
; Function Attrs: noinline optnone
define dso_local void @_Z15throw_exceptionii(i32 signext %a, i32 signext %b) #0 {                                                                                                                                                                                              entry:
  %a.addr = alloca i32, align 4
  %b.addr = alloca i32, align 4
  %ex1 = alloca %class.Ex1, align 1
  store i32 %a, i32* %a.addr, align 4
  store i32 %b, i32* %b.addr, align 4
  %0 = load i32, i32* %a.addr, align 4
  %1 = load i32, i32* %b.addr, align 4
  %cmp = icmp sgt i32 %0, %1
  br i1 %cmp, label %if.then, label %if.end

if.then:                                          ; preds = %entry
  %exception = call i8* @__cxa_allocate_exception(i32 1) #1
  %2 = bitcast i8* %exception to %class.Ex1*
  call void @__cxa_throw(i8* %exception, i8* bitcast ({ i8*, i8* }* @_ZTI3Ex1 to i8*), i8* null) #2
  unreachable

if.end:                                           ; preds = %entry
  ret void
}

そこで、以下のコードを追加した。

  • lib/Target/MYRISCVX/MYRISCVXISelLowering.h
  /// 例外アドレスが格納されるレジスタを指定する。今回はA0レジスタ
  unsigned
  getExceptionPointerRegister(const Constant *PersonalityFn) const override {
    return MYRISCVX::A0;
  }

  /// 例外要因が格納されるレジスタを指定する。今回はA1レジスタ
  unsigned
  getExceptionSelectorRegister(const Constant *PersonalityFn) const override {
    return MYRISCVX::A1;
  }

LLVMをリビルドし、同じソースコードを試してみる。

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch12_eh.cpp -emit-llvm -o - | ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm -o - | less

ビルドできた。test_try_catch()を確認する。

_Z14test_try_catchv:
$tmp3:
.set $func_begin0, ($tmp3)
        .cfi_startproc
        .cfi_personality 0, __gxx_personality_v0
        .cfi_lsda 0, $exception0
        .frame  $x8,24,$x1
        .mask   0x00000100,-4
        .set    noreorder
        .cpload $t9
        .set    nomacro
# %bb.0:                                # %entry
        lui     x10, %hi(_gp_disp)
        addi    x10, x10, %lo(_gp_disp)
        addi    x2, x2, -24
        .cfi_def_cfa_offset 24
        sw      x8, 20(x2)              # 4-byte Folded Spill
        .cfi_offset 8, -4
        move    x8, x2
        .cfi_def_cfa_register 8
        .cprestore      8
$tmp0:
        lw      x3, %call16(_Z15throw_exceptionii)(x3)
        addi    x10, zero, 2
        addi    x11, zero, 1
        jalr    x3
        lw      x3, 8(x2)
$tmp1:
        jal     $BB1_1
$BB1_1:                                 # %invoke.cont
        jal     $BB1_4
$BB1_2:                                 # %lpad
$tmp2:
        lw      x3, 8(x2)
        sw      x30, 12(x2)
        sw      x31, 8(x2)
        jal     $BB1_3

スレッドローカルストレージのサポート

スレッドローカルストレージとは、C++11からサポートされているスレッド固有の変数で、スレッド毎の静的記憶領域に変数が書き込まれる。 staticに対してthread_localキーワードの場合はスレッド固有の領域に変数が用意される。

  • lbdex/input/ch12_thread_var.cpp
__thread int a = 0;
thread_local int b = 0; // need option -std=c++11
int test_thread_var()
{
    a = 2;
    return a;
}

int test_thread_var_2()
{
    b = 3;
    return b;
}

上記をコンパイルしてみると、以下のようなエラーが発生してしまった。GlobalTLSAddressLLVM IRを追加する必要がありそうだ。

./bin/clang -target mips-unknown-linux-gnu -c ../lbdex/input/ch12_thread_var.cpp -emit-llvm -o - | ./bin/llc -march=myriscvx32 -relocation-model=pic -filetype=asm -o - | less
Selecting: t9: ch = MYRISCVXISD::Ret t8, Register:i32 $a0, t8:1
Selecting: t8: ch,glue = CopyToReg t5, Register:i32 $a0, t6
Selecting: t6: i32,ch = load<(dereferenceable load 4 from @a)> t5, GlobalTLSAddress:i32<i32* @a> 0, undef:i32
Selecting: t5: ch = store<(store 4 into @a)> t0, Constant:i32<2>, GlobalTLSAddress:i32<i32* @a> 0, undef:i32
Selecting: t7: i32 = Register $a0
Selecting: t2: i32 = GlobalTLSAddress<i32* @a> 0
LLVM ERROR: Cannot select: t2: i32 = GlobalTLSAddress<i32* @a> 0
In function: _Z15test_thread_varv
  • MYRISCVXAsmParser.cpp : evaluateRelocExpr()
  • MYRISCVXMCExpr.cpp : printImpl()

を改造する必要がある。追加しなければならないLLVM IRは、tlsgd, tlsldm, dtp_hi, dtp_lo, gottp, tp_hi, tp_loです。

  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXAsmBackend.cpp
//@getFixupKindInfo {
const MCFixupKindInfo &MYRISCVXAsmBackend::
getFixupKindInfo(MCFixupKind Kind) const {
  const static MCFixupKindInfo Infos[MYRISCVX::NumTargetFixupKinds] = {
...
    { "fixup_MYRISCVX_TLSGD"    , 0, 16, 0 },
    { "fixup_MYRISCVX_GOTTP"    , 0, 16, 0 },
    { "fixup_MYRISCVX_TP_HI"    , 0, 16, 0 },
    { "fixup_MYRISCVX_TP_LO"    , 0, 16, 0 },
    { "fixup_MYRISCVX_TLSLDM"   , 0, 16, 0 },
    { "fixup_MYRISCVX_DTP_HI"   , 0, 16, 0 },
    { "fixup_MYRISCVX_DTP_LO"   , 0, 16, 0 }
  • lib/Target/MYRISCVX/MCTargetDesc/MYRISCVXBaseInfo.h
  namespace MYRISCVXII {

    /// Target Operand Flag enum.
    enum TOF {
      //===------------------------------------------------------------------===//
      // MYRISCVX Specific MachineOperand flags.
...
      // MO_TLSGD : モジュールIDとTSLブロックオフセットが存在するグローバルオフセットテーブルのオフセットを示している。
      // グローバル動的TLS向け
      MO_TLSGD,

      // MO_TLSLDM : モジュールIDとTSLブロックオフセットが存在するグローバルオフセットテーブルのオフセットを示している。
      // ローカル動的TLS向け
      MO_TLSLDM,
      MO_DTP_HI,
      MO_DTP_LO,

      // MO_GOTTPREL : スレッドポインタのオフセットを示す
      // 初期動作TLS向け
      MO_GOTTPREL,

      // MO_TPREL_HI/LO : スレッドポインタからのオフセットのHIとLO領域を示す。
      // ローカル動作TLS向け
      MO_TP_HI,
      MO_TP_LO,
    }; // enum TOF {

さらに、target descriptionファイルに以下を追加します。

// TlsGd node is used to handle General Dynamic TLS
def MYRISCVXTlsGd : SDNode<"MYRISCVXISD::TlsGd", SDTIntUnaryOp>;

// TpHi and TpLo nodes are used to handle Local Exec TLS
def MYRISCVXTpHi  : SDNode<"MYRISCVXISD::TpHi", SDTIntUnaryOp>;
def MYRISCVXTpLo  : SDNode<"MYRISCVXISD::TpLo", SDTIntUnaryOp>;

def : Pat<(MYRISCVXHi tglobaltlsaddr:$in), (LUi tglobaltlsaddr:$in)>;

def : Pat<(MYRISCVXLo tglobaltlsaddr:$in), (ORi ZERO, tglobaltlsaddr:$in)>;

def : Pat<(add CPURegs:$hi, (MYRISCVXLo tglobaltlsaddr:$lo)),
              (ORi CPURegs:$hi, tglobaltlsaddr:$lo)>;

def : WrapperPat<tglobaltlsaddr, ORi, CPURegs>;

これで最終的にスレッドローカルストレージを含むソースコードコンパイルできるようになった。LLVMがスレッドローカルストレージのアドレスを生成する手順を以下にまとめた。

  • IR:load i32* @a, align 4;(add Cpu0ISD::Hi Cpu0ISD::Lo);ori $2, $zero, %tp_lo(a); lui $3, %tp_hi(a); addu $3, $3, $2;
  • IR: ret i32* @b;%0=(add Cpu0ISD::Hi Cpu0ISD::Lo);...ori $2, $zero, %tp_lo(a); lui $3, %tp_hi(a); addu $3, $3, $2;