目标文件与库文件,为什么?


69

我了解编译的基础知识。将源文件编译为目标文件,然后链接程序将其链接到可执行文件中。这些目标文件由包含定义的源文件组成。

所以我的问题是:


  • 为什么我们有一个单独的库实现?.a .lib,.dll ...
  • 我可能会弄错,但是在我看来,.o文件本身与库是一样的东西吗?
  • 有人不能给您他们的某个声明(.h)的实现,您可以替换为该声明并将其链接成可执行相同功能但执行不同操作的可执行文件吗?

Answers:


73

从历史上看,一个目标文件完全或根本不链接到一个可执行文件(如今,有一些例外,例如函数级链接整个程序优化变得越来越流行),因此,如果使用一个目标文件的一个功能,则可执行文件会接收到全部其中。

为了使可执行文件更小且没有死代码,标准库被分为许多小目标文件(通常为数百个)。出于效率方面的考虑,拥有数百个小文件是非常不希望的:打开许多文件效率很低,并且每个文件都有一定的余量(文件末尾的未使用磁盘空间)。这就是为什么将目标文件分组到库中的原因,这有点像没有压缩的ZIP文件。在链接时,将读取整个库,并且当链接程序开始读取库或它们所需的目标文件时,该库中所有解析符号的对象文件都已包含在输出中。这可能意味着整个库必须一次在内存中以递归方式解决依赖关系。由于内存量非常有限,因此链接器一次只能加载一个库,

为了提高性能(加载整个库需要花费一些时间,尤其是从软盘等慢速介质加载时),库通常包含一个索引,该索引告诉链接程序哪些对象文件提供了哪些符号。索引是由诸如ranlib或库管理工具之类的工具创建的(Borland的工具tlib可以生成索引)。一旦有了索引,即使所有目标文件都在磁盘高速缓存中并且从磁盘高速缓存中加载文件都是免费的,链接库绝对比单个目标文件更有效。

您完全正确,我可以在保留头文件的同时进行替换.o.a归档,并更改函数的功能(或它们的工作方式)。这是由所使用的LPGL-license,它要求使用LGPL-licensed库的程序的作者为用户提供通过修补的,改进的或替代的实现替换该库的可能性。交付自己应用程序的目标文件(可能被分组为库文件)足以为用户提供所需的自由度。无需交付源代码(如GPL)。

如果两组库(或目标文件)可以成功用于相同的头文件,则它们被认为是ABI兼容的,其中ABI表示应用程序二进制接口。这比仅具有两组库(或目标文件)以及它们各自的头文件要狭窄得多,并保证如果您为此特定库使用头文件,则可以使用每个库。这称为API兼容性,其中API表示应用程序接口。作为区别的示例,请看以下三个头文件:

文件1:

typedef struct {
    int a;
    int __undocumented_member;
    int b;
} magic_data;
magic_data* calculate(int);

档案2:

struct __tag_magic_data {
    int a;
    int __padding;
    int b;
};
typedef __tag_magic_data magic_data;
magic_data* calculate(const int);

文件3:

typedef struct {
    int a;
    int b;
    int c;
} magic_data;
magic_data* do_calculate(int, void*);
#define calculate(x) do_calculate(x, 0)

前两个文件不完全相同,但是它们提供的可互换定义(据我所期望的)不违反“一个定义规则”,因此提供文件1作为头文件的库也可以与文件2一起使用。头文件。另一方面,文件3提供了与程序员非常相似的接口(库作者向库用户承诺的所有接口可能都是相同的),但是用文件3编译的代码无法与旨在使用的库链接使用文件1或文件2,因为为文件3设计的库不会导出calculate,而只能导出do_calculate。同样,该结构具有不同的成员布局,因此使用文件1或文件2而不是文件3将无法正确访问b。提供文件1和文件2的库与ABI兼容,但是所有三个库都与API兼容(假定c和功能更强大的功能do_calculate不计入该API)。

对于动态库(.dll,.so),情况完全不同:它们开始出现在可以同时加载多个(应用程序)程序的系统上(在DOS上不是这种情况,在Windows上是这种情况)。 。多次在内存中使用相同的库函数实现是浪费的,因此只能加载一次,并且多个应用程序都使用它。对于动态库,所引用功能的代码不包括在可执行文件中,而仅包括对动态库内部功能的引用(对于Windows NE / PE,它指定哪个DLL必须提供哪个功能。对于在Unix .so文件中,仅指定函数名称和一组库。)操作系统包含一个加载程序,也称为动态链接程序 可以解析这些引用并在程序启动时加载动态库(如果它们尚未在内存中)。


嗨,倒数第二段不清楚,您介意重写吗?关于“伴随它们各自的标题”,那么如果各自的标题相等则有什么区别?如果它们不相等,怎么能说是与API兼容的呢?
Pacerier's

1
@Pacerier我重新阅读了该段落,但是找不到任何听起来像我能够以更容易理解的方式重写它的内容。我改为添加示例。有帮助吗?
Michael Karcher

30

好的,让我们从头开始。

程序员(您)创建了一些源文件,.cpp并且.h。这两个文件之间的区别只是一个约定:

  • .cpp 打算被编译
  • .h 旨在包含在其他源文件中

但是没有什么(除了担心有不可维护的事物)禁止您将cpp文件导入其他.cpp文件。

在C(C ++的祖先)早期,.h文件仅包含函数,结构(C中没有方法)和常量的声明。您也可以有一个宏(#define),但除此之外,也不应包含任何代码.h

在带有模板的C ++中,您还必须添加.h模板类的实现,因为C ++使用模板而不是Java之类的泛型,因此模板的每个实例化都是一个不同的类。

现在回答您的问题:

每个.cpp文件都是一个编译单元。编译器将:

  • 在预处理阶段,所有#include#define(内部)生成完整的源代码
  • 将其编译为对象格式(通常为.o.obj

该对象格式包含:

  • 可重定位代码(即代码或变量中的地址是与导出符号相对的地址)
  • 导出的符号:可以从其他编译单元(函数,类,全局变量)使用的符号
  • 导入的符号:在该编译单元中使用并在其他编译单元中定义的符号

然后(现在暂时忘记库),链接器将所有编译单元放在一起,并将解析符号以创建可执行文件。

静态库更进一步。

静态库(通常是.a.lib)或多或少是一堆目标文件。它的存在是为了避免单独列出所需的每个目标文件,以及从中使用导出符号的目标文件。链接包含您使用的目标文件的库和链接目标文件本身是完全相同的。只需添加-lc-lm或者-lx11缩短它们即可添加数百个.o文件。但是至少在类似Unix的系统上,静态库是一个存档,您可以根据需要提取单个目标文件。

动态库是完全不同的。动态库应被视为特殊的可执行文件。它们通常与创建普通可执行文件的链接程序一起构建(但具有不同的选项)。但是.dll,与其简单地声明一个入口点(在Windows上,文件确实声明了一个可用于初始化.dll),它们声明导出(和导入)符号的列表。在运行时,存在一些系统调用,这些调用允许获取那些符号的地址并几乎可以正常使用它们。但是实际上,当您在动态加载的库中调用例程时,代码位于加载器最初从您自己的可执行文件加载的代码之外。通常,从动态库加载所有使用的符号的操作要么是在加载时直接由加载器(在Unix之类的系统上),要么是在Windows上的导入库。

现在回顾一下包含文件。老式的K&R C和最新的C ++都没有像Java或C#这样的全局模块导入概念。在这些语言中,导入模块时,将同时获得其导出符号的声明以及以后将其链接的指示。但是在C ++中(与C相同),您必须分别进行操作:

  • 首先,声明函数或类-通过包括.h源代码中的文件来完成,以便编译器知道它们是什么
  • 接下来链接对象模块,静态库或动态库以实际访问代码

不能.h像普通.c文件一样编译文件吗?另外,“在Windows上导入库”是什么意思?关于“链接包含您使用的目标文件的库和链接目标文件本身是完全相同的”,这与stackoverflow.com/questions/30186256/…上的信息如何匹配?
Pacerier's

我对c而​​不是c ++有同样的问题。您知道您的答案是否也可以以某种方式适用于此,还是我应该问另一个问题?
Santropedro

@Santropedro:通用性是相同的,我的帖子也谈到了C的一些要点。由您决定是否足够满足您的需求,或者是否需要对C提出新的问题。如果这样做,最好的方法是就是指这个问题,并说出您想澄清的具体问题。
Serge Ballesta

8

目标文件包含函数的定义,这些函数使用的静态变量以及编译器输出的其他信息。这是一种可以通过链接器进行连接的形式(例如,调用函数的链接点与函数的入口点)。

库文件通常打包为包含一个或多个目标文件(并因此包含其中的所有信息)。这样做的优点是,比起一堆目标文件,分发单个库要容易得多(例如,如果将编译后的对象分发给另一个开发人员以在其程序中使用),并且还使链接更简单(需要定向链接器以访问较少的文件,这样可以更轻松地创建脚本进行链接)。同样,链接程序通常在性能上有较小的好处-打开一个大型库文件并解释其内容比打开并解释许多小对象文件的内容更有效,尤其是在链接程序需要多次通过它们的情况下。还有一些小优点,

将对象文件打包到库中通常是值得的,因为这是一次可以完成的操作,并且好处是无数次实现的(每次链接器使用该库来生成可执行文件时)。

由于人类可以更好地理解源代码-因此有更多的机会使源代码正常工作-当源代码很小时,大多数大型项目都包含大量(相对)小的源文件,这些源文件被编译为对象。一步将目标文件组装到库中,可以得到我上面提到的所有好处,同时允许人们以对人类有意义的方式(而不是链接程序)来管理其源代码。

也就是说,使用库是开发人员的选择。链接器不在乎,与将大量目标文件链接在一起相比,建立链接库和使用它可能需要更多的精力。因此,没有什么能够阻止开发人员使用目标文件和库的混合使用(除非明显需要避免在多个对象或库中重复函数和其他事情,否则会导致链接过程失败)。归根结底,开发人员的工作是制定管理软件构建和分发的策略。

实际上(至少)有两种类型的库。

链接器使用静态链接库来构建可执行文件,链接器将静态编译的库中的编译代码复制到可执行文件中。示例是Windows下的.lib文件和Unix下的.a文件。库本身(通常)不需要与程序可执行文件分开分发,因为需要的部分在可执行文件中。

动态链接的库在运行时加载到程序中。这样做的两个优点是可执行文件较小(因为它不包含目标文件或静态库的内容),并且每个可执行文件都可以使用多个可执行文件(例如,只需分发/安装一次库,并且使用这些库的所有可执行文件都可以使用)。抵消的是,程序的安装变得更加复杂(如果找不到动态链接的库,则可执行文件将无法运行,因此安装过程必须应对至少一次安装库的潜在需求)。另一个优点是动态库可以更新,而无需更改可执行文件-例如,修复库中所包含功能之一的缺陷,因此,无需更改可执行文件即可修复使用该库的所有程序的功能。抵消这是因为,如果在运行库时仅找到库的较旧版本,则依赖库的最新版本的程序可能会发生故障。这给库带来了维护方面的顾虑(通过各种名称调用,例如DLL hell),尤其是当程序依赖于多个动态链接的库时。动态链接库的示例包括Windows下的DLL,Unix下的.so文件。操作系统提供的功能通常与操作系统一起以动态链接库的形式安装,该库允许所有程序(正确构建时)都可以使用操作系统服务。抵消这是因为,如果在运行库时仅找到库的较旧版本,则依赖库的最新版本的程序可能会发生故障。这给库带来了维护方面的顾虑(通过各种名称调用,例如DLL hell),尤其是当程序依赖于多个动态链接的库时。动态链接库的示例包括Windows下的DLL,Unix下的.so文件。操作系统提供的功能通常与操作系统一起以动态链接库的形式安装,该库允许所有程序(正确构建时)都可以使用操作系统服务。抵消这是因为,如果在运行库时仅找到库的较旧版本,则依赖库的最新版本的程序可能会发生故障。这给库带来了维护方面的顾虑(通过各种名称调用,例如DLL hell),尤其是当程序依赖于多个动态链接的库时。动态链接库的示例包括Windows下的DLL,Unix下的.so文件。操作系统提供的功能通常与操作系统一起以动态链接库的形式安装,该库允许所有程序(正确构建时)都可以使用操作系统服务。这给库带来了维护方面的顾虑(通过各种名称调用,例如DLL hell),尤其是当程序依赖于多个动态链接的库时。动态链接库的示例包括Windows下的DLL,Unix下的.so文件。操作系统提供的功能通常与操作系统一起以动态链接库的形式安装,该库允许所有程序(正确构建时)都可以使用操作系统服务。这给库带来了维护方面的顾虑(通过各种名称调用,例如DLL hell),尤其是当程序依赖于多个动态链接的库时。动态链接库的示例包括Windows下的DLL,Unix下的.so文件。操作系统提供的功能通常与操作系统一起以动态链接库的形式安装,该库允许所有程序(正确构建时)都可以使用操作系统服务。

程序也可以开发为使用静态和动态库的混合体-再次由开发人员决定。静态库也可以链接到程序中,并负责与使用动态加载的库相关的所有簿记工作。


2

您所描述的是静态链接的工作方式。

为什么我们有一个单独的库实现?.a .lib,.dll ...

.dlls是动态链接的-链接在运行程序后发生。根据您使用库的方式,函数地址将在执行程序后立即加载,或尽可能晚地加载。

.sos是相同的想法,但是在Linux上。

.as,通常在Linux(以及MinGW)中使用的是库归档文件,其行为基本上类似于增强的目标文件:

  • 它们是静态链接的。
  • 您可以将多个目标文件打包到单个库存档中。
  • 名称被索引。

.lib由Visual Studio中的Microsoft链接器使用。

有人不能给您他们的某个声明(.h)的实现,您可以替换其中的内容并将其链接成可执行相同功能但使用不同操作的可执行文件吗?

是! 使用动态库,您可以走得更远:您可以在不重新编译的情况下替换库,有时甚至无需重新启动程序

实际的例子是Wine-它们提供WinAPI的开源和可移植实现。


“即使不重新启动程序”也可能吗?
Pacerier's

@Pacerier当然。您可以在Windows上使用LoadLibrary/GetProcAddressdlopen/dlsym并显式获取您要调用的函数的地址:这是linux的示例,在该示例中,我们从程序中编译动态库,调用该函数,将其卸载,然后编译一个不同的库,然后加载该库。一个,然后从同一程序再次调用它。
milleniumbug
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.