和局部变量相比,全局变量是指在函数外部声明的变量,也叫外部变量。程序/模块中的函数均可以访问它,所以其作用域是全局的,通常存放在数据区。
全局变量在LLVM IR中的表示如下:
@global_variable = global i32 0
该语句是定义一个i32类型的全局变量 @global_variable
,并且将其初始化为0。在LLVM IR中,所有的全局变量的名称都需要用@开头。
1. 全局变量的访问
当汇编器生成一个目标模块时,它并不知道数据和代码最后会放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。局部变量在模块内部定义与引用,无外部链接属性;全局变量的作用域为全局,当全局变量在A模块中定义,可以在B模块中引用。访问全局变量时需要修改代码和数据中对于全局变量符号的引用,使它们指向唯一的运行时内存地址。这一过程在链接器中实现。
链接器将多个代码和数据片段收集并组合成为一个单一文件,这个文件可被加载(复制)到内存并执行。链接器的主要任务有两个:
符号解析:将符号定义和符号引用关联起来,这里的符号可以是一个函数、一个全局变量等。
重定位:将多个输入模块合并,并为每个符号分配运行时地址。
全局变量在编译器中的解析与编译时选择的重定位模式有关。根据重定位的时机可以分为静态重定位和动态重定位。
静态重定位是在程序执行之前进行重定位,直接修改装配模块中的有关使用地址的指令。动态重定位是在程序执行过程中进行地址重定位。
2. 代码实现
主要包括从LLVM IR到汇编语言的编译和汇编到ELF文件的编译,下面以CPU0后端为例进行介绍。
2.1 生成汇编
(1) 在Cpu0BaseInfo.h中
声明全局变量偏移表的类型枚举,如MO_GOT。全局变量偏移表(GOT:Global Offset Table),是位于目标文件中的一块数据引用,里边存放着全局变量的地址。用于实现位置无关代码(Position-Independent Code,PIC)技术。
(2) 在Cpu0ISelLowering.h/.cpp中
对全局变量进行自定义实现。
Cpu0TargetLowering::Cpu0TargetLowering(const Cpu0TargetMachine &TM, const Cpu0Subtarget &STI) : TargetLowering(TM), Subtarget(STI), ABI(TM.getABI()) { ... setOperationAction(ISD::GlobalAddress, MVT::i32, Custom); ... }
在操作数处理函数中增加对全局变量的自定义实现。
SDValue Cpu0TargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG)const { switch (Op.getOpcode()) { ... case ISD::GlobalAddress: return lowerGlobalAddress(Op, DAG); ... } return SDValue(); }
Cpu0 同时支持静态模式和 PIC 模式的全局变量重定位模式。
SDValue Cpu0TargetLowering:: lowerGlobalAddress(SDValue Op, SelectionDAG &DAG) const { ... if (!isPositionIndependent()) { // %gp_rel relocation if (GO && TLOF->IsGlobalInSmallSection(GO, getTargetMachine())) { SDValue GA = DAG.getTargetGlobalAddress(GV, DL, MVT::i32, 0, Cpu0II::MO_GPREL); SDValue GPRelNode = DAG.getNode(Cpu0ISD::GPRel, DL, DAG.getVTList(MVT::i32), GA); SDValue GPReg = DAG.getRegister(Cpu0::GP, MVT::i32); return DAG.getNode(ISD::ADD, DL, MVT::i32, GPReg, GPRelNode); } // %hi/%lo relocation return getAddrNonPIC(N, Ty, DAG); } if (GV->hasInternalLinkage() || (GV->hasLocalLinkage() && !isa<Function>(GV))){ return getAddrLocal(N, Ty, DAG); } // large section if (!TLOF->IsGlobalInSmallSection(GO, getTargetMachine())) return getAddrGlobalLargeGOT(N, Ty, DAG, Cpu0II::MO_GOT_HI16, Cpu0II::MO_GOT_LO16, DAG.getEntryNode(), MachinePointerInfo::getGOT(DAG.getMachineFunction())); return getAddrGlobal(N, Ty, DAG, Cpu0II::MO_GOT, DAG.getEntryNode(), MachinePointerInfo::getGOT(DAG.getMachineFunction())); }
该函数主要根据不同重定位模式和是否使用small section选择使用不同的计算全局变量符号地址方法,当全局变量为内部链接时会调用
getAddrLocal()
函数;当选择静态模式编译时会调用getAddrNonPIC()
函数;当选择PIC模式编译时会调用getAddrGlobal()
函数;当选择PIC和small section模式编译时会调用getAddrGlobalLargeGOT()
函数。
(3)Cpu0MCInstLower.h/.cpp
在生成汇编时对全局变量进行处理:在LowerSymbolOperand()中生成对应的符号表达式,如%got_lo。
2.2 汇编到ELF文件的编译
在Cpu0AsmParser.cpp中
在ParseOperand()中对符号表达式中%进行识别,随后调用函数
bool Cpu0AsmParser::parseRelocOperand()
解析符号。
3. 测试用例
C语言用例:
int gStart = 3;
int gI = 100;
int test_global() {
int c = 0;
c = gI;
return c;
}
LLVM IR表示
@gStart = dso_local global i32 3, align 4
@gI = dso_local global i32 100, align 4
; Function Attrs: noinline nounwind optnone
define dso_local i32 @test_global() #0 {
entry:
%c = alloca i32, align 4
store i32 0, i32* %c, align 4
%0 = load i32, i32* @gI, align 4
store i32 %0, i32* %c, align 4
%1 = load i32, i32* %c, align 4
ret i32 %1
}
生成的汇编结果
# %bb.0: # %entry
addiu $sp, $sp, -8
addiu $2, $zero, 0
st $2, -4($sp)
lui $2, %hi(gI)
ori $2, $2, %lo(gI)
ld $2, ttt 0($2)
st $2, -4($sp)
ld $2, ttt -4($sp)
addiu $sp, $sp, 8
ret $lr
...
$func_end0:
.size test_global, ($func_end0)-test_global
# -- End function
.type gStart,@object # @gStart
.data
.globl gStart
.p2align 2
gStart:
.4byte 3 # 0x3
.size gStart, 4
.type gI,@object # @gI
.globl gI
.p2align 2
gI:
.4byte 100 # 0x64
.size gI, 4
ELF文件
.rel.text:已编译程序的机器代码(.text 节)中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。
Relocation section '.rel.text' at offset 0xd0 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000000c 00000205 unrecognized: 5 00000004 gI
00000010 00000206 unrecognized: 6 00000004 gI
.symtab:存放在程序中定义和引用的函数和全局变量的信息的符号表。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS ch5.c
2: 00000004 4 OBJECT GLOBAL DEFAULT 4 gI
3: 00000000 4 OBJECT GLOBAL DEFAULT 4 gStart
4: 00000000 44 FUNC GLOBAL DEFAULT 2 test_global
4. 总结
本文针对LLVM编译器后端中全局变量的实现做了简单的介绍。受限于笔者知识水平,文中可能会存在某些理解上的偏差,欢迎批评指正。
参考资料
《深入理解计算机系统》
LLVM 后端实践笔记 5:全局变量(https://zhuanlan.zhihu.com/p/378338026)