本文先介绍 Go CGO 的概念和应用场景,以项目用到的 mattn/go-sqlite3 为例,介绍 CGO 程序的静态链接实现方案,其中涉及到动态链接的问题分析、 ubunut/centos 系统的静态编译环境搭建,CGO 静态编译遇到的问题和解决方案,最终生成最小化系统环境依赖的静态链接二进制。
Go CGO 动态链接和静态链接 #
Go CGO 项目时,默认链接系统的 glibc 库和依赖的三方库:
# 编译
$ GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build -o myagent ./cmd/
# 动态链接 ELF
$ file myagent
myagent: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=f531e4906b378654bb2faa188153cdc56459c96c, for GNU/Linux 3.7.0, with debug_info, not stripped
# 链接系统 libc 库
$ ldd myagent
linux-vdso.so.1 (0x0000ffff8ffdc000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff8fdd0000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff8ff90000)
动态链接 glibc 库带来常见问题是版本兼容性,各 Linux 发行版一般只能保证大版本的 glibc 兼容,如 CentOS 7.9 和 7.2 的 glibc 是兼容的,但是 CentOS 8.1 和 CentOS 7.9 的 glibc 不一定兼容,也就是在较新 glibc 版本的机器上构建出的二进制可能不能在低版本 glibc 机器上运行(升级内核不会有这个问题),报错:
#ldd /tmp/myagent
/tmp/myagent: /lib64/libc.so.6: version `GLIBC_2.33' not found (required by /tmp/myagent)
/tmp/myagent: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by /tmp/myagent)
/tmp/myagent: /lib64/libc.so.6: version `GLIBC_2.32' not found (required by /tmp/myagent)
/tmp/myagent: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /tmp/myagent)
linux-vdso.so.1 => (0x00007fff931d2000)
libc.so.6 => /lib64/libc.so.6 (0x00007f6ee76e9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6ee7ac4000)
解决 glibc 版本兼容性问题的常用方案:
- 静态链接:将 glibc 等依赖库静态链接到可执行程序中,运行时不依赖系统 glibc 库;
- 手动打包:通过 tar 包等机制,将依赖的动态库和程序二进制一起打包,运行时通过 LD_LIBRARY 环境变量来注入包中的 glibc 等动态库;
- 下载合适版本的 glibc 源码,进行编译,生成对应版本的 glibc 库;
- 除了 LD_LIBRARY 外,也可以使用 patchelf –set-interpreter 命令来修改二进制的动态链接库路径。
- 容器镜像:和手动打包类似,但是打包到一个 Docker 容器镜像中;
方案 2 和 3 都有一些局限性和运行环境要求,本文主要讨论静态链接的方案 1。
Go CGO 程序的静态链接 #
Go CGO 程序能否静态链接,取决于使用的 C 部分代码(包括 C 项目本身及其依赖的库,如 glibc、libm 等)是否支持静态链接。
以 x86_64 架构的 fedora:40 编译环境为例(CentOS/RHEL 类似),需要额外安装静态 glibc 库:
go build 的 -ldflags 的 -linkmode 参数只可以为:
- 默认值 auto (参考:cmd/cgo/doc.go Implementation details):
- 如果是未启用 cgo 或者只使用了标准库的 CGO,则使用 internal 模式。
- 否则为 external 模式。
- internal: 使用 go 实现的原生链接器进行链接,因为上述的 cgo 过程已经进行过链接了,因此动态库的信息已经知晓了,因此在此阶段不需要再进行动态库查找了。直接生成 a.out 即可。
- external: 使用外部的链接器进行链接,一般是 gcc。
- 先将 .a 转换为链接器可识别的 .o 文件。
- 使用 gcc 进行链接生成 a.out。
# docker run -it -v fedora:40 bash
[root@d8e784ed1d22 tmp]# uname -a
Linux d8e784ed1d22 6.12.5-orbstack-00287-gf8da5d508983 #19 SMP Tue Dec 17 08:07:20 UTC 2024 x86_64 GNU/Linux
# 安装 go 编译工具链和 CGO 编译所需的 gcc
[root@d8e784ed1d22 myagent]# yum install -y go gcc
# 启用 CGO 的情况下,动态链接成功
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o myagent ./cmd/
[root@d8e784ed1d22 myagent]# ldd myagent
libresolv.so.2 => /lib64/libresolv.so.2 (0x00007ffffffb4000)
libc.so.6 => /lib64/libc.so.6 (0x00007fffffdc0000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffffffc9000)
# 启用 CGO 的情况下,静态链接失败,原因是系统缺少静态 glibc 库
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o myagent -ldflags="-linkmode external -extldflags '-static'" ./cmd/
# gitlab.mysite.com/paas/myagent/cmd
/usr/lib/golang/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/ld: cannot find -lresolv: No such file or directory
/usr/bin/ld: cannot find -lc: No such file or directory
collect2: error: ld returned 1 exit status
# 安装 glibc 静态库
[root@d8e784ed1d22 myagent]# yum install glibc-static
# 再次静态编译,可以生成二进制,但是有警告
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o myagent -ldflags="-linkmode external -extldflags '-static'" ./cmd/
# gitlab.mysite.com/paas/myagent/cmd
/usr/bin/ld: /tmp/go-link-16410204/000034.o: in function `unixDlOpen':
/root/go/pkg/mod/github.com/mattn/[email protected]/sqlite3-binding.c:44707:(.text+0x7719): 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-16410204/000001.o: in function `mygetgrgid_r':
/_/GOROOT/src/os/user/cgo_lookup_cgo.go:45:(.text+0x40): warning: Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-16410204/000001.o: in function `mygetgrnam_r':
/_/GOROOT/src/os/user/cgo_lookup_cgo.go:54:(.text+0xe1): warning: Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-16410204/000002.o: in function `mygetgrouplist':
/_/GOROOT/src/os/user/getgrouplist_unix.go:15:(.text+0x1e): warning: Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-16410204/000007.o: in function `_cgo_97ab22c4dc7b_C2func_getaddrinfo':
/tmp/go-build/cgo_unix_cgo.cgo2.c:60:(.text+0x33): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-16410204/000001.o: in function `mygetpwnam_r':
/_/GOROOT/src/os/user/cgo_lookup_cgo.go:36:(.text+0x186): warning: Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/bin/ld: /tmp/go-link-16410204/000001.o: in function `mygetpwuid_r':
/_/GOROOT/src/os/user/cgo_lookup_cgo.go:27:(.text+0x235): warning: Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
上面静态编译打印的警告中,以 /_/GOROOT/src/os/user/
开头的部分来源于 Go 标准库的 os/user
package,这是由于 Go 为这个 package(还有 net package)同时提供了 Go 实现和 CGO 实现,在启用 CGO 编译参数的情况下会使用 CGO 实现,CGO 实现会动态链接系统 glibc 库来镜像域名解析和获取用户信息(如上面警告中的 getaddrinfo 是域名解析, getgrname_r 是获得用户 group 名称)。
对于这两个 package,可以通过 go build tags 机制来切换到纯 Go 实现:为 go build 添加 -tags ‘osusergo netgo’ 。
再次静态编译,只剩下一条 sqlite3 相关警告,这是由于 sqlite3 使用了 dlopen 函数,它必须使用动态 glibc 库:
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags 'osusergo netgo' -o myagent -ldflags="-linkmode external -extldflags '-static'" ./cmd/
# gitlab.mysite.com/paas/myagent/cmd
/usr/bin/ld: /tmp/go-link-3263404225/000011.o: in function `unixDlOpen':
/root/go/pkg/mod/github.com/mattn/[email protected]/sqlite3-binding.c:44707:(.text+0x7719): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
查看 go-sqlite3 项目的 sqlite3-binding.c 源码:
// https://raw.githubusercontent.com/mattn/go-sqlite3/refs/heads/master/sqlite3-binding.c
#ifndef SQLITE_OMIT_LOAD_EXTENSION
/*
** Open the dynamic library located at zPath and return a handle.
*/
static void *rbuVfsDlOpen(sqlite3_vfs *pVfs, const char *zPath){
sqlite3_vfs *pRealVfs = ((rbu_vfs*)pVfs)->pRealVfs;
return pRealVfs->xDlOpen(pRealVfs, zPath);
}
可见:在没有定义 SQLITE_OMIT_LOAD_EXTENSION 宏时,会使用 dlopen 动态链接特性。
查看 go-sqlite3 项目源码 sqlite3_load_extension_omit.go,当为 go build 指定了 sqlite_omit_load_extension tag 时会定义这个宏:
// https://github.com/mattn/go-sqlite3/blob/master/sqlite3_load_extension_omit.go
//
//go:build sqlite_omit_load_extension
// +build sqlite_omit_load_extension
package sqlite3
/*
#cgo CFLAGS: -DSQLITE_OMIT_LOAD_EXTENSION
*/
import "C"
所以,解决办法是在 go build 时添加 sqlite_omit_load_extension tag,这样编译 sqlite3 C 源码时将不再启用它依赖的动态链接特性。
再次编译,只剩下 go plugin 相关的 dlopen 警告:
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags 'osusergo netgo sqlite_omit_load_extension' -o myagent -ldflags="-linkmode external -extldflags '-static'" ./cmd/
# gitlab.mysite.com/cmd
/usr/bin/ld: /tmp/go-link-3794635646/000028.o: in function `pluginOpen':
/_/GOROOT/src/plugin/plugin_dlopen.go:19:(.text+0x82): warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
go plugin 是 Go 提供的插件机制,查看源码 plugin/plugin_dlopen.go 文件可知,在构建 Linux 系统二进制和启用 CGO 的情况下,会进行动态链接( LDFALGS 中添加了 -dl),所以编译静态二进制时有警告:
// https://cs.opensource.google/go/go/+/refs/tags/go1.22.9:src/plugin/plugin_dlopen.go;l=5
//
//go:build (linux && cgo) || (darwin && cgo) || (freebsd && cgo)
package plugin
/*
#cgo linux LDFLAGS: -ldl
#include <dlfcn.h>
#include <limits.h>
#include <stdlib.h>
#include <stdint.h>
使用 go mod why plugin
命令来查看项目为何会依赖 plugin package:
[root@d8e784ed1d22 myagent]# go mod why plugin
# plugin
gitlab.mysite.com/paas/myagent/pkg/containers/cri
github.com/containerd/containerd
github.com/containerd/containerd/plugin
plugin
可见,项目使用的 containerd/plugin
package 使用了 go 的 plugin 库,所以问题聚焦在关闭 containerd 使用 Go plugin 的特性。
搜索 github.com/containerd/containerd 项目 issue 和源码,最终发现较新的 1.7.x 版本提供了 no_dynamic_plugins tag 来关闭 Go plugin 支持:https://github.com/containerd/containerd/pull/11203
所以解决方案是:升级 continerd 到最新版本(v1.7.25),go build 时添加 no_dynamic_plugins tag。
再次编译,静态编译链接成功:
[root@d8e784ed1d22 myagent]# go get github.com/conptainerd/containerd@latest
go: downloading github.com/containerd/containerd v1.7.25
[root@d8e784ed1d22 myagent]# GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags 'osusergo netgo sqlite_omit_load_extension no_dynamic_plugins' -buildmode=exe -o myagent -ldflags=" -extldflags '-static'" ./cmd/
[root@d8e784ed1d22 myagent]# ldd /tmp/myagent
not a dynamic executable
总结: Go CGO 静态链接需要看情况 #
在启用 CGO 的情况下,可以为 go build 添加 -tags ‘osusergo netgo’ 参数来使用 os/user 和 net 包的纯 Go 实现(默认是 CGO 实现),这样 Go 的标准库可以实现完全静态链接。
但是当 Go 程序还使用了其它 CGO 三方库,如上面的 go-sqlite3,则需要先确保该库本身能实现静态编译链接,而这个过程,可能需要查看库的源码、issue 和文档来解决。