对象文件vs库文件,为什么?

时间:2014-05-12 17:30:25

标签: c++

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

所以我的问题是:


  • 为什么我们有一个单独的库实现? .a .lib, 的.dll ...
  • 我可能错了,但在我看来,像.o文件本身 和图书馆有点相同吗?
  • 不能有人给你他们的.o实现某个 声明(.h),您可以替换它并将其链接到 成为执行相同功能的可执行文件,但使用 不同的业务?

4 个答案:

答案 0 :(得分:52)

从历史上看,目标文件完全链接或根本不链接到可执行文件中(现在,像功能级别链接整个程序优化这样的例外变得越来越流行),所以如果使用目标文件的一个函数,则可执行文件将接收所有这些函数。

为了使可执行文件保持较小且没有死代码,标准库被拆分为许多小目标文件(通常大约为数百个)。出于效率原因,拥有数百个小文件是非常不可取的:打开许多文件效率低下,并且每个文件都有一些松弛(文件末尾未使用的磁盘空间)。这就是为什么目标文件被分组到库中,这有点像没有压缩的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一起用作头文件。另一方面,File 3提供了一个与程序员非常相似的接口(在库作者承诺库的用户的所有内容中可能完全相同),但是使用File 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文件,只指定了函数名和一组库)。操作系统包含一个 loader aka 动态链接器,它解析这些引用并加载动态库(如果它们在程序启动时尚未在内存中)。

答案 1 :(得分:26)

好的,让我们从头开始吧。

程序员(您)创建了一些源文件.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文件确实声明了一个可用于初始化.dll)的入口点,它们声明了一个导出(和导入)符号的列表。在运行时,有系统调用允许获取这些符号的地址并几乎正常使用它们。但事实上,当您在动态加载库中调用例程时,代码驻留在加载器最初从您自己的可执行文件加载的内容之外。通常,从动态库加载所有使用过的符号的操作在加载时直接由加载器(在类Unix系统上)或Windows上的导入库加载。

现在回顾一下包含文件。好的旧K& R C和最新的C ++都没有导入全局模块的概念,例如Java或C#。在这些语言中,当您导入模块时,您将获得其导出符号的声明,并指示您稍后将其链接。但是在C ++中(与C相同)你必须单独进行:

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

答案 2 :(得分:8)

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

库文件通常打包为包含一个或多个目标文件(因此包含其中的所有信息)。这提供了以下优点:分发单个库比分发对象文件更容易(例如,如果将编译对象分发给另一个开发人员以在其程序中使用),并且还使链接更简单(链接器需要被定向以访问更少的文件,这使得创建脚本更容易进行链接)。此外,通常,链接器的性能优势很小 - 打开一个大型库文件并解释其内容比打开和解释许多小型目标文件的内容更有效,特别是如果链接器需要多次传递它们。还有一些小优势,取决于硬盘驱动器的格式化和管理方式,一些大文件比许多小文件消耗更少的磁盘空间。

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

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

也就是说,开发人员选择使用库。链接器并不关心,设置库并使用它比连接大量目标文件需要更多的努力。因此,没有什么能阻止开发人员使用目标文件和库的混合(除了显然需要避免多个对象或库中的函数和其他东西的重复,这导致链接过程失败)。毕竟,开发人员的工作是制定管理软件构建和分发的策略。

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

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

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

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

答案 3 :(得分:1)

您所描述的是静态链接的工作原理。

  

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

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

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

传统上在Linux(以及MinGW)中使用的

.a是库存档,其行为基本上类似于增强的目标文件:

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

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

  

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

是的!使用动态库,您可以更进一步:无需重新编译即可替换库,有时即使没有重新启动程序

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