静态和动态链接 w.r.t。可移植性,在 Go 的上下文中
Static and dynamic linking w.r.t. portability, in the context of Go
首先让我在 table 上了解一些事实,以便对它们进行事实核查,以免造成混淆:
- 带有动态部分的 ELF 二进制文件在编译时会带有一些未解析的符号。解析将在二进制文件执行期间的某个时间由链接器执行。
- 动态链接有利也有弊。但是,如果您的二进制文件所需的目标库不存在于系统中(在所需版本中),则二进制文件不会 运行.
- 静态链接缓解了这个问题,但在下层引入了一个新的链接。通过静态链接二进制文件,库的 executable 代码嵌入到您的二进制文件中,因此二进制库接口不再有问题。但是,现在可以在 library-OS 接口上中断。那是对的吗?这里可能会出现什么问题?
现在让我们在 Go 的上下文中讨论这个问题。我注意到,如果我用 CGO_ENABLED=1 go build ...
构建一个二进制文件,我会得到一个带有动态部分的二进制文件:
david@x1 /tmp (git)-[master] % readelf -d rtloggerd.cgo1
Dynamic section at offset 0x7a6140 contains 19 entries:
Tag Type Name/Value
0x0000000000000004 (HASH) 0x914e40
0x0000000000000006 (SYMTAB) 0x915340
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x915100
0x000000000000000a (STRSZ) 570 (bytes)
0x0000000000000007 (RELA) 0x914a38
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0xba6000
0x0000000000000015 (DEBUG) 0x0
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000006ffffffe (VERNEED) 0x914de0
0x000000006fffffff (VERNEEDNUM) 2
0x000000006ffffff0 (VERSYM) 0x914d80
0x0000000000000014 (PLTREL) RELA
0x0000000000000002 (PLTRELSZ) 816 (bytes)
0x0000000000000017 (JMPREL) 0x914a50
0x0000000000000000 (NULL) 0x0
david@x1 /tmp (git)-[master] % ldd rtloggerd.cgo1
linux-vdso.so.1 (0x00007ffd9a972000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007fcb2853c000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fcb28378000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fcb2858a000)
另一方面,如果我 CGO_ENABLED=0 go build ...
时没有动态部分:
130 david@x1 /tmp (git)-[master] % readelf -d rtloggerd.cgo0
There is no dynamic section in this file.
- 这是否意味着库是静态链接的?我想是的,但大小差异在我的机器上可以忽略不计(大约 72 kB),这让我感到惊讶。
- 关于跨 Linux 系统的可移植性,哪个更好,为什么?
- Go的标准库是如何开展业务的?它实际上是在调用
libc
(在我的例子中是 glibc
)提供的 C 函数吗?我假设有一个本地系统调用接口。另一方面,我想在原生 Go 中重新实现整个 stdlib 会很困难。
- 终于听说有"no guarantee that different distros, or even different versions of the same distro, are ABI compatible"了。这是真的?我假设 ABI 主要是二进制 executable 格式(ELF 在 Linux 上使用了很长一段时间),所以我假设这里没有问题。这意味着什么?
谢谢!
在 GNU/Linux,几乎所有的 Go 可执行文件都属于这些类别:
- 那些包括应用程序、Go 运行 时间和 glibc(部分)的静态 linked 副本。
- 那些只包括应用程序和 Go 运行 时间、静态 linked 和 glibc 的 none。
- 那些仅包含应用程序和 Go 运行 时间的静态 linked,以及 link 动态 glibc。
不幸的是,与 Go 相关的工具经常混淆这些 linking 模式。 glibc 依赖的主要原因是应用程序使用主机名和用户查找(getaddrinfo
和 getpwuid_r
等函数)。 CGO_ENABLED=0
从 src/os/user/cgo_lookup_unix.go
(使用 glibc)等实现切换到 src/os/user/lookup_unix.go
(不使用 glibc)。非 glibc 实现不使用 NSS,因此提供的功能有限(通常不会影响未在 LDAP/Active 目录中存储用户信息的用户)。
对于您的情况,设置 CGO_ENABLED=0
会将您的应用程序从第三类移至第二类。 (还有其他与 Go 相关的工具可以构建第一种应用程序。)非 NSS 查找代码不是很大,因此二进制大小的增加并不显着。由于 Go 运行-time 已经静态 linked,静态 linking 减少的开销甚至可能导致可执行文件大小的净减少。
这里要考虑的最重要的问题是 NSS、线程和静态 linking 在 glibc 中并不是那么好。所有的 Go 程序都是多线程的,之所以(静态地)link glibc 进入 Go 程序,正是为了访问 NSS 函数。因此,针对 glibc 静态 linking Go 程序始终是错误的做法。基本上就是always buggy。即使 Go 程序不是多线程的,使用 NSS 函数的静态 linked 程序也需要在 运行 时使用 完全相同的 glibc 版本在构建时,因此此类应用程序的静态 linking 降低了 可移植性。
所有这些都是第一种 Go 应用程序如此糟糕的原因。使用 CGO_ENABLED=0
生成静态 linked 应用程序不存在这些问题,因为这些应用程序(第二类)不包含任何 glibc 代码(以 user/host 的功能减少为代价查找函数)。
如果你想创建一个需要 glibc 的可移植二进制文件,你需要 link 你的应用程序 动态地 (第三种),在最旧的系统上你想要支持的 glibc。然后应用程序将在该 glibc 版本和所有更高版本上 运行(目前,Go does not link libc correctly,因此即使对于 glibc 也没有强兼容性保证)。发行版通常与 ABI 兼容,但它们具有不同版本的 glibc。 glibc 竭尽全力确保针对旧版本 glibc 动态 linked 的应用程序将在 glibc 的新版本上保持 运行ning,但反之则不然:一旦你 link某个版本的 glibc 上的应用程序,它可能会选择旧版本上不可用的功能(符号),因此该应用程序将无法与那些旧版本一起使用。
首先让我在 table 上了解一些事实,以便对它们进行事实核查,以免造成混淆:
- 带有动态部分的 ELF 二进制文件在编译时会带有一些未解析的符号。解析将在二进制文件执行期间的某个时间由链接器执行。
- 动态链接有利也有弊。但是,如果您的二进制文件所需的目标库不存在于系统中(在所需版本中),则二进制文件不会 运行.
- 静态链接缓解了这个问题,但在下层引入了一个新的链接。通过静态链接二进制文件,库的 executable 代码嵌入到您的二进制文件中,因此二进制库接口不再有问题。但是,现在可以在 library-OS 接口上中断。那是对的吗?这里可能会出现什么问题?
现在让我们在 Go 的上下文中讨论这个问题。我注意到,如果我用 CGO_ENABLED=1 go build ...
构建一个二进制文件,我会得到一个带有动态部分的二进制文件:
david@x1 /tmp (git)-[master] % readelf -d rtloggerd.cgo1
Dynamic section at offset 0x7a6140 contains 19 entries:
Tag Type Name/Value
0x0000000000000004 (HASH) 0x914e40
0x0000000000000006 (SYMTAB) 0x915340
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000005 (STRTAB) 0x915100
0x000000000000000a (STRSZ) 570 (bytes)
0x0000000000000007 (RELA) 0x914a38
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0xba6000
0x0000000000000015 (DEBUG) 0x0
0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000006ffffffe (VERNEED) 0x914de0
0x000000006fffffff (VERNEEDNUM) 2
0x000000006ffffff0 (VERSYM) 0x914d80
0x0000000000000014 (PLTREL) RELA
0x0000000000000002 (PLTRELSZ) 816 (bytes)
0x0000000000000017 (JMPREL) 0x914a50
0x0000000000000000 (NULL) 0x0
david@x1 /tmp (git)-[master] % ldd rtloggerd.cgo1
linux-vdso.so.1 (0x00007ffd9a972000)
libpthread.so.0 => /usr/lib/libpthread.so.0 (0x00007fcb2853c000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fcb28378000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fcb2858a000)
另一方面,如果我 CGO_ENABLED=0 go build ...
时没有动态部分:
130 david@x1 /tmp (git)-[master] % readelf -d rtloggerd.cgo0
There is no dynamic section in this file.
- 这是否意味着库是静态链接的?我想是的,但大小差异在我的机器上可以忽略不计(大约 72 kB),这让我感到惊讶。
- 关于跨 Linux 系统的可移植性,哪个更好,为什么?
- Go的标准库是如何开展业务的?它实际上是在调用
libc
(在我的例子中是glibc
)提供的 C 函数吗?我假设有一个本地系统调用接口。另一方面,我想在原生 Go 中重新实现整个 stdlib 会很困难。 - 终于听说有"no guarantee that different distros, or even different versions of the same distro, are ABI compatible"了。这是真的?我假设 ABI 主要是二进制 executable 格式(ELF 在 Linux 上使用了很长一段时间),所以我假设这里没有问题。这意味着什么?
谢谢!
在 GNU/Linux,几乎所有的 Go 可执行文件都属于这些类别:
- 那些包括应用程序、Go 运行 时间和 glibc(部分)的静态 linked 副本。
- 那些只包括应用程序和 Go 运行 时间、静态 linked 和 glibc 的 none。
- 那些仅包含应用程序和 Go 运行 时间的静态 linked,以及 link 动态 glibc。
与 Go 相关的工具经常混淆这些 linking 模式。 glibc 依赖的主要原因是应用程序使用主机名和用户查找(getaddrinfo
和 getpwuid_r
等函数)。 CGO_ENABLED=0
从 src/os/user/cgo_lookup_unix.go
(使用 glibc)等实现切换到 src/os/user/lookup_unix.go
(不使用 glibc)。非 glibc 实现不使用 NSS,因此提供的功能有限(通常不会影响未在 LDAP/Active 目录中存储用户信息的用户)。
对于您的情况,设置 CGO_ENABLED=0
会将您的应用程序从第三类移至第二类。 (还有其他与 Go 相关的工具可以构建第一种应用程序。)非 NSS 查找代码不是很大,因此二进制大小的增加并不显着。由于 Go 运行-time 已经静态 linked,静态 linking 减少的开销甚至可能导致可执行文件大小的净减少。
这里要考虑的最重要的问题是 NSS、线程和静态 linking 在 glibc 中并不是那么好。所有的 Go 程序都是多线程的,之所以(静态地)link glibc 进入 Go 程序,正是为了访问 NSS 函数。因此,针对 glibc 静态 linking Go 程序始终是错误的做法。基本上就是always buggy。即使 Go 程序不是多线程的,使用 NSS 函数的静态 linked 程序也需要在 运行 时使用 完全相同的 glibc 版本在构建时,因此此类应用程序的静态 linking 降低了 可移植性。
所有这些都是第一种 Go 应用程序如此糟糕的原因。使用 CGO_ENABLED=0
生成静态 linked 应用程序不存在这些问题,因为这些应用程序(第二类)不包含任何 glibc 代码(以 user/host 的功能减少为代价查找函数)。
如果你想创建一个需要 glibc 的可移植二进制文件,你需要 link 你的应用程序 动态地 (第三种),在最旧的系统上你想要支持的 glibc。然后应用程序将在该 glibc 版本和所有更高版本上 运行(目前,Go does not link libc correctly,因此即使对于 glibc 也没有强兼容性保证)。发行版通常与 ABI 兼容,但它们具有不同版本的 glibc。 glibc 竭尽全力确保针对旧版本 glibc 动态 linked 的应用程序将在 glibc 的新版本上保持 运行ning,但反之则不然:一旦你 link某个版本的 glibc 上的应用程序,它可能会选择旧版本上不可用的功能(符号),因此该应用程序将无法与那些旧版本一起使用。