【问题标题】:What's the difference between "statically linked" and "not a dynamic executable" from Linux ldd?Linux ldd的“静态链接”和“非动态可执行文件”有什么区别?
【发布时间】:2020-08-16 14:33:15
【问题描述】:

考虑这个 AMD64 汇编程序:

.globl _start
_start:
    xorl %edi, %edi
    movl $60, %eax
    syscall

如果我用gcc -nostdlib 编译它并运行ldd a.out,我会得到这个:

        statically linked

如果我改为使用 gcc -static -nostdlib 编译它并运行 ldd a.out,我会得到:

        not a dynamic executable

statically linkednot a dynamic executable 有什么区别?如果我的二进制文件已经静态链接,为什么添加 -static 会影响任何事情?

【问题讨论】:

  • 好问题; PIE 与非 PIE 可执行文件引入了多种方法来获得静态可执行文件,并且有很多不明显的东西需要指出。似乎是编写有关这些区别和 GCC / ld 行为的规范答案的好地方。

标签: linux gcc x86-64 static-linking ldd


【解决方案1】:

这里有两个不同的东西:

  • 是否请求 ELF 解释器 (ld.so)。
    #!/bin/sh 类似,但对于二进制文件,在您的 _start 之前运行。
    这是静态与动态可执行文件之间的区别。
  • 要加载的 ld.so 动态链接库列表恰好是空的。
    这显然是 ldd 所称的“静态链接”,即您可能在构建时链接的任何库都是静态库。

filereadelf 等其他工具可提供更多信息并使用符合您期望的术语。


您的 GCC 是 configured so -pie is the default,而 gcc 不会为没有动态库的特殊情况制作静态饼图。

  • gcc -nostdlib 只是创建了一个 PIE,它碰巧没有链接到任何库,但在其他方面与普通 PIE 相同,指定了 ELF 解释器。
    ldd 混淆地称之为“静态链接”。
    file : ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 ...
  • gcc -nostdlib -static 覆盖 -pie 默认值并生成真正的静态可执行文件。
    file : ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked ...
  • gcc -nostdlib -no-pie 还选择制作静态可执行文件,作为根本没有动态库的情况的优化。由于非 PIE 可执行文件无论如何都不可能被 ASLRed,所以这是有道理的。逐个字节与-static 的情况相同。
  • gcc -nostdlib -static-pie 生成不需要 ELF 解释器的 ASLRable 可执行文件。默认情况下,GCC 不会对 gcc -pie -nostdlib 执行此操作,这与在不涉及动态链接库时选择回避 ld.so 的非派情况不同。
    file : ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), statically linked ...

    -static-pie 晦涩难懂,很少使用,而且较旧的file 不会将其识别为静态链接。

-nostdlib 并不意味着 -no-pie-static,必须明确指定 -static-pie 才能得到它。

gcc -static-pie 调用ld -static -pie,所以ld 必须知道这意味着什么。与不需要显式请求动态可执行文件的非 PIE 情况不同,如果您传递 ld 任何 .so 库,您只会得到一个。我认为这就是为什么您碰巧从 gcc -nostdlib -no-pie 获得静态可执行文件的原因 - GCC 不需要做任何特别的事情,只需 ld 进行优化。

ld 不会在指定-pie 时隐式启用-static,即使没有要链接的共享库也是如此。


详情

使用gcc --version gcc (Arch Linux 9.3.0-1) 9.3.0 生成的示例
ld --version GNU ld (GNU Binutils) 2.34 (readelf 也是 binutils)
ldd --version ldd (GNU libc) 2.31
file --version file-5.38 - 请注意,静态饼图检测在最近的补丁中发生了变化,Ubuntu 挑选了一个未发布的补丁。 (感谢@Joseph 的侦探工作)-this in 2019 检测到动态 = 有一个 PT_INTERP 来处理静态饼图,但基于 PT_DYNAMIC 进行检测是reverted,因此共享库算作dynamicdebian bug #948269static-pie 是一个不起眼的很少使用的功能。

GCC 最终运行 ld -pie exit.o 并指定了动态链接器路径,并且没有库。 (还有很多其他选项来支持可能的 LTO 链接时间优化,但这里的关键是 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -piecollect2 只是 ld 的包装。)

$ gcc -nostdlib exit.s -v      # output manually line wrapped with \ for readability
...
COLLECT_GCC_OPTIONS='-nostdlib' '-v' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/collect2  \
-plugin /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/liblto_plugin.so \
-plugin-opt=/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/lto-wrapper \
-plugin-opt=-fresolution=/tmp/ccoNx1IR.res \
--build-id --eh-frame-hdr --hash-style=gnu \
-m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0 \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/../../../../lib -L/lib/../lib \
-L/usr/lib/../lib \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/../../.. \
/tmp/cctm2fSS.o

您将获得一个不依赖于其他库的动态 PIE。运行它仍然会调用它上面的“ELF 解释器”/lib64/ld-linux-x86-64.so.2,它会在跳转到您的_start 之前运行。 (虽然内核已经将可执行文件的 ELF 段映射到 ASLRed 虚拟地址,连同 ld.so 的 text/data/bss)。

file 和 readelf 更具描述性。

来自gcc -nostdlib 的 PIE 非静态可执行文件

$ gcc -nostdlib exit.s -o exit-default
$ ls -l exit-default 
-rwxr-xr-x 1 peter peter 13536 May  2 02:15 exit-default 
$ ldd exit-default 
        statically linked
$ file exit-default
exit-default: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=05a4d1bdbc94d6f91cca1c9c26314e1aa227a3a5, not stripped

$ readelf -a exit-default
...
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1000
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000001f8 0x00000000000001f8  R      0x8
  INTERP         0x0000000000000238 0x0000000000000238 0x0000000000000238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000002b1 0x00000000000002b1  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000009 0x0000000000000009  R E    0x1000
  ...   (the Read+Exec segment to be mapped at virt addr 0x1000 is where your text section was linked.)

如果你strace它,你也可以看到差异:

$ gcc -nostdlib exit.s -o exit-default
$ strace ./exit-default
execve("./exit-default", ["./exit-default"], 0x7ffe1f526040 /* 51 vars */) = 0
brk(NULL)                               = 0x5617eb1e4000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffcea703380) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9ff5b3e000
arch_prctl(ARCH_SET_FS, 0x7f9ff5b3ea80) = 0
mprotect(0x5617eabac000, 4096, PROT_READ) = 0
exit(0)                                 = ?
+++ exited with 0 +++

对比-static-static-pie 在用户空间中执行的第一条指令是您的 _start(您也可以使用 starti 与 GDB 进行检查)。

$ strace ./exit-static-pie 
execve("./exit-static-pie", ["./exit-static-pie"], 0x7ffcdac96dd0 /* 51 vars */) = 0
exit(0)                                 = ?
+++ exited with 0 +++

gcc -nostdlib -static-pie

$ gcc -nostdlib -static-pie exit.s -o exit-static-pie
$ ls -l exit-static-pie
-rwxr-xr-x 1 peter peter 13440 May  2 02:18 exit-static-pie
peter@volta:/tmp$ ldd exit-static-pie
        statically linked
peter@volta:/tmp$ file exit-static-pie
exit-static-pie: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=daeb4a8f11bec1bb1aaa13cd48d24b5795af638e, not stripped

$ readelf -a exit-static-pie 
...
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1000
...

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000229 0x0000000000000229  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x0000000000000009 0x0000000000000009  R E    0x1000
  ... (no Interp header, but still a read+exec text segment)

请注意,地址仍然是相对于映像库的,将 ASLR 留给内核。

令人惊讶的是,ldd 并没有说它不是动态可执行文件。这可能是一个错误,或者是某些实现细节的副作用。


gcc -nostdlib -static 传统的非 PIE 老式静态可执行文件

$ gcc -nostdlib -static exit.s -o exit-static
$ ls -l exit-static
-rwxr-xr-x 1 peter peter 4744 May  2 02:26 exit-static
peter@volta:/tmp$ ldd exit-static
        not a dynamic executable
peter@volta:/tmp$ file exit-static
exit-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1b03e3d05709b7288fe3006b4696fd0c11fb1cb2, not stripped
peter@volta:/tmp$ readelf -a exit-static
ELF Header:
...
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x401000
...   (Note the absolute entry-point address nailed down at link time)
      (And that the ELF type is EXEC, not DYN)

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x000000000000010c 0x000000000000010c  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x0000000000000009 0x0000000000000009  R E    0x1000
  NOTE           0x00000000000000e8 0x00000000004000e8 0x00000000004000e8
                 0x0000000000000024 0x0000000000000024  R      0x4

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.build-id 
   01     .text 
   02     .note.gnu.build-id 
   ...

这些都是程序头;与 pie / static-pie 不同,我没有遗漏任何内容,只是 readelf -a 输出的其他整个部分。

还要注意程序头中的绝对虚拟地址,它不会让内核选择在虚拟地址空间中映射文件的位置。这是 ELF 对象的 EXEC 和 DYN 类型之间的区别。 PIE 可执行文件是具有入口点的共享对象,允许我们获取主可执行文件的 ASLR。实际的 EXEC 可执行文件具有链接时间选择的内存布局。


ldd 显然只在两者都报告“不是动态可执行文件”时:

  • 没有 ELF 解释器(动态链接器)路径
  • ELF 类型 = 执行

【讨论】:

  • 是的,就是这样! Ubuntu 的 file 误认了它。在容器中,Arch 的file 称它为静态链接,但是当我将二进制文件复制出来时,Ubuntu 的file 称它为动态链接。
  • 我尝试从 Ubuntu 上的最新源代码构建 file 以使其工作,但在那里也没有工作。经过一番调查,事实证明它可以在最新版本的file 中运行,但a commit to git master since then 再次破坏了它(因为显然它搞砸了其他东西12),Ubuntu 樱桃挑选了那个补丁,但 Arch 没有。
  • @JosephSible-ReinstateMonica:有趣且出色的侦探工作:P。 PIE 可执行文件是 ELF 共享对象;它基本上是一个带有入口点的共享库。因此,没有进一步依赖于其他共享库(并且没有 ELF 解释器)的实际共享库可以被试图将静态饼图检测为静态链接的代码检测为“静态链接”是有道理的。不同之处在于不可执行的共享库没有入口点。
  • (或者会不会:如果它包括一个自检功能作为_start,它可以运行,同时仍然是其他程序可以链接到的普通共享库。我记得读过一些东西在 PIE 成为现实之前,关于在共享库中拥有 ELF 入口点的可能性。这种以前非官方的功能是 PIE 在实现 ASLR 的好处时建立在其之上的。)
  • 是的,github.com/file/file/commit/FILE5_37-59-g24c9c086 通过将linking_style = "dynamically"; 移动到它所属的case PT_INTERP: 来解决这个问题。
猜你喜欢
  • 1970-01-01
  • 2011-05-08
  • 2016-08-29
  • 2011-06-07
  • 2014-02-08
  • 2013-04-25
  • 2011-04-23
  • 2014-07-29
  • 2018-11-23
相关资源
最近更新 更多