下哉ZY:https://www.sisuoit.com/3318.html
问:go是如何产生的?
在Go 1.5版本发布的时候,提到了Go语言实现了bootstrapping(中文叫bootstrapping,或者自展,简而言之就是一个口)。
那么,这个概念(或者说这个技术)到底是什么意思呢?
在我看到的各种资讯文章中,自举是否总是出现,似乎大多数人都非常熟悉这个重要的概念。
个人认为,长期以来,自举只是一个“用Go语言实现自我”的模糊概念。 所以,明确理解这个概念,弥补自己的知识盲点,是本刊的宗旨。
和c的关系。 刚开始用Go (Go 1.3)的时候,一些相关的工具,无论是gccgo、6a/6c/6l,还是LDD、NM、objdump这些熟悉的面孔,总是让人很容易想到C语言。
结合Go源代码中的大量C代码,我们可以说:
Go最早的版本是用C和plan9 toolchain实现的。 先确定目标。 因为Go语言的运行时(比如内存管理和GC,goroutine,scheduling等。),大部分在很早的时候就已经用Go语言实现了。所以当我们说“Go是怎么来的”或者“Go是用什么写的”的时候,其实(应该)是指“Go编译器是怎么实现的”。
我们已经知道的:
最早的Go编译器是用c写的。 现在Go编译器(以及相关工具)都是由Go实现的。 从1开始的过程。敬2。叫做自举。 此外,实现自举还有很好的理由和理由: 1. 2. 比Go C好(更清晰,更方便测试,更容易剖析,更容易编写)。 3. 优化工具链的实现,代码更少,维护更简单。 4. 提高便携性。 5. 未来的后端优化(使用SSA)比用c写更容易。 6. 关于自举的定义 更严格的定义如下:
在计算机科学中,自举是一种产生自编译编译器的技术——也就是说,一种用它打算编译的源编程语言编写的编译器(或汇编程序)。 即:
引导:用要编译的目标编程语言编写它的编译器。
同时,维基列出了可以自举的语言(基本包括所有“严肃”的编程语言):C/Go/Java/Python/Rust。...
回到过去,我们先来关注一下C语言。 刚开始学C语言的时候,我思考了一个问题:
C编译器是如何实现的? 如果C编译器是用C语言实现的,那么编译器是怎么编译出来的? 如果一个平台上没有原始的C编译器,如何生成这个平台的C编译器? 最早的C编译器是怎么来的? 请参考C语言的发展,丹尼斯·里奇在其中解释道:
c由B语言演变而来,B来自BCPL。 c真正实现了代码到机器指令的转换,有了自己的编译器(B是以线程代码的方式实现的)。 最初的编译器是使用Thompson的汇编程序在PDP-7上实现的。 所以最早的C不可能是自举。
接下来,我们来关注一下GCC。
GCC自举 根据文档描述,GCC的引导过程如下:
首先需要有一个老版本的C语言编译器,就说1.0版本吧。现在开发了更快的1.1版本。当然,这个编译器是用C语言(或者有很多c++特性的C文件)实现的。
用1.0版的编译器a编译1.1版的编译器b。 编译器B本身很慢(编译其他程序时),但它生成的目标程序运行速度很快。 b .如果编译器中有bug,那么有两种可能:
编译器a的错误 1.1版引入的新bug。 然后用编译器b编译一个新的1.1版本的编译器C,C编译器不仅执行快,而且生成目标程序也快。现在的问题是:编译器C也可能有bug,它生成的目标程序可能有缺陷。我们必须验证它的正确性。
处理方法:
用编译器C再次编译(编译器代码)生成编译器d。 比较编译器C和编译器D,看是否相同。(实际上是检查B和C是否等价) 注意,这不是为了执行测试套件。这是用编译器程序的= = feature = =来检验自己。
疑惑:为什么不对比一下B和C? 答:B是1.0编译器生成的,和c本来就不一样,你要检查的是B和c生成的目标程序。
怀疑:为什么不怀疑B有问题而怀疑C有问题?
自举需要编译三次。旧编译器A生成B,B生成C,C生成d。
交叉编译器无法引导。(如果交叉编译器产生了这个平台的目标程序,那么就不是交叉编译器)。
关于上述GCC构建步骤的一些解释 有个术语来形容:3阶段自举。
上述GCC引导过程是标准的3阶段引导。
编译器B(阶段1)-编译器C(阶段2)-编译器D(阶段3)
C编译器的原C实现 很多人可能对此感兴趣。这部分是我自己的理解:
用汇编语言实现一个C编译器,然后用这个编译器编译用C实现的编译器,再用它编译自己,实现自举。 一个用汇编实现最小功能的C编译器,和一个用这个有限功能实现C的编译器(编译器只用最小功能来实现),可以支持C的所有功能..然后用这个编译器自己编译。 无论如何,我们得到了一个支持所有语言的C实现的C编译器。
版本依赖问题: C实现自举后,可以通过原版本的编译器,用C继续开发新的编译器,实现编译器的迭代: 1.0 -> 1.1 -> 1.2 -> 1.3 ...
那么是不是每一个版本的编译器都紧密依赖于上一个版本的编译器,我们需要一点一点的从最初的编译器构建到最新的编译器?
GCC的推荐流程是:自举不跨大版本,可以这样理解:
GCC的1.x版本可以使用1.0编译器构建。 2.0版需要用1.x中较新的编译器来构建。 不建议使用1.x版编译器构建3.x版。 一般来说,我们不需要通过小版本来构建小版本,但是我们不支持一步更新。如果我手头只有6.x的GCC编译器,要构建9.x的新编译器,我需要按照7.x 8.x 9.x的顺序迭代构建
它不同于Go语言。为什么会这样?
我的理解是,GCC不是在某个标准版本的C语言中实现的,它自己的代码可能很大程度上依赖于早期版本编译器提供的新特性。
解释引导的含义 Go最早是用C实现的,包括一些汇编代码。然后实现了自举(1.5版)。 后续的Go (1.18之前)可以用GO(1.18构建。
那么你可能会说:这个Go 1.4不也是用C和汇编实现的吗? 是的,这个Go 1.4依赖于C编译器。而且,即使这个C编译器是自举的,也必须追溯到一个祖先C编译器。这个祖先C编译器大部分是用汇编实现的。而且这个汇编程序也必须依赖机器指令。 继续下去,你得有计算机,有电子元件,有金工,有电……最终你才能得到生命(或者宇宙)的初始点。
所以,不是这样的。 只要编译器能自己编译,生成的新编译器也能自己编译,我们就可以称之为自举。
回去 Go的源代码大概有几个部分:
Dist:用于帮助构建过程的工具。 工具链:编译器、汇编器、链接器。之前的6c/6a/6l。 运行时依赖性(goroutine/GC/内存管理)。 标准库。 很多工具。 引导过程:
准备一个旧版本的Go (1.x & x >= 4)。 Bootstrap会自行编译。 新的引导程序编译新版本的Go。 新版Go自己编译。 文档的官方引导描述:
对于x ≥ 5,安装Go 1.x的过程是: 1.用Go 1.4构建cmd/dist。->旧版Go编译的自举工具。 2.使用Dist,用Go 1.4构建GO 1.x编译器工具链。->使用第一步中的工具,用GO 1.4编译工具链。 3.使用Dist重新构建Go 1.x编译器工具链。->用第二步的工具链(编译器/汇编器/链接器)再次编译自己。 4.使用dist,用go1.x编译器工具包构建go1.x cmd/go(作为go \ _ bootstrap)。->用步骤3中的工具链编译引导编译器。 5.使用Go \ _ bootstrap,构建剩余的GO 1.x标准库和命令。->用bootstrap编译器编译剩下的新版本GO库。 注意:第三步更重要。
以下日志是Go 1.18在我自己机器上编译的Go 1.18的输出日志:
使用/usr/local/go构建Go cmd/dist。(go1.1x达尔文/arm64) 使用/usr/local/go构建Go工具链1。 使用go工具链1构建Go引导cmd/go (go_bootstrap)。 使用go_bootstrap和go工具链1构建Go工具链2。 使用go_bootstrap和go工具链2构建Go工具链3。 为darwin/arm64构建包和命令。 关于Go bootstrapping的一些有趣的事情 最早的Go编译器是用C写的,当语言开发者决定用Go代替C时,他们就考虑到了。
A.重写一遍。 B.c代码变成Go代码。 Russ Cox选择了B:从C到Go!,并实现了一个翻译C代码Go的工具。
他还特意打电话解释说:
为什么不用Go完全重写编译器:我舍不得。 为什么不按照C用Go复制一个版本:
类似的事情其实也有人做过,只是代码太多; 写作很无聊; 容易发现bug; 也可以继续用C修改代码(然后自动生成)。 注:我注意到Go 1.5发行说明中有一句话,自动生成的Go代码性能比精心编写的Go语言差。
从C到Go的编译器和链接器的自动翻译导致了不地道的Go代码,与写得好的Go相比,其性能很差。 然后,这些自动转换成Go代码的C代码,在Go 1.8中被干掉了。
如果你对此感兴趣,可以分别下载1.3.3、1.5和1.1x的代码,你会发现:
1.3.3: src/cmd/gc这里是原C代码。 1.5.X: Go代码在C代码被src/cmd/compile/internal/GC自动翻译后。 1.1x:,这些翻译的Go代码已经消失(重新实现)。 在旧的Go源代码(
一个用C实现的工具,名为Na/Ng/Nc/Nl(N是数字):
5: ARM,6: AMD64,8: Intel386 答:plan9 asm汇编程序 C: plan9 c编译器 G: go编译器 去链接器 比如6c,就是x86-64上的C编译器。
之所以用了旧版Go还要重新编译新版Go。 这一步其实挺重要的,可以带来很多好处。
首先,您可以检查新编译器代码的准确性。 其次,如果新版Go经过优化,重新编译后,编译器本身也能获得速度提升。 论编译器的“速度” 编译器的速度。 编译器执行“编译”操作的速度。 编译器生成可执行文件的速度。 显然,3是至关重要的,而对于开发者来说,2的提升也是一个极大的好处,而1,只要你不是某个系统的包管理和维护人员,应该是没人关心的(自举会拖慢这一步)。
完整的编译过程: 预处理器->词法分析器->解析器->代码生成器->局部优化器->汇编器 前端:词法分析、语法分析、类型检查、中间代码生成。代码-> AST(抽象语法树) 后端:代码优化,目标代码生成,目标代码优化。AST -> SSA ->机器代码
在最初的版本中,bison/yacc用于前端相关处理,源代码中的go.y就是相关内容。 新版本(go1.8)也换了,现在是手写的。
面向Go的编译器: 它是半抽象指令集,不一定对应机器指令(传统概念的汇编程序与机器指令有很强的对应关系)。 汇编程序提供了一种将半抽象指令转换成真实指令的方法,并将它们交给链接程序。
引导的最低构建要求 原设计:1.2 -> 1.3 -> 1.4... 然后在Go 1.5的时候,决定使用Go 1.4作为基础版本,所有后续的Go代码都可以使用Go 1.4来构建。这样可以从流程上简化自举,对打包者也更友好。
然后:rsc大叔又变卦了。构建:采用go 1.17作为go 1.20的引导工具包 六年后,在Go 1.19的时候,切换到Go 1.17作为构建Go的基础版本。Go维护者给出了升级的原因:
新功能和错误修复。 部分平台不再支持Go 1.4。 去掉很多兼容补丁(提到现在的C编译器也不是用ANSI C写的)。 这不得不提醒我们前面提到的GCC的自举过程。