Optimus1 Asked:2022-04-26 18:07:57 +0800 CST2022-04-26 18:07:57 +0800 CST 2022-04-26 18:07:57 +0800 CST 构建库“共享库”时的参数 - 这是什么意思? 772 你能告诉我用“shared lib”参数构建一个库是什么意思吗?也就是说,一个“共享库”——它与谁共享什么以及与谁共享? 这是否意味着库将被动态链接?像 Windows 的 .dll 吗? c++ 1 个回答 Voted Best Answer AnyKeyShik 2022-04-27T21:54:12+08:002022-04-27T21:54:12+08:00 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 如果您想阅读一些关于它的信息,那么这里是一个很好的切入点
TL;博士
动态库是编译后的二进制文件,它本身不是程序,但提供了一些其他程序可以在运行时导入的功能。一个例子
sprintf()
来自libc
*NIX 和ntdll.dll
Windows。关于编译步骤
编译一个程序大致可以分为两个阶段:翻译和链接。第一阶段有时也称为编译,但对于初学者来说这会造成混淆,因此我们将坚持使用上述术语。
播送
此时,编译器(负责将人类可读源代码转换为二进制形式的编译器部分)解析输入文件,检查基本语法并尝试将其转换为二进制形式。在输出中,我们得到
*.o
一个*.obj
包含已编译源的二进制形式的文件。此时,我们仍然无法启动它——在这种形式下,任何不在程序中的跳转都没有定义。即使在线性程序中,也有足够的跳转,例如“Hello world!” - 我们以某种方式连接库,这样我们自己就不会从头开始编写用于在屏幕上显示的函数(并且,剧透,在不影响操作系统内核的情况下,这仍然是不可能的,但仍然需要库来与内核一起工作)。林科夫卡
这是编译器的下一部分,称为链接器的时刻,它获取分支的目标文件并计算它应该跳转到哪里以及如何跳转,也就是说,实际上,它可以执行它。在同一阶段,可以从动态库中加载函数。但更多关于以下内容。
一些关于图书馆的一般知识
首先,关于术语。我多次看到标题(
*.h
文件)如何被称为库。这并不完全正确。一个好的方法是,在头文件中(如果写得正确的话)没有函数定义——只有它们的声明。但是函数本身已经在库中,仅需要标头才能正确调用程序中的函数。静态库
第一种类型的库是静态的。这意味着在链接期间,库被硬连线到二进制文件中。实际上,静态库是一个包含目标文件的存档,其中包含头文件中函数的实现。也就是说,当我们创建一个静态库时,我们不会在翻译后调用链接器,而是将所有头文件收集到一个存档中,并将其作为库提供给最终用户。
动态库
此外,什么是动态的(它们也是共享的,尽管这种翻译非常歪曲,并且应该避免)库。它们与静态的全局区别在于它们最终不会在二进制文件中,而是位于系统中的某个位置,但是当程序启动时,由于文件映射机制,它们最终会在其内存中(您可以在此处阅读有关此的更多信息)。
UPD。我几乎忘记了——还有第二种方法可以使用动态库而不是指定
LD_LIBRARY_PATH
. 您可以将库路径硬连接到二进制文件本身,但这会带来很多后果。这是通过以下方式完成的rpath
:UPD2。在某些情况下,有必要加载您自己的库而不是系统库 - 例如,您自己的
rand()
. 这是用LD_PRELOAD
. 举个例子,下面的代码:如果您构建一个单独的动态库和一个单独的二进制文件,那么一切都会照常 - 即使您在以下位置指定库
LD_LIBRARY_PATH
:但只要确定
LD_PRELOAD
魔法是如何发生的,我们就会得到宇宙主要问题的答案:如果您想阅读一些关于它的信息,那么这里是一个很好的切入点