今回は、グローバル変数を取り扱うための手法について調査する。 現状では、グローバル変数の入っているプログラムをコンパイルすると以下のようにエラーが発生する。
global_access.cpp
int global0 = 0; int global1 = 100; void global_access () { int i = 0; i = global1; return; }
./bin/clang -target riscv32-unknown-elf -c global_access.cpp -emit-llvm --target=riscv32-unknown-elf -o global_access.rv32.bc ./bin/llc -debug -march=myriscvx64 -filetype=asm rotate_test.rv64.bc -o -
ISEL: Starting pattern match Initial Opcode index to 0 Match failed at index 0 LLVM ERROR: Cannot select: t5: i32 = GlobalAddress<i32* @global1> 0
GlobalAddress
というIRを処理する必要がありそうだ。
./bin/llvm-dis global_access.rv32.bc -o -
@global0 = dso_local global i32 0, align 4 @global1 = dso_local global i32 100, align 4 ; Function Attrs: noinline nounwind optnone define dso_local void @_Z13global_accessv() #0 { entry: %i = alloca i32, align 4 store i32 0, i32* %i, align 4 %0 = load i32, i32* @global1, align 4 store i32 %0, i32* %i, align 4 ret void }
このグローバル変数を処理するためには、コンパイラには2種類のポリシーがある。
- PIC(Position Independent Code) : ポジション独立。グローバル変数のアドレスはランタイムに計算される。ライブラリなどを生成した場合、グローバル変数の位置はグローバル変数がロードされたときに決定される。このような場合は、グローバル変数の場所は固定せずに、動的に計算できるような仕組みを持っておく。
- Static : グローバル変数のアドレスは固定で計算される。ライブラリなどで使用せず、常にグローバル変数へのアクセスする命令からの距離が変わらない場合に使用される。
ライブラリとして提供する場合、グローバル変数はPICモードでコンパイル必要があるわけだが、そうでない場合はstaticでコンパイルしても良いわけだ。
この2つのポリシーを切り替えるためには、llcで-relocation-model
オプションを使用して指定する。
これらの2種類のポリシーについて、命令をどのように生成すればよいかを見ていく。
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
まず、setOperationAction(Custom)
で、GlobalAddress
IRの場合はカスタム実装したLower処理を実行するように設定する。
//@MYRISCVXTargetLowering { MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM, const MYRISCVXSubtarget &STI) : TargetLowering(TM), Subtarget(STI), ABI(TM.getABI()) { setOperationAction(ISD::GlobalAddress, XLenvT, Custom); }
SDValue MYRISCVXTargetLowering:: LowerOperation(SDValue Op, SelectionDAG &DAG) const { switch (Op.getOpcode()) { case ISD::GlobalAddress: return lowerGlobalAddress(Op, DAG); } return SDValue(); }
lowerGlobalAddress
では、PICモードとStaticモードの両方に対してDAGの生成を行う。
SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op,
SelectionDAG &DAG) const {
- PICモードの場合:
getAddrGlobalGOT
を呼び出してGOTを用いたアドレス計算命令を生成する。
SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op, SelectionDAG &DAG) const { ... return getAddrGlobalGOT( N, Ty, DAG, MYRISCVXII::MO_GOT_HI20, MYRISCVXII::MO_GOT_LO12, DAG.getEntryNode(), MachinePointerInfo::getGOT(DAG.getMachineFunction()));
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
グローバルポインタレジスタ(GP)にグローバル変数にアクセスするためのベースアドレスが格納されている。
そこで、このベースアドレスからのアドレスオフセットを計算する。
GPに対してのアドレスオフセットは、ここでは計算しないのでMYRISCVXII::MO_GOT_HI20, MYRISCVXII::MO_GOT_LO12
というノードを使用している。
GP + MO_GOT_HI20
をHI、MO_GOT_LO12
をLOとして、これをWrapper
ノードを作成してまとめている。
このWrapperノードを対象として、ロード命令を生成することでグローバル変数にアクセスしている。
//@getAddrGlobalGOT { // This method creates the following nodes, which are necessary for // computing a global symbol's address in large-GOT mode: // // (load (wrapper (add %hi(sym), $gp), %lo(sym))) template<class NodeTy> SDValue getAddrGlobalGOT(NodeTy *N, EVT Ty, SelectionDAG &DAG, unsigned HiFlag, unsigned LoFlag, SDValue Chain, const MachinePointerInfo &PtrInfo) const { SDLoc DL(N); SDValue Hi = DAG.getNode(MYRISCVXISD::Hi, DL, Ty, getTargetNode(N, Ty, DAG, HiFlag)); Hi = DAG.getNode(ISD::ADD, DL, Ty, Hi, getGlobalReg(DAG, Ty)); SDValue Wrapper = DAG.getNode(MYRISCVXISD::Wrapper, DL, Ty, Hi, getTargetNode(N, Ty, DAG, LoFlag)); return DAG.getLoad(Ty, DL, Chain, Wrapper, PtrInfo); } //@getAddrGlobalGOT }
ここで、MYRISCVXISD::Hi
, MYRISCVXISD::Lo
, MYRISCVXISD::Wrapper
ノードを使っている。これはMYRISCVXInstrInfo.td
に定義している。
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Hi and Lo nodes are used to handle global addresses. Used on // MYRISCVXISelLowering to lower stuff like GlobalAddress, ExternalSymbol // static model. (nothing to do with MYRISCVX Registers Hi and Lo) def MYRISCVXHi : SDNode<"MYRISCVXISD::Hi", SDTIntUnaryOp>; def MYRISCVXLo : SDNode<"MYRISCVXISD::Lo", SDTIntUnaryOp>;
これに対する命令の生成パタンを追加する。ここでは、MYRISCVXISD::Hi
はLUI命令、MYRISCVXISD::Lo
はORI命令で生成する。
// hi/lo relocs def : Pat<(MYRISCVXHi tglobaladdr:$in), (LUI tglobaladdr:$in)>; def : Pat<(MYRISCVXLo tglobaladdr:$in), (ORI ZERO, tglobaladdr:$in)>; def : Pat<(add GPR:$hi, (MYRISCVXLo tglobaladdr:$lo)), (ORI GPR:$hi, tglobaladdr:$lo)>;
Wrapperは以下のように定義している。
def MYRISCVXWrapper : SDNode<"MYRISCVXISD::Wrapper", SDTIntBinOp>;
生成パタンは以下だ。
//@ wrapper_pic class WrapperPat<SDNode node, Instruction ORiOp, RegisterClass RC>: Pat<(MYRISCVXWrapper RC:$gp, node:$in), (ORiOp RC:$gp, node:$in)>; def : WrapperPat<tglobaladdr, ORI, GPR>; def : WrapperPat<texternalsym, ORI, GPR>;
tglobaladdr
, texternalsym
ノードに対してパタンを生成している。tglobaladdr
, texternalsym
の定義はTargetSelectionDAG.td
に定義されている。
llvm-myriscvx80/include/llvm/Target/TargetSelectionDAG.td
... def tglobaladdr : SDNode<"ISD::TargetGlobalAddress", SDTPtrLeaf, [], "GlobalAddressSDNode">; ... def texternalsym: SDNode<"ISD::TargetExternalSymbol", SDTPtrLeaf, [], "ExternalSymbolSDNode">;
これらの生成手法についてまとめる。
- Staticモードの場合
Staticモードの場合は、getAddrNonPIC
関数を呼び出してアドレスを計算する。
//@ %hi/%lo relocation return getAddrNonPIC(N, Ty, DAG);
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
SDValue MYRISCVXTargetLowering::lowerGlobalAddress(SDValue Op, SelectionDAG &DAG) const { ... if (!isPositionIndependent()) { //@ %hi/%lo relocation return getAddrNonPIC(N, Ty, DAG); } ...
getAddrNonPIC
では、単純にアクセスする対象の変数に対して、上位アドレス、下位アドレスを計算し、それを加算することでアドレスとしている。
グローバル変数の場所は常に固定なので、GPを使わずに単純にアドレスを加算するだけで算出できる。
llvm-myriscvx80/lib/Target/MYRISCVX/MYRISCVXISelLowering.h
template<class NodeTy> SDValue getAddrNonPIC(NodeTy *N, EVT Ty, SelectionDAG &DAG) const { SDLoc DL(N); SDValue Hi = getTargetNode(N, Ty, DAG, MYRISCVXII::MO_ABS_HI); SDValue Lo = getTargetNode(N, Ty, DAG, MYRISCVXII::MO_ABS_LO); return DAG.getNode(ISD::ADD, DL, Ty, DAG.getNode(MYRISCVXISD::Hi, DL, Ty, Hi), DAG.getNode(MYRISCVXISD::Lo, DL, Ty, Lo)); }
それぞれのコンパイルされた命令は以下のようになる。
- PICモードの場合 (32ビット)
sw $x10, 0($x2) lui $x10, %got_hi(global1) add $x10, $x10, $x3 ori $x10, $x10, %got_lo(global1) lw $x10, 0($x10) lw $x10, 0($x10)
- PICモードの場合 (64ビット)
lui $x10, %got_hi(global1) add $x10, $x10, $x3 ori $x10, $x10, %got_lo(global1) ld $x10, 0($x10) lw $x10, 0($x10)
まず変数global1の、GOTの先頭からの距離を%got_hi(global1)
、%got_lo(global1)
を使って計算している(%got_hi(global1)
, %got_lo(global1)
の具体的な値はリンク時に計算され、値が埋め込まれる。
そのためのブラックボックスを作っている、というイメージだ。
そしてそのアドレスをGOTの先頭を指しているGPに加算して具体的なglobal1のGOT内での絶対アドレスを得る。
そのアドレスに対してロード命令を行い、GOT内のglobal1用のエントリを参照して、global1の具体的な存在アドレスを算出し、その場所に改めてロード命令を行う、という手順になる。
- Staticモードの場合 (32ビット)
lui $x10, %hi(global1) ori $x10, $x10, %lo(global1) lw $x10, 0($x10)
- Staticモードの場合 (64ビット)
lui $x10, %hi(global1) ori $x10, $x10, %lo(global1) lw $x10, 0($x10)
Staticモードの場合は単純だ。リンク時に計算するためにグローバル変数のアドレスは%hi(global1)
, %lo(global1)
として予約しておき、その場所に対するアドレスを計算して、ロード命令を実行する、というイメージだ。