llvm后端之函数栈帧的创建与销毁
- 引言
- 1 目标扩展实现
- 1.1 emitPrologue和emitEpilogue
- 1.2 storeRegToStackSlot和loadRegFromStackSlot
- 2 寄存器存栈与恢复
引言
llvm后端在物理寄存器分配后、指令发射前会调用PEI这个pass来生成函数栈帧的创建与销毁。
1 目标扩展实现
在target下,需要实现如下两个方法:
- TargetFrameLowering子类重写emitPrologue与emitEpilogue方法;
- TargetInstrInfo子类重写storeRegToStackSlot和loadRegFromStackSlot方法。
1.1 emitPrologue和emitEpilogue
TargetFrameLowering类的emitPrologue与emitEpilogue方法用于创建和消耗函数栈帧实现。以RISCV为例:
emitPrologue其核心逻辑如下:
- 基于对齐考虑,调用RISCVFrameLowering::determineFrameLayout调整栈大小;
- 若栈指针寄存器SPReg被引用,则报错退出;否则继续;
- 若栈指针需要通过多次调整,则将临时变量StackSize设置为第一次调整的大小。这是通过getFirstSPAdjustAmount计算的;
- 通过adjustReg生成扩展栈空间的指令。由于栈向下生长,是加上负StackSize大小;此外,该指令是插入到基本块最前端;
- 将插入指令的迭代器位置往后移需要保存的寄存器的个数;
- 通过adjustReg调整帧指针FPReg的位置,使其指向参数所在位置;
- 如果getFirstSPAdjustAmount返回非0,则需要第二次扩展栈空间;
- 如果TargetRegisterInfo::needsStackRealignment返回要求改函数需要比正常调用约定更严格的栈对齐,则对栈指针SPReg再向下扩展MaxAlignment长度;
注:此外源码中还有包括生成用于栈回溯的cfi指令。
emitEpilogue其核心逻辑如下:
- 首先将插入指令的位置MBBI设置为基本块末尾;
- 调整指令插入位置。若有终止指令,将MBBI指向第一个终止指令的前面,否则指向最后一个非终止且非debug指令的后面;
- 如果栈指针SPReg在emitPrologue中做了多次调整,则需要反序恢复回来。前面的恢复指令插入位置是MBBI向前移动该函数作为被调用者需要保存的寄存器个数;最后一次插入位置是MBBI
1.2 storeRegToStackSlot和loadRegFromStackSlot
TargetInstrInfo类的storeRegToStackSlot和loadRegFromStackSlot方法用于寄存器压栈和出栈指令的生成。同样,以RISCV为例:
- storeRegToStackSlot则是根据寄存器分类获取不同的压栈指令类型,例如在32位目标下的GPR寄存器类,则是sw指令;
- loadRegFromStackSlot也是根据寄存器分类获取不同的出栈指令类型,例如在32位目标下的GPR寄存器类,则是lw指令;
2 寄存器存栈与恢复
细心的朋友可能会发现之前的代码逻辑缺失了寄存器的保留与恢复操作。这部分指令是在PEI这个pass中完成的,该pass是在TargetPassConfig::addMachinePasses中完成添加的。在PEI::runOnMachineFunction中:
- 首先就调用PEI::spillCalleeSavedRegs生成了该函数作为被调用者需要保护寄存器的保留与恢复指令操作,其中包括调用XXXInstrInfo实现的storeRegToStackSlot和loadRegFromStackSlot方法;
- 后续,调用PEI::insertPrologEpilogCode生成栈帧的创建与销毁指令。
注:由于提前生成保留寄存器的压栈与出栈指令,所以目标在实现emitPrologue和emitEpilogue方法时,需要注意指令的插入位置。