LLVM参考手册之高级结构(三)
高级结构(High Level Structure)
1.2 模块结构
LLVM 程序由模块(Module)组成,每个模块都是输入程序的一个翻译单元。每个模块由函数、全局变量和符号表组成。可以使用 LLVM 链接器将模块合并在一起,它会合并函数(和全局变量)定义,解析前向声明并合并符号表条目。以下是“Hello World”模块的示例:; Declare the string constant as a global constant. @.str = private unnamed_addr constant [13 x i8] c"hello world\0A\00" ; External declaration of the puts function declare i32 @puts(ptr nocapture) nounwind ; Definition of main function define i32 @main() { ; Call puts function to write out the string to stdout. call i32 @puts(ptr @.str) ret i32 0 } ; Named metadata !0 = !{i32 42, null, !"string"} !foo = !{!0}
这个示例由一个名为“.str”的全局变量、一个 puts 函数的外部声明、一个 main 函数的定义function definition
以及名为“foo” 的 命名元数据组成。 named metadata。 通常而言,一个模块由全局值列表组成(其中函数和全局变量都是全局值)。全局值由指向内存位置的指针表示(在这个示例中,一个指向 char 数组的指针和一个指向函数的指针),并且具有以下其中一个链接类型 。
1.2 链接类型
所有全局变量和函数都有以下其中一种链接类型:private
链接类型为“private”的全局值只能被当前模块中的对象直接访问。特别是,将代码链接到一个带有私有全局值的模块中可能会导致必要时对私有全局值进行重命名以避免冲突。由于这个符号对于模块是私有的,所有引用都可以被更新。这不会出现在目标文件的任何符号表中。
internal
与private类似,但该值在目标文件中显示为本地符号(在ELF中为STB_LOCAL)。这对应于C语言中“static”关键字的概念。available_externally
链接类型为“available_externally” 的全局变量永远不会被发射到与LLVM模块对应的目标文件中。从链接器的角度来看,available_externally全局变量等效于外部声明。由于链接类型为available_externally的全局变量实际上是外部声明,因此它们可以随意丢弃,这使得编译器可以更好地进行优化。此外,这种链接类型只能用于全局变量的定义,而不能用于全局变量的声明。
linkonce
linkonce链接类型的全局变量在链接发生时会与同名的其他全局变量合并。这可以用于实现某些内联函数、模板或其他代码,该代码必须在使用它的每个翻译单元中生成,但是其中的主体可能会被稍后更准确的定义覆盖。未引用的linkonce全局变量可以被丢弃。请注意,linkonce链接并不能实际上将函数体内联到调用者中,因为它不知道该函数的这个定义是否是程序中的最终定义,或者它是否将被更强的定义所替代。要启用内联和其他优化,请使用“linkonce_odr”链接。
weak
weak链接类型与linkonce链接类型具有相同的合并语义,但是具有weak链接的未引用全局变量可能不会被丢弃。这主要用于在C源代码中声明为“weak”的全局变量。
common
“common”链接类型与“weak”链接类型最相似,但它们用于C中的尝试性定义,例如全局范围内的“int X;”。具有“common”链接的符号和弱符号相同地合并,如果未引用,则可能无法删除。common符号可能没有显式的节(section),必须具有零初始化程序,并且可能不能标记为“constant”。函数和别名可能没有公共链接。
appending
“appending”链接类型只能应用于指向数组类型的全局变量。当具有“appending”链接的两个全局变量链接在一起时,这两个全局数组将被追加在一起。这是LLVM中的、类型安全的等效方式,相当于系统链接器在链接.o文件时将具有相同名称的“section”追加在一起。
不幸的是,这与.o文件中的任何功能都不对应,因此它只能用于像LLVM.global_ctors这样的变量,这些变量在LLVM中被特殊解释。
extern_weak
这种链接类型的语义遵循ELF对象文件模型:在链接之前,该符号是弱引用; 如果没有链接,则该符号变为空指针,而不是未定义的引用。
linkonce_odr, weak_odr
一些语言允许合并不同的全局变量,例如具有不同语义的两个函数。其他语言(例如C++)则确保只有等效的全局变量才会被合并(“一定义规则” - “ODR”)。这样的语言可以使用linkonce_odr和weak_odr链接类型来指示该全局变量仅将与等效的全局变量合并。这些链接类型在其他方面与其非odr版本相同。
external
如果没有使用上述标识符,则该全局变量是外部可见的,这意味着它参与链接并可用于解析外部符号引用。
全局变量或函数声明的链接类型除了external或extern_weak之外,任何链接类型都是非法的。
1.3 Calling Conventions
LLVM中的 functions、calls、和invokes都可以为调用指定一个可选的调用约定。任何动态调用方/被调用方的调用约定必须匹配,否则程序的行为是未定义的。LLVM支持以下调用约定,将来可能会添加更多支持:“ccc”-C调用约定
这种调用约定(如果没有指定其他调用约定,则为默认值)与目标C调用约定相匹配。这种调用约定支持可变参数函数调用,并容忍在函数的声明原型和实现声明之间有一些不匹配(与普通C语言一样)。
“fastccc”-快速调用约定
这种调用约定试图使调用尽可能快(例如通过寄存器传递参数)。这种调用约定允许目标使用任何技巧来为目标生成快速的代码,而无需遵守外部指定的ABI(应用程序二进制接口)。
当使用fastcc、tailcc、GHC和HiPE调用约定时, Tail调用可以被优化 这种调用约定不支持可变参数,并要求所有被调用函数的原型与函数定义的原型完全匹配。
“coldcc”-冷调用约束
这种调用约定试图使调用方的代码在假设不常执行调用的情况下尽可能高效。因此,这些调用通常保留所有寄存器,以便调用不会破坏调用方中的任何活跃区间。这种调用约定不支持可变参数,并要求所有被调用函数的原型与函数定义的原型完全匹配。此外,内联程序不会考虑这样的函数调用进行内联。
“cc 10”-GHC 约束
该调用约定是专门为 Glasgow Haskell编译器(GHC) 使用而实现的。它将所有参数都通过寄存器传递,并通过禁用被调用者保存的寄存器来达到这一极端目的。这种调用约定不应轻易使用,而只应用于特定情况,例如作为实现函数式编程语言时常用的寄存器固定性能技术的替代方法。目前只有X86支持这种约定,并且它具有以下限制:
● 在X86-32平台上,只支持最多4 位类型的参数,不支持浮点类型参数。
● 在X86-64上,该约定仅支持最多10位类型参数和6个浮点参数。
“cc 11”- Hipe 调用约束
该调用约定是专门为Ericsson开源Erlang/OTP系统的本地代码编译器
High-Performance Erlang(HiPE) 设计和实现的。该调用约定比普通的C调用约定使用更多的寄存器用于参数传递,并且不定义被调用者保存的寄存器。这个调用约定完全支持 tail call optimization,但要求调用方和被调用方都必须使用它。
“webkit_jscc”- Webkit's JavaScript调用约束 这个调用约定是为了 WebKit FTL JIT
而实现的。它按照cdecl的方式从右到左通过栈传递参数,并通过平台惯常的返回寄存器返回值。
“anyregcc”- 动态调用约定用于代码修补
这是一种特殊的调用约定,支持在调用点上直接替换任意代码序列进行修补。该约定强制调用参数进入寄存器,但允许它们动态分配。目前,这仅可用于对llvm.experimental.patchpoint的调用,因为只有这个内置函数记录了其参数在侧表中的位置。请参阅 《LLVM中的Stack maps和patch points》
“preserve_mostcc”- PreserveMost 调用约束
这个调用约定试图尽可能减少调用方代码的影响。在参数和返回值传递方面,该约定与C调用约定完全相同,但使用了不同的调用方/被调用方保存的寄存器集合。这可以减轻调用方在调用前后保存和恢复大量寄存器集时的负担。如果参数传递在被调用者保存的寄存器中进行,则在调用过程中这些寄存器会被被调用者保留。对于以被调用者保存的寄存器返回的值,这并不适用。
- 在X86-64体系结构上,被调用者会保留除了R11和返回寄存器以外的所有通用寄存器。R11可以用作临时寄存器。浮点寄存器(XMMs/YMMs)不会被保留,需要由调用者保存。
- 在AArch64架构中,被调用者会保留除了X0-X8和X16-X18之外的所有通用寄存器。
这个约定的想法是支持对运行时函数进行调用,这些函数具有热路径和冷路径。热路径通常是一个不使用太多寄存器的小代码段。冷路径可能需要调用另一个函数,因此只需保留调用者未保存的调用者保存寄存器。PreserveMost调用约定在调用者/被调用者保存的寄存器方面与coldcc非常相似,但它们用于不同类型的函数调用。coldcc用于很少执行的函数调用,而preserve_mostcc函数调用旨在位于热路径并且肯定会经常执行。此外,preserve_mostcc不会阻止内联程序内联函数调用。
这个调用约定将被ObjectiveC运行时的未来版本使用,因此目前仍应被视为实验性质。虽然此约定是为了优化对ObjectiveC运行时的某些运行时调用而创建的,但它并不限于此运行时,并且将来也可能被其他运行时使用。目前的实现仅支持X86-64,但意图是在未来支持更多的体系结构。
“preserve_allcc”- PreserveAll调用约束
该调用约定试图使调用方的代码比PreserveMost调用约定更少侵入性。该调用约定在传递参数和返回值方面与C调用约定完全一致,但它使用一组不同的调用者和被调用者保存的寄存器。这消除了在调用方中在调用前后保存和恢复大量寄存器集的负担。如果参数在被调用者保存的寄存器中传递,则它们将在调用期间由被调用者保留。对于以被调用者保存的寄存器返回的值,这并不适用。
-
在X86-64上,被调用者保留除R11之外的所有通用寄存器。 R11可以用作暂存寄存器。此外,它还保留所有浮点寄存器(XMMs / YMMs)。
-
在AArch64上,被调用者保留除X0-X8和X16-X18之外的所有通用寄存器。此外,它还会保留V8-V31 SIMD浮点寄存器的低128位。
这个约定的想法是支持调用运行时函数,这些函数不需要调用任何其他函数。
与PreserveMost调用约定一样,这种调用约定将由ObjectiveC运行时的未来版本使用,并且目前应被视为实验性质。
“cxx_fast_tlscc”- 用于访问函数的CXX_FAST_TLS调用约束
Clang生成一个访问函数来访问C++风格的TLS。访问函数通常具有入口块、出口块和在第一次运行时运行的初始化块。入口和出口块可以访问一些TLS IR变量,每个访问都将被降低为特定于平台的序列。
这种调用约定旨在通过尽可能保留尽可能多的寄存器(由入口和出口块组成的快速路径上保留的所有寄存器)来最小化调用方的开销。
这种调用约定在参数和返回值传递方面与C调用约定完全相同,但它使用不同的调用方/被调用方保留的寄存器集。
鉴于每个平台都有自己的降序列,因此具有自己的保留寄存器集,因此我们无法使用现有的PreserveMost。
- 在X86-64上,被调用者保留除RDI和RAX之外的所有通用寄存器。
“tailcc”- 尾部可调用调用约定(Tail callable calling convention)
这种调用约定确保尾调用位置中的函数调用总是进行尾调用优化。这种调用约定等效于fastcc,只是多了一个额外的保证,在可能的情况下始终会产生尾调用。
只有在使用此调用约定、fastcc、GHC或HiPE约定时,才能进行尾调用优化。这种调用约定不支持可变参数,并要求所有被调用函数的原型与函数定义的原型完全匹配。
“swiftcc”-这种调用约定用于Swift语言
- 在X86-64上,RCX和R8可用于额外的整数返回值,而XMM2和XMM3可用于额外的浮点/向量返回值。
- 在IOS平台上,我们使用AAPCS-VFP调用约束。
“cfguard_checkcc”-窗口控制流保护(检查机制)
这种调用约定用于Control Flow Guard检查函数,可以在间接调用之前插入对该函数的调用,以检查调用目标是否为有效函数地址。检查函数没有返回值,但如果地址不是有效目标,则会触发操作系统级别的错误。由检查函数保留的寄存器集以及包含目标地址的寄存器是与体系结构相关的。
- 在X86架构中,目标地址通过ECX寄存器传递。
- 在ARM架构中,目标地址通过R0寄存器传递。
- 在AArch64中,目标地址通过X15寄存器传递。
“cc <n>”-编号约定
任何调用约定都可以通过数字来指定,允许使用特定于目标的调用约定。特定于目标的调用约定从64开始。
1.3 可见性风格
所有的全局变量和函数都有以下可见性风格之一:
“default”-默认风隔
对于使用 ELF 目标文件格式的目标, 默认可见性意味着声明对其他模块可见,在共享库中,意味着声明实体可以被覆盖。 在 Darwin 上,默认可见性意味着声明对其他模块可见。在 XCOFF 上,默认可见性意味着不会设置显式的可见性位,并且符号是否对其他模块可见(即“导出”)主要取决于提供给链接器的导出列表。默认可见性与语言中的“外部链接”对应。
“hidden”-隐私风隔
如果两个具有隐藏可见性的对象声明在同一个共享对象中,则它们引用的是同一个对象。通常,隐藏可见性表示该符号不会被放置到动态符号表中,因此没有其他模块(可执行文件或共享库)可以直接引用它。
“protected”-保护风格
在 ELF 格式中,受保护的可见性表示符号将被放置在动态符号表中,但定义模块内的引用将绑定到本地符号。也就是说,该符号无法被另一个模块覆盖。
具有internal或private链接的符号必须具有默认可见性。
1.5 DLL 存储级别
所有全局变量、函数和别名都可以具有以下 DLL 存储类之一:
dllimport
"dllimport"会导致编译器通过由导出符号的DLL设置的全局指向指针引用函数或变量。在Microsoft Windows目标上,指针名称是由__imp_和函数或变量名称组合而成。
dllexport
在Microsoft Windows目标上,“dllexport”会导致编译器提供一个指向DLL中指针的全局指针,以便可以使用dllimport属性进行引用。指针名称由__imp_和函数或变量名称组合而成。在XCOFF目标上,dllexport表示该符号将对其他模块可见,使用“exported”可见性,并因此被链接器置于加载器部分符号表中。由于这个存储类是为了定义一个DLL接口而存在的,所以编译器、汇编器和链接器知道它是外部引用的,并且必须避免删除符号。
具有internal或private链接的符号不能具有DLL存储类。
1.6 线程局部存储模型
thread_local可以用来定义一个变量,这意味着它不会被线程共享(每个线程都有该变量的独立副本)。并非所有目标平台都支持线程局部变量。可选地,可以指定TLS(Thread Local Storage)模型:localdynamic
对于仅在当前共享库中使用的变量
initialexec
对于不会被动态加载的模块中的变量。
localexec
对于在可执行文件中定义的变量且只在其中使用。
如果没有明确指定模型,则使用“通用动态”模型。这个模型对应于ELF TLS模型;请参阅 ELF处理线程局部存储 以获取有关在哪些情况下可以使用不同模型的详细信息。如果指定的模型不受支持,或者可以选择更好的模型,则目标可能会选择不同的TLS模型。
模型也可以在别名中指定,但这只会影响如何访问别名。它不会对被别名指向的对象产生任何影响。
对于没有链接器支持的ELF TLS模型的平台,可以使用“-femulated-tls”标志生成与GCC兼容的模拟TLS代码。
1.7 运行时的抢占说明符(Runtime Preemption Specifiers)
全局变量、函数和别名可以具有可选的运行时抢占说明符。如果未明确给出抢占说明符,则假定符号是“dso_preemptable”。
dso_preemptable
表示在运行时可以用来替换函数或变量的符号来自于链接单元之外。
dso_local
编译器可以假定已标记为“dso_local”的函数或变量将解析为链接单元内的符号。即使定义不在此编译单元中,也会生成直接访问。
1.8 结构类型(Structure Types)
LLVM IR允许您同时指定“identified”和“literal” 结构类型。字面量类型在结构上唯一,但标识类型永远不会是唯一的。 不透明的结构类型(opaque structural type) 也可以用于前向声明一个尚未可用的类型。一个标识结构说明的例子是:
%mytype = type { %mytype*, i32 }
在LLVM 3.0版本之前,标识类型在结构上被唯一化。但在最近的LLVM版本中,只有字面量类型会被唯一化。
1.9 非整数指针类型
非整数指针类型仍在开发中,目前应将其视为实验性质。 LLVM IR可以选择性地允许前端通过 datalayout 字符串
将某些地址空间中的指针标记为“非整数”。非整数指针类型表示具有未指定位表示的指针;也就是说,整数表示可能与目标相关或不稳定(不由固定的整数支持)。
inttoptr 和 ptrtoint 指令与整数(即常规)指针的语义相同,它们用于将整数转换为相应的指针类型,反之亦然。但是,还需要注意其他一些影响。由于非整数指针的位表示可能不稳定,对同一操作数的两个相同转换可能会返回不同的值。换句话说,非整数类型的转换取决于实现定义的环境状态。
如果前端希望在转换后观察特定的值,生成的IR必须以实现定义的方式与底层环境进行同步。(在实践中,这通常需要对此类操作使用noinline例程。)
从优化器的角度来看,对于非整数类型,inttoptr和ptrtoint与整数类型的情况类似,但有一个关键的例外:优化器通常不能插入新的动态转换。如果插入了新的转换,优化器需要确保:a)所有可能的值都是有效的,或者b)插入适当的同步操作。由于适当的同步操作是实现定义的,优化器无法执行后者。前者很具挑战性,因为对于非整数类型,许多常见的预期属性(如ptrtoint(v)-ptrtoint(v) == 0)并不成立。
全局变量
全局变量定义了在编译时分配内存的内存区域,而不是运行时 run-time。 全局变量定义必须初始化。 在其他翻译单元中,也可以声明全局变量,这种情况下它们不需要初始化。 全局变量也可以选择性地指定一个 链接类型全局变量的定义或声明可以在其中指定一个明确的段落,并且可以指定可选的明确对齐方式。如果变量的声明和定义之间在明确或推断的段落信息上存在不匹配,会导致行为未定义。
变量可以被定义为全局常量,这表示变量的内容永远不会被修改(从而实现更好的优化,允许将全局数据放置在可执行文件的只读段等)。需要注意的是,需要在运行时初始化的变量无法被标记为常量,因为变量需要进行存储操作。
LLVM明确允许将全局变量的声明标记为常量,即使最终的全局定义并非如此。这种能力可以用于稍微改善程序的优化,但需要语言定义来保证基于“常量性”的优化对不包含定义的翻译单元是有效的。
作为SSA值,全局变量定义了在程序中所有基本块范围内(即它们支配)的指针值。全局变量始终定义指向其“内容”类型的指针,因为它们描述了一个内存区域,而LLVM中的所有内存对象都通过指针访问。
全局变量可以使用unnamed_addr标记,这表示地址不重要,只有内容是重要的。使用这种标记的常量可以与具有相同初始化器的其他常量合并。需要注意的是,具有重要地址的常量可以与unnamed_addr常量合并,结果是一个具有重要地址的常量。
如果给定了local_unnamed_addr属性,表示在模块内部地址是不重要的。
全局变量可以声明为位于目标特定的编号地址空间中。对于支持的目标,地址空间可能会影响优化的执行方式和/或用于访问变量的目标指令。默认的地址空间为零。地址空间限定符必须位于其他属性之前。
LLVM允许为全局变量指定一个明确的段落(section)。如果目标平台支持,它将把全局变量放置在指定的段落中。此外,如果目标平台具备必要的支持,全局变量可以放置在一个comdat中。
外部声明可以指定一个明确的段落(section)。如果目标平台使用这些信息,段落信息将在LLVM IR中保留。将段落信息附加到外部声明是断言其定义位于指定段落的一种方式。如果定义位于不同的段落,则行为是未定义的。
默认情况下,通过假设模块中定义的全局变量在全局初始化器开始之前不会被修改,对全局初始化器进行优化。即使是从模块外部可能访问的变量,包括具有外部链接的变量或出现在@llvm.used或dllexported变量中的变量也是如此。通过使用externally_initialized标记变量,可以取消这种假设。
可以为全局变量指定一个明确的对齐方式,该对齐方式必须是2的幂。如果未指定或者对齐方式设置为零,则目标平台会根据其自身需要将全局变量的对齐方式设置为合适的值。如果指定了明确的对齐方式,则全局变量将被强制具有该对齐方式。如果全局变量已分配了段落,则目标平台和优化器不得过度对齐该全局变量。在这种情况下,额外的对齐方式可能会导致观察到问题:例如,代码可能假设全局变量在其段落中密集排列,并尝试将其作为数组进行迭代,而对齐填充会打破这种迭代。对于TLS变量,如果存在模块标志MaxTLSAlign,则对齐方式限制为给定值。优化器不得对这些变量施加更强的对齐方式。最大对齐方式为1 << 32。
对于全局变量的声明,以及可能在链接时替换的定义(如linkonce、weak、extern_weak和common链接类型),其解析到的定义的分配大小和对齐方式必须大于或等于声明或可替换定义的分配大小和对齐方式,否则行为是未定义的。
全局变量还可以具有
DLL存储Class
可选的运行时抢占指示符、
可选的全局属性和可选的附加 元数据列表。
变量和别名可以有一个 线程局部存储模型
可扩展向量(向量元素个数不定)
不能作为全局变量或数组的成员,因为它们的大小在编译时是未知的。它们可以用于结构体中,以便支持返回多个值的内部函数。通常情况下,包含可扩展向量的结构体被认为是“无大小”的,不能在加载、存储、alloca或GEP(GetElementPtr)操作中使用。唯一的例外是包含相同类型的可扩展向量的结构体(例如{<vscale x 2 x i32>,<vscale x 2 x i32>} 包含相同类型,而 {<vscale x 2 x i32>,<vscale x 2 x i64>}则不包含)。这种类型的结构体(我们称之为同质可扩展向量结构体)被认为是有大小的,可以在加载、存储、alloca操作中使用,但不能用于GEP操作。
语法:
@<GlobalVarName> = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [ExternallyInitialized] <global | constant> <Type> [<InitializerConstant>] [, section "name"] [, partition "name"] [, comdat [($name)]] [, align <Alignment>] [, no_sanitize_address] [, no_sanitize_hwaddress] [, sanitize_address_dyninit] [, sanitize_memtag] (, !name !N)*
例如,以下定义了一个带有初始化、段落section和对齐方式的带编号地址空间的全局变量:
@G = addrspace(5) constant float 1.0, section "foo", align 4
以下例子只声明了一个全局变量
@G = external global i32
下面的示例定义了一个使用initialexec TLS模型的线程本地全局变量:
@G = thread_local(initialexec) global i32 0, align 4
函数 Functions
LLVM函数定义由以下内容组成:“define”关键字、可选的链接类型、可选的运行时抢占指示符、可选的可见性样式、可选的DLL存储类别、可选的调用约定、可选的unnamed_addr属性、返回类型、可选的返回类型参数属性、函数名称、(可能为空的)参数列表(每个都可以有可选的参数属性)、可选的函数属性、可选的地址空间、可选的段落、可选的分区、可选的对齐方式、可选的comdat、可选的垃圾收集器名称、可选的前缀、可选的序言、可选的异常处理函数、可选的附加元数据列表、左大括号、基本块列表和右大括号。
https://llvm.org/docs/LangRef.html#dllstorageclass
https://llvm.org/docs/LangRef.html#callingconv 参数属性 函数属性: comdat: 垃圾收集器名称: 前缀 序言: 异常处理函数(personality):语法:
define [linkage] [PreemptionSpecifier] [visibility] [DLLStorageClass] [cconv] [ret attrs] <ResultType> @<FunctionName> ([argument list]) [(unnamed_addr|local_unnamed_addr)] [AddrSpace] [fn Attrs] [section "name"] [partition "name"] [comdat [($name)]] [align N] [gc] [prefix Constant] [prologue Constant] [personality Constant] (!name !N)* { ... }
参数列表是一个逗号分隔的参数序列,每个参数的格式如下:
<type> [parameter Attrs] [name]
LLVM函数声明由以下内容组成:“declare”关键字、可选的链接类型、可选的可见性样式、可选的DLL存储类、可选的调用约定、可选的unnamed_addr或local_unnamed_addr属性、可选的地址空间、返回类型、可选的返回类型参数属性、函数名称、可能为空的参数列表、可选的对齐方式、可选的垃圾收集器名称、可选的前缀和可选的序言。
语法:
declare [linkage] [visibility] [DLLStorageClass] [cconv] [ret attrs] <ResultType> @<FunctionName> ([argument list]) [(unnamed_addr|local_unnamed_addr)] [align N] [gc] [prefix Constant] [prologue Constant]
函数定义包含一个基本块列表,形成函数的控制流图(Control Flow Graph)。每个基本块可以选择以标签开始(给基本块一个符号表项),包含一系列指令,并以终结指令(例如分支或函数返回)结束。如果没有提供显式的标签名称,将为块分配一个隐式的编号标签,使用与未命名临时变量相同的计数器的下一个值(参见上文)。例如,如果函数入口块没有显式标签,它将被分配标签“%0”,然后该块中的第一个未命名临时变量将是“%1”等等。如果显式指定了数字标签,则必须与隐式使用的数字标签匹配。
函数中的第一个基本块有两个特殊之处:它在函数入口处立即执行,并且不允许有前驱基本块(即函数的入口块不能有任何分支指向它)。由于该块没有前驱,因此也不能有任何 PHI节点 。
LLVM允许为函数指定显式的节(section)。如果目标支持,它将将函数发射到指定的节中。此外,函数可以放置在一个COMDAT中。
可以为函数指定显式对齐方式。如果未提供对齐方式,或者对齐方式设置为零,则目标会根据自己的方便性来设置函数的对齐方式。如果指定了显式对齐方式,则函数将至少具有该对齐方式。所有的对齐方式必须是2的幂次。
如果给出了unnamed_addr属性,地址被认为是不重要的,并且可以合并两个相同的函数。
如果给出了local_unnamed_addr属性,模块内部已知地址是不重要的。
如果没有给出显式地址空间,它将默认为datalayout字符串中的程序地址空间。
别名
与函数或变量不同,别名(Aliases)不会创建任何新的数据。它们只是现有位置的一个新符号和元数据。别名(Aliases)具有一个名称和一个别名目标(aliasee),该别名目标可以是全局值或常量表达式。
别名(Aliases)可以具有可选的链接类型(linkage type),可选的运行时抢占说明符(runtime preemption specifier),可选的可见性风格(visibility style),可选的动态链接库存储类(DLL storage class)和可选的线程局部存储模型(tls model)。
语法:
@<Name> = [Linkage] [PreemptionSpecifier] [Visibility] [DLLStorageClass] [ThreadLocal] [(unnamed_addr|local_unnamed_addr)] alias <AliaseeTy>, <AliaseeTy>* @<Aliasee> [, partition "name"]
链接类型必须为private、internal、linkonce、weak、linkonce_odr、weak_odr、external或available_externally之一。请注意,一些系统链接器可能无法正确处理被别名引用的弱符号(weak symbol)的删除操作。
非unnamed_addr的别名(Aliases)保证具有与别名目标表达式相同的地址。而unnamed_addr的别名仅保证指向相同的内容。
由于别名只是第二个名称,所以会应用一些限制,其中一些限制只能在生成目标文件时进行检查:
-
定义别名的表达式必须在汇编时可计算。由于它只是一个名称,因此不能使用重定位。
-
表达式中的任何别名都不能是弱别名,因为在目标文件中无法表示中间别名被覆盖的可能性。
-
如果别名具有available_externally的链接类型,那么别名目标必须是一个available_externally的全局值;否则别名目标可以是一个表达式,但是表达式中的全局值不能是一个声明,因为这将需要进行重新定位,而这是不可能的。
-
如果在链接时或运行时,别名(alias)或别名目标(aliasee)可能被模块外的符号替换,任何优化都不能将别名替换为别名目标,因为行为可能会有所不同。别名可以被用作一个确保指向当前模块内容的名称。
IFuncs
IFuncs(Indirect Functions)与别名类似,不会创建任何新的数据或函数。它们只是一个动态链接器在运行时通过调用解析器函数来解析的新符号。IFuncs具有一个名称和一个解析器(resolver),解析器是由动态链接器调用的函数,它返回与该名称相关联的另一个函数的地址。
IFunc可以有可选的链接类型(linkage type)和可选的可见性样式(visibility style)
语法:
@<Name> = [Linkage] [PreemptionSpecifier] [Visibility] ifunc <IFuncTy>, <ResolverTy>* @<Resolver> [, partition "name"]
Comdats
Comdat IR提供了对对象文件COMDAT/section 组功能的访问,它代表相关联的节(sections)。Comdat具有一个名称,它代表COMDAT键(COMDAT key),以及一个选择类型(selection kind),用于在两个不同的目标文件中决定如何去重具有相同键的comdat。一个comdat必须作为一个整体被包含或者忽略。允许丢弃整个comdat,但不允许丢弃其中的子集。
一个全局对象最多可以是一个comdat的成员。如果存在别名(alias),则别名将放置在与其指向的目标对象(aliasee)相同的comdat中(如果有的话)。
语法:
$<Name> = comdat SelectionKind
对于除了非重复节点(nodeduplicate)之外的选择类型,链接器只能保留重复comdat中的一个,并且必须丢弃其余comdat中的成员。支持以下选择类型:
any
链接器可以选择任何COMDAT键,选择是任意的。
exactmatch
链接器可以选择任何COMDAT键,但是这些sections必须包含相同的数据。
largest
链接器将选择包含最大COMDAT键的section。
nodeduplicate
不进行去重操作。
samesize
链接器可以选择任何COMDAT键,但是这些节必须包含相同数量的数据。
- XCOFF和Mach-O不支持COMDAT。
- COFF支持所有选择类型。非nodeduplicate的选择类型需要一个非局部链接的COMDAT符号。
- ELF支持any和nodeduplicate。
- WebAssembly仅支持任意选择类型。
以下是一个COFF COMDAT的示例,其中只有当COMDAT键所在的节是最大的时候,函数才会被选择:
$foo = comdat largest @foo = global i32 2, comdat($foo) define void @bar() comdat($foo) { ret void }
在一个COFF目标文件中,这将创建一个包含@foo符号内容的选择类型为IMAGE_COMDAT_SELECT_LARGEST的COMDAT节,以及另一个与第一个COMDAT节(section)相关联的、选择类型为IMAGE_COMDAT_SELECT_ASSOCIATIVE的COMDAT节(section),其中包含@bar符号的内容。
为一种语法糖(syntactic sugar),如果名称与全局名称相同,则可以省略$name。
$foo = comdat any @foo = global i32 2, comdat @bar = global i32 3, comdat($foo)
全局对象的属性存在一些限制。当目标为COFF时,它本身或其别名必须与COMDAT组具有相同的名称。在链接时,可以使用该对象的内容和大小来确定根据选择类型选择哪个COMDAT组。由于对象的名称必须与COMDAT组的名称匹配,因此全局对象的链接性不能是局部的;如果符号表中发生冲突,局部符号可能会被重命名。
翻译: COMDATS和节属性的组合使用可能会产生令人惊讶的结果。例如:
$foo = comdat any $bar = comdat any @g1 = global i32 42, section "sec", comdat($foo) @g2 = global i32 42, section "sec", comdat($bar)
从目标文件的角度来看,这要求创建两个具有相同名称的节。这是必要的,因为这两个全局对象属于不同的COMDAT组,而在目标文件级别上,COMDATs是由节来表示的。
请注意,除了使用COMDAT IR指定的COMDAT之外,某些IR结构(如全局变量和函数)还可以在目标文件中创建COMDAT。当代码生成器配置为在单独的section中发出全局变量(例如,使用-data-sections或-function-sections参数运行llc时),就会出现这种情况。
命名元数据
命名元数据是元数据的集合。命名元数据的有效操作数只能是元数据节点(Metadata nodes )(而不是元数据字符串)。命名元数据以带有元数据前缀的字符字符串表示。元数据名称的规则与标识符相同,但不允许使用带引号的名称。仍然有效的"\xx"类型转义允许任何字符作为名称的一部分。
语法:
; Some unnamed metadata nodes, which are referenced by the named metadata. !0 = !{!"zero"} !1 = !{!"one"} !2 = !{!"two"} ; A named metadata. !name = !{!0, !1, !2}
参数属性
函数类型的返回类型和每个参数都可以具有一组与之关联的参数属性。参数属性用于传递有关函数的结果或参数的其他信息。参数属性被视为函数的一部分,而不是函数类型的一部分,因此具有不同参数属性的函数可以具有相同的函数类型。
参数属性是紧跟在指定类型之后的简单关键字。如果需要多个参数属性,则它们之间用空格分隔。例如:
declare i32 @printf(ptr noalias nocapture, ...) declare i32 @atoi(i8 zeroext) declare signext i8 @returns_signed_char()
请注意,对于函数结果的任何属性(如nonnull、signext),它们都位于结果类型之前。
目前,只定义了以下参数属性:
zeroext
这表示对于参数而言,调用者(caller)应根据目标平台的ABI将其零扩展到所需的长度,对于返回值而言,被调用者(callee)应进行零扩展。
signext
这表示对于参数而言,调用者(caller)应根据目标平台的ABI(通常为32位)将其符号扩展到所需的长度,对于返回值而言,被调用者(callee)应进行符号扩展。
inreg
这表示在为函数调用或返回发出代码时,应以特定于目标的方式处理该参数或返回值(通常是将其放入寄存器而不是内存中,尽管一些目标使用它来区分两种不同类型的寄存器)。对于此属性的使用是与目标相关的。
byval(<ty>)
这表示指针参数应该以按值传递给函数。此属性意味着在调用者和被调用者之间创建了指针所指对象的隐藏副本,因此被调用者无法修改调用者中的值。此属性仅对LLVM指针参数有效。通常用于按值传递结构体和数组,但对指向标量的指针也有效。副本被视为属于调用者而不是被调用者(例如,只读函数不应该写入byval参数)。这不是返回值的有效属性。
byval类型参数指定内存中的值类型,必须与参数的指针类型相同。
byval属性还支持使用align属性指定对齐方式。它指示形成堆栈插槽的对齐方式,并指定调用点处指针的已知对齐方式。如果未指定对齐方式,则代码生成器会进行特定于目标的假设。
preallocated(<ty>)
这表示指针参数应该以按值传递给函数,并且在调用指令之前已经初始化了指针参数所指的对象。此属性仅适用于LLVM指针参数。在非musttail调用中,参数必须是由适当的
llvm.call.preallocated.arg 返回的值,而在musttail调用中,参数必须是相应的调用者参数,尽管在代码生成过程中会被忽略。
在任何参数中具有预分配属性的非musttail函数调用必须具有一个"preallocated"操作数捆绑。而musttail函数调用不能有"preallocated"操作数捆绑。
preallocated属性需要一个类型参数,该参数必须与参数的指针类型相同。
preallocated属性还支持使用align属性指定对齐方式。它指示形成堆栈插槽的对齐方式,并指定调用点处指针的已知对齐方式。如果未指定对齐方式,则代码生成器会根据目标进行特定的假设。
inalloca(<ty>)
inalloca参数属性允许调用者获取传出堆栈参数的地址。inalloca参数必须是由alloca指令生成的指向堆栈内存的指针。alloca或参数分配还必须带有inalloca关键字的标记。只有最后一个参数可以具有inalloca属性,并且保证该参数将以内存方式传递。
参数分配最多只能被一次调用使用,因为调用可能会取消分配。inalloca属性不能与影响参数存储的其他属性(如inreg、nest、sret或byval)同时使用。inalloca属性还禁用了LLVM对大型聚合返回值的隐式降低,这意味着前端作者必须使用sret指针来降低它们。
当到达调用点时,参数分配必须是最近的尚存活的堆栈分配,否则行为未定义。在参数分配的调用点之后,可以分配额外的堆栈空间,但必须使用llvm.stackrestore清除它。
inalloca属性需要一个类型参数,该参数必须与参数的指针类型相同。
有关如何使用此属性的更多信息,请参阅"InAlloca属性的设计和用法"。
readonly
该属性表示函数不会通过该指针参数进行写操作,即使它可能会对指针所指向的内存进行写操作。如果函数对只读指针参数进行写操作,行为是未定义的。
writeonly
该属性表示函数可能会对该指针参数进行写操作,但不会通过该指针参数进行读操作(即使它可能会从指针所指向的内存中读取数据)。
如果函数从只写指针参数中进行读取操作,行为是未定义的。
垃圾收集器策略名称
每个函数可以指定一个垃圾回收器策略名称,它只是一个简单的字符串:define void @f() gc "name" { ... }
支持的name值包括LLVM内置的值以及由加载的插件提供的值。指定垃圾回收策略将导致编译器修改其输出,以支持指定的垃圾回收算法。需要注意的是,LLVM本身并不包含垃圾回收器,该功能仅限于生成与外部提供的收集器进行交互的机器码。
Prefix Data
前缀数据是与函数相关联的数据,代码生成器将在函数入口之前立即生成。此功能的目的是允许前端将特定函数与语言特定的运行时元数据关联,并通过函数指针使其可用,同时仍然允许调用函数指针。要访问给定函数的数据,程序可以将函数指针位转换为指向常量类型的指针,并解引用索引-1。这意味着IR符号指向前缀数据的末尾之后。例如,考虑一个带有单个i32注释的函数的例子,
define void @f() prefix i32 123 { ... }
前缀数据可以被引用为:
%a = getelementptr inbounds i32, ptr @f, i32 -1 %b = load i32, ptr %a
前缀数据的布局方式类似于它是前缀数据类型的全局变量的初始化器。函数将被放置在前缀数据的开头位置对齐的地方。这意味着,如果前缀数据的大小不是对齐大小的倍数,函数的入口点将不对齐。如果需要对齐函数的入口点,则必须在前缀数据中添加填充。
一个函数可以具有前缀数据但没有函数体。这与available_externally链接具有相似的语义,即数据可以被优化器使用,但不会在目标文件中生成
前导数据
前导属性(prologue attribute)允许在函数主体之前插入任意代码(以字节形式编码)。这可以用于实现函数的动态修改和仪器化。为了保持普通函数调用的语义,前导数据必须具有特定的格式。具体而言,它必须以一系列字节开头,这些字节解码成目标模块有效的一系列机器指令,这些指令将控制转移到紧随前导数据后的位置,而不执行任何其他可见操作。这使得内联器和其他传递能够在不涉及前导数据的情况下推断出函数定义的语义。很明显,这使得前导数据的格式高度依赖于目标系统。
对于x86架构来说,一个简单的有效前导数据示例是i8 144,它编码了nop指令。
define void @f() prologue i8 144 { ... }
一般情况下,可以通过编码相对分支指令来形成前导数据,以跳过元数据。以下是针对x86_64架构的有效前导数据示例,其中前两个字节编码了jmp .+10指令,实现了跳转操作:
%0 = type <{ i8, i8, ptr }> define void @f() prologue %0 <{ i8 235, i8 8, ptr @md}> { ... }
一般情况下,可以通过编码相对分支指令来形成前导数据,以跳过元数据。以下是针对x86_64架构的有效前导数据示例,其中前两个字节编码了jmp .+10指令,实现了跳转操作:
Personality Function
personality属性允许函数指定用于异常处理的函数。属性组
属性组是被IR中的对象引用的一组属性。它们对于保持.ll文件的可读性非常重要,因为许多函数将使用相同的属性集。在.ll文件对应于单个.c文件的退化情况下,单个属性组将捕捉用于构建该文件的重要命令行标志。
属性组是模块级别的对象。要使用属性组,对象引用属性组的ID(例如#37)。一个对象可以引用多个属性组。在这种情况下,来自不同属性组的属性将被合并。
以下是一个函数的属性组示例,该函数应始终被内联,具有4字节的堆栈对齐,并且不应使用SSE指令:
; Target-independent attributes: attributes #0 = { alwaysinline alignstack=4 } ; Target-dependent attributes: attributes #1 = { "no-sse" } ; Function @f has attributes: alwaysinline, alignstack=4, and "no-sse". define void @f() #0 #1 { ... }
函数属性 Function Attributes
函数属性被设置用于传递有关函数的额外信息。函数属性被认为是函数的一部分,而不是函数类型的一部分,因此具有不同函数属性的函数可以具有相同的函数类型。
函数属性是紧跟在指定类型后面的简单关键字。如果需要多个属性,它们之间用空格分隔。例如:
define void @f() noinline { ... } define void @f() alwaysinline { ... } define void @f() alwaysinline optsize { ... } define void @f() optsize { ... }
alignstack(<n>)
此属性指示在生成函数的入口和出口代码时,后端编译器应强制对齐堆栈指针。您可以在括号中指定所需的对齐方式,该对齐值必须是2的幂。
"alloc-family"="FAMILY"
这个属性指示分配器函数所属的“Family”。为了避免冲突,Family名称应与主要分配器函数的名称相匹配,即 malloc/calloc/realloc/free 对应的名称是 "malloc",::operator::new 和 ::operator::delete 对应的名称是 "_Znwm",aligned ::operator::new 和 ::operator::delete 对应的名称是 "_ZnwmSt11align_val_t"。在一个Family中匹配的 malloc/realloc/free 调用可以进行优化,但不匹配的调用会被保留下来不进行优化。
allockind("KIND")
描述了分配函数的行为。KIND 字符串包含以下选项中以逗号分隔的条目:
-
"alloc": 函数返回一个新的内存块或者 null。
-
"realloc": 该函数返回一个新的内存块或者 null。如果返回值非空,那么从内存块的起始位置到原始分配大小和新分配大小中较小的部分将与 allocptr 参数的内存内容匹配,并且即使函数返回相同的地址,allocptr 参数也会失效。
-
"free": 该函数释放由 allocptr 指定的内存块。标记为 "free" 的 allockind 的函数必须返回 void 类型。
-
"uninitialized": 任何新分配的内存(无论是从 "alloc" 函数得到的新块,还是从 "realloc" 函数得到的扩容后的容量)都将是未初始化的。
-
"zeroed" : 任何新分配的内存(无论是从 "alloc" 函数得到的新块,还是从 "realloc" 函数得到的扩容后的容量)都将被清零。
-
"aligned": 该函数返回根据 allocalign 参数对齐的内存。
前三个选项互斥,而剩下的选项描述了函数行为的更多细节。对于 "free" 类型的函数,剩下的选项是无效的。
allocsize(<EltSizeParam >[, <NumEltsParam>])
此属性表示被标注的函数将始终返回至少给定数量的字节(或 null)。它的参数是从零开始的参数编号;如果提供了一个参数,则假设返回指针处至少有CallSite.Args[EltSizeParam] 字节可用。如果提供了两个参数,则假设可用 CallSite.Args[EltSizeParam] * CallSite.Args[NumEltsParam] 字节。所引用的参数必须是整数类型。对于返回的内存块的内容不作任何假设。
alwaysinline
该属性表示内联器应尽可能将此函数内联到调用者中,忽略对于该调用者的任何活动内联大小阈值。
builtin
这表示调用点处的被调用函数应被识别为内置函数,尽管函数的声明使用了 nobuiltin 属性。这仅对于直接调用使用 nobuiltin 属性声明的函数的调用点有效。
cold
该属性表示该函数很少被调用。在计算边权重时,被冷函数调用后支配的基本块也被认为是冷块;因此,其权重较低。
convergent
在某些并行执行模型中,存在无法对任何额外值进行控制依赖的操作。我们将这样的操作称为收敛操作,并使用此属性标记它们。
收敛属性可以出现在函数或调用/调用指令上。当它出现在函数上时,表示对该函数的调用不应该对额外的值进行控制依赖。例如,内置函数 llvm.nvvm.barrier0 是收敛的,因此对该内置函数的调用不能对额外的值进行控制依赖。
当收敛属性出现在调用/调用指令上时,它表示我们应该将该调用视为对一个收敛函数的调用。这在间接调用中特别有用;如果没有这个属性,我们可能会将这样的调用视为目标不是收敛的。
当优化器能够证明函数不执行任何收敛操作时,可能会删除函数上的收敛属性。类似地,当优化器能够证明调用/调用指令不能调用一个收敛函数时,可能会删除调用/调用指令上的收敛属性。
disable_sanitizer_instrumentation
在使用工具进行代码检测时,跳过某些函数以确保不对它们应用任何工具的检测是非常重要的。
该属性并不总是与缺少 sanitize_<name> 属性相似:根据具体的检查器,代码可能会被插入到函数中,而不管 sanitize_<name> 属性如何,以防止假阳性报告(positive reports)。
disable_sanitizer_instrumentation 属性会禁用所有类型的插桩,优先于 sanitize_<name> 属性和其他编译器标志。它会彻底阻止对代码进行任何插桩操作。
dontcall-error
此属性表示,当通过优化未消除带有此属性的函数的调用时,应发出错误诊断。前端可以在这些被调用函数的调用点上提供可选的 srcloc 元数据节点,以附加有关源语言中该调用的信息。可以提供字符串值作为注释。
dontcall-warn
此属性表示,当通过优化未消除带有此属性的函数的调用时,应发出警告诊断。前端可以在这些被调用函数的调用点上提供可选的 srcloc 元数据节点,以附加有关源语言中该调用的信息。可以提供字符串值作为注释。
fn_ret_thunk_extern
这个属性告诉代码生成器,函数的返回应该被替换为跳转到外部定义的与特定架构相关的符号。对于X86架构,这个符号的标识符是__x86_return_thunk。
"frame-pointer"
这个属性告诉代码生成器函数是否应该保留帧指针。即使此属性指示可以消除帧指针,代码生成器也可能会发出帧指针。允许的字符串值有:
- "none"(默认值)- 可以消除帧指针。
- "non-leaf" - 如果函数调用了其他函数,则应保留帧指针
- "all" - 应该保留帧指针。
hot
该属性表示此函数是程序执行的hot pot 。该函数将进行更加积极的优化,并将被放置在文本部分的特殊子部分中,以提高局部性。
当启用了配置文件反馈时,此属性优先于配置文件信息。通过将函数标记为热点,用户可以解决在训练输入中没有完全覆盖所有热点函数的情况。
inlinehint
该属性表示源代码中含有提示,希望对此函数进行内联(例如,在C/C++中使用的"inline"关键字)。这只是一个提示,对内联器没有强制要求。
jumptable
该属性指示在代码生成时,应将该函数添加到跳转指令表中,并且所有引用此函数地址的引用都应替换为对应的跳转指令表函数指针的引用。请注意,这将为原始函数创建一个新的指针,这意味着依赖于函数指针标识的代码可能会被破坏。因此,任何带有"jumptable"注解的函数也必须是"unnamed_addr"的。
memory(...)
该属性指定调用点或函数可能的内存效果。它允许指定可能的内存位置类型(argmem、inaccessiblemem以及默认值)的可能访问类型(none、read、write或readwrite)。通过示例最容易理解:
其他属性 略
调用点属性
除了函数属性之外,还支持以下仅适用于调用点的属性:vector-function-abi-variant
该属性可以附加到函数调用上,用于列出与该函数相关联的向量函数。请注意,该属性不能附加到invoke或callbr指令上。该属性由逗号分隔的名称列表组成。列表的顺序不表示首选项(它在逻辑上是一个集合)。编译器可以自由选择任何列出的向量函数。
mangled names 的语法如下所示:
_ZGV<isa><mask><vlen><parameters>_<scalar_name>[(<vector_redirection>)]
ELF
ELF 是英文 Executable and Linkable Format(可执行和可链接文件格式)的缩写,是一种用于表示可执行文件、目标文件、共享库等可执行二进制程序的标准文件格式。它是在 UNIX 系统中广泛使用的文件格式,一般由编译器生成,并被动态连接器(ld.so)使用来将代码和数据加载到内存中并执行。
ELF 文件格式定义了程序的入口地址、段信息、符号表、重定位表等重要信息,同时还支持许多高级特性,如动态链接、调试信息等。通过 ELF 格式的使用,不仅能够方便地将多个目标文件链接到一起形成可执行程序,而且能够在运行时对共享库进行动态链接,减小程序体积,提高程序的灵活性和性能。
section
在LLVM中,section是代码或数据在ELF目标文件中的逻辑分组。它们允许将特定类型的数据和代码聚集在一起,以便于链接器进行处理。可以使用指定的属性将函数或全局变量放置在特定的节中,这有助于进行调试、以及根据应用程序的需要优化代码和数据。
DLL Storage Classes
DLL Storage Classes指的是在Microsoft Visual C++编译器中使用的一种存储类别,用于控制函数和变量在动态链接库(DLL)中的可见性和访问方式。DLL Storage Classes共有三种:__declspec(dllexport)、__declspec(dllimport)和__stdcall。__declspec(dllexport)用于定义从DLL导出的函数和变量,在DLL外部可见和可访问。__declspec(dllimport) 用于定义从DLL导入的函数和变量,与 __declspec(dllexport)相反,它表明这个函数或变量是从DLL中引入的。__stdcall 也是一种存储类别,用于定义被存在动态链接库中的函数如何进行调用,它规定了函数参数的传递顺序和参数压栈方式,以确保安全有效的调用函数。
comdat
在LLVM中,comdat是一种指定具有相同名称和特定属性的重复定义的合并方式的机制。comdat常用于解决链接时的符号冲突问题。
当多个源文件定义了相同的符号时,在链接时会发生符号冲突。comdat允许将所有这些 定义单独编译,并将它们按照特定的属性分组,最终在链接时只选择一个 comdat 作为目标文件的定义。这个过程涉及到一个称为选择函数(selection function)的函数,它确定哪个 comdat 将被链接器选择。
在LLVM IR中,使用 comdat 关键字来声明一个符号应该在链接时合并的方式,例如:
@my_global = global i32 0, align 4, comdat, !dbg !5
在这个例子中,@my_global 是一个全局变量,通过 comdat 属性指定了它的合并方式。在链接时,如果存在多个具有相同名称和 comdat 属性的全局变量,只有其中一个会被保留并链接到目标文件中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?