c - 静态库和动态库之间的差异,忽略了链接器/加载器的使用方式

标签 c gcc compiler-construction static-linking dynamic-linking

我了解链接器/加载器如何使用静态/动态库。


但是,为什么不使用单一类型的库文件并带有指示该库应如何链接的编译器标志(静态与动态)呢?
通过简单的事实,我们确实拥有静态和动态库,我认为这些文件具有特定的内容,分别可以启用静态和动态链接。有人可以阐明静态库文件和共享库文件的内容之间的区别吗?

最佳答案

可惜静态库和动态库这两个词
这两种形式都是ADJECTIVE库,因为它永久地导致程序员
认为它们表示本质上相同的事物的变体。
这几乎与羽毛球法院和最高法院的想法一样具有误导性
本质上是同一种东西。实际上,它具有更大的误导性,
因为没有人真正想到羽毛球馆和最高法院
法院本质上是同一种东西。


有人可以阐明静态库文件和共享库文件的内容之间的区别吗?


让我们使用示例。推反羽毛球场/最高场雾
我将使用更准确的技术术语。我会说ar存档而不是静态库,而我会说动态库
动态共享对象,简称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。我只希望这些文件以这种方式编译。

然后,我将创建一个名为arlibsundry.a存档,其中包含目标文件foo.obar.o
limerick.txt

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


当然,会使用ar创建ar存档,
GNU通用存档器。因此,它不是由链接器创建的。没有联系
发生。以下是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.


问:将两个目标文件和一个ASCII原始文件放入同一个ar存档有什么意义?

答:表明我可以。要显示ar存档只是一袋文件。

让我们看看filelibsundry.a.构成的

$ file libsundry.a 
libsundry.a: current ar archive


现在,我将编写一些在链接中使用libsundry.a的程序。

fooprog

extern void foo(void);

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


编译,链接和运行该代码:

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


那太笨拙了。显然,链接器不受存在的困扰
libsundry.a中的ASCII打油诗。

这样做的原因是链接器甚至没有尝试链接limerick.txt
进入程序。让我们再次进行链接,这次使用诊断选项
这将向我们确切显示链接了哪些输入文件:

$ 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


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

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


链接器对./libsundry.a所做的全部操作使foo.o脱离了
包并在程序中将其链接。将fooprog.o链接到程序后,
它需要找到foo的定义。
它看着袋子。它在foo.o中找到了定义,因此将foo.o
袋子并将其链接到程序中。在链接fooprog时,

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


与以下链接完全相同:

$ 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


这是我的第二个程序:

foob​​arprog.c

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

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


编译,链接和运行:

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


这又是与-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


因此,这次,链接器消耗的目标文件为:

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


foobarprog.o链接到程序后,需要找到foobar的定义。
它看着袋子。它分别在foo.obar.o中找到定义,因此将其从
包并在程序中将它们链接起来。在链接foobarprog时,

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


与以下链接完全相同:

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


总结所有这些。 ar存档只是文件袋。您可以使用
ar归档文件,用于向链接器提供一堆目标文件,从中可以
选择继续链接所需的内容。它将带那些目标文件
从包中取出并将它们链接到输出文件。它绝对没有其他
用于袋子。袋子对联动毫无贡献。

无需担心,手提包使您的生活更加简单
确切地说,特定链接需要什么对象文件。你只需要
要知道:好吧,他们在那个袋子里。

什么是DSO

让我们做一个。

foob​​ar.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由链接器创建。它
就像程序被链接一样被链接。 libfoopar.so的链接看起来非常多
类似于foobarprog的链接,但是该选项的添加
-shared指示链接器生成DSO而不是程序。在这里,我们看到了我们的对象
链接消耗的文件为:

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


现在,如果我们使用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本身已链接。在其中没有一些目标文件。那里
里面没有任何目标文件。以及如何
可以在程序的动态依赖项中看到对链接有贡献的内容:

$ 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。这就是链接DSO的作用。
我们可以看到此运行时依赖关系得到满足。因此程序将运行:

$ ./foobarprog
foo
bar


和以前一样

DSO和程序(与ar存档不同)都是产品
链接器的注释表明DSO和程序是基本上相同类型的事物的变体。
file输出也表明了这一点。 DSO和程序都是ELF二进制文件
OS加载程序可以映射到进程地址空间。不只是一袋文件。
ar存档不是任何形式的ELF二进制文件。

程序类型的ELF文件和非程序类型的ELF的区别在于不同的值
链接器写入ELF标头结构和程序标头中
ELF文件格式的结构。这些差异指示OS加载程序
加载程序类型的ELF文件时启动一个新进程,并进行扩充
加载非程序ELF文件时正在构建的过程。从而
非程序DSO被映射到其父程序的进程中。一个程序的事实
启动一个新过程要求程序应具有单个默认入口点
操作系统将控制权传递给的对象:该入口点是必需的main功能
在C或C ++程序中。另一方面,非程序DSO不需要单个强制入口点。可以通过其导出的任何全局函数(通过来自
父程序。

但是从文件结构和内容的角度来看,DSO和程序
是非常相似的东西。它们是可以作为流程组成部分的文件。
程序必须是初始组件。 DSO可以是辅助组件。

进一步区分仍然很普遍:DSO必须完全由
可重定位代码(因为在链接时不知道加载器可能需要
将其放置在进程地址空间中),而程序则由绝对代码组成,
始终加载在同一地址。但实际上很有可能将可重定位链接
程序:

$ 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


为什么没有单一类型的库文件并带有编译器标志,
指出应该如何链接库(静态与动态)?


动态和静态链接之间的选择已经可以通过以下方式完全控制
命令行链接选项,因此无需放弃ar存档或DSO,也无需发明另一种
图书馆实现这一目标。如果链接器无法使用ar存档,
这将带来极大的不便。当然,如果链接器无法链接
DSO我们又回到了操作系统时代。

关于c - 静态库和动态库之间的差异,忽略了链接器/加载器的使用方式,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47116485/

相关文章:

c++ - 我怎样才能告诉 gcc 在不中断的情况下对 switch/case 语句发出警告(或失败)?

使用 Maven 编译器进行 JAVA 转换

c# - 为什么编译器决定 2.3 是 double 而不是十进制?

c - 在 C 程序中使用带有指针数组的 strcmp() 函数

c - 构建 C 代码的技巧/资源?

c - 我如何解决我的代码在linux mint上检查数据类型的大小(是带有gcc编译器的c语言)

javascript - 寻找转译器 : php to javascript

c - windows 2000平台如何处理win32服务

c - linux clone() 成功,但是 child 崩溃了

C++ Eclipse 生成无效的 exe