LLD-LLVM链接器,ELF、COFF与Wasm Linkers

LLD-LLVM链接器,ELF、COFF与Wasm Linkers

LLD是LLVM项目中的一个链接器,它是系统链接器的替代品,运行速度比它们快得多。它还提供了对工具链开发人员有用的功能。

链接器支持ELF(Unix)、PE/COFF(Windows)、Mach-O(macOS)和WebAssembly(按完整性降序排列)。在内部,LLD由几个不同的连接体组成。ELF端口将在本文档中描述。PE/COFF端口已完成,包括Windows调试信息(PDB)支持。WebAssembly端口仍在进行中(请参阅WebAssembly lld端口)。

特征

LLD是GNU链接器的替代品,它接受与GNU相同的命令行参数和链接器脚本。

LLD非常快。当在多核机器上链接大型程序时,可以预期LLD的运行速度是GNU黄金链接器的两倍以上。不过,mileage可能会有所不同。

它支持各种CPU/ABI,包括AArch64、AMDGPU、ARM、Hexagon、MIPS 32/64大/小端、PowerPC、PowerPC64、RISC-V、SPARC V9、x86-32和x86-64。其中,AArch64、ARM(>=v6)、PowerPC、PowerPC64、x86-32和x86-64具有生产质量。MIPS似乎也不错。

它始终是一个交叉链接器,这意味着无论它是如何构建的,它始终支持所有上述目标。事实上,没有提供启用/禁用每个目标的构建时选项。这应该可以很容易地将链接器用作交叉编译工具链的一部分。

可以在程序中嵌入LLD,以消除对外部链接器的依赖。所要做的就是构造对象文件和命令行参数,就像调用外部链接器,然后从代码中调用链接器的主函数lld::elf::link一样。

使用LLVM libObject库来读取对象文件,因此这不是一个完全公平的比较,但截至2017年2月,LLD/ELF仅包含21k行C++代码,而GNU gold包含198k行C++。

默认情况下支持链路时间优化(LTO)。基本上,要执行LTO,您所要做的就是将-flto选项传递给clang。然后clang创建的对象文件不是本地对象文件格式,而是LLVM位代码格式。LLD读取位代码对象文件,使用LLVM编译它们并发出输出文件。因为这样LLD可以看到整个程序,它可以进行整个程序的优化。

古代Unix系统(90年代以前甚至更早)的一些非常古老的功能已经被删除。一些默认设置已针对21世纪进行了调整。例如,默认情况下,堆栈标记为不可执行,以加强安全性。

Demo

这是带有SSD驱动器的2插槽20核40线程Xeon E5-2680 2.80 GHz机器上的链路时间比较。无论是否支持多线程,都运行了gold和lld。为了禁用多线程,在命令行中添加了-no线程。

 

 

 正如所看到的,lld明显快于GNU链接器。请注意,这只是环境的基准结果。根据可用内核的数量、可用内存量或磁盘延迟/吞吐量,结果可能会有所不同。

由于GNUld不支持-icf=all和-gdb索引选项,从GNUld的命令行中删除了它们。如果GNU ld有这些选项,它的速度会比这慢。

架构

如果已经使用SVN签出了LLVM,可以在tools目录下签出LLD,就像可能对clang所做的那样。有关详细信息,请参阅LLVM系统入门。

如果还没有签出LLVM,构建LLD的最简单方法是从git镜像中签出整个LLVM项目/子项目并构建该树。需要cmake,当然还有C++编译器。

$ git clone https://github.com/llvm/llvm-project llvm-project

$ mkdir build

$ cd build

$ cmake -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS=lld -DCMAKE_INSTALL_PREFIX=/usr/local ../llvm-project/llvm

$ make install

使用LLD

LLD安装为ld.LLD。在Unix上,链接器由编译器驱动程序调用,因此不需要直接使用该命令。有几种方法可以告诉编译器驱动程序使用ld.lld而不是默认链接器。

最简单的方法是覆盖默认链接器。将LLD安装到磁盘上的某个位置后,可以通过执行ln-s/path/to/ld.LLD/usr/bin/ld创建符号链接,以便将/usr/bin/l解析为LLD。

如果不想更改系统设置,可以使用clang的-fuse ld选项。这样,在构建程序时,希望将-fuse ld=lld设置为LDFLAGS。

LLD将其名称和版本号保留在输出中的.comment部分。如果怀疑是否成功使用了LLD,请运行readelf--string dump.comment<output file>并检查输出。如果输出中包含字符串“Linker:LLD”,则使用的是LLD。

历史

以下是ELF和COFF端口的简要项目历史。

2015年5月:决定重写COFF链接器并做到了这一点。注意到新的链接器比MSVC链接器快得多。

2015年7月:基于COFF链接器架构开发了新的ELF端口。

2015年9月:第一批支持MIPS和AArch64的补丁发布。

2015年10月:成功自托管ELF端口。我们注意到链接器比GNU链接器更快,但当时我们不确定是否能够保持差距,因为我们将为链接器添加更多功能。

2016年7月:开始改进链接器脚本支持。

2016年12月:成功构建了包括内核在内的整个FreeBSD基础系统。已经扩大了与GNU链接器的性能差距。

内部构件

关于链接器的内部结构,请阅读ELF、COFF和Wasm链接器。这有点过时,但基本概念仍然有效。将很快更新文档。

ELF、COFF和Wasm Linkers

作为lib的ELF链接器

通过链接LLD并调用链接器的入口点函数LLD::elf::link,可以将LLD嵌入到程序中。

当前的策略是,有提供可信的对象文件。只要不传递损坏的或恶意的对象文件,该函数就保证返回。损坏的文件可能会导致严重错误或SEGV。也就是说,如果以通常的方式创建对象文件并将其提供给链接器,则不必太担心。它自然会工作,否则就是链接器的bug。

设计

将在文档的其余部分描述链接器的设计。

关键概念

链接器是相当大的软件。要创建一个完整的链接器,需要进行许多设计选择。

这是为ELF和COFF LLD所做的设计选择列表。相信这些高级设计选择在速度、简单性和可扩展性之间取得了正确的平衡。

作为本机链接器实现

将链接器实现为每种文件格式的本地链接器。

链接器共享相同的设计,但共享的代码很少。如果收益与其成本相符,共享代码是有意义的。在例子中,对象格式差异很大,以至于认为抽象这些差异的层不值得其复杂性和运行时成本。取消抽象层大大简化了实现。

设计速度

归档高性能最重要的事情之一是减少而不是高效地进行。因此,高级设计比局部优化更重要。由于尝试创建一个高性能的链接器,因此保持设计尽可能高效是非常重要的。

广义地说,不做任何事情,直到必须做。例如,不重新定位,直到我们需要继续链接。当需要执行一些cost的操作(例如查找每个符号的哈希表)时,只执行一次。在第一个操作上获得句柄(通常只是指向实际数据的指针),并在整个过程中使用它。

高效的归档文件处理

LLD对归档文件(文件扩展名为“.a”的文件)的处理与传统的Unix链接器不同,与Windows链接器相似。将描述传统的Unix链接器如何处理归档文件,问题是什么,以及LLD如何解决问题。

传统的Unix链接器在链接期间维护一组未定义的符号。链接器按照命令行中显示的顺序访问每个文件,直到集合变为空。链接器将做什么取决于文件类型。

如果链接器访问对象文件,则链接器将对象文件链接到结果,并且对象文件中未定义的符号将添加到集合中。

如果链接器访问存档文件,它将检查存档文件的符号表,并提取所有具有集合中任何符号定义的对象文件。

这种算法有时会导致一种反直觉的行为。如果在对象文件之前提供归档文件,则不会发生任何事情,因为当链接器访问归档文件时,集合中没有未定义的符号。因此,不会从第一个归档文件中提取任何文件,并且在此时完成链接,因为集合在访问一个文件后是空的。您可以通过重新排序文件来解决问题,但这不能解决相互依赖的存档文件的问题。

链接相互依赖的存档文件很棘手。可以多次指定同一存档文件,以便链接器多次访问它。或者,可以使用特殊的命令行选项–start group和–end group,让链接器在选项之间循环文件,直到没有新符号添加到集合中。

多次访问同一存档文件会使链接器变慢。

以下是LLD解决问题的方法。不是只存储未定义的符号,而是对LLD进行编程,以便它存储所有符号。当它看到一个未定义的符号,可以通过从以前访问过的存档文件中提取一个对象文件来解析该符号时,它会立即提取该文件并将其链接。这是可行的,因为LLD不会忘记它在存档文件中看到的符号。

LLD的方式是有效的,并且易于证明。

LLD的归档处理的语义不同于传统的Unix。如果仔细制作存档文件来利用它,可以观察到它。然而,实际上,迄今为止,还不知道有任何程序不能与算法相联系,所以它不会造成麻烦。

想知道的数字

为了直观地了解链接器主要处理的数据类型,提供LLD必须读取和处理的对象列表及其编号,以便链接一个非常大的可执行文件。为了将Chrome与调试信息(输出大小约为2GB)链接起来,LLD读取

17000个文件,

180000节,

6300000个符号,以及

1300000次重置。

LLD在15秒内生成2GB的可执行文件。

这些数字因程序而异,但通常情况下,每个文件都有很多重定位和符号。如果程序是用C++编写的,符号名称可能会很长,因为名称会被篡改。

重要的是不要在重新定位和符号上浪费时间。

在上述情况下,符号字符串的总量为450MB,将所有符号字符串插入哈希表需要1.5秒。因此,如果为每个符号添加哈希表查找,将使链接器的速度降低10%。所以,不要这样做。

另一方面,处理文件时不必追求效率。

重要数据结构

描述LLD中的关键数据结构。链接器可以理解为它们之间的相互作用。一旦了解了它们的功能,链接器的代码应该看起来很明显。

符号

此类表示符号。它们是为对象文件或存档文件中的符号创建的。链接器还会创建链接器定义的符号。

基本上有三种类型的符号:已定义、未定义或Lazy。

 

定义符号用于所有被视为“已解析”的符号,包括实际定义符号、COMDAT符号、通用符号、绝对符号、链接器创建符号等。

未定义符号表示未定义符号,解析程序需要将其替换为已定义符号,直到链接完成。

惰性符号表示在存档文件头中找到的符号,如果读取存档成员,这些符号可以转换为“已定义”。

每个唯一符号名称只有一个符号实例。符号表保证了这种唯一性。当解析器从输入文件中读取符号时,它会使用新的放置方式将现有符号替换为其符号名称的“最佳”符号。

上述机制允许您使用指向符号的指针作为访问名称解析结果的一种非常廉价的方式。例如,假设在名称解析之前有一个指向未定义符号的指针。如果解析器将符号解析为已定义的符号,则指针将“自动”指向已定义符号,因为指针指向的未定义符号将被已定义符号替换。

符号表

SymbolTable基本上是一个从字符串到符号的哈希表,具有解决符号冲突的逻辑。它按符号类型解决冲突。

如果添加定义和未定义的符号,符号表将保留前者。

如果添加Defined和Lazy符号,它将保持前者。

如果添加Lazy和Undefined,它将保留前者,但也将触发Lazy符号加载存档成员以实际解析符号。

块(COFF特定)

块表示将在输出中占用空间的数据块。每个常规部分都成为一个块。为公共或BSS符号创建的块不受节的支持。链接器可以创建块以将附加数据附加到输出。

区块知道它们的大小,如何将数据复制到mmap的输出,以及如何对它们应用重定位。具体来说,基于节的块知道如何读取重定位表以及如何应用它们。

输入部分(特定于ELF)

由于ELF的合成数据较少,所以不会将输入文件的切片抽象为ELF的块。相反,直接使用输入部分作为内部数据类型。

InputSection知道它们的大小以及如何将它们复制到mmap的输出,就像COFF Chunks一样。

输出部分

OutputSection是InputSections(ELF)或Chunks(COFF)的容器。InputSection或Chunk最多属于一个OutputSection。

这个链接器中主要有三个角色。

输入文件

InputFile是文件读取器的超类。对于每种输入文件类型,我们都有一个不同的子类,例如常规对象文件、存档文件等。它们负责创建和拥有Symbol和InputSection/Cchunk。

编写

编写器负责将文件头和InputSection/Cchunk写入文件。它创建OutputSections,将所有InputSections/Chunk放入其中,为其分配唯一的、不重叠的地址和文件偏移量,然后将其写入文件。

Drivers

链接过程由驱动程序驱动。drivers:

处理命令行选项,

创建符号表,

为每个输入文件创建InputFile,并将其中的所有符号放入符号表中,

检查是否没有剩余的未定义符号,

创建写入器,

并将符号表传递给写入器以将结果写入文件。

链路时间优化

LTO是通过将LLVM位代码文件作为对象文件处理来实现的。链接器通常解析位代码文件中的符号。如果成功解析了所有符号,那么它将使用所有位代码文件运行LLVM传递,将它们转换为一个大的常规ELF/COFF文件。最后,链接器将位代码符号替换为ELF/COFF符号,以便将它们链接起来,就像它们从一开始就采用本机格式一样。

本文件中描述了详细信息。https://llvm.org/docs/LinkTimeOptimization.html

术语汇编

RVA(COFF)

相对虚拟地址的缩写。

Windows可执行文件或DLL不是位置独立的;它们与称为图像库的固定地址相链接。RVA是图像基的偏移量。

可执行文件的默认映像库为0x140000000,DLL为0x18000000。例如,当创建可执行文件时,假设加载程序将在地址0x140000000处加载可执行文件,因此相应地应用重定位。结果文本和数据将包含原始绝对地址。

VA

虚拟地址的缩写。对于COFF,它相当于RVA+图像库。

重置(COFF)

装载机的重新定位信息。如果加载器决定将可执行文件或DLL映射到与其映像库不同的地址,它将使用库重新定位表中包含的信息修复二进制文件。基本重新定位表由包含地址的位置列表组成。加载器将RVA和实际加载地址之间的差异添加到此处列出的所有位置。

注意,这种运行时重新定位机制比ELF简单得多。没有PLT或GOT。只需将内存中的整个图像移动一些偏移量,即可将图像作为一个整体重新定位。尽管这样做打破了文本共享,但认为这种机制在今天的计算机上其实并不坏。

集成电路板

相同COMDAT折叠(COFF)或相同代码折叠(ELF)的缩写。

ICF是一种优化,通过不仅按名称而且按内容合并只读节来减少输出大小。如果两个只读部分恰好具有相同的元数据、实际内容和重定位,则ICF会将它们合并。它被称为一种有效的技术,通常会将C++程序的大小减少百分之几或更多。

请注意,这并不是完全合理的优化。C/C++要求不同的函数具有不同的地址。如果程序依赖于该属性,那么它将在运行时失败。

在Windows上,这不是真正的问题,因为MSVC link.exe默认启用了优化。只要程序使用链接器的默认设置,程序就应该使用ICF安全。

在Unix上,尽管大型程序恰好可以正常工作,但ICF通常不能保证您的程序是安全的。例如,LLD与ICF配合良好。

 

参考文献了解

https://lld.llvm.org/#

https://lld.llvm.org/NewLLD.html

posted @ 2023-01-18 04:51  吴建明wujianming  阅读(679)  评论(0编辑  收藏  举报