今までの関数処理の中で、引数は基本的にポインタか値渡し、そして値も何らかの型の値を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属性が付加されている引数には、特別な処理を付け加える。
- CallingConvの定義を修正して、
ByVal
属性の引数が入ってきたときの挙動を変える。 MYRISCVXTargetLowering::HandleByVal()
を実装して、ByVal属性の挙動を実装する。MYRISCVXTargetLowering::LowerFormalArguments()
の実装を追加して、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_LP32
とCC_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); }
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)では、レジスタ渡しで引数を受け取っても、受け取った値をいったんスタックに戻しているのである。