golang快速入门[5.1]-go语言是如何运行的-连接器

前文

前言

  • 在上一篇文章中,咱们详细介绍了go语言编译为机器码经历的:词法分析 => 语法分析 => 类型检查 => 中间代码 => 代码优化 => 生成机器码windows

  • 可是在源代码生成执行程序的过程当中,其实还经历了连接等过程。总的来讲一个程序的生命周期能够归纳为: 编写代码 => 编译 => 连接 => 加载到内存 => 执行app

  • 在第5章咱们将对其进行逐一解释ide

连接(link)

  • 咱们编写的程序可能会使用其余程序或程序库( library ) 正如咱们在helloworld程序中使用的fmt package函数

  • 咱们编写的程序必须与这些程序或程序库一块儿才可以执行优化

  • 连接是将咱们编写的程序与咱们须要的外部程序组合在一块儿的过程

  • 连接器是系统软件,在系统开发中起着相当重要的做用,由于它能够进行单独的编译。您能够将它分解为更小,更易管理的块,而后分别进行修改和编译,而不是将一个大型应用程序组织为一个总体的源文件。当您更改其中一个模块时,只需从新编译它并从新连接应用程序,而无需从新编译其余源文件。

  • 连接分为两种,静态连接与动态连接

  • 静态连接的特色在于连接器会将程序中使用的全部库程序复制到最后的可执行文件中。而动态连接只会在最后的可执行文件中存储动态连接库的位置,并在运行时调用。

  • 所以静态连接要更快,可移植,由于它不须要在运行它的系统上存在该库。可是在磁盘和内存上占用更多的空间

  • 连接发生的过程会在两个地方,一种是静态连接会在编译时的最后一步发生,一种是动态连接在程序加载到内存时发生。

  • 下面咱们简单对比一下静态连接与动态连接

v2-f0aef22627dd4293e8cd471ebf2caa66_hd.jpg

go语言是静态连接仍是动态连接?

  • 有时会看到一些比较老的文章说go语言是静态连接的,但这种说法是不许确的

  • 如今的go语言不只支持静态连接也支持动态编译

  • 总的来讲,go语言在通常默认状况下是静态连接的,可是一些特殊的状况,例如使用了CGO(即引用了C代码)的地方,则会使用操做系统的动态连接库。例如go语言的net/http包在默认状况下会应用libpthread与 libc 的动态连接库,这种状况会致使go语言程序虚拟内存的增长(下一文介绍)

  • go语言也支持在go build编译时传递参数来指定要生成的连接库的方式,咱们可使用go help buildmode 命令查看

» go help buildmode                                                                                                                                                             jackson@192
    -buildmode=archive
        Build the listed non-main packages into .a files. Packages named
        main are ignored.

    -buildmode=c-archive
        Build the listed main package, plus all packages it imports,
        into a C archive file. The only callable symbols will be those
        functions exported using a cgo //export comment. Requires
        exactly one main package to be listed.

    -buildmode=c-shared
        Build the listed main package, plus all packages it imports,
        into a C shared library. The only callable symbols will
        be those functions exported using a cgo //export comment.
        Requires exactly one main package to be listed.

    -buildmode=default
        Listed main packages are built into executables and listed
        non-main packages are built into .a files (the default
        behavior).

    -buildmode=shared
        Combine all the listed non-main packages into a single shared
        library that will be used when building with the -linkshared
        option. Packages named main are ignored.

    -buildmode=exe
        Build the listed main packages and everything they import into
        executables. Packages not named main are ignored.

    -buildmode=pie
        Build the listed main packages and everything they import into
        position independent executables (PIE). Packages not named
        main are ignored.

    -buildmode=plugin
        Build the listed main packages, plus all packages that they
        import, into a Go plugin. Packages not named main are ignored.
  • archive: 将非 main package构建为 .a 文件. main 包将被忽略。

  • c-archive: 将 main package构建为及其导入的全部package构建为构建到 C 归档文件中

  • c-shared: 将mainpackage构建为,以及它们导入的全部package构建到C 动态库中。

  • shared: 将全部非 main package合并到一个动态库中,当使用-linkshared参数后,可以使用此动态库

  • exe: 将main package和其导入的package构建为成为可执行文件

  • 本文再也不介绍go如何手动使用动态库这一高级功能,读者只需如今知道go能够实现这一功能便可

编译与连接的具体过程

  • 下面咱们以helloworld程序为例,来讲明go语言编译与连接的过程,咱们可使用go build命令,-x参数表明了打印执行的过程

go build  -x main.go

输出以下:

WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
EOF
cd /Users/jackson/go/src/viper/XXX
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D _/Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cp $WORK/b001/_pkg_.a /Users/jackson/Library/Caches/go-build/cf/cf0dc65f39f01c8494192fa8af14570b445f6a25b762edf0b7258c22d6e10dc8-d # internal
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
...
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/
  • 下面咱们对输出进行逐行分析

  • 建立了一个临时目录,用于存放临时文件。默认状况下命令结束时自动删除此目录,若是须要保留添加-work参数。

WORK=/var/folders/g2/0l4g444904vbn8wxnrw0j_980000gn/T/go-build757876739
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
  • 生成编译配置文件,主要为编译过程须要的外部依赖(如:引用的其余包的函数定义)

# import config
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
  • 编译,生成中间结果$WORK/b001/pkg.a,

/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid JqleDuJlC1iLMVADicsQ/JqleDuJlC1iLMVADicsQ -goversion go1.13.6 -D _/Users/jackson/go/src/viper/args -importcfg $WORK/b001/importcfg -pack -c=4 ./main.go
  • .a文件由compile命令生成,也能够经过go tool compile进行调用

  • .a类型的文件又叫作目标文件(object file),其是一个压缩包,内部包含了_.PKGDEF`、`_go.o两个文件,分别为编译目标文件和连接目标文件

$ file _pkg_.a # 检查文件格式
_pkg_.a: current ar archive # 说明是ar格式的打包文件
$ ar x _pkg_.a #解包文件
$ ls
__.PKGDEF  _go_.o
  • 文件内容由代码导出的函数、变量以及引用的其余包的信息组成。为了弄清这两个文件包含的信息须要查看go编译器实现的相关代码,相关代码在src/cmd/compile/internal/gc/obj.go文件中(源码中的文件内容可能随版本更新变化,本系列文章以Go1.13.5版本为准)

  • 下面代码中生成ar文件,ar文件 是一种很是简单的打包文件格式,普遍用于linux中静态连接库文件中,文件以 字符串"!\n"开头。随后跟着60字节的文件头部(包含文件名、修改时间等信息),以后跟着文件内容。由于ar文件格式简单,Go编译器直接在函数中实现了ar打包过程。

  • startArchiveEntry用于预留ar文件头信息位置(60字节),finishArchiveEntry用于写入文件头信息,由于文件头信息中包含文件大小,在写入完成以前文件大小未知,因此分两步完成。

func dumpobj1(outfile string, mode int) {
    bout, err := bio.Create(outfile)
    if err != nil {
        flusherrors()
        fmt.Printf("can't create %s: %v\n", outfile, err)
        errorexit()
    }
    defer bout.Close()
    bout.WriteString("!<arch>\n")

    if mode&modeCompilerObj != 0 {
        start := startArchiveEntry(bout)
        dumpCompilerObj(bout)
        finishArchiveEntry(bout, start, "__.PKGDEF")
    }
    if mode&modeLinkerObj != 0 {
        start := startArchiveEntry(bout)
        dumpLinkerObj(bout)
        finishArchiveEntry(bout, start, "_go_.o")
    }
}
  • 生成连接配置文件,主要为须要连接的其余依赖

cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
...
EOF
  • 执行连接器,生成最终可执行文件main,同时可执行文件会拷贝到当前路径,最后删除临时文件

/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=exe -buildid=zCU3mCFNeUDzrRM33f4L/JqleDuJlC1iLMVADicsQ/r7xJ7p5GD5T9VONtmxob/zCU3mCFNeUDzrRM33f4L -extld=clang $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out main
rm -r $WORK/b001/

总结

  • 在本文中,咱们介绍了go程序从源代码到运行须要经历的重要一环——连接,并介绍了静态连接与动态连接

  • 在本文中,咱们用一个例子介绍了编译与连接的具体过程

  • 在下文中,咱们将介绍go语言的内存分配

参考资料

相关文章
相关标签/搜索