LLVMにはすでにRISC-Vのバックエンドサポートが追加されている。しかし、勉強のために独自のRISC-V実装をLLVMに追加している。
第9章の後半では、可変長引数と、動的スタックの割り当てを実装する。
可変長引数のサポート
可変長引数は、例えば以下のようなコードをどのようにコンパイルするか、という問題だ。
#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(6, 0, 1, 2, 3, 4, 5); return a; }
この場合、まず行うことは引数からamountを取得し、引数の全体サイズを取得し、引数の各値をメモリからロードし、加算を行うという処理になる。 引数の数は6つのなので、スタックに6つの値が積み上げられ、関数に飛ぶ。
// int test_vararg() 内 addiu $2, $zero, 5 st $2, 24($sp) addiu $2, $zero, 4 st $2, 20($sp) addiu $2, $zero, 3 st $2, 16($sp) addiu $2, $zero, 2 st $2, 12($sp) addiu $2, $zero, 1 st $2, 8($sp) addiu $2, $zero, 0 st $2, 4($sp) addiu $2, $zero, 6 st $2, 0($sp)
- `lib/Target/MYRISCVX/Cpu0ISelLowering.cpp
SDValue MYRISCVXTargetLowering::lowerVASTART(SDValue Op, SelectionDAG &DAG) const { ... // 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 { /// LowerFormalArguments - transform physical registers into virtual registers /// and generate load operations for arguments places on the stack. SDValue MYRISCVXTargetLowering::LowerFormalArguments(SDValue Chain, ... if (IsVarArg) writeVarArgRegs(OutChains, MYRISCVXCCInfo, Chain, DL, DAG); ...
void MYRISCVXTargetLowering::writeVarArgRegs(std::vector<SDValue> &OutChains, const MYRISCVXCC &CC, SDValue Chain, ... // Copy the integer registers that have not been used for argument passing // to the argument register save area. For O32, the save area is allocated // in the caller's stack frame, while for N32/64, it is allocated in the // callee's stack frame. for (unsigned I = Idx; I < NumRegs; ++I, VaArgOffset += RegSize) { unsigned Reg = addLiveIn(MF, ArgRegs[I], RC); SDValue ArgValue = DAG.getCopyFromReg(Chain, DL, Reg, RegTy); FI = MFI.CreateFixedObject(RegSize, VaArgOffset, true); SDValue PtrOff = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout())); SDValue Store = DAG.getStore(Chain, DL, ArgValue, PtrOff, MachinePointerInfo()); cast<StoreSDNode>(Store.getNode())->getMemOperand()->setValue( (Value *)nullptr); OutChains.push_back(Store); }
動的スタック割り当てのサポート
C言語ではほとんど使わないらしい。alloca
という関数は初めて知った。スタック上にメモリを動的に取る関数らしい。
ch9_3_alloc.cpp
int weight_sum(int x1, int x2, int x3, int x4, int x5, int x6) { // int *b = (int*)alloca(sizeof(int) * 1 * x1); int* b = (int*)__builtin_alloca(sizeof(int) * 1 * x1); int *a = b; *b = x3; int weight = sum(3*x1, x2, x3, x4, 2*x5, x6); return (weight + (*a)); }
これをclangでIRとして出力すると、以下のようになっていた。なるほど、allocというIRが呼ばれるのか。
; Function Attrs: noinline nounwind optnone define dso_local i32 @_Z10weight_sumiiiiii(i32 signext %x1, i32 signext %x2, i32 signext %x3, i32 signext %x4, i32 signext %x5, i32 signext %x6) #0 { entry: %x1.addr = alloca i32, align 4 %x2.addr = alloca i32, align 4 %x3.addr = alloca i32, align 4 %x4.addr = alloca i32, align 4 ...
基本的には、adjustStackPtrを使ってこれらのスタックを処理する関数を生成するようだ。
上記のC言語では、コンパイル時にallocで確保されるスタックのサイズを特定することができない。
そこで、スタックのサイズを示しているx1
のサイズだけスタックポインタを移動し、その領域を確保するようにしている。
その際にallocが呼ばれる前のspの場所をfpに保存しておき、関数から戻るときは、spの値をfpから書き戻す。
// Eliminate ADJCALLSTACKDOWN, ADJCALLSTACKUP pseudo instructions MachineBasicBlock::iterator MYRISCVXFrameLowering:: eliminateCallFramePseudoInstr(MachineFunction &MF, MachineBasicBlock &MBB, MachineBasicBlock::iterator I) const { unsigned SP = MYRISCVX::SP; if (!hasReservedCallFrame(MF)) { int64_t Amount = I->getOperand(0).getImm(); if (I->getOpcode() == MYRISCVX::ADJCALLSTACKDOWN) Amount = -Amount; // const MYRISCVXSEInstrInfo &TII = // *static_cast<const MYRISCVXSEInstrInfo*>(STI.getInstrInfo()); STI.getInstrInfo()->adjustStackPtr(SP, Amount, MBB, I); } return MBB.erase(I);
もう一つの方法は、allocよりも上の領域へのアクセスはfpを使い、allocよりも下の領域はspを使ってアクセスする、というものらしい。 この方式のほうが、spだけを使うよりも処理を高速化することができる、ということだ。