【问题标题】:Dynamic linking - Linux Vs. Windows动态链接 - Linux Vs。视窗
【发布时间】:2020-02-24 11:29:33
【问题描述】:

在 Windows 下,当我在 MSVC 的 DLL 项目中编译 C/C++ 代码时,我得到 2 个文件:

  1. MyDll.dll
  2. MyDll.lib

据我了解,MyDll.lib 包含某种指针表,指示 dll 中的函数位置。当使用这个 dll 时,比如在一个 exe 文件中,MyDll.lib 在链接期间被嵌入到 exe 文件中,因此在运行时它“知道”函数在 MyDll.dll 中的位置并可以使用它们。

但如果我在 Linux 下编译相同的代码,我只会得到 一个 文件 MySo.so 而没有 MySo.a(相当于 Linux 中的 lib 文件)那么Linux下的可执行文件如果在链接过程中没有嵌入任何内容,如何知道函数在MySo.so中的位置?

【问题讨论】:

    标签: c++ c linux windows shared-libraries


    【解决方案1】:

    MSVC 链接器可以将目标文件 (.obj) 和目标库 (.lib) 链接在一起以生成 .EXE 或 .DLL。

    要与 DLL 链接,MSVC 中的过程是使用所谓的导入库 (.LIB),它充当 C 函数名称和 DLL 导出表之间的粘合剂(在 DLL 中,可以导出函数按名称或 ordinal - 后者通常用于未记录的 API)。

    然而,在大多数情况下,DLL 导出表包含所有函数名称,因此导入库 (.LIB) 包含大量冗余信息(“导入函数 ABC -> 导出函数 ABC”等)。
    甚至可以从现有 .DLL 中generate .LIB。

    其他平台的链接器没有这个“特性”,可以直接链接动态库。

    【讨论】:

    • “其他平台上的链接器没有这个功能”——虽然它很容易实现(例如,Implib.so 为 Linux 做这个)来实现延迟加载和其他好处。
    • @yugr:这就是为什么“功能”用引号引起来的原因——这不是您通常想要做的事情,而是您必须在 Windows 上做的额外工作。
    【解决方案2】:

    在 Linux 上,链接器(不是动态链接器)搜索链接时指定的共享库,并在可执行文件中创建对它们的引用。当动态链接器加载这些可执行文件时,它会将它们所需的共享库加载到内存中并解析符号,从而允许运行二进制文件。

    MySo.a,如果创建,实际上将包含要直接链接到二进制文件中的符号,而不是 Windows 上使用的“符号查找表”。

    rustyx's answer 比我更彻底地解释了 Windows 上的过程;好久没用Windows了。

    【讨论】:

    • “Windows 采用了不同的方法......指定操作系统在 DLL 中的确切位置” - 这与 wiki 相矛盾,它表示函数名称仍然被解析(在启动时或在第一次调用库函数时),即使您使用序数(除非使用直接地址绑定,因为它会强制库用户在库更改时重新编译和重新部署他们的代码)。
    • @yugr 删除了那部分,反正我是在抓稻草。
    【解决方案3】:

    您看到的差异更多的是实现细节 - 在 Linux 和 Windows 的底层工作方式类似 - 您的代码调用了一个静态链接到可执行文件中的存根函数,然后这个存根在必要时加载 DLL/shlib(在delayed loading 的情况,否则程序启动时会加载库)并且(在第一次调用时)通过 GetProcAddress/dlsym 解析符号。

    唯一的区别是,在 Linux 上,这些存根函数(称为 PLT 存根)是在您将应用与动态库(库包含生成它们的足够信息)链接时动态生成的,而在 Windows 上,它们是在创建 DLL 本身时在单独的 .lib 文件中生成的。

    这两种方法非常相似,实际上可以在 Linux 上模仿 Windows 导入库(参见 Implib.so 项目)。

    【讨论】:

      【解决方案4】:

      在 Linux 上,您将 MySo.so 传递给链接器,它能够仅提取链接阶段所需的内容,并在运行时添加 MySo.so 所需的引用。

      【讨论】:

        【解决方案5】:

        .dll.so 是共享库(在运行时链接),而 .a.lib 是静态库(在编译时链接)。这在 Windows 和 Linux 之间没有区别。

        不同之处在于,它们是如何处理的。注意:区别仅在于海关,它们是如何使用的。以 Windows 的方式构建 Linux 并不太难,反之亦然,但实际上没有人这样做。

        如果我们使用 dll,或者我们甚至从我们自己的二进制文件中调用一个函数,有一个简单明了的方法。例如,在 C 中,我们看到:

        int example(int x) {
          ...do_something...
        }
        
        int ret = example(42);
        

        但是,在 asm 级别上,可能存在许多差异。例如,在 x86 上,执行 call 操作码,并在堆栈上给出 42。或在某些寄存器中。或任何地方。没有人知道在编写 dll 之前,它将如何使用它。或者项目将如何使用它,可能是用现在甚至不存在的编译器(或用一种语言!)编写的(或者对于 dll 的开发人员来说是未知的)。

        例如,默认情况下,C 和 Pascal 都从堆栈中放入参数(并获取返回值) - 但它们的执行顺序不同。您还可以通过一些依赖于编译器的优化在寄存器中的函数之间交换参数。

        如你所见,Windows 的习惯是构建一个 dll,我们还用它创建了一个最小的.a/.lib。这个最小的静态库只是一个包装器,通过它可以访问该 dll 的符号(函数)。这会进行所需的 asm 级调用转换。

        它的优点是兼容性。它的缺点是,如果你只有一个 .dll,你可能很难弄清楚它的函数是如何被调用的。这使得 dll 的使用成为一项黑客任务,如果 dll 的开发者没有给你.a。因此,它主要服务于封闭性目的,例如更容易为 SDK 获得额外的现金。

        它的另一个缺点是即使使用动态库,也需要静态编译这个小包装器。

        在 Linux 中,dll 的二进制接口是标准的并且遵循 C 约定。因此,不需要.a,并且共享库之间存在二进制兼容性,作为交换,我们没有微软自定义的优势。

        【讨论】:

        • 请提供存根函数可以更改参数顺序的证明链接。我以前从未听说过这件事,而且考虑到性能开销有多大,我很难相信。
        • @yugr 简单的寄存器/堆栈重新排序不是性能开销。如果你使用 msvc 编译的二进制文件中的 msvc 编译的 dll,那么显然不会发生太多事情,但它可能会发生。
        • 我们可以就此争论,但如果你是对的,应该很容易提供证明链接,证明存根函数能够对参数进行非平凡的处理(并且不仅仅是虚拟蹦床)。
        • @yugr 存根可以访问 dll 的函数签名,这使得非平凡的处理变得微不足道。
        • 我只建议你用一些关于导入库做什么的证明链接来完成你的答案(因为有些说法是有问题的)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-04-07
        • 1970-01-01
        • 1970-01-01
        • 2011-05-19
        • 1970-01-01
        相关资源
        最近更新 更多