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

问题

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

/bin/sh: chaincode not found

进入容器查看,文件确实是存在的,也有可执行权限,只是无法正常运行。
在这里插入图片描述

分析

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

go elf有外部依赖吗?

runtime

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

在这里插入图片描述

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程序外部依赖

​ 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?

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

LinuxWindowsMacOS
ELFPEMach-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)

当出现"go' 不是内部或外部命令,也不是可运行的程序或批处理文件"的错误提示时,通常有几个可能的原因和解决方式。 首先,检查一下GOPATH是否已经添加到Path环境变量中。如果未将GOPATH添加到Path环境变量中,系统就无法找到Go命令所在的路径。您可以按照以下步骤解决这个问题: 1. 打开系统的环境变量设置窗口。 2. 在系统变量中找到Path变量,并点击编辑。 3. 添加GOPATH\bin到Path变量的值中,确保路径之间使用分号进行分隔。 4. 保存并关闭设置窗口。 5. 重新启动命令行或终端窗口,再次运行Go命令查看是否解决了问题。 其次,检查一下GOOS变量是否与当前系统环境一致。如果GOOS变量设置与当前系统环境不匹配,也会导致类似的错误。您可以按照以下步骤解决这个问题: 1. 打开命令行或终端窗口。 2. 运行命令 "go env",查看当前的GOOS值。 3. 如果GOOS值与当前系统环境不匹配,可以使用命令 "go env -w GOOS=当前系统环境对应的变量" 来设置正确的GOOS值。 4. 重新运行Go命令查看是否解决了问题。 最后,如果以上两个解决方式都没有解决问题,您可以尝试删除之前下载的依赖包,并重新执行 "go get" 命令来重新下载依赖包。这有时可以解决一些编译或运行时的问题。 综上所述,当出现"go' 不是内部或外部命令,也不是可运行的程序或批处理文件"的错误提示时,您可以先检查GOPATH是否已添加到Path环境变量中,然后再检查GOOS变量是否与当前系统环境一致。如果问题仍然存在,可以尝试删除依赖包并重新下载。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [go get xxx 之后 ‘xxx‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。](https://blog.csdn.net/qq_50487743/article/details/129320140)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [关于问题:‘go‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件 的解决方案。(window10版本下)](https://blog.csdn.net/lakersssss24/article/details/108649181)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值