您当前的位置:首页 > 计算机 > 编程开发 > Go语言

GO语言源码学习:引导工具CMD/DIST

时间:03-07来源:作者:点击数:

一、简介

cmd/dist是生成Go distribution的引导工具。它负责构建C程序(像Go编译器)和初始化Go工具的拷贝(即go_bootstrap)。

dist本身是用C语言写的。所有和C库,甚至和C标准库的交互,都通过一个系统相关的文件封装起来,以方便移植,如:plan9.c、unix.c和windows.c等。通过这个可移植层提供功能,供其他文件使用。在这个可移植层中,为了避免和现有函数混淆,相同功能的函数带有前缀:x。例如:xprintf是printf的可移植版本。

到目前为止,dist中大部分普通数据类型是字符串或字符串数组。然而,dist中使用两个命名的数据结构:Buf和Vec,保存它们拥有的所有数据,而不是使用char*和char**。Buf相当于Go中的[]byte,Vec相当于Go中的[]string。对Buf的操作函数以b开头;对Vec的操作函数以v开头。

二、文件概览

a.h arg.h arm.c buf.c build.c buildgc.c buildruntime.c goc2c.c main.c plan9.c README unix.c windows.c

注:go1.0.3源码中没有plan9.c文件,tips中有该文件。

1、README

学习前看README。本文简介部分基本来自该文件。

2、a.h

定义了一些类型,比如简介中提到的Buf和Vec;声明了dist中所有.c文件中的函数

Buf和Vec的结构:


// A Buf is a byte buffer, like Go's []byte.
typedef struct Buf Buf;
struct Buf
{
     char *p;
     int len;
     int cap;
};

// A Vec is a string vector, like Go's []string.
typedef struct Vec Vec;
struct Vec
{
     char **p;
     int len;
     int cap;
};

可以看到,结构很像Go中slice的内部定义

3、arg.h

该文件中声明了一个变量和定义了五个宏,它和libc.h中的定义是一样的。源码中大量使用了这些宏,特别是ARGBEGIN和ARGEND。通过源码,可以知道,ARGBEGIN宏其实就是循环遍历命令行参数(不包括dist和其子命令),参数要求以-开头展开如下(以build.c中的cmdclean为例):


for((argv0?0:(argv0=*argv)),argv++,argc--; argv[0] && argv[0][0]=='-' && argv[0][1]; argc--, argv++) {
        char *_args, *_argt;
        char _argc;
        _args = &argv[0][1];
        if(_args[0]=='-' && _args[1]==0){
            argc--;
            argv++;
            break;
        }
        _argc = 0;
        while((_argc = *_args++) != 0)
        switch(_argc){
            case 'v':
                vflag++;
                break;
            default:
                usage();
         }
        _argt=0;
        USED(_argt);
        USED(_argc);
        USED(_args);
    }
    USED(argv);
    USED(argc);

4、plan9.c/unix.c/windows.c/main.c

感觉上main.c是dist的入口。实际上特定操作系统的文件才包含了main入口函数。main.c中只是一个“伪入口”函数:xmain

如简介中所说,特定操作系统的文件封装了特定操作系统的一些函数,以方便移植。

5、buf.c

提供了对Buf和Vec的操作

6、build.c

初始化对dist的任何调用,即运行dist时需要调用build.c中的函数执行初始化

7、buildgc.c

构建cmd/gc时的辅助文件

8、buildruntime.c

构建pkg/runtime时的辅助文件

9、goc2c.c

将.goc文件转为.c文件。一个.goc文件是一个组合体:包含Go代码和C代码。注意:goc文件和cgo是不一样的。

三、关键源码解读

以Linux操作系统为例,Windows版本基本一样,只是系统调用等不一样,区别就是unix.c和windows.c的不同。

1、执行流程

1)在unix.c中的main入口函数中,首先设置了gohostos和gohostarch,这是本机的操作系统和系统架构。main中会调用build.c中的init和main.c中的xmain

2)build.c中init函数负责全局状态的初始化,如:goroot、goos、goarch等

3)main.c中xmain函数是main的可移植版本,即特定平台的main将入口转发到xmain中

4)根据传递给dist的具体命令执行相应的函数

2、dist的功能(命令)

在main.c中定义了可用的命令:


// cmdtab records the available commands.
static struct {
     char *name;
     void (*f)(int, char**);
} cmdtab[] = {
     {"banner", cmdbanner},
     {"bootstrap", cmdbootstrap},
     {"clean", cmdclean},
     {"env", cmdenv},
     {"install", cmdinstall},
     {"version", cmdversion},
};

这些命令对应的f是在build.c中实现的。

1)banner->cmdbanner

输出Go安装完成的提示。

提示语句类似:

---
Installed Go for linux/amd64 in /home/polaris/go
Installed commands in /home/polaris/go/bin
*** You need to add /home/polaris/go/bin to your PATH.

其中,如果go/bin已经在PATH中,则不会提示最后一句。

2)bootstrap->cmdbootstrap

这是一个重要的命令,通过这个命令生成Go引导工具:go_bootstrap,其实就是go命令,以及其他工具。 该命令首先会执行build.c中的setup函数,初始化Go目录树,为构建Go做准备,初始化工作包括:创建bin目录、pkg目录及其子目录(如obj/tool等),同时做一些必要的清除工作。

接着为本机操作系统和架构构建Go,具体构建顺序在build.c中的buildorder数组中定义了,这个顺序在安装Go的过程中的输出可以看到。实际的构建是通过build.c中的install函数执行的。

install函数比较长,做的事情就是:根据传给它的源码目录(这是按照buildorder中来的),用gcc或其他编译器(如6a/6c/6g)编译源码,生成pkg/obj、pkg/tool和pkg中的C库、命令和Go包。注意,这个时候tool中有一个go_bootstrap,这个其实就是go命令。(可以通过在make.bash中的./cmd/dist/dist bootstrap $buildall -v后面加上exit 0来看到这个中间过程)

总结一下这一步所做的事情: ①没有构建全部的工具(从buildorder中也可以看出),构建了关键的工具go_bootstrap ②构建了C库(在pkg/obj/下) ③构建了编译器等([568]a/c/g/l) ④将runtime包中的goc文件转为c文件 ⑤构建一些Go包(标准库),之所以构建这些包,是因为cmd/go命令是用Go语言写的,它依赖这些包。但实际上接下来会将这些构建好的Go包删除,这里实际上产生一些文件(要产生的文件定义在gentab结构中),产生这些文件的目的是使runtime包能够通过go命令统一编译。 go_bootstrap是通过[568]g构建的

关于产生的文件,这里详细解释一下: 在build.c中的deptab结构数组中定义了库和工具的依赖关系,其中pkg/runtime包依赖一些文件:

{"pkg/runtime", {
          "zasm_$GOOS_$GOARCH.h",
          "zgoarch_$GOARCH.go",
          "zgoos_$GOOS.go",
          "zruntime_defs_$GOOS_$GOARCH.go",
          "zversion.go",
     }}

这些文件是通过该过程动态生成的,具体怎么生成,在gentab结构数组中有定义,

static struct {
     char *nameprefix;
     void (*gen)(char*, char*);
} gentab[] = {
     // 以下两个是编译器依赖,在buildgc.c中实现
     {"opnames.h", gcopnames}, // gc编译器依赖,在gcopnames函数中通过cmd/gc/go.h生成
     {"enam.c", mkenam},           // [568]c/g/l依赖,在mkenam函数中通过对应的[568].out.h文件生成
     // 以下是runtime包依赖的,在buildruntime.c中实现
     {"zasm_", mkzasm},     // 汇编文件需要用到
     {"zgoarch_", mkzgoarch}, // 当前主机的架构
     {"zgoos_", mkzgoos},     // 当前主机的操作系统
     {"zruntime_defs_", mkzruntimedefs}, // 数据结构定义(Go版),和runtime.h中一些结构定义类似。
                                         // 从runtimedefs结构数组中的文件提取。
     {"zversion.go", mkzversion}, // 定义GOROOT和GoVersion,分别是goroot_final和goversion
};

可见,安装并不需要配置GOOS、GOARCH之类的环境变量,在unix.c或windows.c中会判断出当前环境

对于交叉编译,第一次准备目标环境时,会install(“pkg/runtime”)

另外,该过程会将runtime.h和cgocall.h拷贝到了pkg/$GOOS_$GOARCH目录下,这是给cgo编译的时候使用的。

关于make.bash中接下来通过go_bootstrap安装其他命令和包的过程,在后续文章中分析。

3)clean->cmdclean

删除临时对象。clean -i会将安装了的对象也删除(即pkg中的.a文件)

4)env->cmdenv

输出默认的环境变量。env -p会输出PATH环境变量。

默认的输出格式是:

GOROOT="/home/polaris/go"
GOBIN="/home/polaris/go/bin"
GOARCH="amd64"
GOOS="linux"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOTOOLDIR="/home/polaris/go/pkg/tool/linux_amd64"
GOCHAR="6"     // 5、6、8啥意思大家懂的

而env -w是:

set GOROOT=/home/polaris/go
set GOBIN=/home/polaris/go/bin
set GOARCH=amd64
set GOOS=linux
set GOHOSTARCH=amd64
set GOHOSTOS=linux
set GOTOOLDIR=/home/polaris/go/pkg/tool/linux_amd64
set GOCHAR=6

在make.bash中,执行eval $(./cmd/dist/dist env -p),就相当于在make.bash中定义了上面的变量,包括PATH。

由此可见,源码安装的时候,并不需要设置GOROOT等环境变量。当然,安装完后必须的环境变量还是需要设置的,要不然会提示go命令找不到,因为这里设置的环境变量只是临时用的。

5)install->cmdinstall

通过build.c中install方法安装某个包,比如:

go tool dist install pkg/runtime

注意这种方式和go install的区别

6)version->cmdversion

输出当前Go版本信息。版本信息是通过build.c中的findgoversion函数得到的。

四、总结

从上面的分析可以看出,在Go中,通过gcc编译出dist后,由dist负责其他编译器等的编译,包括各种环境,这样,Go包中的.s、.c、.g都由自身的编译器[568]s/c/g/、链接器[568]l和打包工具pack 负责编译、链接和打包,完全自给自足。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门