LLVM IR:1.IR高级结构(High Level Structure)

LLVMIR是一种中间表示,用于编译器优化。它基于SSA形式,其中每个变量只被赋值一次。IR代码包含模块、函数、基本块和指令等结构。标识符包括局部变量(%)、全局变量(@)和元数据(!)。链接类型定义了全局变量和函数的可见性和合并规则,调用约定如ccc和fastcc影响函数调用的方式。可见性样式控制符号的可见范围,而DLL存储类型用于DLL导入和导出。函数定义涉及返回类型、参数列表和各种属性。
摘要由CSDN通过智能技术生成


前言

LLVM是一个基于SSA(单静态赋值)表示,它是高度灵活、可扩展和优化的编译器基础设施。

什么是SSA(单静态赋值):SSA(Static Single Assignment)是一种中间表示形式。在SSA中,每个变量只能被赋值一次,并且每个赋值都有一个唯一的定义点。这样每个变量的使用都能追踪到它的唯一定义点,因此,编译器可以更容易进行优化。
在 SSA 中,每个变量都是一个不可变的值,因为它只被定义一次。如果需要修改一个变量的值,就必须将其赋值给一个新的变量。这就是为什么 SSA 中的每个赋值语句都必须给出一个新的变量名。

LLVM的代码表示形式被设计为使用三种不同的格式:

  • 表示在内存中的编译器中间语言(.bc)
  • 表示为在磁盘上的位码(适合于即时编译器的快速加载);
  • 表示为人类可读的汇编语言(.ll)

1. 标识符

LLVM IR中常见的标识符类型:

  • %开头的局部变量标识符:以%开头的标识符是局部变量的标识符,用于表示函数内部的变量。例如,%0、%1、%2等是常见的局部变量标识符。
  • @开头的全局变量标识符:以@开头的标识符是全局变量的标识符,用于表示程序中的全局变量。例如,@var1、@var2等是常见的全局变量标识符。
  • !开头的元数据标识符:以!开头的标识符是元数据的标识符,用于表示代码中的元数据信息。例如,!0、!1等是常见的元数据标识符。
  • 未命名的值由标签加无符号数字表示:比如%2、@2。

以下是一个LLVM IR代码示例,展示了这些标识符类型的使用:

define i32 @add(i32 %a, i32 %b) {
  %sum = add i32 %a, %b
  ret i32 %sum
}

@var1 = global i32 0

!0 = metadata !{i32 42}

在这个示例中,我们定义了一个名为add的函数,它有两个参数a和b,返回值为i32类型。函数体内部使用了局部变量标识符%sum来计算a和b的和,并使用ret指令返回结果。

我们还定义了一个全局变量标识符@var1,它的初始值为0。

最后,我们定义了一个元数据标识符!0,表示一个值为42的元数据信息。

注释:以;分隔直到当前行结尾。

2. IR代码结构

  • 模块(Module)
    在LLVM IR中,模块是最高层的结构单元。它是一个代码单元,通常由一个源文件生成,包含了全局变量和函数定义等内容;
  • 函数(Function)
    函数是LLVM IR中的主要代码结构单元。它由多个基本块组成,每个基本块由一组指令组成,函数定义以define关键字开始,接着是返回值类型、函数名和参数列表信息,最后是函数体。函数体由基本块和指令组成,每个基本块都由一个唯一的标签标识。指令可以是任何LLVM IR指令。
  • 基本块(BasicBlock)
    基本块是一组按照控制流程顺序执行的指令序列。基本块中的每个指令都必须能够到达下一个指令或者分支语句。 基本块由一个唯一的标签标识,接着是一组指令序列。基本块中的指令按照顺序执行,除非遇到分支语句。
  • 指令(Instruction)
    指令是LLVM IR中的基本代码结构单元。它由操作码(opcode)和操作数(operand)组成,指令以%开头的变量名定义一个操作数,接着是操作码和操作数列表。

3. 链接类型(Linkage Types)

全局的值(全局变量和函数)都由指向某个特定位置的指针表示,并且有一个链接类型:

  • private private类型的全局变量只能被当前模块内的对象直接访问。而且,当前代码链接到一个包含private类型值得模块的时候,这个private值可能被重命名以避免冲突。由于这个值是模块私有的,所以所有的引用都可以直接更新。private的值不会出现在目标代码的符号表中。
  • internal 与private类似,不过它的值会作为局部变量出现在目标文件中。类似C语言中的Static概念。
  • available_externally 该类型的变量不会被输出到对应模块的目标文件中。对于链接者来说,该类型相当于一个外部申明。这种类型只允许在定义时使用,不能在申明时使用。
  • linkonce 链接时,这种类型的变量会和其他同名变量合并,该类型的变量如果没被使用则可以被丢弃。这种类型的函数不允许优化器将其内联到调用者的函数体中,这个它有可能被重写。如果想允许内联或者其他优化,请使用“linkonce_odr”类型。
  • weak 与linkonce类似,但即使没有使用,也不能丢弃。类似C语言中的“weak”类型的变量(多个变量拥有相同的名字不会造成冲突,只要其中一个是weak类型的。链接时,weak类型的定义被忽略而使用正常的定义,如果没有正常的定义,则使用weak的定义)。
  • common 与weak类似,用于C语言中的临时定义,比如全局作用域中的“int x;”。函数和别名没有common类型。
  • appending 只用于数组类型的指针。两个这种类型的变量链接时,对应的全局数组被链接在一起。
  • extern_weak 这种类型在链接之前是weak的,如果不能被链接,这个变量是null而不是undefined。
  • linkonce_odr, weak_odr ODR(one definition relu),只有等效的变量才能被合并,这个链接类型表示只能跟等效的变量合并。
  • external 如果没有指定上面的任意类型,那么就是external。

函数的声明只能使用external或者extern_weak

4. 调用约定(Calling Conventions)

LLVM支持的调用约定如下:

  • ccc C调用约定。如果没有指定其他类型,那么默认是该类型。这个类型支持可变参数的函数调用并允许声明的原型和函数声明的实现有一些不匹配。
  • fastcc 快速调用约定。使生成的目标代码尽可能快,可以使用任意tricks。不支持可变参数,并且原型和实现要严格匹配。
  • coldcc 冷调用。假定这种调用不经常发生。不支持可变参数,并且原型和实现要严格匹配。
  • cc 10 GHC调用。Glasgow Haskell Compiler 专用。支持尾调用优化,但是要求caller和callee都是它。
  • cc 11 HiPE调用约定。
  • webkit_jscc webkit的js调用约定。
  • anyregcc 代码补丁的动态调用
  • preserve_mostcc PreserveMost调用约定。目前是实验性的,未来会被objectiveC使用。
  • preserve_allcc 也是实验性的。
  • cxx_fast_tlscc clang生成访问函数去访问C++风格的TLS。访问函数包括入口块、出口块和初始化块,入口和出口块可以访问一些TLS IR变量。这个调用约定就是通过保留尽可能多的寄存器变量来降低这种访问的开销。
  • swiftcc Swift语言的调用约定
  • cc n 从64开始

5. 可见性(Visibility Styles)

所有的全局变量和函数都有一种可见性样式:

  • default:默认样式。对于使用ELF(Executable and Linking Format)格式的目标文件,这种可见性样式意味着声明对其他模块可见;在共享库中,意味着声明的实体可以被重写;在Darwin中,声明对其他模块可见。
  • hidden 具有这种可见性样式的声明引用相同的对象,如果它们在相同的共享对象中的话。通常,具有这种可见性样式的符号不会出现在动态符号表中,所有其他的模块不能直接引用它。
  • protected 对于ELF,这种样式表明符号会放置在动态符号表中,但是在定义这个符号表的模块中引用都会绑定到局部变量,也就是说这个符号表不能被其他的模块所重写。

attention:用于internal或者private链接类型的符号必须是default类型的。

6. 函数(Function)

函数的定义就是一个个基本块构成了一个CFG(控制流程图),每个基本块由一条条的指令组成,并且都有一个进入标签(entry label)和一条终止指令(Terminator Instruction),终止指令有好多条,如分支指令和返回指令brretswitch等。如果没有提供一个确切的标签名称,块(block)就会被分配一个隐式的标签编号,编号使用的是从计数器中返回的一个值,就如同未命名的零时变量一样。For Example,如果函数入口块没有明确的标签,则会分配标签%0,块中下一个未命名的临时块将为%1,以此类推;如果显示定义了数字标签(如%2),应该与隐式规则相匹配。

函数中第一个BasicBlock有两个特别之处:刚进该函数就执行第一个BasicBlock,并且不允许有前置基本块(即函数的入口块不能有任何分支)。由于该块可以没有前驱,所以它不能有任何PHI节点。

LLVM允许为函数指定一个明确的显示部分,如果目标支持它(函数),会将函数发送给指定的section。另外,该函数可以放置在COMDAT中。

可以为函数指定确切的对齐方式,如果没有指定或者指定的对方值是0,则对其方式由编译器自己决定。如果指定了对其方式,则会强制按照指定的对齐方式对齐。对齐必须是2的幂次方。

定义一个函数的语法:

define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass]
       [cconv] [ret attrs]
       <ResultType> @<FunctionName> ([argument list])
       [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [fn Attrs]
       [section "name"] [comdat [($name)]] [align N] [gc] [prefix Constant]
       [prologue Constant] [personality Constant] (!name !N)* { ... }
  • LLVM function 由define关键字进行定义,每个函数都有一个可选的链接类型,一个可选的运行时抢占说明(runtime preemption specifier),一个可选的可见性模式,一个可选的DLL存储类别,一个可选的调用约定,一个可选的unnamed_addr属性(该属性表示地址不重要,只要内容),一个返回值类型,一个可选的返回类型的参数属性,一个函数名,一个(可能为空的)参数列表(每一个都带有可选的参数属性),可选的函数属性,一个可选的地址空间,一个可选的section,一个可选的对齐方式,一个可选的comdat(common data),一个可选的GC(垃圾回收器),一个可选的前缀数据,一个可选的序言数据,一个可选的personality(有该属性的函数可以指定异常处理),一个可选的附加元数据列表,一个左花括号,一个基本块列表和一个右花括号
  • LLVM使用declare关键字对Function进行声明,一个可选的可见类型,一个可选的DLL存储类别,一个可选的调用约定,一个可选的地址空间,返回值类型和可选的返回值参数属性,一个函数名,一个可以为空的参数列表,一个可选的对齐方式一个可选的垃圾回收名,一个可选的前缀和一个可选的序言。
declare [linkage] [visibility] [DLLStorageClass]
        [cconv] [ret attrs]
        <ResultType> @<FunctionName> ([argument list])
        [(unnamed_addr|local_unnamed_addr)] [align N] [gc]
        [prefix Constant] [prologue Constant]

7. DLL存储类型

所有的Global Variables,Function和Aliases(别名)都有一个DLL 存储类型,如下:

  • dllimport会导致编译器通过一个指向到被DLL导出的指针的全局指针,来引用一个函数或者变量。在微软windows平台,这个指针的格式为__imp__直接引用的函数或变量的名称(__imp__函数名)。
  • dllexport会导致编译器提供一个指向 一个在DLL中的指针的全局变量,所以它可以被引用到带有dllimport属性的实体。在微软windows平台,这个指针名的格式为 __imp_接上引用的函数或变量的名称(__imp_函数名)。为了使编译器,汇编器和链接器知道某个符号是被外部引用并且防止这个符号被删除,因此这个存储类别为了定义一个dll接口而存在的。

总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值