跳过正文

Go CGO 程序静态编译链接

·2347 字
Cgo Go Compile Gcc
目录

本文先介绍 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 版本兼容性问题的常用方案:

  1. 静态链接:将 glibc 等依赖库静态链接到可执行程序中,运行时不依赖系统 glibc 库;
  2. 手动打包:通过 tar 包等机制,将依赖的动态库和程序二进制一起打包,运行时通过 LD_LIBRARY 环境变量来注入包中的 glibc 等动态库;
  • 下载合适版本的 glibc 源码,进行编译,生成对应版本的 glibc 库;
  • 除了 LD_LIBRARY 外,也可以使用 patchelf –set-interpreter 命令来修改二进制的动态链接库路径。
  1. 容器镜像:和手动打包类似,但是打包到一个 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 和文档来解决。

相关文章

链接器 ld
··6277 字
Gnu Gcc Ld
使用 linux bpftrace 进行内核和应用性能分析
·9685 字
Bpftrace Kernel Ebpf Performance Tool
介绍 bpftrace 工具的使用方式、局限性和问题。
使用 linux perf 进行内核和应用性能分析
·5365 字
Perf Kernel Performance Tool
介绍 perf 工具的使用方式、局限性和问题。