全局变量在LLVM编译器后端中的实现

和局部变量相比,全局变量是指在函数外部声明的变量,也叫外部变量。程序/模块中的函数均可以访问它,所以其作用域是全局的,通常存放在数据区。

全局变量在LLVM IR中的表示如下:

@global_variable = global i32 0

该语句是定义一个i32类型的全局变量 @global_variable,并且将其初始化为0。在LLVM IR中,所有的全局变量的名称都需要用@开头。

1. 全局变量的访问

当汇编器生成一个目标模块时,它并不知道数据和代码最后会放在内存中的什么位置,也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。局部变量在模块内部定义与引用,无外部链接属性;全局变量的作用域为全局,当全局变量在A模块中定义,可以在B模块中引用。访问全局变量时需要修改代码和数据中对于全局变量符号的引用,使它们指向唯一的运行时内存地址。这一过程在链接器中实现。

链接器将多个代码和数据片段收集并组合成为一个单一文件,这个文件可被加载(复制)到内存并执行。链接器的主要任务有两个:

  1. 符号解析:将符号定义和符号引用关联起来,这里的符号可以是一个函数、一个全局变量等。

  2. 重定位:将多个输入模块合并,并为每个符号分配运行时地址。

全局变量在编译器中的解析与编译时选择的重定位模式有关。根据重定位的时机可以分为静态重定位和动态重定位。

静态重定位是在程序执行之前进行重定位,直接修改装配模块中的有关使用地址的指令。动态重定位是在程序执行过程中进行地址重定位。

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编译器后端中全局变量的实现做了简单的介绍。受限于笔者知识水平,文中可能会存在某些理解上的偏差,欢迎批评指正。

f8a290e3d8b563aa453b42dc07a22600.png


参考资料

  1. 《深入理解计算机系统》

  2. LLVM 后端实践笔记 5:全局变量(https://zhuanlan.zhihu.com/p/378338026)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值