FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(11. グローバル変数の生成について)

今回は、グローバル変数を取り扱うための手法について調査する。 現状では、グローバル変数の入っているプログラムをコンパイルすると以下のようにエラーが発生する。

  • global_access.cpp
int global0 = 0;
int global1 = 100;

void global_access ()
{
  int i = 0;
  i = global1;

  return;
}
./bin/clang -target riscv32-unknown-elf -c global_access.cpp -emit-llvm --target=riscv32-unknown-elf -o global_access.rv32.bc
./bin/llc -debug -march=myriscvx64 -filetype=asm rotate_test.rv64.bc -o -
ISEL: Starting pattern match
  Initial Opcode index to 0
  Match failed at index 0
LLVM ERROR: Cannot select: t5: i32 = GlobalAddress<i32* @global1> 0

GlobalAddressというIRを処理する必要がありそうだ。

まず、llvm-disでLLVM IRを見てみる。

./bin/llvm-dis global_access.rv32.bc -o -
@global0 = dso_local global i32 0, align 4
@global1 = dso_local global i32 100, align 4

; Function Attrs: noinline nounwind optnone
define dso_local void @_Z13global_accessv() #0 {
entry:
  %i = alloca i32, align 4
  store i32 0, i32* %i, align 4
  %0 = load i32, i32* @global1, align 4
  store i32 %0, i32* %i, align 4
  ret void
}

このグローバル変数を処理するためには、コンパイラには2種類のポリシーがある。

  • PIC(Position Independent Code) : ポジション独立。グローバル変数のアドレスはランタイムに計算される。ライブラリなどを生成した場合、グローバル変数の位置はグローバル変数がロードされたときに決定される。このような場合は、グローバル変数の場所は固定せずに、動的に計算できるような仕組みを持っておく。
  • Static : グローバル変数のアドレスは固定で計算される。ライブラリなどで使用せず、常にグローバル変数へのアクセスする命令からの距離が変わらない場合に使用される。

ライブラリとして提供する場合、グローバル変数はPICモードでコンパイル必要があるわけだが、そうでない場合はstaticでコンパイルしても良いわけだ。

この2つのポリシーを切り替えるためには、llcで-relocation-modelオプションを使用して指定する。

これらの2種類のポリシーについて、命令をどのように生成すればよいかを見ていく。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp まず、setOperationAction(Custom)で、GlobalAddress IRの場合はカスタム実装したLower処理を実行するように設定する。
//@MYRISCVXTargetLowering {
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
    : TargetLowering(TM), Subtarget(STI), ABI(TM.getABI()) {
  setOperationAction(ISD::GlobalAddress, XLenvT, Custom);
}
SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
    case ISD::GlobalAddress: return lowerGlobalAddress(Op, DAG);
  }
  return SDValue();
}
f:id:msyksphinz:20190928014712p:plain
lowerGlobalAddress()の概要

lowerGlobalAddressでは、PICモードとStaticモードの両方に対してDAGの生成を行う。

SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op,
                                                   SelectionDAG &DAG) const {
  • PICモードの場合:getAddrGlobalGOTを呼び出してGOTを用いたアドレス計算命令を生成する。
SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op,
                                                   SelectionDAG &DAG) const {
...
  return getAddrGlobalGOT(
      N, Ty, DAG, MYRISCVXII::MO_GOT_HI20, MYRISCVXII::MO_GOT_LO12,
      DAG.getEntryNode(),
      MachinePointerInfo::getGOT(DAG.getMachineFunction()));
  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h

グローバルポインタレジスタ(GP)にグローバル変数にアクセスするためのベースアドレスが格納されている。 そこで、このベースアドレスからのアドレスオフセットを計算する。 GPに対してのアドレスオフセットは、ここでは計算しないのでMYRISCVXII::MO_GOT_HI20, MYRISCVXII::MO_GOT_LO12というノードを使用している。 GP + MO_GOT_HI20をHI、MO_GOT_LO12をLOとして、これをWrapperノードを作成してまとめている。 このWrapperノードを対象として、ロード命令を生成することでグローバル変数にアクセスしている。

    //@getAddrGlobalGOT {
    // This method creates the following nodes, which are necessary for
    // computing a global symbol's address in large-GOT mode:
    //
    // (load (wrapper (add %hi(sym), $gp), %lo(sym)))
    template<class NodeTy>
    SDValue getAddrGlobalGOT(NodeTy *N, EVT Ty, SelectionDAG &DAG,
                             unsigned HiFlag, unsigned LoFlag,
                             SDValue Chain,
                             const MachinePointerInfo &PtrInfo) const {
      SDLoc DL(N);
      SDValue Hi = DAG.getNode(MYRISCVXISD::Hi, DL, Ty,
                               getTargetNode(N, Ty, DAG, HiFlag));
      Hi = DAG.getNode(ISD::ADD, DL, Ty, Hi, getGlobalReg(DAG, Ty));
      SDValue Wrapper = DAG.getNode(MYRISCVXISD::Wrapper, DL, Ty, Hi,
                                    getTargetNode(N, Ty, DAG, LoFlag));
      return DAG.getLoad(Ty, DL, Chain, Wrapper, PtrInfo);
    }
    //@getAddrGlobalGOT }

ここで、MYRISCVXISD::Hi, MYRISCVXISD::Lo, MYRISCVXISD::Wrapperノードを使っている。これはMYRISCVXInstrInfo.tdに定義している。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Hi and Lo nodes are used to handle global addresses. Used on
// MYRISCVXISelLowering to lower stuff like GlobalAddress, ExternalSymbol
// static model. (nothing to do with MYRISCVX Registers Hi and Lo)
def MYRISCVXHi    : SDNode<"MYRISCVXISD::Hi", SDTIntUnaryOp>;
def MYRISCVXLo    : SDNode<"MYRISCVXISD::Lo", SDTIntUnaryOp>;

これに対する命令の生成パタンを追加する。ここでは、MYRISCVXISD::HiはLUI命令、MYRISCVXISD::LoはORI命令で生成する。

// hi/lo relocs
def : Pat<(MYRISCVXHi tglobaladdr:$in), (LUI tglobaladdr:$in)>;
def : Pat<(MYRISCVXLo tglobaladdr:$in), (ORI ZERO, tglobaladdr:$in)>;
def : Pat<(add GPR:$hi, (MYRISCVXLo tglobaladdr:$lo)),
          (ORI GPR:$hi, tglobaladdr:$lo)>;

Wrapperは以下のように定義している。

def MYRISCVXWrapper    : SDNode<"MYRISCVXISD::Wrapper", SDTIntBinOp>;

生成パタンは以下だ。

//@ wrapper_pic
class WrapperPat<SDNode node, Instruction ORiOp, RegisterClass RC>:
      Pat<(MYRISCVXWrapper RC:$gp, node:$in),
          (ORiOp RC:$gp, node:$in)>;

def : WrapperPat<tglobaladdr, ORI, GPR>;
def : WrapperPat<texternalsym, ORI, GPR>;

tglobaladdr, texternalsymノードに対してパタンを生成している。tglobaladdr, texternalsymの定義はTargetSelectionDAG.tdに定義されている。

  • llvm-myriscvx80/include/llvm/Target/TargetSelectionDAG.td
...
def tglobaladdr : SDNode<"ISD::TargetGlobalAddress",  SDTPtrLeaf, [],
                         "GlobalAddressSDNode">;
...
def texternalsym: SDNode<"ISD::TargetExternalSymbol", SDTPtrLeaf, [],
                         "ExternalSymbolSDNode">;

これらの生成手法についてまとめる。

f:id:msyksphinz:20190928014800p:plain
MYRISCVXHi,MYRISCVXLoのノードと生成パタン
f:id:msyksphinz:20190928014831p:plain
MYRISCVXWrapperの生成ノードと生成パタン
  • Staticモードの場合

Staticモードの場合は、getAddrNonPIC関数を呼び出してアドレスを計算する。

    //@ %hi/%lo relocation
    return getAddrNonPIC(N, Ty, DAG);
  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op,
                                                   SelectionDAG &DAG) const {
...
  if (!isPositionIndependent()) {
    //@ %hi/%lo relocation
    return getAddrNonPIC(N, Ty, DAG);
  }
...

getAddrNonPICでは、単純にアクセスする対象の変数に対して、上位アドレス、下位アドレスを計算し、それを加算することでアドレスとしている。 グローバル変数の場所は常に固定なので、GPを使わずに単純にアドレスを加算するだけで算出できる。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
    template<class NodeTy>
    SDValue getAddrNonPIC(NodeTy *N, EVT Ty, SelectionDAG &DAG) const {
      SDLoc DL(N);
      SDValue Hi = getTargetNode(N, Ty, DAG, MYRISCVXII::MO_ABS_HI);
      SDValue Lo = getTargetNode(N, Ty, DAG, MYRISCVXII::MO_ABS_LO);
      return DAG.getNode(ISD::ADD, DL, Ty,
                         DAG.getNode(MYRISCVXISD::Hi, DL, Ty, Hi),
                         DAG.getNode(MYRISCVXISD::Lo, DL, Ty, Lo));
    }

それぞれのコンパイルされた命令は以下のようになる。

  • PICモードの場合 (32ビット)
        sw      $x10, 0($x2)
        lui     $x10, %got_hi(global1)
        add     $x10, $x10, $x3
        ori     $x10, $x10, %got_lo(global1)
        lw      $x10, 0($x10)
        lw      $x10, 0($x10)
  • PICモードの場合 (64ビット)
        lui     $x10, %got_hi(global1)
        add     $x10, $x10, $x3
        ori     $x10, $x10, %got_lo(global1)
        ld      $x10, 0($x10)
        lw      $x10, 0($x10)

まず変数global1の、GOTの先頭からの距離を%got_hi(global1)%got_lo(global1)を使って計算している(%got_hi(global1), %got_lo(global1)の具体的な値はリンク時に計算され、値が埋め込まれる。 そのためのブラックボックスを作っている、というイメージだ。 そしてそのアドレスをGOTの先頭を指しているGPに加算して具体的なglobal1のGOT内での絶対アドレスを得る。 そのアドレスに対してロード命令を行い、GOT内のglobal1用のエントリを参照して、global1の具体的な存在アドレスを算出し、その場所に改めてロード命令を行う、という手順になる。

  • Staticモードの場合 (32ビット)
        lui     $x10, %hi(global1)
        ori     $x10, $x10, %lo(global1)
        lw      $x10, 0($x10)
  • Staticモードの場合 (64ビット)
        lui     $x10, %hi(global1)
        ori     $x10, $x10, %lo(global1)
        lw      $x10, 0($x10)

Staticモードの場合は単純だ。リンク時に計算するためにグローバル変数のアドレスは%hi(global1), %lo(global1)として予約しておき、その場所に対するアドレスを計算して、ロード命令を実行する、というイメージだ。