模具:现代连接器

2021-02-24 21:29:54

这是链接器I' m的存储库,该存储库当前正在开发为现有UNIX链接器的释放,例如GNU BFD,GNU Gold Orllvm LLD。

我的目标是制作一个链接器,它与Cat命令用Cat命令连接到CatreObject文件。它可能听起来像是一个不可能的目标,但它没有完全不可能因为以下两个原因而完全不可能:

CAT是一个简单的单线程程序,它是一个' t的空间作为文件复制命令。我的链接器可以更有效地使用多个线程tocopy文件内容以节省额外的工作。

复制文件内容是I / O界,并且许多CPU内核在文件副本期间应该是BEAVAILABLE。我们可以用它们来做额外的工作,否则文件内容。

具体说话,我想使用链接器将Chromiumexecutable与完整的调试信息(约2个Gib大小)联系在1秒内.LLVM' S LLD,我最初创建的最快的开源链接器,这是几年前的最快的开源链接器,大约需要12秒钟到我的机器上链接铬。所以目标是LLD的12倍性能凸点。与GNU金,它超过50倍。

它看起来像模具已经实现了目标。它可以用8个芯/ 16线路将铬链接在2秒,如果我启用预加载术(I' LL以后解释),则交氮机的接头延迟小于900毫秒。它是般的更强的速度。

请注意,即使模具可以创建一个可追加的Chrome可执行文件,它远非完整,不使用生产。模具仍然是一个玩具连接器,这仍然只是我的宠物项目。

尽管LLD显着改善了这种情况,但仍将isstill联系在构建中最慢的步骤之一。当我改变一行代码时,它是浓度的,但必须等待几个甚至更多的链接器完成。它应该是不应该的。有需要更快的链接器。

PC上的核心数最近增加了很多,并且预计将继续下来。然而,现有的接头可以'因为他们不适合幻象而不是'潮流的优势。我有一个64核/ 128线程机器,因此我的目标是创建使用CPU的CONTEA链接器。不过,模具也应该比4或8核机器上的速度更快。

它看着我的设计,现有的联系人的设计是些什么样的,我相信还有很多巨大的不同程度,尚未探讨。 Develoeprs通常不会'只要他们正常工作,他们就可以正常工作,而且他们不会想到创建一个新的链接器。因此,这方面可能有很多低频水果。

为了实现类似的猫般的性能,最重要的是尽快修复输出文件的布局,我们可以尽快开始将实际数据从输入对象文件复制到Anoutput文件。

将数据从输入文件复制到输出文件是I / O绑定的,Sothere应该是从一个文件到另一个文件的计算型密集型任务的空间。

我们应该允许链接器从磁盘预加载对象文件,并在一整套输入对象filesis准备好的内存中的内存中。我的想法是:如果用户用--preload标志调用链接器,则与其他命令行标志一起使用实际的链接器调用,那么与相同的命令行选项(--pread标志除外)的以下Actuallinker调用变得神奇地快点。在幕后,Thelinker在第一个调用和守护程序上开始预加载对象文件。链接器的第二个调用通知Thedaemon重新加载更新的对象文件,然后继续。

单独守护骗子' t使连接器变得更快。我们需要将链接器拆分为两个,使得过程的后半部分通过在过程的前半部分中的可推测解析和预处理输入文件尽可能快地完成。成功的特点是设计良好的数据结构,只需将我们从第二次卸载尽可能多的处理。

链接器阶段中最耗时的阶段之一是符号解析。要解析符号,我们基本上必须将所有符号字符串放入哈希表中,以将未定义的符号与定义的符号进行匹配。但这可以在守护进程中使用stringinterning完成。

目标文件可能包含称为可合并字符串部分的特殊部分。该部分包含许多以null终止的字符串,并且链接程序应收集所有可合并的字符串部分并合并其内容。因此,例如,如果两个目标文件包含相同的字符串文字,则结果输出将包含单个合并的字符串。此步骤很耗时,但是可以在后台驻留程序中使用字符串插入来完成字符串合并。

静态档案(.a文件)包含目标文件,但staticarchive的字符串表仅包含成员对象文件的已定义符号,而没有其他类型的符号。这使得静态归档不适合进行推测性分析。守护程序应忽略静态档案的字符串表,并直接读取所有档案的所有成员对象文件以获取所有可能的输入文件的全貌。

如果存在使用符号GOT的重定位,则我们必须为该符号创建GOT条目。否则,我们不应该。这意味着我们需要扫描所有重定位表以固定.got节的长度和内容。这可能很耗时,但是此步骤是可并行的。

GNU ld,GNU gold和LLVM lld本质上支持相同的命令行选项和功能集。模具不必与它们完全兼容。只要它可用于链接大型用户界面程序,我就可以了。可以保留一些未实现的命令行选项。如果发霉的速度非常快,其他项目仍然很乐意通过修改其项目来采用它。构建文件。

Mould会发出Linux可执行文件,并且只能在Linux上运行。在编写代码时,我不会回避Unix主义(例如,我可能会使用fork(2))。我不想考虑可移植性,直到模具成为值得成为的东西为止移植。

链接描述文件是链接器的嵌入式语言。它主要用于控制输入节如何映射到输出节以及输出的布局,但它也可以做很多棘手的事情。它的功能特别适用于嵌入式编程,但是它文档化程度很低,而且非常复杂语言。

我们必须实现链接器脚本语言的子集,因为在Linux上,/ usr / lib / x86_64-linux-gnu / libc.so(尽管其名称)不是共享对象文件,而是实际上是一个包含链接器脚本代码的ASCII文件。加载实际的libc.so文件。但是,为此目的而设置的功能集非常有限,可以将其实现为模型。

除此之外,我们真的不想实现链接程序脚本语言。但是同时,我们希望满足用户对链接脚本语言当前所满足的需求。那么,我们该怎么办?这是我的观察:

链接器脚本可以执行很多棘手的工作,例如指定文件的确切布局,在节之间插入任意字节等。但是,大多数操作都可以使用链接后的二进制编辑工具(例如objcopy)来完成。

看起来,post-link编辑工具确实无法完成两件事:(a)将输入节映射到输出节,以及(b)应用重定位。

从以上观察,我相信我们仅需要提供以下功能,而不是整个链接描述文件语言:

一种将地址设置为输出节的方法,以便根据所需的地址应用重定位。

如果我们将Chromium的目标定为1秒,则每毫秒计数一次。我们不能忽略进程退出的延迟。如果我们映射大量文件,则_exit(2)不是瞬时的,而是花费几百毫秒,因为内核必须清理大量资源。解决方法是,将链接器命令组织为两个过程;第一个过程分叉第二个过程,第二个过程进行实际工作。第二个进程将结果文件写入文件系统后,它立即通知第一个进程,第一个进程退出。第二个过程可能需要一些时间才能退出,因为它不是交互式过程。

至少在Linux上,它看起来像文件系统' S performance toallocy toallocate新文件是当您已经在文件系统上有一个大文件时,填充其内容的限制因素是使用MMAP。对于ISMUCH比创建一个新的新鲜文件和写入它的速度更快。基于此观察,模具应该覆盖到现有的文件中,如果存在。我的快速基准显示,在创建2个GIB输出文件时,我可以在创建2个Gib输出文件时允许打开一个可执行文件,以便在isrunning(' ll get"文本忙碌"错误如果你尝试)。如果未能打开输出文件,模具将返回通常的方式。

作为一个实施策略,我们不关心内存泄漏,因为我们真的可以通过执行PrecideMemory Management保存那个很多内存。这是因为大多数正在分配的对象都需要执行模具的执行,直到图中编程的结束。 i' m确定这是一个奇的内存管理方案(或其小块),但这也是LLVM LLD所做的。

为了构建再现性和便于调试,链接器的输出应该是确定性的。这可能会添加一点点的开销,但这应该是太多的。

a .build-id,嵌入到输出文件的唯一ID,通常通过应用加密散列函数(例如SHA-1)Toan输出文件来计算。这是一个慢的步骤,但我们可以将文件加速到一个文件中,将一个文件加速成小块,计算每个块的SHA-1,然后计算连接的SHA-1哈希的SHA-1(即构建高度2的Markree 2)。现代X86处理器对SHA-1和Cancan Compute Sha-1的专用说明非常迅速,速度约为2个Gib / s速率。使用16cores,可以在60到70milliSonds中计算2个GIB可执行文件的构建ID。

BFD,Gold和LLD支持部分垃圾收集。也就是说,Alinker在输入图上运行标记扫描垃圾收集,次数是顶点,重新定位是边缘,以丢弃从入口点符号(即_start)或几个其他根部分无法访问的重放。在模具中,我们使用多个线程来同时标记部分。

同样,BFD,金色LLD支持相同的梳子折叠(ICF)作为又一尺寸优化。 ICF融合了两个或多个恰好的读取,恰好具有相同的内容和重新定位。这样做,我们必须从较大的图表中查找同构Sub..i为模具实施了新的模具算法,这比LLD为铬更快5倍。 (从5秒到1秒)。

英特尔穿线构建块(TBB)是一个平行执行的好的纤维纤维,并且有几个并发帐号。我们特别感兴趣地使用并行_for_each和concurrent_hash_map。

TBB提供了tbbmalloc,它比glib malloc更适合多线程应用程序,但看起来jemalloc和mimalloc的可扩展性比tbbmalloc好。

链接Chrome时,链接器总共读取3,430,966,844字节的数据。该数据包含以下各项:

¹必须从输入目标文件复制到输出文件的节。例如,排除包含重定位或符号的节。

从概念上讲,链接器的作用非常简单。编译器将程序片段(单个源文件)编译为一段机器代码和数据片段(一个目标文件,通常具有.oextension),然后链接程序将它们片段化为一个可执行文件或共享库映像。

实际上,类Unix系统的现代链接器比幼稚的理解要复杂得多,因为它们在Unix的50年历史中一次逐渐获得了一个功能,而现在却变成了一堆很多杂项功能,其中没有一个功能比其他功能更重要。错过树林是很容易的事情,因为对于那些不了解Unix链接器细节的人来说,不清楚哪个功能是必需的,哪个不是必需的。

话虽这么说,很明显,在Unix历史的任何时候,Unix链接器都为那个时代的Unix设置了一致的功能。因此,让我纠结历史,看看操作系统,运行时和链接程序如何获得我们今天看到的功能。那应该让您知道为什么首先将特定功能添加到链接器。

原始Unix不支持共享库,并且始终将程序加载到固定地址。可执行文件就像是内存转储,内核刚刚将其加载到特定地址。加载后,内核通过将指令指针设置为特定地址来开始执行程序。

任何链接器最重要的功能是重新定位处理。当然支持的原始UNIX链接器。让我解释一下。

单个对象文件不可避免地作为程序不完整,因为当编译器创建它们时,它只看到了Anentire程序的一部分。例如,如果对象文件包含引用其他对象文件的函数程序,则无法完成对象的呼叫指令,因为编译器没有想到哪个被称为函数' s地址。要处理此问题,编译器占位符值(通常只是零)而不是RealAddress,并在对象文件中留下一个元数据,然后在&#34中留下exfset xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof xof这个文件,用y&#34的地址偏移这个文件;元数据被称为#34;搬迁"重新定位通常由链接器处理。

连接器很容易应用于原始UNIX的重新定位,因为程序始终将程序加载到固定地址。它完全了解链接Aprogram时所有功能和数据的地址。

静态库支持,仍然是UnixLinker的一个重要特征,也会追溯到Unix历史的这个早期。要了解它是什么,想象您正在尝试为早期Unix编译计划。您不会每次编译程序时都要浪费时间才能浪费时间才能携带libc函数(时代的电脑令人难以置信的速度),因此您已将每个libc函数函数归档为单独的源文件并单独编译。这意味着,您对每个LibcFunction的对象文件,例如printf.o,scanf.o,atoi.o,write.o等。

鉴于此配置,您必须执行的所有配置,以链接Programagainst libc函数是挑选一组正确的libc objectfiles,并将它们带到链接器以及yourprogram的对象文件。但是,使用您在程序中使用的TheLibc函数同步地保持链接器命令行是困扰的。你可以保守;您可以将所有libc对象文件指定为thecommand行,但导致程序膨胀,因为linkEruncOuditionsally链接到它的所有对象文件,无论是否使用该对象。因此,将添加一个新功能在链接器中添加到问题。这是静态库,也被称为存档文件。

存档文件只是一系列对象文件,就像zipfile,但以未压缩的形式。 Acnive文件通常具有.a文件扩展名并以其内容命名。例如,包含所有libc对象的分类文件名为libc.a.

如果将存档文件与其他对象文件一起传递给Thelinker,则链接器仅从存档中从其他对象文件引用时从存档中拔出对象文件。换句话说,与直接给定的目标文件不同,归档文件中的对象文件包裹不链接到默认情况下的输出.An存档作为补充剂以完成您的程序。

即使在今天,您仍然可以找到libc存档文件。在Linux上运行/usr/lib/x86_64-linux-gnu/libc.a应该会在libc归档文件中提供目标文件列表。

在80年代,当时领先的商用Unix供应商Sun Microsystems向其Unix变体SunOS添加了共享库支持。

在大多数地方,模具采用数据并行性。也就是说,我们拥有大量相同种类的数据,并且我们使用并行for循环分别处理每个数据。例如,在确定了输入对象文件的确切集合之后,我们需要扫描所有重定位表以确定.got和.plt节的大小。我们使用并行的for循环进行此操作。在这种情况下,并行处理的粒度是重定位表。

数据并行性非常有效且可扩展,因为在处理每个数据元素时,线程之间不需要通信。除此之外,数据并行性很容易理解,因为它只是一个for循环,可以并行执行多个迭代。我们不会在模型中使用高级通信或同步机制,例如渠道,期货,承诺,闩锁或类似的东西。

在某些情况下,我们需要在线程之间共享一点数据,同时执行并行的for循环。例如,扫描重定位的循环会打开"需要GOT"。或"需要PLT"标志中的符号。 Symbol是共享资源,从多个线程写入它们而不进行同步是不安全的。为了解决这个问题,我们将标志设为原子变量。

您可以在基于并行for循环顶部的模具中找到的另一个常见模式是map-reduce模式。也就是说,我们在大型数据集上运行并行for循环以生成小型数据集,并使用单个线程处理小型数据集。让我以一个build-id计算为例。通常,通过在链接程序的输出文件上应用诸如SHA-1之类的加密哈希函数来计算Build-id。为了对其进行计算,我们首先将输出视为1个MiB块的序列,并为每个块并行计算SHA-1哈希值,然后将SHA-1哈希值连接起来并在哈希值上计算SHA-1哈希值以获得最终值内部编号。

最后,我们在模具中的几个位置使用并发哈希图。 Concurrenthashmap是一个哈希图,多个线程可以在其中并行安全地插入项目。例如,我们在符号解析阶段使用它。要解析符号,基本上必须将所有定义的符号都放入哈希表中,以便按名称查找未定义符号的匹配定义符号。我们从并行的for循环中进行哈希表插入,该循环遍历输入文件列表。

总的来说,即使模具是高度可扩展的,它也成功地避免了您经常在复杂的并行程序中找到。从高级别,模具恰好地执行链接器'内部通过一个通了。每次通过并行使用平行的循环并行化。

在本节中,我解释了我目前捐赠计划实施的替代设计以及为什么我把它们倒闭。

在修复输出文件布局之前将可变长度部分放置在输出文件的末尾和Startcopying文件内容

想法:修复常规部分的布局似乎很容易,如果我们在文件开头时,我们可以开始将其从其输入文件复制到输出文件。在复制文件内容时,我们可以将可变长度部分的大小计算为.got或.plt并将它们放在文件的末尾。

拒绝原因:我没有选择这种设计,因为我怀疑它实际上可以缩短链接时间,我认为我不需要Itanyway。

链接器必须重复兼容梳理部分(即包含在多个对象文件中的内联函数),因此Wecannot在我们解析Allsymbols和De-Duplicate Comdats之前计算常规部分的布局。这需要几百百分之几。之后,我们可以在较少的tha中计算不可变长部分的大小

......