【go可执行文件的外部依赖】

问题

多阶段编译镜像,编译基础镜像是ubuntu,运行时基础镜像是alpine,运行容器时报错如下:

/bin/sh: chaincode not found

进入容器查看,文件确实是存在的,也有可执行权限,只是无法正常运行。
【go可执行文件的外部依赖】_第1张图片

分析

虽然报错信息不清晰,但是怀疑是缺失外部依赖导致的。

go elf有外部依赖吗?

runtime

runtime可以理解为语言与操作系统之间的抽象层,接口统一;

【go可执行文件的外部依赖】_第2张图片

C runtime(CRT)

c语言的runtime,由各个平台自己实现。Linux和Windows平台下的两个主要C语言运行库分别为:glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time)。

​ 这里以linux系统为例进行讨论;在Linux平台上最广泛使用的C运行库是glibc,其中包括C标准库的实现,也包括所有系统函数。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础。

Go runtime

Go自己实现了runtime,独立实现的go runtime层将Go user-level code与OS syscall解耦,从下图可以发现runtime属于go程序范围内。

因此,同一个程序go编译结果往往比c的要大很多;相应的,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。

【go可执行文件的外部依赖】_第3张图片

go程序外部依赖

​ c程序有外部依赖CRT,因为go自己实现了runtime,其linux下的编译结果可以运行在大多数linux发行版本上。

CRT的差异

为什么有些发行版本(alpine)无法运行centos编译的go程序呢?

因为c是有glibc依赖的,而部分go程序通过cgo调用了c库,这类go程序也会依赖c的CRT。

前文也提到,linux平台下最广泛使用的CRT是glibc(centos、ubuntu等都使用glibc),而alpine使用的是 musl libc,这个库相比于 glibc 更小、更简单、更安全,但是与大家常用的标准库 glibc 并不兼容。

所以,go程序可能存在CRT外部依赖,所以编译基础镜像运行时基础镜像CRT要一致,尽量避免此类问题。

如何查看外部依赖

readelf

readelf <选项> elf-文件
	-h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
  -d --dynamic           Display the dynamic section (if present)
readelf -d elf-文件

有外部依赖的elf,可以看到dynamic section

ldd

ldd可以查看外部依赖

file和nm

file

可以展示静态链接还是动态链接,静态链接没有外部依赖

  • alpine
/usr/local/bin # file chaincode
chaincode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=Hup64vKKQkCbQFl-pUQv/bYePqNcoSev1c2zpxNsh/SSKw5s6m2NRn4MMyKS3v/xVTmraP8f_kDzrlz7CxY, not stripped
  • centos

helloworld比较简单,是静态链接

file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
nm
nm elf-文件|grep " U "

有外部依赖的elf,可以查看有很多未定义的symbol。

静态编译go程序

go默认采用静态链接。

默认情况下,Go的runtime环境变量CGO_ENABLED=1,即默认开始cgo,允许你在Go代码中调用C代码,Go的pre-compiled标准库的.a文件也是在这种情况下编译出来的。

比如CGO_ENABLED=1时,net.a就用c的实现,这样用net包的LookupIP时,就会用到CRT,有外部依赖,动态链接

nm net.a | grep ' U '
                 U ___error
                 U __cgo_topofstack
                 U _getnameinfo
                 U ___error
                 U __cgo_topofstack
                 U _freeaddrinfo
                 U _gai_strerror
                 U _getaddrinfo

所以用net/http写一个简单的http服务,在默认CGO_ENABLED=1情况下,就会采用c的实现。

// 示例程序
package main
import "net"

const host = "baidu.com"

func main() {
	ips, err := net.LookupIP(host)
	if err != nil {
		panic(err)
	}
	for _, ip := range ips {
		println("lookup:" + ip.String())
		return
	}
}

LookupIP用到的方法,如果cgo not available或者–tags=netgo,则用go的实现;否则,用c的实现。

func (r *Resolver) lookupIP(ctx context.Context, network, host string) (addrs []IPAddr, err error) {
	if r.preferGo() {
		return r.goLookupIP(ctx, host)
	}
	order := systemConf().hostLookupOrder(r, host)
	if order == hostLookupCgo {
		if addrs, err, ok := cgoLookupIP(ctx, network, host); ok {
			return addrs, err
		}
		// cgo not available (or netgo); fall back to Go's DNS resolver
		order = hostLookupFilesDNS
	}
	ips, _, err := r.goLookupIPCNAMEOrder(ctx, host, order)
	return ips, err
}


// src/net/cgo_unit.go
// +build cgo,!netgo
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris

package net

func cgoLookupIP(ctx context.Context, network, name string) (addrs []IPAddr, err error, completed bool) {
	if ctx.Done() == nil {
		addrs, _, err = cgoLookupIPCNAME(network, name)
		return addrs, err, true
	}
	result := make(chan ipLookupResult, 1)
	go cgoIPLookup(result, network, name)
	select {
	case r := <-result:
		return r.addrs, r.err, true
	case <-ctx.Done():
		return nil, mapErr(ctx.Err()), false
	}
}

如果CGO_ENABLED=0或者–tags=netgo,你使用build的 “-x -v”选项,你将看到go compiler会重新编译依赖的包的静态版本,包括net、mime/multipart、crypto/tls等,并将编译后的.a(以包为单位)放入临时编译器工作目录($WORK)下,然后再静态连接这些版本。

internal linking和external linking

在CGO_ENABLED=1这个默认值的情况下,是否可以实现纯静态连接呢?答案是可以。在$GOROOT/cmd/cgo/doc.go中,文档介绍了cmd/link的两种工作模式:internal linking和external linking

go tool link -h
  -extldflags flags
        pass flags to external linker
  -linkmode mode
        set link mode
  • internal linking

    internal linking的大致意思是若用户代码中仅仅使用了net、os/user等几个标准库中的依赖cgo的包时,cmd/link默认使用internal linking,而无需启动外部external linker(如:gcc、clang等),不过由于cmd/link功能有限,仅仅是将.o和pre-compiled的标准库的.a写到最终二进制文件中。因此如果标准库中是在CGO_ENABLED=1情况下编译的,那么编译出来的最终二进制文件依旧是动态链接的,即便在go build时传入-ldflags '-extldflags "-static"'亦无用,因为根本没有使用external linker mode。

比如,前文已经查看过本机的$GOROOT/pkg下的.a都是在cgo下编译的,所以无效,结果go程序依然是动态链接

go build -o server-fake-static-link  -ldflags '-extldflags "-static"' -v -x net.go

ldd server-fake-static-link
	linux-vdso.so.1 =>  (0x00007ffc4f768000)
	libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb4595b0000)
	libc.so.6 => /lib64/libc.so.6 (0x00007fb4591e2000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb4597cc000)
  • external linking

    而external linking机制则是cmd/link将所有生成的.o都打到一个.o文件中,再将其交给外部的链接器,比如gcc或clang去做最终链接处理。如果此时,我们在cmd/link的参数中传入-ldflags '-linkmode "external" -extldflags "-static"',那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码。我们可以通过-linkmode=external来强制cmd/link采用external linker,还是以server.go的编译为例

go build -o server-static-link  -ldflags '-linkmode "external" -extldflags "-static"' net.go              

就这样,我们在CGO_ENABLED=1的情况下,也编译构建出了一个纯静态链接的Go程序。

总结

  • 如果go程序没有用到net、os/user等,默认程序就是静态链接,没有外部依赖

  • 如果使用了net这样包含cgo的包,那么CGO_ENABLED的值决定是静态还是动态

  • CGO_ENABLED=0的情况下,Go采用纯静态编译(如果用到c的代码则无法编译)

  • 如果CGO_ENABLED=1,但依然要强制静态编译,需传递-linkmode=external给cmd/link。

  • 如果项目本身没有用到cgo,而使用外部链接的方式,就会有如下报错。

    go build -o test -ldflags '-linkmode "external" -extldflags "-static"'
    loadinternal: cannot find runtime/cgo
    

补充阅读

什么是elf?

可执行文件在不同的操作系统上规范不一样

Linux Windows MacOS
ELF PE Mach-O

Linux 的可执行文件 ELF(Executable and Linkable Format) 为例, ELF 由几部分构成:

  • ELF header

  • Section header

  • Sections

glibc和GNU/GCC

glibc

​ 这里以linux系统为例进行讨论;在Linux平台上最广泛使用的C运行库是glibc,其中包括C标准库的实现,也包括所有系统函数。几乎所有C程序都要调用glibc的库函数,所以glibc是Linux平台C程序运行的基础。

GNU/GCC

GNU软件包列表:该系统的基本组成包括GNU编译器套装(GCC)、GNU的C库(glibc)、以及GNU核心工具组(coreutils)、(GDB)。

GCC原名GNU C Compiler,后来逐渐支持更多的语言编译(C++、Fortran、Pascal、Objective-C、Java、Ada、Go等),
所以变成了GNU Compiler Collection(GNU编译器套装

GCC是GUN Compiler Collection的简称,是Linux系统上常用的编译工具。

GCC工具链软件包括GCC、Binutils、C运行库等。

  • GCC:

    GCC(GNU C Compiler)是编译工具。本文所要介绍的将C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。

  • Binutils:

    一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。这一组工具是开发和调试不可缺少的工具。

    ldd:可以用于查看一个可执行程序依赖的共享库。
    objdump:主要的作用是反汇编。
    readelf:显示有关ELF文件的信息

.o | .a | .so的区别

​ .o,是目标文件,相当于windows中的.obj文件

.so 为共享库,是shared object,用于动态连接的,相当于windows下的dll

.a为静态库,是好多个.o合在一起,用于静态连接 。注意,.a用于静态链接不代表其本身没有外部依赖。

参考

[1] 也谈Go的可移植性

[2] 基础概念——C标准、C运行库和glibc

[3] 不要轻易使用 Alpine 镜像来构建 Docker 镜像,有坑!

[4] Linux中的动态库和静态库(.a/.la/.so/.o)

你可能感兴趣的:(golang,golang,linux)