在 Golang 中开启 cgo 时遇到的问题与思考

对纯 Go 代码,可以在 go build 时添加 GOOS 和 GOARCH 变量来指定二进制的目标系统与架构,实现交叉编译;但对开启了 CGO 的代码,如何做到交叉编译呢?错误的示范总共在项目中遇到了两类错误。环境搭建在宿主机上使用 multipass 开启 3 个不同版本的 Ubuntu 系统

对纯 Go 代码,可以在 go build 时添加 GOOSGOARCH 变量来指定二进制的目标系统与架构,实现交叉编译;但对开启了 CGO 的代码,如何做到交叉编译呢?

错误的示范

总共在项目中遇到了两类错误。

环境搭建

在宿主机上使用 multipass 开启 3 个不同版本的 Ubuntu 系统,如下:

multipass launch --name=a release:18.04
multipass launch --name=b release:20.04
multipass launch --name=c release:22.04

# 虚拟机列表
$ multipass list
Name                    State             IPv4             Image
a                       Running           172.16.73.16     Ubuntu 18.04 LTS
b                       Running           172.16.73.17     Ubuntu 20.04 LTS
c                       Running           172.16.73.18     Ubuntu 22.04 LTS

依赖安装

sudo apt update
sudo apt upgrade
sudo apt install -y make golang-go

查看当前系统 GLIBC 版本

getconf GNU_LIBC_VERSION

glibc 2.35

ldd --version

ldd (Ubuntu GLIBC 2.35-0ubuntu3) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

查看各系统的 glibc 版本

multipass exec a getconf GNU_LIBC_VERSION
multipass exec b getconf GNU_LIBC_VERSION
multipass exec c getconf GNU_LIBC_VERSION

详细如下:

系统版本 glibc 版本
a 18.04 glibc 2.27
b 20.04 glibc 2.31
c 22.04 glibc 2.35

二进制编译时依赖的库的版本与实际运行时系统中存在的库之间的差异

在 Ubuntu 22.04 LTS 中编译的二进制,无法在 Ubuntu 18.04.6 LTS 运行(架构:amd64)。

按步骤来操作,此问题必现

在 c 机器上构建 linux amd64 架构的二进制 demo,并将 demo 分发到 a, b 两台机器上。

1pnLi71pnLi7

分别在 a, b 两台机器上执行二进制 demo。

a机器上
uhFx0CuhFx0C

对 demo 文件的分析

8Iojys8Iojys

在 macOS 12 中开启 CGO 编译的二进制,无法在 macOS 11 中运行(架构:amd64)。

后续未复现

二进制编译时用的系统运行时的系统不同

GLIBC版本不兼容问题

构建 linux 的二进制文件时,做静态打包,将 glibc 的依赖一起打包进二进制文件中。

GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -ldflags "-extldflags '-static'" -o $(NAME)

但是遇到以下的 Warning,大致意思是虽然用了静态编译,但是还是会动态链接到进行 linker 操作时的 glibc 版本上。如果放任这个错误不管,可能会出现一些莫名其妙的问题。

/usr/bin/ld: /tmp/go-link-922118976/000010.o: in function `unixDlOpen':
/home/ubuntu/go/pkg/mod/github.com/mattn/[email protected]/sqlite3-binding.c:39881: warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-922118976/000016.o: in function `_cgo_6cc2654a8ed3_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:58: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

这里一种可行的方式是不使用 glibc 这种 libc,换成 musl,在 Ubuntu 上面安装:

sudo apt install -y musl musl-dev musl-tools

编译时只需要在 go build 时添加 CC=/usr/bin/musl-gcc 变量。

CC=/usr/bin/musl-gcc GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -ldflags "-extldflags '-static'" -o $(NAME)-musl

在 c 机器上面构建完成后,分发到 a、b 机器上面,都可以正常运行,结果如下:

ubuntu@b:~$ ./demo-musl  up
INFO[2022-07-20 21:54:09.228] done init db
INFO[2022-07-20 21:54:09.228] scheduler started...
INFO[2022-07-20 21:54:09.229] Connecting to wss://xxx/ncd/ws/connection
INFO[2022-07-20 21:54:09.362] Response: &{Status:101 Switching Protocols StatusCode:101 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Connection:[upgrade] Date:[Wed, 20 Jul 2022 13:54:05 GMT] Sec-Websocket-Accept:[7PJbg/gEi9umEJ4bnOaWAciTq0k=] Server:[Nginx] Upgrade:[websocket]] Body:{Reader:0xc00042f9e0} ContentLength:0 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc0000ad000 TLS:<nil>}
INFO[2022-07-20 21:54:09.363] send message: [RequestId: 015e63a3-7bad-4cba-a3d6-c515e61f6e42, Command: GET_AGENT_UPDATE_INFO, Code: 0, Message: , MaxFramePayload: 0, TimeoutSeconds: 30]

ubuntu@a:~$ ./demo-musl up
INFO[2022-07-20 21:57:37.345] done init db
INFO[2022-07-20 21:57:37.345] scheduler started...
INFO[2022-07-20 21:57:37.347] Connecting to wss://xxx/ncd/ws/connection
INFO[2022-07-20 21:57:37.560] Response: &{Status:101 Switching Protocols StatusCode:101 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Connection:[upgrade] Date:[Wed, 20 Jul 2022 13:57:34 GMT] Sec-Websocket-Accept:[NgGXU/yXwW9Hk6ecEhVICTjmKiI=] Server:[Nginx] Upgrade:[websocket]] Body:{Reader:0xc00061b1d0} ContentLength:0 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc0000a5100 TLS:<nil>}
INFO[2022-07-20 21:57:37.561] send message: [RequestId: eb05129e-3722-4ecd-88f5-23cb2e734df3, Command: GET_AGENT_UPDATE_INFO, Code: 0, Message: , MaxFramePayload: 0, TimeoutSeconds: 30]

如何交叉编译

在 macOS (x86) 上搭建编译 linux amd64 和 arm64 平台的环境

  1. 安装 musl-cross
brew install FiloSottile/musl-cross/musl-cross --with-aarch64

安装完成后,可以看到对应架构的编译器

iWZkLniWZkLn
  1. 编译
BUILD_FLAGS="-X $(PKG)/version.Commit=$(COMMIT)\
	-X '$(PKG)/version.BuildTime=$(BUILD_TIME)'\
	-X '$(PKG)/version.Version=$(VERSION)' -extldflags '-static'"
  • amd64 架构
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC="/usr/local/bin/x86_64-linux-musl-gcc" go build -ldflags $(BUILD_FLAGS) -o $(NAME)-amd64
  • arm64 架构
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC="/usr/local/bin/aarch64-linux-musl-gcc" go build -ldflags $(BUILD_FLAGS) -o $(NAME)-arm64  

在 macOS (x86) 上搭建编译 windows amd64 架构的环境

brew install mingw-w64
  • amd64 架构
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC="/usr/local/bin/x86_64-w64-mingw32-gcc" go build -ldflags $(BUILD_FLAGS) -o $(NAME).exe

Reference

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd