每当开始学习一门新编程语言的时候,你总是可以找到大量的 “hello world” 教程、新手指南或者关于语言的主要概念、语法甚至标准库的文档。然而,当你想找一些介绍得更加深入的资料,比如语言运行时分配的数据结构在内存中的布局,或者调用一个内置函数时到底生成了什么样的汇编代码,你就会发现这并非易事。显然,这些问题的答案都藏在源代码中。但是,以我的个人经验来看,你很可能花费数小时在源代码中摸索却最终一无所获。 我并不是打算装得自己什么都懂,也没有打算介绍得面面俱到。而是希望可以帮助你去探索 Go 语言的源代码。 在我们开始之前,我们需要自己有一份 Go 源代码的拷贝。要获得它的源代码非常容易,只需要执行如下代码: Shell
请注意,这份代码的主分支是在不断改进中的,我在这个博客中使用的是 release-brach.go1.4 这个分支。 搞清楚项目结构如果你看一下 Go 仓库的 /src 文件夹,你会看到很多文件夹。其中,大部分文件夹都是 Go 标准库的源文件。该项目使用标准命名规则,所以每一个包(pakage)都在一个独立的文件夹中,而且这个文件夹的名称与包名称相同。除了标准库以外,该目录中还有很多其它的东西。就我各人看法,其中最有用的文件中主要有:
Go 编译器内部机制正如提到的那样,Go 编译器中与系统结构无关的部分被放在 /src/cmd/gc 目录下。其入口点在 lex.c 文件中。除了一些共同的部分,比如命令行参数解析,编译器还要完成如下的工作: 1. 初始化一些通用数据结构。 2. 遍历提供的所有 Go 源代码文件,并针对每个文件调用 yyparse 方法。该方法会完成真正的语法分析。Go 编译器使用 Bison 作为程序分析生成器。语法描述存储在文件 go.y 中(后面我会提供详细的说明)。最终,这一步会生成一个完整的分析树,其中每个结点表示编译后程序的一个元素。 3. 递规地遍历生成的树,并做出一定修改,例如为那些应当隐式定义的节点指定类型信息、重写在运行时包中传递给函数的某些语言元素——如类型转换,以及其它一些工作。 4. 语法解析树处理完成后,再执行真正的编译,将结点翻译成汇编代码。 5. 在磁盘上创建目标文件,并将翻译生成的汇编代码以及一些额外的数据结构,如符号表等,写入目标文件中。 深入 Go 语言语法现在让我们再进一步。 go.y 文件中包含了语言的语法规则,所以这个文件是一个探索 Go 编译器的很好突破口,也是我们理解语言语法规则的关键。这个文件主要包括如下几部分: C
这个声明中定义了 xfndcl 以及 fundcl 两个结点。 fundcl 结点可以有以下两种形式。第一种对应于如下的语法结构: C
其第二种形式对应于下面这种语法结构: C
xfndcl 结点中包含存储于常量 LFUNC 中的关键字 func,以及其后的 fndcl 与 fnbody 结点。 Bison(或者说 Yacc)语法一个十份重要的特征是,它允许将任意 C 代码放在结点定义之后。每当在源代码文件中找到匹配该结点定义的部分的时候,相应的 C 代码就会执行。这里,我们把最终结果结点定义为 通过一个例子更加容易理解。注意下面这段简化后的代码: C
首先,我们创建了一个新结点,该结点包含函数声明的类型信息。同时,此结点的参数列表引用了结点 $3,结果列表引用了结点 $5。随后创建了结果结点 如何理解结点是时候看一下结点到底是什么东西了。首先,结点是一个结构体(你可以在这里找到其定义)。这个结构体包含了大量的属性,这是因为它需要各种不同类型的结点类型,而不同类别的结点又有着不同的属性。下面列出了一些我认为比较重要一些属性:
到目前为止,你已经明白了结点树的基本结构了,你可以去运用一下这些知识。在接下来的博文中,我们会用一个简单的 Go 应用作为实例来分析 Go 编译器到底是如何编译代码的。 |
|