FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (38. 可変長引数のサポート)

f:id:msyksphinz:20190425001356p:plain

C言語の可変長引数では、例えば以下のような記述が可能となる。

  • vararg.cpp
#include <stdarg.h>

int sum_i(int amount, ...)
{
  int i = 0;
  int val = 0;
  int sum = 0;
    
  va_list vl;
  va_start(vl, amount);
  for (i = 0; i < amount; i++)
  {
    val = va_arg(vl, int);
    sum += val;
  }
  va_end(vl);
  
  return sum; 
}

int test_vararg()
{
  int a = sum_i(10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
  return a;
}

C言語の文法としては少し特殊な形をしているかもしれないが、フロントエンドによって一度IRの形式になってしまえば、あとはレジスタを割り当てて命令を生成するだけなのでそこまで難しくはなさそうだ。ただし、この可変長引数をサポートするためには、DAGによってさらに別の種類のノードをサポートする必要がある。

まずは、可変長引数のC言語プログラムから生成されたLLVM IRを見てみる。

./bin/clang vararg.cpp -emit-llvm -o vararg.bc
./bin/llvm-dis vararg.bc -o -
define dso_local i32 @_Z11test_varargv() #0 {
entry:
...
   %call = call i32 (i32, ...) @_Z5sum_iiz(i32 10, i32 0, i32 1, i32 2, i32 3, i32 4, i32 5, i32 6, i32 7, i32 8, i32 9)
...
define dso_local i32 @_Z5sum_iiz(i32 %amount, ...) #0 {
entry:
...
  %arraydecay = getelementptr inbounds [1 x %struct.__va_list_tag], [1 x %struct.__va_list_tag]* %vl, i32 0, i32 0
  %arraydecay1 = bitcast %struct.__va_list_tag* %arraydecay to i8*
  // va_start()を呼ぶ
  call void @llvm.va_start(i8* %arraydecay1)
...
vaarg.in_reg:                                     ; preds = %for.body
...
for.end:                                          ; preds = %for.cond
  // va_end()を呼ぶ
  call void @llvm.va_end(i8* %arraydecay34)
    ret i32 %11
}

in_regの場合と、in_memの場合が挙げられている。for.bodyから進んでいき、%gp_offsetが40よりも小さい場合、in_regに飛んでいき、そうでない場合はin_memに飛んでいくようだ。

このコードの意図をはっきりと説明した文章は見つけられないが、考えられるのは、Callee側が受け入れることのできる引数が可変長だと、コンパイル時にCalleeが使用するスタックの量をあらかじめ算出すことができない、こうすると面倒なので、40バイトまでの可変引数までならスタック経由で渡せるようにする、それ以降はグローバル領域を確保して受け渡す、というようにしていると思われる。

SelectionDAGの生成

VAARG, VACOPY, VAENDの3つは最適化の最中に生成されないように抑制する。つまり、MYRISCVXTargetLoweringにて、setOperationInAction()で対象となるノードの生成を抑制する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
  // For Var Arguments
  setOperationAction(ISD::VASTART,   MVT::Other, Custom);

  // Support va_arg(): variable numbers (not fixed numbers) of arguments
  //  (parameters) for function all
  setOperationAction(ISD::VAARG,        MVT::Other, Expand);
  setOperationAction(ISD::VACOPY,       MVT::Other, Expand);
  setOperationAction(ISD::VAEND,        MVT::Other, Expand);
...
  • VASTART : 可変長引数の処理を開始する。
  • VAARG : 可変長引数から実際の値を取り出す。
  • VACOPY
  • VAEND : 可変長引数の処理を終了する。

VASTARTだけはカスタム実装に設定したので、lowerVASTART()のみ実装する。

SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
    case ISD::SELECT        : return lowerSELECT(Op, DAG);
    case ISD::GlobalAddress : return lowerGlobalAddress(Op, DAG);
    case ISD::VASTART       : return lowerVASTART(Op, DAG);
  }
  return SDValue();
}

lowerVASTARTでは、

SDValue MYRISCVXTargetLowering::lowerVASTART(SDValue Op, SelectionDAG &DAG) const {
  MachineFunction &MF = DAG.getMachineFunction();
  MYRISCVXFunctionInfo *FuncInfo = MF.getInfo<MYRISCVXFunctionInfo>();

  SDLoc DL = SDLoc(Op);
  SDValue FI = DAG.getFrameIndex(FuncInfo->getVarArgsFrameIndex(),
                                 getPointerTy(MF.getDataLayout()));

  // vastart just stores the address of the VarArgsFrameIndex slot into the
  // memory location argument.
  const Value *SV = cast<SrcValueSDNode>(Op.getOperand(2))->getValue();
  return DAG.getStore(Op.getOperand(0), DL, FI, Op.getOperand(1),
                      MachinePointerInfo(SV));
}

LowerFormalArguments()つまり可変長引数を持つ関数が呼び出されたときの引数の処理について、可変長引数であれば専用のルーチンを呼び出す。writeVarArgRegsを呼び出す。

SDValue
MYRISCVXTargetLowering::LowerFormalArguments (SDValue Chain,
                                              CallingConv::ID CallConv,
                                              bool IsVarArg,
                                              const SmallVectorImpl<ISD::InputArg> &Ins,
                                              const SDLoc &DL, SelectionDAG &DAG,
                                              SmallVectorImpl<SDValue> &InVals) const {
...
  // 可変長引数を処理する場合は、特殊なwriteVarArgRegsに飛ぶ。
  if (IsVarArg)
    writeVarArgRegs(OutChains, Chain, DL, DAG, CCInfo);

  return Chain;
}

このwriteVarArgRegsは、ABIがLP32の時にレジスタ渡しをしたデータをスタックに戻す。せっかくレジスタ渡しをしたのにどうしてスタックに戻すんだ?と思われるかもしれないが、それ以降の処理は一律してfor文の処理で統一するため、レジスタ渡し、メモリロードの2種類でアセンブリを生成したくない、という意図があると思われる。

  /* for文の前に、レジスタ渡しした可変長引数の要素をスタックに置いておく。 */
  for (i = 0; i < amount; i++)
  {
    /* valの取得はスタックから一律に受け取る */
    val = va_arg(vl, int);
    sum += val;
  }
void MYRISCVXTargetLowering::writeVarArgRegs(std::vector<SDValue> &OutChains,
                                             SDValue Chain, const SDLoc &DL,
                                             SelectionDAG &DAG,
                                             CCState &State) const {
  ArrayRef<MCPhysReg> ArgRegs = ABI.GetVarArgRegs();
  unsigned Idx = State.getFirstUnallocated(ArgRegs);
  unsigned RegSizeInBytes = Subtarget.getGPRSizeInBytes();
  MVT RegTy = MVT::getIntegerVT(RegSizeInBytes * 8);

  ...
  /* 引数渡しに使用しているレジスタを1つ1つ退避していく */
  for (unsigned I = Idx; I < ArgRegs.size();
       ++I, VaArgOffset += RegSizeInBytes) {

    unsigned Reg = addLiveIn(MF, ArgRegs[I], RC);
    SDValue ArgValue = DAG.getCopyFromReg(Chain, DL, Reg, RegTy);
    FI = MFI.CreateFixedObject(RegSizeInBytes, VaArgOffset, true);
    SDValue PtrOff = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout()));
    /* Store命令を生成 */
    SDValue Store =
        DAG.getStore(Chain, DL, ArgValue, PtrOff, MachinePointerInfo());
    cast<StoreSDNode>(Store.getNode())->getMemOperand()->setValue(
        (Value *)nullptr);
    OutChains.push_back(Store);
  }
}