FPGA開発日記

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

オリジナルLLVMバックエンド実装をまとめる(25. LLVM IRをサポートするためのIntrinsicsのサポート)

LLVM IRには、いくつかの特殊な構文が存在し、これらをサポートすることでより多くのコードを生成できるようになる。 これらについては、clang側からIntrinsic関数を呼び出すことでLLVM IRを生成し、llcに渡してテストすることができる。

f:id:msyksphinz:20191017002523p:plain
frame_addres, return_addressの取得と命令の生成。

FRAMEADDR

関数のフレームポインタを取得する。C言語では、__builtin_frame_address()により取得することができる。

  • func_frame_addr.cpp
void* display_frameaddress() {
  return (void *)__builtin_frame_address(0);
}

上記のプログラムからLLVMの中間表現を作ってみると、以下のようになった。 llvm.frameaddressが使用されている。llvm.frameaddressはスタックフレームのフレームポインタの値を返す。以下のウェブサイトに使用が説明されている。

https://llvm.org/docs/LangRef.html#llvm-frameaddress-intrinsic

./bin/clang ../myriscvx-tests/tests/func_frame_return_addr.cpp -c -emit-llvm -o - | ./bin/llvm-dis -o -
define dso_local i8* @_Z20display_frameaddressv() #0 {
entry:
  %0 = call i8* @llvm.frameaddress(i32 0)
  ret i8* %0
}

このframeaddressは、LLVM IRノードのFRAMEADDRに相当する。このFRAMEADDRを処理するために、MYRISCVXISelLowering.cppに処理を付け加える。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
//@MYRISCVXTargetLowering {
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
  setOperationAction(ISD::FRAMEADDR,     MVT::Other, Custom);
...
}

まず、setOperationAction()にてISD::FRAMEADDRの処理をカスタム関数に投げるように設定する。そして、実際にカスタム関数を作成して命令生成のためのノードを構築する。

SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
...
     case ISD::FRAMEADDR    : return lowerFRAMEADDR(Op, DAG);
...
  }
}
...
SDValue MYRISCVXTargetLowering::
lowerFRAMEADDR(SDValue Op, SelectionDAG &DAG) const {
  // check the depth
  assert((cast<ConstantSDNode>(Op.getOperand(0))->getZExtValue() == 0) &&
         "Frame address can only be determined for current frame.");

  MachineFrameInfo &MFI = DAG.getMachineFunction().getFrameInfo();
  MFI.setFrameAddressIsTaken(true);
  EVT VT = Op.getValueType();
  SDLoc DL(Op);
  SDValue FrameAddr = DAG.getCopyFromReg(
      DAG.getEntryNode(), DL, MYRISCVX::FP, VT);
  return FrameAddr;
}

lowerFRAMEADDRでは、フレームポインタレジスタFPの値を読み取り、getCopyFromReg()でノードを作成し、そのノードを返す。

_Z20display_frameaddressv:              # @_Z20display_frameaddressv
        .cfi_startproc

# %bb.0:                                # %entry
        addi    x10, x8, 0
        ret

フレームポインタレジスタFP(x8)の値を、戻地理レジスタx10にコピーしてから戻る。正しく動作しているようだ。

RETURNADDR

関数の戻りアドレスを取得する。つまり、関数実行中の戻りアドレス、レジスタで言うとraレジスタの中身を戻す。C言語では、__builtin_return_address()により取得することができる。

  • func_return_addr.cpp
extern int fn();

int display_returnaddress() {
  int a = (int)__builtin_return_address(0);
  fn();
  return a;
}

どのようなLLVM中間表現が生成されているか確認してみる。

./bin/clang func_return_addr.cpp -c -emit-llvm --target=riscv32-unknown-elf -o - | ./bin/llvm-dis -o -
define dso_local i32 @_Z21display_returnaddressv() #0 {
entry:
  %a = alloca i32, align 4
  %0 = call i8* @llvm.returnaddress(i32 0)
  %1 = ptrtoint i8* %0 to i32
  store i32 %1, i32* %a, align 4
  %call = call i32 @_Z2fnv()
  %2 = load i32, i32* %a, align 4
  ret i32 %2
}

llvm.returnaddressというノードが生成されており、frameaddress同様、戻りアドレスを示すノードになる。 このノードは、LLVM IRのRETURNADDRに相当する。このノードを処理するための実装を加える。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
//@MYRISCVXTargetLowering {
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
      setOperationAction(ISD::RETURNADDR,    MVT::Other, Custom);
...
}

SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
...
    case ISD::RETURNADDR   : return lowerRETURNADDR(Op, DAG);
  }
}
SDValue MYRISCVXTargetLowering::lowerRETURNADDR(SDValue Op,
                                                SelectionDAG &DAG) const {
  if (verifyReturnAddressArgumentIsConstant(Op, DAG))
    return SDValue();

  // check the depth
  assert((cast<ConstantSDNode>(Op.getOperand(0))->getZExtValue() == 0) &&
         "Return address can be determined only for current frame.");

  MachineFunction &MF = DAG.getMachineFunction();
  MachineFrameInfo &MFI = MF.getFrameInfo();
  MVT VT = Op.getSimpleValueType();
  unsigned RA = MYRISCVX::RA;
  MFI.setReturnAddressIsTaken(true);

  // Return RA, which contains the return address. Mark it an implicit live-in.
  unsigned Reg = MF.addLiveIn(RA, getRegClassFor(VT));
  return DAG.getCopyFromReg(DAG.getEntryNode(), SDLoc(Op), Reg, VT);
}

FRAMEADDR同様、raレジスタgetCopyFromRegでノードに取り込み、ノードを返すという仕組みになっている。

実際にコンパイルしてみる。

./bin/clang func_return_addr.cpp -c -emit-llvm --target=riscv32-unknown-elf -o func_return_addr.rv32.bc
./bin/llc -mtriple=myriscvx32 -filetype=asm -target-abi=lp func_return_addr.rv32.bc -o 
_Z21display_returnaddressv:             # @_Z21display_returnaddressv
        .cfi_startproc

# %bb.0:                                # %entry
        addi    x2, x2, -16
        .cfi_def_cfa_offset 16
        sw      x2, 12(x2)              # 4-byte Folded Spill
        .cfi_offset 2, -4
        sw      x1, 8(x2)
        addi    x2, x2, 0
        jal     _Z2fnv
        addi    x2, x2, 0
        lw      x10, 8(x2)
        lw      x2, 12(x2)              # 4-byte Folded Reload
        addi    x2, x2, 16
        ret

むだなアセンブリ命令が多くて分かりにくいが、要点は、最初のsw x1, 8(x2)raレジスタの値をスタックに退避し、lw x10, 8(x2)で戻り値レジスタに書き戻している。これで、現在の戻りアドレスを参照することができる。