FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (37. 構造体を値渡しするためのByval属性の引数 1)

f:id:msyksphinz:20190425001356p:plain

今までの関数処理の中で、引数は基本的にポインタか値渡し、そして値も何らかの型の値を1つずつ渡していくという形式だった。しかし、C言語では構造体などの複数の要素をまとめた型を渡すことができる。また、C言語では構造体の値をそのまま値渡しで引数として渡すことができる。

  • func_struct_simple.cpp
struct S
{
  int x[17];
};

void func (struct S elem) {
  for(int i = 0; i < 17; i++) {
    elem.x[i] ++;
  }
}

void call_func () {
  struct S elem;
  for (int i = 0; i < 17; i++) {
    elem.x[i] = i;
  }
  func (elem);
}

上記のプログラムでは、funcに構造体struct S elemを値渡ししている。つまり、func内でstruct S elemの変更を行っても、その変更の結果は関数の呼び出し側にの値に影響を与えない。ポインタではないのでアセンブリ的には、引数の要素を一つ一つコピーして、関数呼び出され側に値を渡してやる必要がある。

上記の例では、実際に渡さなければならない値はx[17]の17個だ。しかしMYRISCVXのABIでは引数私で使用できるレジスタの数はA0-A7の8個までだ。この場合、どのようにして構造体の値を渡していけばよいだろう?

まず、clangでbyval属性の引数が生成されることを確認する。上記のfunc_struct_simple.cppをclangでLLVM IRを生成する。

./bin/clang -O1 func_struct_simple.cpp -emit-llvm
./bin/llvm-dis func_struct_simple.bc -o -
%struct.S = type { [17 x i32] }
; byval属性が引数に付加された。
define dso_local void @_Z4func1S(%struct.S* byval nocapture align 8 %elem) local_unnamed_addr #0 {
entry:
  br label %for.body
...

引数にbyval属性が付加されていることが分かる。byval属性が付加されている引数には、特別な処理を付け加える。引数の解析だから、当然処理を追加しなければならないのはMYRISCVXTargetLowering::LowerFormalArguments()となる。

まず、MYRISCVXのCalling ConventionにByVal渡しを有効にしなければなりない。このためには、MYRISCVXCallingConv.tdを追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
...
def CC_MYRISCVX_ByVal : CallingConv<[   // ByVal渡しを有効とする。
  CCIfByVal<CCPassByVal<4, 4>>
]>;
...
def CC_LP32 : CallingConv<[
  CCIfByVal<CCDelegateTo<CC_MYRISCVX_ByVal>>,   // LP32ではByVal渡しを有効にする。
...
]>;
...
def CC_STACK32 : CallingConv<[
  CCIfByVal<CCDelegateTo<CC_MYRISCVX_ByVal>>,   // STACK32ではByVal渡しを有効にする。
...
]>;

次にLowerFormalArgumetsを編集する。引数の解析にByvalの条件を追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
SDValue
MYRISCVXTargetLowering::LowerFormalArguments (SDValue Chain,
                                              CallingConv::ID CallConv,
                                              bool IsVarArg,
                                              const SmallVectorImpl<ISD::InputArg> &Ins,
                                              const SDLoc &DL, SelectionDAG &DAG,
                                              SmallVectorImpl<SDValue> &InVals) const {

...
  CCInfo.rewindByValRegsInfo();
...
  // 引数毎に処理を行うループ
  for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
    ...
    ISD::ArgFlagsTy Flags = Ins[i].Flags;
    ...
    // ByVal属性がついているかチェックする
    if (Flags.isByVal()) {
      assert(Ins[i].isOrigArg() && "Byval arguments cannot be implicit");
      CCInfo.getInRegsParamInfo(ByValIdx, FirstByValReg, LastByValReg);

      assert(Flags.getByValSize() &&
             "ByVal args of size 0 should have been ignored by front-end.");
      assert(ByValIdx < CCInfo.getInRegsParamsCount());
      copyByValRegs(Chain, DL, OutChains, DAG, Flags, InVals, &*FuncArg,
                    FirstByValReg, LastByValReg, VA, CCInfo);
      CCInfo.nextInRegsParam();
      continue;
    }
...

ByVal属性が付加されている場合の処理を追加した。まず、ByVal属性が付加されている引数を見つけると、これがレジスタ経由で渡すことができるのか、そしてレジスタ私ができる場合はどのレジスタが使用できるのかを確認する。これがCCInfo.getInRegsParamInfo(ByValIdx, FirstByValReg, LastByValReg); だ。FirstByValRegsLastByValRegsが、引数渡しの場合に使用できるレジスタの最初のインデックスと最後のインデックスになる。この情報を使用してレジスタコピーのコードを生成する。具体的には、RISC-Vの場合はA0からA7までのレジスタのうち、上記のサンプルコードならばx[0]-x[7]までの値がレジスタ渡しで渡される。この値を、スタックに置きなおして実際に使用できるようにするのがcopyByValRegsの働きになる。

void MipsTargetLowering::copyByValRegs(
    SDValue Chain, const SDLoc &DL, std::vector<SDValue> &OutChains,
    SelectionDAG &DAG, const ISD::ArgFlagsTy &Flags,
    SmallVectorImpl<SDValue> &InVals, const Argument *FuncArg,
    unsigned FirstReg, unsigned LastReg, const CCValAssign &VA,
    MipsCCState &State) const {
...
  unsigned NumRegs = LastReg - FirstReg;  // 引数渡しに使用できるレジスタの数
  unsigned RegAreaSize = NumRegs * GPRSizeInBytes;
...
  if (!NumRegs)  // 引数渡しに使用できるレジスタがなければ、この処理は不要
    return;
...
  for (unsigned I = 0; I < NumRegs; ++I) {
    unsigned ArgReg = ByValArgRegs[FirstReg + I];
    unsigned VReg = addLiveIn(MF, ArgReg, RC);
    unsigned Offset = I * GPRSizeInBytes;
    SDValue StorePtr = DAG.getNode(ISD::ADD, DL, PtrTy, FIN,
                                   DAG.getConstant(Offset, DL, PtrTy));
    SDValue Store = DAG.getStore(Chain, DL, DAG.getRegister(VReg, RegTy),
                                 StorePtr, MachinePointerInfo(FuncArg, Offset));
    OutChains.push_back(Store);
  }

スタックポインタをベースにして、ストア命令を生成する。関数の呼びされ側(Callee)では、レジスタ渡しを行った値をスタックに戻しているのだ。

f:id:msyksphinz:20190707001313p:plain
構造体を値渡しで呼んだ場合の引数わたしの処理。ByVal属性が付属される。一部はレジスタ渡し、残りはスタック渡しされる。