静态库和动态库之间的差异忽略了它们如何被 linker/loader 使用

Differences between static libraries and dynamic libraries ignoring how they are used by the linker/loader

我了解 linker/loader 如何使用 static/dynamic 库。

  1. 然而,为什么不使用一种带有编译器标志的库文件类型来指示库应该如何链接(静态与动态)?
  2. 根据我们确实拥有静态和动态库这一简单事实,我推测这些文件具有分别启用静态和动态链接的特定内容。有人可以阐明静态库文件和共享库文件内容之间的区别吗?
  1. 在 运行 时加载的动态库的格式由操作系统编写者决定。静态库的格式由工具链编写者设置。 类 程序员之间通常会有一些重叠,但他们倾向于保持关注点分离。
  2. 运行-time loader 需要知道要加载的图像的大小,可能是一些堆栈和数据段的大小以及 DLL 中函数的名称和入口点。链接器需要更多地了解静态库中归档的每个对象 (functions/data)。诸如函数签名、数据类型、事物大小、初始化、访问范围之类的东西。

每个 OS 和工具链都有自己的特定要求和修订历史,因此在这里描述所有这些工具的确切文件布局是不切实际的。有关详细信息,请参阅 OS 和工具链文档。

可共享库倾向于编译成与位置无关的代码。这意味着,例如,控制转移使用 PC 相对寻址。这是因为它们可能被映射到不同客户端程序的地址空间中的不同位置。

可共享库应明确区分只读代码+数据和 read/write 数据。这样,只需要将只读部分的单个副本加载到所有客户端进程的内存中。

如果客户端程序 A 引用库 B 和 C,而库 B 也引用 C,那么您必须确保所有内容都是使用一致的共享性设置构建的。例如,如果 C 是在可共享和不可共享版本中构建的,而 B 是针对 C 的可共享版本构建的可共享版本,那么 A 也必须针对 B 和 C 的可共享版本构建。如果它试图使用不可共享版本C 的版本,这可能会导致构建问题,因为与 B 正在使用的 C 的可共享版本冲突。

This document 作者 Ulrich Drepper(GNU libc 库的前维护者)比你想知道的关于可共享库的更多细节。

因为它们是完全不同的东西。静态库只是编译器生成的目标代码的集合。动态库是链接的。

可惜静态库动态库这两个词是 两者都是 ADJECTIVE library 形式,因为它永远引领着程序员 认为它们表示本质上相同的事物的变体。 这几乎与认为羽毛球场和最高法院一样具有误导性 本质上是同一种东西。其实误导性更大 因为实际上没有人会因为认为羽毛球场和最高 法院本质上是一回事。

Can someone throw some light on the differences between the contents of static and shared library files?

让我们举个例子。反击羽毛球场/最高法院迷雾 我将使用更准确的技术术语。而不是 static library 我会说 ar archive 而不是 dynamic library我会说 动态共享对象,简称DSO

什么是 ar 存档

我将从这三个文件开始创建一个 ar 存档:

foo.c

#include <stdio.h>

void foo(void)
{
    puts("foo");
}

bar.c

#include <stdio.h>

void bar(void)
{
    puts("bar");
}

limerick.txt

There once was a young lady named bright
Whose speed was much faster than light
She set out one day
In a relative way
And returned on the previous night.

我会将这两个 C 源代码编译成与位置无关的目标文件:

$ gcc -c -Wall -fPIC foo.c
$ gcc -c -Wall -fPIC bar.c

没有必要使用 ar 归档文件来编译目标文件 -fPIC。我只希望这些编译成那样。

然后我将创建一个名为 libsundry.aar 存档,其中包含目标文件 foo.obar.o, 加上 limerick.txt:

$ ar rcs libsundry.a foo.o bar.o limerick.txt

创建一个ar存档,当然,ar, GNU 通用存档器。所以它不是linker创造的。没有link年龄 发生。以下是 ar 报告存档内容的方式:

$ ar -t libsundry.a 
foo.o
bar.o
limerick.txt

存档中的打油诗是这样的:

$ rm limerick.txt
$ ar x libsundry.a limerick.txt; cat limerick.txt
There once was a young lady named bright
Whose speed was much faster than light
She set out one day
In a relative way
And returned on the previous night.

Q. 将两个目标文件和一个 ASCII 打油诗放入同一个 ar 存档有什么意义?

A.为了证明我可以。显示 ar 档案是 只是一包文件

让我们看看 filelibsundry.a.

做了什么
$ file libsundry.a 
libsundry.a: current ar archive

现在我将编写几个在 link 年龄使用 libsundry.a 的程序。

fooprog.c

extern void foo(void);

int main(void)
{
    foo();
    return 0;
}

编译,link和运行那个:

$ gcc -c -Wall fooprog.c
$ gcc -o fooprog fooprog.o -L. -lsundry
$ ./fooprog 
foo

太棒了。 linker 显然没有被 libsundry.a.

中的 ASCII 打油诗

原因是 linker 甚至没有 尝试 link limerick.txt 进入程序。让我们再做一次 linkage,这次有一个诊断选项 这将向我们准确显示哪些输入文件是 linked:

$ gcc -o fooprog fooprog.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
fooprog.o
(./libsundry.a)foo.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

那里有很多默认库和对象文件,但唯一的对象 我们创建的文件 linker 消耗的是:

fooprog.o
(./libsundry.a)foo.o

linker 对 ./libsundry.a 所做的一切都是 foo.o 包和 link 它在程序中 。 linking fooprog.o 进入程序后, 它需要找到 foo 的定义。 它在袋子里看了看。它在 foo.o 中找到了定义,所以它从 foo.o 袋子并 link 在程序中编辑它。在 linking fooprog,

gcc -o fooprog fooprog.o -L. -lsundry

与link年龄完全相同:

$ gcc -o fooprog fooprog.o foo.o

filefooprog 说了什么?

$ file fooprog
fooprog: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), \
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, \
for GNU/Linux 2.6.32, BuildID[sha1]=32525dce7adf18604b2eb5af7065091c9111c16e,
not stripped

这是我的第二个程序:

foobarprog.c

extern void foo(void);
extern void bar(void);

int main(void)
{
    foo();
    bar();
    return 0;
}

编译,link和运行:

$ gcc -c -Wall foobarprog.c
$ gcc -o foobarprog foobarprog.o -L. -lsundry
$ ./foobarprog 
foo
bar

这里又是link年龄 -trace:

$ gcc -o foobarprog foobarprog.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
foobarprog.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

所以这一次,linker 使用的目标文件是:

foobarprog.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o

在 linking foobarprog.o 进入程序后,它需要找到 foobar 的定义。 它在袋子里看了看。它分别在 foo.obar.o 中找到了定义,所以它从 袋子并 link 在程序中编辑它们。在 linking foobarprog,

gcc -o foobarprog foobarprog.o -L. -lsundry

与link年龄完全相同:

$ gcc -o foobarprog foobarprog.o foo.o bar.o

总结一下。 ar 存档 只是一包文件 。您可以使用 一个 ar 存档,为 linker 提供一堆 object 文件,从中 选择它需要继续 linkage 的那些。它将获取那些目标文件 从包中取出并将 link 它们放入输出文件中。它绝对没有其他 用于袋子。这个包对link年龄没有任何贡献。

这款包让您无需了解,让您的生活更简单 特定 linkage 所需的目标文件。你只需要 要知道:嗯,他们在那个袋子里

什么是 DSO

我们来做一个吧。

foobar.c

extern void foo(void);
extern void bar(void);

void foobar(void)
{
    foo();
    bar();
}

我们将编译这个新的源文件:

$ gcc -c -Wall -fPIC foobar.c

然后使用 foobar.o 制作 DSO 并重新使用 libsundry.a

$ gcc -shared -o libfoobar.so foobar.o -L. -lsundry -Wl,-trace
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbeginS.o
foobar.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtendS.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

这使得 DSO libfoobar.so。注意:一个DSO是由linker制作的。它 linked 就像程序 linked 一样。 libfoopar.so的link年龄看起来很 类似于 foobarprog 的 linkage,但增加了选项 -shared 指示 linker 生成 DSO 而不是程序。在这里我们看到我们的对象 linkage 使用的文件是:

foobar.o
(./libsundry.a)foo.o
(./libsundry.a)bar.o

ar根本不懂DSO:

$ ar -t libfoobar.so 
ar: libfoobar.so: File format not recognised

但是 file 会:

$ file libfoobar.so 
libfoobar.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), \
dynamically linked, BuildID[sha1]=16747713db620e5ef14753334fea52e71fb3c5c8, \
not stripped

现在如果我们重新[​​=300=] foobarprog 使用 libfoobar.so 而不是 libsundry.a:

$ gcc -o foobarprog foobarprog.o -L. -lfoobar -Wl,-trace,--rpath=$(pwd)
/usr/bin/ld: mode elf_x86_64
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o
/usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o
foobarprog.o
-lfoobar (./libfoobar.so)
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/lib/x86_64-linux-gnu/libc.so.6
(/usr/lib/x86_64-linux-gnu/libc_nonshared.a)elf-init.oS
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
-lgcc_s (/usr/lib/gcc/x86_64-linux-gnu/5/libgcc_s.so)
/usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o

我们看到了

foobarprog.o
-lfoobar (./libfoobar.so)

./libfoobar.so 本身 被 link 编辑。不是一些目标文件 "inside it"。那里 里面没有任何目标文件。这怎么有 贡献了linkage可以在程序的动态依赖中看到:

$ ldd foobarprog
    linux-vdso.so.1 =>  (0x00007ffca47fb000)
    libfoobar.so => /home/imk/develop/so/scrap/libfoobar.so (0x00007fb050eeb000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb050afd000)
    /lib64/ld-linux-x86-64.so.2 (0x000055d8119f0000)

程序已经出来 运行时间依赖于 libfoobar.so。这就是 linkDSO 所做的。 我们可以看到这个 运行 时间依赖性得到满足。所以程序会 运行:

$ ./foobarprog
foo
bar

和以前一样。

事实是 DSO 和程序——不像 ar 存档——都是产品 linker 认为 DSO 和程序 本质上是同一类事物的变体。 file 输出也表明了这一点。 DSO 和程序都是 ELF 二进制文件 OS 加载程序可以映射到进程地址 space。不仅仅是一袋文件。 ar 存档不是任何类型的 ELF 二进制文件。

程序型ELF文件和非程序型ELF文件的区别在于取值不同 linker 写入 ELF Header 结构和 Program Headers ELF文件格式的结构。这些差异指示 OS 加载程序 当它加载一个程序类型的 ELF 文件时启动一个新进程,并增加 它在加载非程序 ELF 文件时正在构建的进程。因此 非程序 DSO 被映射到其父程序的进程中。一个程序的事实 启动一个新进程要求程序应具有单一默认入口点 OS 将控制权传递给它:该入口点是必需的 main 函数 在 C 或 C++ 程序中。另一方面,非程序 DSO 不需要单一的强制入口点。它可以通过它通过函数调用导出的任何全局函数输入 父程序。

但是从文件结构和内容来看,一个DSO和一个程序 是非常相似的东西。它们是可以作为流程组件的文件。 程序必须是初始组件。 DSO 可以是次要组件。

进一步区分仍然很常见:DSO必须完全由 可重定位代码(因为在 link 时间不知道加载程序可能需要 将它放在进程地址 space) 中,而程序由绝对代码组成, 始终加载在同一地址。但实际上它很有可能 link 一个可重定位的 程序:

$ gcc -pie -o foobarprog foobarprog.o -L. -lfoobar -Wl,--rpath=$(pwd)

这就是 -pie(与位置无关的可执行文件)在这里所做的。然后:

$ file foobarprog
foobarprog: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), ....

file 会说 foobarprog 是一个 DSO,它确实是,虽然它是 还是一个程序:

$ ./foobarprog
foo
bar

PIE 可执行文件正在流行。在 Debian 9 和衍生发行版 (Ubuntu 17.04...) 中,GCC 工具链 默认构建 PIE 程序

如果您想了解 arELF 文件的详细知识 格式,这里是 details of the ar format 这里是 details of the ELF format.

why not have a single type of library file accompanied by compiler flags which indicate how the library should be linked (static vs dynamic)?

动态和静态的选择link年龄已经完全可以控制 命令行 linkage 选项,因此无需放弃 ar 档案或 DSO 或发明另一种 库来实现这一点。如果 linker 不能像现在这样使用 ar 存档, 那将是相当大的不便。当然,如果 linker 不能 link DSO 让我们回到石器时代的操作系统。