前言:

    让我们一起来了解下go build命令都做了些啥;并进行源码追踪其过程;在golang早期版中编译器,连接器都是用C开发的。后期版本中go的编译器连接器都用go重写了一套,这一套都是开源的,我们都可以阅读;

    golang版本: go1.13.4 darwin/amd64

    调试工具:      dlv

    dlv如果不太会用的可以看一下我前一篇文章:好未来技术交流社区(TTC)

go build命令参数可选项:

可选项

备注

-n

打印编译过程

-a

将命令源码文件与库源码文件全部重新构建

-x

打印编译期间用到的命名,它与 -n 的区别是,它不仅打印还会执行

-o

输出执行文件保存的文件名

-race

开启竞态条件的检测,支持的平台有限制

一.过程解析:

 

go语言编译器架构 go语言编译器源码_linux

 

输入如下命令,可以看到图中:

#go build -n default.go

 

go语言编译器架构 go语言编译器源码_go语言编译器架构_02

 上图中过程,和上图中略有不同的地方是cat >$WORK/b001/_gomod_.go 这行没有写入;这一块是由于gomod的缘故;这个不是关键点所以没有写入;

#创建目录 
mkdir -p $WORK/b001/           

#编译文件 
cd /data/webroot/qingke/godemo/dump 
/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid Rw-tIgJPD3wmhn5zj7iu/Rw-tIgJPD3wmhn5zj7iu -goversion go1.13.4 -D _/data/webroot/qingke/godemo/dump -importcfg $WORK/b001/importcfg -pack -c=4 ./default.go $WORK/b001/_gomod_.go 

#生成链接库配置importcfg.link文件 
cat >$WORK/b001/importcfg.link << 'EOF' # internal 
packagefile  command-line-arguments=$WORK/b001/_pkg_.a 
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a 
packagefile internal/bytealg=/usr/local/go/pkg/darwin_amd64/internal/bytealg.a 
packagefile internal/cpu=/usr/local/go/pkg/darwin_amd64/internal/cpu.a 
packagefile runtime/internal/atomic=/usr/local/go/pkg/darwin_amd64/runtime/internal/atomic.a 
packagefile runtime/internal/math=/usr/local/go/pkg/darwin_amd64/runtime/internal/math.a 
packagefile runtime/internal/sys=/usr/local/go/pkg/darwin_amd64/runtime/internal/sys.a 
EOF 

#创建b001中exe目录 
mkdir -p $WORK/b001/exe/ 
cd . 

#连接生成a.out可执行文件 
/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=TwK52M06SyTj6MiQ9MRi/Rw-tIgJPD3wmhn5zj7iu/Rw-tIgJPD3wmhn5zj7iu/TwK52M06SyTj6MiQ9MRi -extld=clang $WORK/b001/_pkg_.a 

#更新a.out id
/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal 

#mv a.out改变名为default的可执行程序 
mv $WORK/b001/exe/a.out default

 上面部分有三个命令link、buildid、compile这三个命令是编译的核心;

compile: 编译器

link:        连接器

buildid:  ID生成器

二.源码分析:

执行调试go命令,dlv需要用完整路径,可以用which go查看一下

命令执行如下:

dlv exec /usr/local/Cellar/go/1.13.4/libexec/bin/go build default.go

设置字符串打印长度,为了查看变量,不设置的话,变量看不完整

(dlv)config max-string-len 99999

然后下一个断点:

(dlv)b main.main

如图所示:

 

go语言编译器架构 go语言编译器源码_go_03

  

go语言编译器架构 go语言编译器源码_go语言编译器架构_04

 

我打印了args数组的结果,其实就是我们go后面的build 和 default.go参数;

在此时我们可要注意src/cmd/go/main.go代码中有base.Go.Commands的初始化,可以简单看一下Command结构

结构原型在src/cmd/go/internal/base/base.go代码中:

type Command struct {
    //命令运行方法
    Run func(cmd *Command, args []string)
    
    //命令行提示信息
    UsageLine string
    
    //go help中简短描述
    Short string
    
    //go help中详细描述
    Long string
    
    //命令标志
    Flag flag.FlagSet
    
    //CustomFlags指示命令将执行其自己的标志解析。
    CustomFlags bool
    // Commands lists the available commands and help topics.    
    // The order here is the order in which they are printed by 'go help'.   
    // Note that subcommands are in general best avoided.    
    Commands []*Command 
}

此时可以可以注意到base.Go.Commands中的work.CmdBuild对应的就是我们build命令的映射。

在我们的src/cmd/go/internal/work/build.go中;work.CmdBuild的初始化在build.go中进行初始化,对应方法runBuild。

 

go语言编译器架构 go语言编译器源码_调试器_05

 

此时可以对runBuild下一个断点继续跟踪;

 

go语言编译器架构 go语言编译器源码_go_06

 

打印387行中的a

 

go语言编译器架构 go语言编译器源码_调试器_07

 

结构体在src/cmd/go/internal/work/action.go中,结构如下:

type Action struct {
    Mode       string                        // 动作操作说明
    Package    *load.Package                 // 此操作的工作包
    Deps       []*Action                     // 在此之前必须采取的行动
    Func       func(*Builder, *Action) error // 动作方法(nil = no-op)
    IgnoreFail bool                          // 即使依赖项失败,是否运行f
    TestOutput *bytes.Buffer                 // 测试输出缓冲区
    Args       []string                      // 运行程序的其他参数
    
     triggers []*Action // inverse of deps    
     buggyInstall bool // is this a buggy install (see -linkshared)?    
     TryCache func(*Builder, *Action) bool // callback for cache bypass
     
     
     // Generated files, directories.    
     Objdir   string         // 中间对象目录    
     Target   string         // 操作的目标:创建的包或可执行文件    
     built    string         // 实际创建的包或可执行文件    
     actionID cache.ActionID // 动作输入的缓存ID    
     buildID  string         // 操作输出的生成ID
     
     VetxOnly  bool       // Mode=="vet": only being called to supply info about dependencies    
     needVet   bool       // Mode=="build": need to fill in vet config    
     needBuild bool       // Mode=="build": need to do actual build (can be false if needVet is true)    
     vetCfg    *vetConfig // vet config    
     output    []byte     // output redirect buffer (nil means use b.Print)
     
     // Execution state.    
     pending  int         // number of deps yet to complete    
     priority int         // relative execution priority    
     Failed   bool        // whether the action failed        
     json     *actionJSON // action graph information
}

该结构体主要存储动作行为;比如说执行编译动作,然后通过结构体映射到对应方法;

然后我们看一下Do方法,Do方法其实就是对动作的操作;

func (b *Builder) Do(root *Action) {
    ...
     // Write action graph, without timing information, in case we fail and exit early.
     writeActionGraph := func() {
         if file := cfg.DebugActiongraph; file != "" {
             if strings.HasSuffix(file, ".go") {
                  // Do not overwrite Go source code in:             
                  // go build -debug-actiongraph x.go             
                  base.Fatalf("go: refusing to write action graph to %v\n", file)
             }
             js := actionGraphJSON(root)
             if err := ioutil.WriteFile(file, []byte(js), 0666); err != nil {
                 fmt.Fprintf(os.Stderr, "go: writing action graph: %v\n", err)
                 base.SetExitStatus(1)
             }
         }
      }
      
   writeActionGraph()
   b.readySema = make(chan bool, len(all))
      
   ...
   //Handle运行单个操作并负责触发因此可运行的任何操作。
   handle := func(a *Action) {
        if a.json != nil {
            a.json.TimeStart = time.Now()
        }
        var err error
        if a.Func != nil && (!a.Failed || a.IgnoreFail) {
            err = a.Func(b, a)  //执行事件动作
        }
        ...
   }
   ...
   // Write action graph again, this time with timing information.   
   writeActionGraph()
}

请注意之前打印387行中的a的

 

go语言编译器架构 go语言编译器源码_go语言编译器架构_08

 

动作其实会执行build方法,其实我们可以针对build方法下一个断点;执行

(dlv) b cmd/go/internal/work.(*Builder).build 
(dlv) c 
(dlv) bt

 

go语言编译器架构 go语言编译器源码_golang_09

 

跟踪到397行进入buildActionID方法

 

go语言编译器架构 go语言编译器源码_go语言编译器架构_10

 

按n继续执行

 

go语言编译器架构 go语言编译器源码_golang_11

  

go语言编译器架构 go语言编译器源码_go_12

 

这一块的compile是根据base.Tool获取,不同的操作系统获取的编译器不一致.继续按n执行

 

go语言编译器架构 go语言编译器源码_go语言编译器架构_13

 

exec.Comand方法其实就是调用complie去编译default.go文件;

complie命令会生成一个/var/folders/24/m8_dx27s2g3dqxypxcll0kvw0000gn/T/go-build086150708/b001/exe/a.out

然而/var/folders/24/m8_dx27s2g3dqxypxcll0kvw0000gn/T/go-build086150708/就是我们最初看到的$work,这个每次是不一样的。

然而想得到 complie,link,buildid命令操作都可以对 cmd/go/internal/base.Tool下断点去跟踪,看到对应的过程;

其实在调用complie命令之后,又调用了asm命令。

 

go语言编译器架构 go语言编译器源码_go语言编译器架构_14

 

asm命令其实是生成.o文件;

除此之外,并不是每次go build时都会去调用complie命令,有时候则不会。在程序id一致的时候就会去取对应编译的cache文件;

如图

 

go语言编译器架构 go语言编译器源码_linux_15

 

结束:

    分析源码是一个比较枯燥的过程,需要反复尝试。不过这个过程能让你学习到一些设计理解,对于成长是有帮助的,大家一起共勉;