Go 1.3 链接器检修


摘要

链接器是构建和运行一个标准Go程序中最慢的一部分。为了阐明这一点,我们打算将连接器分为两部分。也许每一部分都可以写入GO中。

背景

链接器一直是Plan9工具链中最慢的一部分,现在它也是Go工具链中最慢的一部分,肯 汤姆森的工具链概述总结道:

新的编译器编译很快速,载入很慢,产生中等质量的编译代码。现在编译器都相对具有移植性,两周左右的时间就可以构建出一个针对特殊机器的编译器。对于Plan9,我们需要几个专门特性和我们自己对象格式的编译器,这个工作是独立的。自由分配在Plan9中的编译器也是非常重要的。

两个问题在回顾中被提出,第一个是必须明确区分编译器和加载器的工作,Plan9运行在多处理器上面,多是并行编译,不幸的是,所有编译工作必须在载入前都完成,载入工作却是单线程的。对于这个过程模型,任何从编译端的工作移动到载入段,这都会导致实际时间的显著的增加。此外对于一些不需要经常编译却需要频繁载入的库来说也是同样的成立的。所以在未来,我们会努力将一些载入工作放到编译时完成。

这篇文档是在90年代初完成的,未来就在这里。

建议计划

当前的链接器执行两个独立的任务。首先,它把输入流中的伪指令翻译成可执行的代码和数据块,和重定位的地址列表。 其次,它删除无用的代码,把余下的代码合并到一个单独的文件,更新重定位地址,生成几个全局的数据结构,比如运行时符号表。

第一部分的工作可以封装成一个库-liblink-它可以链接汇编解释器和编译器。无论是6a,6c还是6g生成的文件,最终都会被liblink写入包含了可执行代码、数据块和重定位地址表的目标文件,这是当前的链接器的前半部分工作的成果。

可以被处理的第二部分,那个剩余的程序会读入新的目标文件和完成链接。那个链接器只有少量的实现代码,而且多数是架构独立的。那么可以做到将这个链接器融入到一个单独的架构独立的在程序,这个程序以“go tool id”被调用。甚至有可能直接在Go中重写链接器,使得并行处理大量链接更为容易。(参见下面的如何去引导)

开始,我们用C的时候会关注于获得新的切片,使用Go语言的探索只会在余下的改变都完成的情况下发生。

为了避免在工具使用上的混淆,目标文件会保持现有的后缀.5 .6 .8。或许在Go1.3中,我们甚至会包含进以5l,6l,8l命名的中间程序,这些中间程序会调用新的链接器,但会在Go 4.1中弃用

目标文件

新的分割需要一种新的目标文件格式。目前的目标文件只包含伪指令流,但是新的目标文件会包含可执行代码和数据块,以及重定向列表。

我们自然会问:是否应该采用一种现行的目标文件格式,如ELF?首先,我们会采用一种自定义格式。一个定制的Go链接器需要构建运行时数据结构,如:符号表。因此,即便是我们使用了ELF目标文件格式,我们可能也不会使用标准的ELF链接器。ELF文件过于通用,而且其语义过于复杂,远远超过定制的Go语言链接器的需要。一个自定义,不太通用的目标文件格式应该容易被产生和使用。另一方面,ELF能被标准工具加工处理,如:readelf,objdump等等。虽然,已经尘埃落定,我们也明确知道我们需要什么样的格式,不过,探讨一下是否应该使用ELF还是非常值得的。

新的目标文件格式的细节还没有设计完成。这节余下部分列出一些设计的注意事项

  • 首先,这个文件显然应该尽可能简单。除了极少数例外,任何应该在链接器的library half中完成的工作都不应该在其他地方完成。可能的例外包括在library half中完成堆切割代码,这会使得目标文件只针对特定系统,虽然,现在已经如此了,不过这应该是由于在包中存在特定系统的代码,以及为了让针对ARM目标文件的library half能够完成浮点运算工作(现在针对ARM的要等linker运行起来才能有)。
  • 我们应该确保目标文件通过内存文件映射可以使用。这会减少通过I/O的复制工作。对于非空地址的错误引用的不安,而非对于崩溃的担心,这可能需要改变Go的运行时。
  • 单纯Go语言的包中包括单一的目标文件,这是由一次调用Go编译器编译完整Go源文件产生的。那个目标文件接着被封装进归档文件中。我们应该安排一个单独的目标文件就应该是一个有效的归档文件,以便在一般情况下不需要封装步骤

自举

如果一个新的Go连接器是使用Go语言写的,这就产生了一个自举问题:如何连接一个连接器?这里有两种方法。

第一种是维护一个CL的自举列表。序列中第一个CL应该说明当前的linker,用C写成。后续的CL应该包含一个新的可以使用之前的连接器连接的连接器。从这些序列中产生的最终二进制文件可以被下载。这些序列必须不能太长,并且和里程碑保持一致。例如,我们安排Go 1.3的连接器可以被当作GO 1.2的程序编译,Go 1.4的连接器可以被当作Go 1.3的程序编译,以此类推。被记录的序列使得当需要时重新自举成为可能,并且提供了一种方法来解决Trusting Trust problem问题。另一种方法是编译gccgo 并且用它来当作Go 1.3的连接器。

第二种方法是保留一个C连接器即使我们用Go语言写了一个更好的,并且保持两者功能基本相同。用C写的版本仅仅需要保留那些连接Go写的连接器的所需要的功能。它需要拣出一些对象文件,合并他们,并长处一个可执行文件。这里不需要cgo的支持,不需要额外的连接,不需要共享的类库,也不必考虑性能问题。它应该只是少量的代码(大约几千行)并且不需要经常改动。C版本连接器会在make .bash时被构建但是不需要安装它。这种方法使得其他的开发者从源代码构建Go时更加容易。

不用太过关注我们使用哪种方法,仅仅知道其中的一种就可以了。我们可以等到将来再决定。

LiteIDE 开发工具指南 (Go语言开发工具)

Ubuntu 安装Go语言包

《Go语言编程》高清完整版电子书

Go语言并行之美 -- 超越 “Hello World”

我为什么喜欢Go语言

在 Cloud 9 中搭建和运行 Go

相关内容