FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

オリジナルLLVMバックエンド実装をまとめる(20. ByVal属性のついた引数を扱うための処理)

今までの関数処理の中で、引数は基本的にポインタか値渡し、そして値も何らかの型の値を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 -c -emit-llvm # -target=riscv32-unknown-elfではbyval属性が出ないので、何もオプションを付けない。
./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) #0 {
entry:
  br label %for.body
...

define dso_local void @_Z9call_funcv() #0 {
...
  ; 呼び出し側の引数にbyval属性が追加された
  call void @_Z4func1S(%struct.S* byval align 8 %agg.tmp)

引数にbyval属性が付加されていることが分かる。byval属性が付加されている引数には、特別な処理を付け加える。

  1. CallingConvの定義を修正して、ByVal属性の引数が入ってきたときの挙動を変える。
  2. MYRISCVXTargetLowering::HandleByVal()を実装して、ByVal属性の挙動を実装する。
  3. MYRISCVXTargetLowering::LowerFormalArguments()の実装を追加して、ByVal属性の引数を受け取った場合の挙動を追加する。
f:id:msyksphinz:20191012020813p:plain
構造体を値渡しで呼んだ場合の引数の処理。ByVal属性が付属される。一部はレジスタ渡し、残りはスタック渡しされる。

呼び出され側の処理

CallingConvの修正

まず、呼び出し規約の定義を修正して、ByVal属性の引数が入ってきたときは特殊な処理を行うように変更する。 MYRISCVXCallingConv.tdを修正するわけだが、ByVal属性の時に動きを変える特殊な命令を記述する。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
def CC_LP32 : CallingConv<[
  // Promote i8/i16 arguments to i32.
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // Put ByVal arguments directly on the stack. Minimum size and alignment of a
  // slot is 32-bit.
  CCIfByVal<CCPassByVal<4, 4>>,
...

def CC_STACK32 : CallingConv<[
  // Promote i8/i16 arguments to i32.
  CCIfType<[i1, i8, i16], CCPromoteToType<i32>>,

  // Put ByVal arguments directly on the stack. Minimum size and alignment of a
  // slot is 32-bit.
  CCIfByVal<CCPassByVal<4, 4>>,
...

上記ではCC_LP32CC_STACK32のみを示したが、CC_LP64およびCC_STACK64にも同様の記述を行う。 CCIfByValという記述を追加した。 これにより、ByVal属性の引数が与えられたときはCCPassByValが呼び出される。 具体的にいうと、生成されたMYRISCVXGenCallingConv.incに以下のような条件文が追加されている。

  • build-myriscvx80/lib/Target/MYRISCVX/MYRISCVXGenCallingConv.inc
static bool CC_LP32(unsigned ValNo, MVT ValVT,
                    MVT LocVT, CCValAssign::LocInfo LocInfo,
                    ISD::ArgFlagsTy ArgFlags, CCState &State) {
...
  if (ArgFlags.isByVal()) {
    State.HandleByVal(ValNo, ValVT, LocVT, LocInfo, 4, 4, ArgFlags);
    return false;
  }
...

CCpasByValにより、State.HandleByValが呼び出される。 これはMYRISCVXTargetLoweringに記述する。

  • llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
void MYRISCVXTargetLowering::HandleByVal(CCState *State, unsigned &Size,
                                         unsigned Align) const
{
...
  if (State->getCallingConv() != CallingConv::Fast) {
    unsigned RegSizeInBytes = Subtarget.getGPRSizeInBytes();
    ArrayRef<MCPhysReg> IntArgRegs = ABI.GetByValArgRegs();
...
    // 割り当てられていない引数渡しレジスタを取得する。
    FirstReg = State->getFirstUnallocated(IntArgRegs);
...
    // 引数の数だけ引数渡しレジスタを割り当てる。
    Size = alignTo(Size, RegSizeInBytes);
    for (unsigned I = FirstReg; Size > 0 && (I < IntArgRegs.size());
         Size -= RegSizeInBytes, ++I, ++NumRegs) {
      State->AllocateReg(IntArgRegs[I], ShadowRegs[I]);
    }
  }
  // CallingConvの情報に、ByValRegsの情報を登録。
  State->addInRegsParamInfo(FirstReg, FirstReg + NumRegs);
}
f:id:msyksphinz:20191012020954p:plain
ByVal属性の引数を関数呼び出され側での取り扱い。AnalyzeFormalArguments()で解析し、HandleByVal()を呼ぶ。

LowerFormalArguments()の修正

次に、関数呼び出され側の引数を解析するためのLowerFormalArgumets()を編集する。引数の解析にByvalの条件を追加する。

  • llvm-myriscvx80/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属性が付加されている引数を見つけると、先ほどHandleByValで解析した情報に基づいて命令の生成に入る。 まず、CCInfo.getInRegsParamInfo()により、当該引数により引数わたしのレジスタの何番から何番までが使用されるかを取得する。 そして、この情報に基づいてcopyByValRegs()を呼び出す。

void MYRISCVXTargetLowering::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;
...
  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);
  }

copyByValRegsで行っている処理だが、これはレジスタ経由で受け取った引数をスタックに戻す処理を行っている。 スタックポインタをベースにして、ストア命令を生成していることが分かる。 関数の呼びされ側(Callee)では、レジスタ渡しで引数を受け取っても、受け取った値をいったんスタックに戻しているのである。

f:id:msyksphinz:20191012021030p:plain
LowerFormalArguments()において、ByVal属性の引数を見つけると、copyByValRegs()が呼び出される。