LLVM IRには、いくつかの特殊な構文が存在し、これらをサポートすることでより多くのコードを生成できるようになる。 これらについては、clang側からIntrinsic関数を呼び出すことでLLVM IRを生成し、llcに渡してテストすることができる。
FRAMEADDR
関数のフレームポインタを取得する。C言語では、__builtin_frame_address()
により取得することができる。
func_frame_addr.cpp
void* display_frameaddress() { return (void *)__builtin_frame_address(0); }
上記のプログラムからLLVMの中間表現を作ってみると、以下のようになった。
llvm.frameaddress
が使用されている。llvm.frameaddress
はスタックフレームのフレームポインタの値を返す。以下のウェブサイトに使用が説明されている。
https://llvm.org/docs/LangRef.html#llvm-frameaddress-intrinsic
./bin/clang ../myriscvx-tests/tests/func_frame_return_addr.cpp -c -emit-llvm -o - | ./bin/llvm-dis -o -
define dso_local i8* @_Z20display_frameaddressv() #0 { entry: %0 = call i8* @llvm.frameaddress(i32 0) ret i8* %0 }
このframeaddress
は、LLVM IRノードのFRAMEADDR
に相当する。このFRAMEADDR
を処理するために、MYRISCVXISelLowering.cpp
に処理を付け加える。
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
//@MYRISCVXTargetLowering { MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM, const MYRISCVXSubtarget &STI) ... setOperationAction(ISD::FRAMEADDR, MVT::Other, Custom); ... }
まず、setOperationAction()
にてISD::FRAMEADDR
の処理をカスタム関数に投げるように設定する。そして、実際にカスタム関数を作成して命令生成のためのノードを構築する。
SDValue MYRISCVXTargetLowering:: LowerOperation(SDValue Op, SelectionDAG &DAG) const { switch (Op.getOpcode()) { ... case ISD::FRAMEADDR : return lowerFRAMEADDR(Op, DAG); ... } } ... SDValue MYRISCVXTargetLowering:: lowerFRAMEADDR(SDValue Op, SelectionDAG &DAG) const { // check the depth assert((cast<ConstantSDNode>(Op.getOperand(0))->getZExtValue() == 0) && "Frame address can only be determined for current frame."); MachineFrameInfo &MFI = DAG.getMachineFunction().getFrameInfo(); MFI.setFrameAddressIsTaken(true); EVT VT = Op.getValueType(); SDLoc DL(Op); SDValue FrameAddr = DAG.getCopyFromReg( DAG.getEntryNode(), DL, MYRISCVX::FP, VT); return FrameAddr; }
lowerFRAMEADDR
では、フレームポインタレジスタFPの値を読み取り、getCopyFromReg()
でノードを作成し、そのノードを返す。
- コンパイル結果
_Z20display_frameaddressv: # @_Z20display_frameaddressv .cfi_startproc # %bb.0: # %entry addi x10, x8, 0 ret
フレームポインタレジスタFP(x8)の値を、戻地理レジスタx10にコピーしてから戻る。正しく動作しているようだ。
RETURNADDR
関数の戻りアドレスを取得する。つまり、関数実行中の戻りアドレス、レジスタで言うとra
レジスタの中身を戻す。C言語では、__builtin_return_address()
により取得することができる。
func_return_addr.cpp
extern int fn(); int display_returnaddress() { int a = (int)__builtin_return_address(0); fn(); return a; }
どのようなLLVM中間表現が生成されているか確認してみる。
./bin/clang func_return_addr.cpp -c -emit-llvm --target=riscv32-unknown-elf -o - | ./bin/llvm-dis -o -
define dso_local i32 @_Z21display_returnaddressv() #0 { entry: %a = alloca i32, align 4 %0 = call i8* @llvm.returnaddress(i32 0) %1 = ptrtoint i8* %0 to i32 store i32 %1, i32* %a, align 4 %call = call i32 @_Z2fnv() %2 = load i32, i32* %a, align 4 ret i32 %2 }
llvm.returnaddress
というノードが生成されており、frameaddress
同様、戻りアドレスを示すノードになる。
このノードは、LLVM IRのRETURNADDR
に相当する。このノードを処理するための実装を加える。
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
//@MYRISCVXTargetLowering { MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM, const MYRISCVXSubtarget &STI) ... setOperationAction(ISD::RETURNADDR, MVT::Other, Custom); ... } SDValue MYRISCVXTargetLowering:: LowerOperation(SDValue Op, SelectionDAG &DAG) const { switch (Op.getOpcode()) { ... case ISD::RETURNADDR : return lowerRETURNADDR(Op, DAG); } }
SDValue MYRISCVXTargetLowering::lowerRETURNADDR(SDValue Op, SelectionDAG &DAG) const { if (verifyReturnAddressArgumentIsConstant(Op, DAG)) return SDValue(); // check the depth assert((cast<ConstantSDNode>(Op.getOperand(0))->getZExtValue() == 0) && "Return address can be determined only for current frame."); MachineFunction &MF = DAG.getMachineFunction(); MachineFrameInfo &MFI = MF.getFrameInfo(); MVT VT = Op.getSimpleValueType(); unsigned RA = MYRISCVX::RA; MFI.setReturnAddressIsTaken(true); // Return RA, which contains the return address. Mark it an implicit live-in. unsigned Reg = MF.addLiveIn(RA, getRegClassFor(VT)); return DAG.getCopyFromReg(DAG.getEntryNode(), SDLoc(Op), Reg, VT); }
FRAMEADDR
同様、ra
レジスタをgetCopyFromReg
でノードに取り込み、ノードを返すという仕組みになっている。
実際にコンパイルしてみる。
./bin/clang func_return_addr.cpp -c -emit-llvm --target=riscv32-unknown-elf -o func_return_addr.rv32.bc ./bin/llc -mtriple=myriscvx32 -filetype=asm -target-abi=lp func_return_addr.rv32.bc -o
_Z21display_returnaddressv: # @_Z21display_returnaddressv .cfi_startproc # %bb.0: # %entry addi x2, x2, -16 .cfi_def_cfa_offset 16 sw x2, 12(x2) # 4-byte Folded Spill .cfi_offset 2, -4 sw x1, 8(x2) addi x2, x2, 0 jal _Z2fnv addi x2, x2, 0 lw x10, 8(x2) lw x2, 12(x2) # 4-byte Folded Reload addi x2, x2, 16 ret
むだなアセンブリ命令が多くて分かりにくいが、要点は、最初のsw x1, 8(x2)
でra
レジスタの値をスタックに退避し、lw x10, 8(x2)
で戻り値レジスタに書き戻している。これで、現在の戻りアドレスを参照することができる。