静态和动态链接Go的可移植性

时间:2019-01-18 10:39:31

标签: linux go linker glibc elf

让我首先从桌子上拿一些事实进行事实检查,以免造成混乱:

  • 带有动态节的ELF二进制文件将被编译,其中一些符号未解析。解决方案将在二进制文件执行期间的某个时候由链接程序执行。
  • 动态链接各有利弊。但是,如果系统上没有二进制文件所需的目标库(所需版本),则二进制文件将无法运行。
  • 静态链接可以缓解此问题,但会在较低层引入一个新的链接。通过静态链接二进制文件,库的可执行代码已嵌入到您的二进制文件中,因此二进制库接口不再存在问题。但是,现在库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会很困难。
  • 最后,我听说“不能保证不同发行版甚至同一发行版的不同版本都兼容ABI”。这是真的?我假设ABI主要是二进制可执行格式(在Linux上相当长的一段时间里是ELF),所以在这里我不会有任何问题。这意味着什么?

谢谢!

1 个答案:

答案 0 :(得分:1)

在GNU / Linux上,几乎所有Go可执行文件都属于以下类别:

  1. 其中包括应用程序,Go运行时以及glibc(部分)的静态链接副本。
  2. 那些仅包含应用程序和Go运行时,静态链接而没有glibc的文件。
  3. 那些只包括应用程序和Go运行时的对象,它们是静态链接的,并且是动态链接到glibc的。

不幸的是,与Go相关的工具常常会使两者混为一谈。 glibc依赖性的主要原因是应用程序使用主机名和用户查找(诸如getaddrinfogetpwuid_r之类的功能)。 CGO_ENABLED=0src/os/user/cgo_lookup_unix.go(使用glibc)之类的实现切换到src/os/user/lookup_unix.go(不使用glibc)。非glibc实现不使用NSS,因此提供的功能有些有限(通常不会影响不在LDAP / Active Directory中存储用户信息的用户)。

根据您的情况,设置CGO_ENABLED=0会将您的应用程序从第三类移动到第二类。 (还有其他与Go相关的工具可以构建第一种应用程序。)非NSS查找代码不是很大,因此二进制大小的增加并不明显。由于Go运行时已经是静态链接的,因此静态链接减少的开销甚至有可能导致可执行文件大小的净减少。

这里要考虑的最重要的问题是,在glibc中,NSS,线程和静态链接并不是很好。所有Go程序都是多线程的,将glibc(静态)链接到Go程序的原因正是访问NSS函数。因此,针对glibc静态链接Go程序始终是错误的选择。它基本上是always buggy。即使Go程序不是多线程的,使用NSS函数的静态链接程序也需要在运行时使用与构建时使用的glibc完全相同的版本,因此此类应用程序减少可移植性。

所有这些都是为什么第一种Go应用程序如此糟糕的原因。使用CGO_ENABLED=0生成静态链接的应用程序不会出现这些问题,因为那些(第二类)应用程序不包含任何glibc代码(以减少用户/主机查找功能的功能为代价)。

如果要创建需要glibc的可移植二进制文件,则需要在具有要支持的最旧glibc的系统上动态地链接应用程序(emem)(第三种)。然后,该应用程序将在该glibc版本和所有更高版本上运行(目前为Go does not link libc correctly,因此即使对于glibc,也没有强大的兼容性保证)。发行版通常与ABI兼容,但是它们具有glibc的不同版本。 glibc竭尽全力确保与旧版本glibc动态链接的应用程序将继续在新版本的glibc上运行,但是反之则不成立:一旦在某个版本的glibc上链接了应用程序,它可能会使用某些功能(符号)在较早版本上不可用,因此该应用程序将无法在那些较早版本上使用。