RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1354207
Accepted
Optimus1
Optimus1
Asked:2022-04-26 18:07:57 +0000 UTC2022-04-26 18:07:57 +0000 UTC 2022-04-26 18:07:57 +0000 UTC

构建库“共享库”时的参数 - 这是什么意思?

  • 772

你能告诉我用“shared lib”参数构建一个库是什么意思吗?也就是说,一个“共享库”——它与谁共享什么以及与谁共享?

这是否意味着库将被动态链接?像 Windows 的 .dll 吗?

c++
  • 1 1 个回答
  • 10 Views

1 个回答

  • Voted
  1. Best Answer
    AnyKeyShik
    2022-04-27T21:54:12Z2022-04-27T21:54:12Z

    TL;博士

    动态库是编译后的二进制文件,它本身不是程序,但提供了一些其他程序可以在运行时导入的功能。一个例子sprintf()来自libc*NIX 和ntdll.dllWindows。


    关于编译步骤

    编译一个程序大致可以分为两个阶段:翻译和链接。第一阶段有时也称为编译,但对于初学者来说这会造成混淆,因此我们将坚持使用上述术语。

    播送

    此时,编译器(负责将人类可读源代码转换为二进制形式的编译器部分)解析输入文件,检查基本语法并尝试将其转换为二进制形式。在输出中,我们得到*.o一个*.obj包含已编译源的二进制形式的文件。此时,我们仍然无法启动它——在这种形式下,任何不在程序中的跳转都没有定义。即使在线性程序中,也有足够的跳转,例如“Hello world!” - 我们以某种方式连接库,这样我们自己就不会从头开始编写用于在屏幕上显示的函数(并且,剧透,在不影响操作系统内核的情况下,这仍然是不可能的,但仍然需要库来与内核一起工作)。

    比如我们分析一下同一个Hello world'a的翻译阶段:如果我们想让编译器不超越翻译,那么GCC有个妙招-c:

    #include <stdio.h>
    
    int main(void)
    {
        puts("Hello world!");
    
        return 0;
    }
    
    $ gcc -c main.o main.c
    

    并查看目标文件的内容objdump:

    
    main.o:     file format elf64-x86-64
    
    
    Disassembly of section .text:
    
    0000000000000000 <main>:
       0:   55                      push   %rbp
       1:   48 89 e5                mov    %rsp,%rbp
       4:   48 8d 05 00 00 00 00    lea    0x0(%rip),%rax        # b <main+0xb>
       b:   48 89 c7                mov    %rax,%rdi
       e:   e8 00 00 00 00          call   13 <main+0x13>
      13:   b8 00 00 00 00          mov    $0x0,%eax
      18:   5d                      pop    %rbp
      19:   c3                      ret
    

    可以看到,这里只是我们的main函数,翻译成asm。此外,call它转到00 00 00 00- 也就是说,根据它objdump告诉我们的零偏移量,表明跳转将打开<main+13>,即下一行。由于显而易见的原因,这样的文件是不可执行的。

    林科夫卡

    这是编译器的下一部分,称为链接器的时刻,它获取分支的目标文件并计算它应该跳转到哪里以及如何跳转,也就是说,实际上,它可以执行它。在同一阶段,可以从动态库中加载函数。但更多关于以下内容。

    让我们继续折腾Hello world。在最后阶段,我们收到了一个未计算地址的目标文件。好吧,现在让我们计算地址并获取可执行文件:

    $ gcc -o main main.o 
    

    (我不会附加objdump'a 的整个输出,因为它真的很大,但我会再次附加 main 函数)。其余功能是辅助功能,用于运行程序/使用动态库中的功能

    0000000000001139 <main>:
        1139:       55                      push   %rbp
        113a:       48 89 e5                mov    %rsp,%rbp
        113d:       48 8d 05 c0 0e 00 00    lea    0xec0(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
        1144:       48 89 c7                mov    %rax,%rdi
        1147:       e8 e4 fe ff ff          call   1030 <puts@plt>
        114c:       b8 00 00 00 00          mov    $0x0,%eax
        1151:       5d                      pop    %rbp
        1152:       c3                      ret
        1153:       66 2e 0f 1f 84 00 00    cs nopw 0x0(%rax,%rax,1)
        115a:       00 00 00
        115d:       0f 1f 00                nopl   (%rax)
    

    正如我们所看到的,这里call'a 有一个地址,现在我们不是在“无处”跳转,而是在定义函数的地方puts(事实上,这也不完全正确,下面会详细介绍)。


    一些关于图书馆的一般知识

    首先,关于术语。我多次看到标题(*.h文件)如何被称为库。这并不完全正确。一个好的方法是,在头文件中(如果写得正确的话)没有函数定义——只有它们的声明。但是函数本身已经在库中,仅需要标头才能正确调用程序中的函数。

    静态库

    第一种类型的库是静态的。这意味着在链接期间,库被硬连线到二进制文件中。实际上,静态库是一个包含目标文件的存档,其中包含头文件中函数的实现。也就是说,当我们创建一个静态库时,我们不会在翻译后调用链接器,而是将所有头文件收集到一个存档中,并将其作为库提供给最终用户。

    让我们看一个例子(再次*NIX):我们有两个带有相应*.c文件的头文件和main.c,它使用这些头文件。让我们从所有这些东西中组装一个静态库,并尝试获得一个成熟的程序:

    #ifndef BYE_H
    #define BYE_H
    
    void print_bye(void);
    
    #endif
    
    #include "bye.h"
    
    #include <stdio.h>
    
    void print_bye()
    {
        puts("Bye world!");
    }
    
    #ifndef PRINT_H
    #define PRINT_H
    
    void print_hello(void);
    
    #endif
    
    #include "hello.h"
    
    #include <stdio.h>
    
    void print_hello(void)
    {
        puts("Hello world!");
    }
    
    #include "hello.h"
    #include "bye.h"
    
    int main(void)
    {
        print_hello();
        print_bye();
    
        return 0;
    }
    

    我们收集标题,因为我们已经知道如何:

    $ gcc -c hello.c bye.c
    

    我们得到两个目标文件。现在您需要使用ar. 这里有一个关于命名的小题外话 - 有一个协议库名称应该以 . 开头lib。正如我们稍后将看到的,链接器在指定库时会尝试查找以lib. 因此,让我们print使用文件名创建一个库libprint.a:

    $ ar rc libprint.a hello.o bye.o
    

    下一步是向库中添加符号索引,以便链接器可以确定哪些函数/变量/等是和不是。通常,在大多数情况下ar,它会自己添加这样的索引,但也有不添加的情况。因此,手动添加它被认为是一种很好的做法,即使它不会改变任何东西:

    $ ranlib libprint.a
    

    之后,我们得到了一个静态库,它在链接时将被硬连接到二进制文件中。让我们看看这意味着什么。让我们用这个库编译并通过打开它objdump(注意我们指定库的名称不带前缀lib,链接器会自己添加它:

    $ gcc main.c -L. -lprint -o main
    $ objdump -d main
    
    0000000000001139 <main>:
        1139:       55                      push   rbp
        113a:       48 89 e5                mov    rbp,rsp
        113d:       e8 22 00 00 00          call   1164 <print_hello>
        1142:       e8 07 00 00 00          call   114e <print_bye>
        1147:       b8 00 00 00 00          mov    eax,0x0
        114c:       5d                      pop    rbp
        114d:       c3                      ret
    
    000000000000114e <print_bye>:
        114e:       55                      push   rbp
        114f:       48 89 e5                mov    rbp,rsp
        1152:       48 8d 05 ab 0e 00 00    lea    rax,[rip+0xeab]        # 2004 <_IO_stdin_used+0x4>
        1159:       48 89 c7                mov    rdi,rax
        115c:       e8 cf fe ff ff          call   1030 <puts@plt>
        1161:       90                      nop
        1162:       5d                      pop    rbp
        1163:       c3                      ret
    
    0000000000001164 <print_hello>:
        1164:       55                      push   rbp
        1165:       48 89 e5                mov    rbp,rsp
        1168:       48 8d 05 a0 0e 00 00    lea    rax,[rip+0xea0]        # 200f <_IO_stdin_used+0xf>
        116f:       48 89 c7                mov    rdi,rax
        1172:       e8 b9 fe ff ff          call   1030 <puts@plt>
        1177:       90                      nop
        1178:       5d                      pop    rbp
        1179:       c3                      ret
        117a:       66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
    

    正如我们所看到的,我们的函数最终变成了二进制文件。这是一个静态库——所有函数都在里面。这种方法的优点是不可能使用库替换,一个文件,更容易分发等等。缺点 - 结果文件的巨大重量。因此,有第二种方法。

    动态库

    此外,什么是动态的(它们也是共享的,尽管这种翻译非常歪曲,并且应该避免)库。它们与静态的全局区别在于它们最终不会在二进制文件中,而是位于系统中的某个位置,但是当程序启动时,由于文件映射机制,它们最终会在其内存中(您可以在此处阅读有关此的更多信息)。

    抒情题外话。由于这种机制,动态库有时被称为共享库。Potmou 认为,事实上,文件在物理上是单独存在于磁盘上的。因此在 Linux/UNIX Shared Object( *.so) 中的名称。但是在俄语中,其他人说“动态库”更容易也更容易理解。此外,在 Windows 和 OS X 中,它们分别称为Dynamic Linked Library( *.dll) 和Dynamic Library( *.dylib)。

    让我们将上面写的代码重新构建成一个动态库,并尝试获取一个可执行文件。这一次,我们将不得不以 Position Independent Code 的形式收集目标文件——毕竟,我们的目标是让代码在不同的内存位置以相同的方式工作。因此,现在目标文件的程序集将如下所示:

    $ gcc -c -fPIC bye.c hello.c
    

    之后,我们得到两个可以编译成*.so. 是时候开始了:

    $ gcc -shared -o libprint.so bye.o hello.o
    

    结果,我们得到了一个动态库libprint.so,其实也用到了动态库!毕竟函数puts()其实是库提供的libc.so。让我们构建一个二进制文件:

    $ gcc main.c -L. -lprint -o main
    

    让我们尝试运行:

    ./main
    

    意外输出:

    ./main: error while loading shared libraries: libprint.so: cannot open shared object file: No such file or directory
    

    事实上,系统正在沿着预先指定的路径寻找动态库,而我们的库位于二进制文件旁边,因此系统找不到它。为了添加您自己的搜索路径,您必须使用环境变量LD_LIBRARY_PATH,您需要在其中指定包含动态库的目录。让我们再试一次:

    LD_LIBRARY_PATH=. ./main
    

    这一次一切都很好,库被加载了,我们看到了我们程序输出的两行珍贵的行。如果我们查看 中的二进制文件objdump,我们可以在 部分中看到函数print_hello和print_bye,该部分plt描述了如何从动态库中调用函数。如果您对这个主题以及表格的工作原理PLT以及GOT一般动态加载感兴趣,我建议您在这个主题上 google,有很多材料而且很容易弄清楚。

    UPD。我几乎忘记了——还有第二种方法可以使用动态库而不是指定LD_LIBRARY_PATH. 您可以将库路径硬连接到二进制文件本身,但这会带来很多后果。这是通过以下方式完成的rpath:

    $ gcc {*.c} -L{путь_до_библиотеки} -l{имя_библиотеки} -rpath={путь до библиотеки} -o {имя выходного файла}
    

    UPD2。在某些情况下,有必要加载您自己的库而不是系统库 - 例如,您自己的rand(). 这是用LD_PRELOAD. 举个例子,下面的代码:

    #ifndef RAND_H
    #define RAND_H
    
    int rand(void);
    
    #endif
    
    #include "rand.h"
    
    int rand(void)
    {
        return 42;
    }
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    int main()
    {
        srand (time(NULL));
    
        for(int i = 0; i < 5; i++) {
            printf ("%d\n", rand() % 100);
        }
    
        return 0;
    }
    

    如果您构建一个单独的动态库和一个单独的二进制文件,那么一切都会照常 - 即使您在以下位置指定库LD_LIBRARY_PATH:

    $ ./main
    74
    59
    1
    47
    11
    
    $ LD_LIBRARY_PATH=. ./main
    88
    21
    88
    79
    9
    

    但只要确定LD_PRELOAD魔法是如何发生的,我们就会得到宇宙主要问题的答案:

    $ LD_PRELOAD=./ld_rand.so ./main
    42
    42
    42
    42
    42
    

    如果您想阅读一些关于它的信息,那么这里是一个很好的切入点

    • 2

相关问题

  • 编译器和模板处理

  • 指针。找到最小数量

  • C++,关于枚举类对象初始化的问题

  • 函数中的二维数组

  • 无法使用默认构造函数创建类对象

  • C++ 和循环依赖

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    表格填充不起作用

    • 2 个回答
  • Marko Smith

    提示 50/50,有两个,其中一个是正确的

    • 1 个回答
  • Marko Smith

    在 PyQt5 中停止进程

    • 1 个回答
  • Marko Smith

    我的脚本不工作

    • 1 个回答
  • Marko Smith

    在文本文件中写入和读取列表

    • 2 个回答
  • Marko Smith

    如何像屏幕截图中那样并排排列这些块?

    • 1 个回答
  • Marko Smith

    确定文本文件中每一行的字符数

    • 2 个回答
  • Marko Smith

    将接口对象传递给 JAVA 构造函数

    • 1 个回答
  • Marko Smith

    正确更新数据库中的数据

    • 1 个回答
  • Marko Smith

    Python解析不是css

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5