关注TechLead,复旦博士,分享云服务领域全维度开发技术。拥有10+年互联网服务架构、AI产品研发经验、团队管理经验,复旦机器人智能实验室成员,国家级大学生赛事评审专家,发表多篇SCI核心期刊学术论文,阿里云认证的资深架构师,上亿营收AI产品研发负责人。
本文介绍了 Go 语言中静态链接和动态链接的概念,解释了它们的区别和各自优势。通过示例,展示了如何生成静态或动态链接的二进制文件,以及使用工具进行检查。文章还讨论了内部和外部链接器的区别,如何在编译时选择链接方式,以及在交叉编译时处理 cgo 的方法。最后,提到了减小二进制文件大小的技巧和安全性方面的考虑。
概述
Go 语言最大的优势之一就是它的编译器,它为程序员抽象了许多细节,让你可以轻松地为几乎任何平台和架构 https://pkg.go.dev/cmd/dist 编译你的程序。
尽管这看起来很简单,但其中有一些细微的差别,同一个程序有多种编译方式,这会导致生成不同的可执行文件。
在本文中,我们将探讨静态链接和动态链接的可执行文件、内部和外部链接器,并使用 file、ld、ldd 等工具检查二进制文件。
什么是静态链接和动态链接?
静态链接是将程序所需的所有库直接复制到最终可执行文件中的做法。
Go 语言非常喜欢并希望在可能的情况下这样做,因为这样生成的二进制文件更加便携,不需要在运行的主机系统上存在库。因此,你的二进制文件可以在任何系统上运行,无论是哪个发行版或版本,而且不依赖任何系统库。
另一方面,动态链接是在运行时按名称将外部或共享库加载到可执行文件中。
动态链接也有其自身的优势。例如,程序可以重用主机系统上可用的常用 libc 库,而不需要重新实现它们,你还可以在不重新链接程序的情况下受益于主机的更新。在许多情况下,它还可以减小可执行文件的大小。
静态链接的程序
让我们来看一个始终进行静态链接的程序。这个程序没有使用 cgo 调用 C 代码,因此所有内容都可以打包在一个静态二进制文件中。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
什么是二进制文件?
我们可以使用 file 命令首先检查文件类型。
它告诉我们这是一个 ELF(可执行和可链接格式)可执行文件,并且是“静态链接”的。
我们不会深入讨论 ELF 是什么,但需要知道的是还有其他可执行文件格式。ELF 是 Linux 上的默认格式,Mach-O 是 macOS 的默认格式,PE/PE32+ 是 Windows 的默认格式,等等。
注意:在本文中,我们将使用 Linux(Ubuntu)及其工具,然而在其他平台上也可以进行类似的操作。
还有一个 Linux 程序 ldd,可以告诉我们二进制文件是静态链接还是动态链接的。
$ ldd main
not a dynamic executable
动态链接的程序
如前所述,Go 有一个机制叫 cgo,可以从 Go 调用 C 代码,甚至 Go 的标准库在多个地方使用了它。例如,在 net 包中,它使用标准的 C 库来处理 DNS。
默认情况下,导入此类包或在代码中使用 cgo 会生成一个动态链接的二进制文件,链接到那些 libc 库。
我们可以再次使用 file 和 ldd 程序来检查第二个二进制文件。
file 命令现在显示这是一个动态链接的二进制文件,而 ldd 则显示了二进制文件的动态依赖关系。在这种情况下,它依赖于 libc.so.6 和 ld-linux,后者是 Linux 系统的动态链接器。
我们可以让它静态链接吗?
当你希望二进制文件静态链接时,可能有多种原因,但主要原因是为了简化部署和分发。然而,并不总是有必要这样做。通过链接 libc,你可以从主机更新中受益,并且在使用 net 包的情况下,可以利用 libc 中包含的复杂的 DNS 查找函数。
有趣的是,Go 的 net 包也有一个纯 Go 版本,这使得在编译时禁用 cgo 成为可能。你可以通过指定构建标签或使用 CGO_ENABLED=0 完全禁用 cgo 来实现。
上面的截图证明在这两种情况下,我们最终都得到了一个静态二进制文件。
内部链接器 vs 外部链接器
链接器是一个程序,它读取 main 包的 Go 存档或对象,以及它的依赖项,并将它们组合成一个可执行的二进制文件。
默认情况下,Go 的工具链使用其内部链接器(go tool link),但是你可以在编译时指定使用哪个链接器,这样可以让我们在获得静态二进制文件的同时,享受完整的 libc 功能。
在 Linux 上,默认的外部链接器是 gcc 的 ld。我们可以告诉它生成一个静态二进制文件。
它能工作,但我们会收到一个警告。在我们的例子中,glibc 使用 libnss 来支持多种地址解析服务提供者,而你无法静态链接 libnss。
其他使用 cgo 的包可能会产生类似的警告,你需要查看文档来判断它们是否严重。
交叉编译
如介绍中所述,交叉编译是 Go 的一个非常好的特性,它允许你为几乎任何平台/架构编译程序。然而,如果你的程序使用了 cgo,这可能会非常棘手,因为交叉编译 C 代码通常很困难。
你可以通过为目标操作系统和/或架构安装工具链来解决这个问题。
如果可能,最好在交叉编译时不要使用 cgo。你将得到静态链接的稳定二进制文件。
加分项:减小二进制文件大小
你可能注意到,上面 file 命令的输出包含:“not stripped”。这意味着我们的二进制文件中包含调试信息。但我们通常不需要它,删除它可以减小二进制文件的大小。
这将删除调试信息和符号表,减小二进制文件的大小。
当心:LD_PRELOAD 技巧
Linux 系统程序 ld-linux.so(动态链接器/加载器)使用 LD_PRELOAD 来加载指定的共享库。特别是,在加载任何其他库之前,动态加载器将首先加载 LD_PRELOAD 中的共享库。
LD_PRELOAD 技巧是在动态链接的二进制文件中使用的一种强大技术,用于覆盖或拦截对共享库的函数调用。
通过将 LD_PRELOAD 环境变量设置为指向自定义的共享对象文件,用户可以将自己的代码注入程序的执行中,从而有效地替换或增强现有的库函数。
这种方法允许各种应用,例如调试、测试,甚至在不修改原始源代码的情况下改变程序行为。
这也表明静态链接的二进制文件更安全,因为它们不存在这个问题,因为它们不依赖任何外部库。此外,还有一个“安全执行模式”——这是由 Linux 系统上的动态链接器实现的安全特性,用于在运行需要提升权限的程序时限制某些行为。