赞
踩
在Linux系统开发中,库文件扮演着至关重要的角色。它们提供了程序运行所需的各种功能,使得开发者能够高效地复用代码,减少重复劳动。库文件通常分为动态库(也称为共享库)和静态库两种类型,它们在程序链接和运行阶段扮演着不同的角色。
当我们使用gcc来编译一个C源文件,会经历如下过程:
从图中我们可以看出GCC处理HelloWorld.c
的大致过程:预处理—>编译—>汇编—>链接。
下面我们详细解释该过程:
预处理(Preprocessing):
预处理阶段会做一些文本操作。注释的删除、#include
头文件的包含、#define
符号的替换等。#define
与#include
称为预处理指令。所有的预处理的指令都是此过程进行。
在这个阶段,编译器会处理源代码文件HelloWorld.c
中的预处理指令,如#include
、#define
等。
#include <stdio.h>
这样的指令会告诉编译器将标准输入输出头文件(stdio.h
)的内容包含到源文件中。
我们通常使用-E
选项来只执行预处理阶段,并将输出重定向到一个文件。例如:
gcc -E HelloWorld.c -o HelloWorld.i
预处理后的输出文件通常是.i
扩展名,如HelloWorld.i
。
编译(Compilation):
编译阶段将预处理后的文件转换成汇编代码。且对预处理后的文件进行语法分析、词法分析、语义分析、符号汇总,并生成汇编代码。
我们通常使用-S
选项来只执行编译阶段,生成汇编代码。例如:
gcc -S HelloWorld.i -o HelloWorld.s
如果直接从.c
文件编译到.s
文件,命令如下:
gcc -S HelloWorld.c -o HelloWorld.s
汇编代码通常以.s
为扩展名。如HelloWorld.s
,该文件包含了汇编代码。
汇编(Assembly):
汇编阶段将汇编语言文件转换成机器语言的目标文件。把汇编代码翻译成二进制指令,生成的是目标文件,目标文件中存放的都是二进制的指令。即形成符号表。
我们通常使用-c
选项来执行汇编阶段,生成目标文件(object file)。
gcc -c HelloWorld.s -o HelloWorld.o
注意:同样地,通常会直接从.c
或.s
文件汇编到.o
文件。从.s
文件汇编的命令如上所示。
如果直接从.c
文件汇编到.o
文件,命令如下:
gcc -c HelloWorld.c -o HelloWorld.o
目标文件通常以.o
为扩展名。如HelloWorld.o
。
链接(Linking):
链接阶段是将目标文件(.o
文件)与所需的库文件合并起来,生成最终的可执行文件。这个过程会进行合并段表、符号表的合并和重定位等。链接器会解析目标文件中的外部符号引用(如函数调用),并将它们与库文件中的定义连接起来。最终生成的可执行文件。
使用gcc
命令(不带-c
、-S
、-E
等选项)来执行链接阶段,生成可执行文件。如果程序使用了任何标准库函数(如printf
),则链接器会自动链接必要的库。例如:
gcc HelloWorld.o -o HelloWorld
或者,如果直接从.c
文件开始,并且没有中间步骤,可以简单地使用:
gcc HelloWorld.c -o HelloWorld
这将生成一个名为HelloWorld
的可执行文件,该文件包含了完整的机器码指令,可以直接运行在计算机上。
静态库 (Static Library) 是一种在编译时链接到程序中的库文件,它的格式为.a
(在Linux系统中)。静态库包含了程序运行所需的所有代码和数据,当程序编译时,链接器会将静态库中的代码和数据直接复制到生成的可执行文件中。
我们观察使用了静态库的程序:
ldd a.out
命令检查 a.out
的动态链接依赖时,输出了 not a dynamic executable
,这是因为 a.out
是一个静态链接的可执行文件,它不依赖于任何外部的动态链接库。file a.out
命令查看文件类型时,输出描述了这是一个静态链接的 ELF 64 位可执行文件,并且没有动态链接器(如 /lib64/ld-linux-x86-64.so.2
)的引用。因此,使用静态库生成可执行程序后,该程序运行时就不依赖外部库文件,可独立运行,但生成的可执行文件体积较大。
gcc -o test_static test.c -static
这个命令使用了 -static
选项将 test.c
编译并链接为一个名为 test_static
的可执行文件。-static
选项告诉链接器在创建可执行文件时,应该使用静态库而不是动态库。这意味着它在运行时不需要动态链接器来解析任何动态库依赖。
gcc -o test_shared test.c
这个命令将 test.c
编译并链接为一个名为 test_shared
的可执行文件,且没有使用任何特殊的链接选项。默认情况下,链接器会使用动态库来解析程序中的依赖。这意味着 test_shared
在运行时需要加载它依赖的共享库。这种方法生成的可执行文件通常较小,因为它不包含它所依赖的库的完整副本,但它需要这些库在运行时可用。
因此,我们可以明显观察到两者的大小差距。
优点:
缺点:
下面我们根据上图来介绍我们的静态库创建过程,我们有两个C源文件,mymath.c
和 mystdio.c
,以及他们对于的.h文件。
使用gcc编译器将源文件编译为目标文件。目标文件是包含机器代码但尚未链接的文件。
gcc -c mymath.c
gcc -c mystdio.c
这将生成mystdio.o
和mymath.o
两个目标文件。
使用ar工具将目标文件打包成静态库。在Linux系统中,静态库通常以.a
为扩展名。
ar -rc libmylib.a mymath.o mystdio.o
这将创建一个名为libmylib.a
的静态库。使用 ar
命令来创建一个静态库文件 libmylib.a
,这个库文件包含了 mymath.o
和 mystdio.o
这两个目标文件。我们分析该命令:
ar
:这是GNU归档器命令,用于创建、修改和提取静态库文件(通常是 .a
文件)。-r
:替换现有的目标文件或添加新的目标文件到归档文件中。(replace)-c
:创建一个归档文件,如果它不存在的话。这个选项与 -r
一起使用时,意味着如果归档文件已经存在,则替换其中的同名目标文件,如果归档文件不存在,则创建一个新的归档文件。(create)编写一个主程序(例如myprogram.c
),它调用静态库中的函数。在程序中,我们需要包含定义库函数的头文件,并在链接时指定库文件。
使用gcc编译器编译主程序,并在链接时指定静态库,以便使用 mymath.o
和 mystdio.o
中定义的函数或变量。在链接时,我们需要告诉编译器和链接器静态库文件的路径和名称,通常使用 -L
和 -l
选项。例如:
gcc -o myprogram myprogram.c -L /path/to/lib_test -lmylib -I /path/to/lib_include
这里,-L
告诉编译器在哪个目录下搜索库文件,这里的 /path/to/lib_test
应该替换为实际存放 libmyc.a
的路径。注意 -l
选项后面跟的是库名(不包括前缀 lib
和后缀 .a
)。-L
选项后跟的是存放库的路径。-I
告诉编译器在哪个目录下搜索头文件,这里的 /path/to/lib_include
应该替换为实际存放所使用头文件的路径。
下面我们进行实验,我们编写如下用于构建并组织构建静态库libmylib.a
的makfile:
#形成静态库 libmylib.a:mymath.o mystdio.o ar -rc $@ $^ %.o:%.c gcc -c $< .PHONY:clean clean: rm -rf *.o mylib *.a .PHONY:output output: mkdir -p mylib/include mkdir -p mylib/lib cp ./*.h mylib/include cp ./*.a mylib/lib
这个makefile定义了两个目标(静态库和对象文件)的构建规则,以及两个伪目标(clean
和output
)用于清理文件和组织构建的输出。
此时我们已经构建好当前的静态库,现在该目录下就只有test.c
和我们刚才打包好的静态库。
首先我们使用gcc来编译我的C源文件:
图中错误表示,编译器在编译 test.c
文件时,它找不到名为 mymath.h
的头文件。 gcc
在编译 test.c
时找不到 mymath.h
头文件。因此我们需要在编译时告诉 gcc
在哪里可以找到这些头文件。我们使用-I
选项,告诉编译器在哪个目录下搜索头文件:
图中错误表示,当编译器编译 test.c
时,它看到了对 myAdd
函数的调用(很可能是在 main
函数中),但是在链接阶段,链接器找不到这个函数在何处定义。 -l
和 -L
选项来告诉链接器链接到 libmylib.a
静态库。-l
选项后面跟着库名(不带前缀 lib
和后缀 .a
),而 -L
选项后面跟着库文件的搜索路径。
注意:
在Linux系统上,动态库通常以
.so
为扩展名,静态库通常以.a
为扩展名。这些库文件在命名时,通常会遵循一个特定的模式,即前缀lib
,然后是库名,再是可选的版本信息,最后是文件扩展名(.so
或.a
)。例如:
libyaml-0.so.2
是一个为软链接,它指向实际的库文件libyaml-0.so.2.0.6
。libyaml-0.so.2.0.6
是实际的库文件。当我们谈论“库名”时,我们通常指的是去掉前缀
lib
、扩展名(.so
、.a
或.so.版本号
)以及任何版本信息之后的部分。因此,在这个例子中:
- 符号链接名(或称为“库引用名”):
libyaml-0.so.2
- 实际库文件名:
libyaml-0.so.2.0.6
- 库名(不包括版本):
libyaml
注意,库名
libyaml
是从文件名libyaml-0.so.2
或libyaml-0.so.2.0.6
中去掉lib
前缀、.so
或.so.版本号
后缀后得到的。这是因为在编写代码并链接到库时,通常会使用不包括这些前缀和后缀的库名。例如,在 C 或 C++ 中,会使用-lyaml
来链接到libyaml
库。在链接程序时,我们通常只需要指定库名(不包括前缀
lib
和后缀),链接器会自动在系统的库路径(如/lib
、/usr/lib
等)中查找相应的库文件。
动态库 (Dynamic Library) 是一种在程序运行时可以动态加载的库。它的格式为.so
(Shared Object) 在 Linux 系统中,动态库也称为共享库。
动态库是一种在程序运行时可以动态加载的库。这意味着动态库的内容(包括函数、变量和类等)并不在编译时被包含进程序本身,而是在程序运行时才根据需要被加载。因此,多个程序可以共享同一个动态库,从而节省内存空间。
我们来观察使用了动态库的程序:
ldd
命令检查了 a.out
的动态链接依赖,输出显示它依赖于 libc.so.6
(C标准库)和 ld-linux-x86-64.so.2
(动态链接器)。file
命令查看了 a.out
的文件类型信息,输出详细描述了它是一个64位的位置无关可执行文件(PIE),动态链接的,并且为 GNU/Linux 3.2.0 或更高版本设计。同时,输出还包含了构建ID(BuildID)和其他一些信息。从这些信息中,我们可以确认。a.out
是一个动态链接的可执行文件,这意味着它在运行时需要依赖其他动态库(如 libc.so.6
)。
那么动态库的原理是什么呢?
动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了内存空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存。
下面我们来理解动态库的使用过程:
使用了动态库的C/C++源文件需要通过编译链接才能被使用。那么我们通常使用gcc
来编译链接得到可执行程序,然后操作系统来运行它的可执行程序。
编译时的搜索路径——gcc需要
在编译程序时,如果程序引用了动态库中的函数或数据,编译器(如gcc
)需要知道这些函数或数据在哪些动态库中存在。这通常通过包含头文件(.h
文件)来实现,这些头文件声明了库中的函数和数据,并可能还包含了指定库文件名的预处理器指令。
然而,仅仅知道函数和数据的声明是不够的,编译器还需要知道这些函数和数据在动态库中的实际定义。这通常通过编译器选项来指定,我们将在后文详细描述该过程的实现方法。
因此,在编译时,编译器(如GCC)需要知道在哪里可以找到所需的动态库的头文件(.h
文件)以及用于链接的库文件。
运行时的库搜索路径——操作系统需要
当程序运行时,操作系统需要找到程序所依赖的动态库文件,并将它们加载到内存中,以便程序能够调用库中的函数。操作系统会按照一定的搜索顺序来查找这些库文件。
如果操作系统在所有这些路径中都没有找到所需的动态库文件,程序就会因为找不到依赖的库而无法正常运行。
编译是编译器的事情,运行是os的事情
这句话的意思是,编译程序是编译器的责任,编译器需要确保程序在语法和语义上是正确的,并且所有引用的函数和数据都有定义。而运行程序则是操作系统的责任,操作系统需要确保程序所需的所有资源(包括动态库文件)都是可用的,并且程序能够正确地执行。
编译器和操作系统在程序的生命周期中扮演着不同的角色,但它们之间需要密切合作,以确保程序能够正确地编译和运行。
编译与运行的区别
因此,编译时关注的是源代码和库的静态关系,而运行时关注的是程序与操作系统的动态交互。动态库在这两个过程中都扮演着重要角色,但需要在不同的阶段以不同的方式进行处理。
综上所述,我们可以得出下面的结论:运行时不需要头文件,只需要动态库。编译需要头文件和库。
在编译一个程序时,编译器需要头文件来解析函数、类和其他实体的声明,以确保源代码中使用的所有标识符都是已定义和可用的。此外,如果程序依赖于某个库中的函数或类,那么编译器还需要这个库的头文件,以便知道如何正确地链接到这些库中的函数或类。
但是,一旦程序被编译并链接成可执行文件,它就不再需要头文件了。在运行时,操作系统加载并执行这个可执行文件,它只需要与可执行文件相关联的动态库(如果有的话)。这些动态库包含了程序在运行时需要调用的函数和类的实现。
所以,简而言之:
- 编译时:需要头文件和库(静态库或动态库的头文件)。
- 运行时:只需要动态库(如果程序依赖于它们)。
注意,静态库在编译时会被链接到可执行文件中,因此运行时不需要额外的静态库文件。而动态库在运行时由操作系统加载到内存中,并与可执行文件一起使用。
下面我们再从进程地址空间的角度来理解:
首先我们理解一下动态库的本质:在多个系统进程中共享公共的代码和数据,只需要存一份。
当程序使用动态库时,操作系统会在内存中为动态库分配一块共享区域(即上图中的共享区),并将该区域映射到所有使用该动态库的进程的地址空间中。这样,当多个进程同时访问动态库中的函数或数据时,它们实际上是在访问同一块内存区域。这种共享机制使得动态库成为了一种非常高效的代码和数据复用方式。
动态库通常存放在系统的特定目录下,如 /usr/lib
、/usr/local/lib
或 /lib
。这些目录会被动态链接器(如 ld-linux.so
)和操作系统搜索以找到需要的库文件。
动态库加载过程
因此,不论多少程序使用,内存中只会存在该动态库指令的一份拷贝,实现了代码共享;在程序运行时才会去引用库中的相关函数,并不把这些函数的指令包含进去。
理解动态库动态链接和加载
虚拟地址空间不仅是操作系统要遵守的,编译器在编译程序时也需要遵守。
进程地址空间是多任务操作系统中每个进程所拥有的独立的、隔离的内存环境。在操作系统中,每个进程都运行在属于自己的地址空间。操作系统通过虚拟地址空间来管理物理内存和磁盘空间,实现内存的保护和隔离,确保每个进程都有独立的内存空间,并且只能访问自己的内存空间,不能访问其他进程的内存空间。
编译器在编译程序时,也需要遵守虚拟地址空间的规则。编译器将源代码转换为机器代码时,会生成程序所需要的虚拟地址空间布局。编译器需要确保程序在运行时能够正确地访问和操作虚拟地址空间中的数据和代码。编译器还需要考虑虚拟地址空间和物理地址空间之间的映射关系,以及如何处理内存不足等问题。
因此,虚拟地址空间是操作系统和编译器都需要遵守的重要概念。操作系统通过虚拟地址空间来管理内存和隔离进程,而编译器则通过虚拟地址空间来生成可执行的程序代码,并确保程序能够正确地访问和操作虚拟地址空间中的数据和代码。
优点:
缺点:
上图中,执行一系列步骤来创建一个动态库文件。该文件可以在多个程序之间共享,而不需要在每个程序中都包含相同的代码。
gcc -c -fPIC mystdio.c
gcc -c -fPIC mymath.c
这两条命令使用 gcc
来编译 *.c
源文件。-c
选项告诉 gcc
只编译源文件但不进行链接。-fPIC
选项告诉编译器生成位置无关代码(Position Independent Code),这是创建共享库所必需的。编译后的输出是一个名为 *.o
的目标文件。
gcc -shared -o libmyc.so mystdio.o mymath.o
这条命令使用 gcc
的 -shared
选项来创建一个共享库。-o libmyc.so
指定了输出文件的名称,即 libmyc.so
。然后,命令列出了要链接的所有目标文件:mystdio.o
和 mymath.o
。链接器将这些目标文件组合成一个共享库,该库可以在运行时由多个程序共享。
我们编写如下用于构建并组织构建静态库libmyc.so
的makfile:
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
%.o:%.c
gcc -c -fPIC $<
# mymath.o:mymath.c
# gcc -c -fPIC $<
# mystdio.o:mystdio.c
# gcc -c -fPIC $<
.PHONY:clean
clean:
rm *.o libmyc.so
现在,可以在其他C或C++程序中链接这个静态库,以便使用 mymath.o
和 mystdio.o
中定义的函数或变量。在链接时,需要告诉编译器和链接器静态库文件的路径和名称,通常使用 -L
和 -l
选项(对于gcc和g++)。
在编译程序时,需要在编译命令中指定包含头文件的目录(如果有的话)和库文件的搜索路径。使用 -I
选项指定头文件搜索路径,使用 -L
选项指定库文件搜索路径,使用 -l
选项指定库名(不包含前缀 lib
和后缀 .so
)。
gcc -o myprogram myprogram.c -I /path/to/headers -L /path/to/lib_test -lmyc
这里的 /path/to/lib_test
应该替换为实际存放 libmyc.so
的路径。/path/to/headers
应该替换为实际存放 *.h
的路径。
当程序运行时,操作系统需要知道在哪里可以找到 libmyc.so
。这通常通过以下几种方式之一实现:
2.1 LD_LIBRARY_PATH 环境变量
可以将包含 libmyc.so
的目录添加到 LD_LIBRARY_PATH
环境变量中。例如:
export LD_LIBRARY_PATH=/path/to/lib_test:$LD_LIBRARY_PATH
./myprogram
这种方法只影响当前终端会话。
2.2 将库安装到标准位置
如果希望多个程序都能使用这个库,可以将 libmyc.so
复制到标准库目录(如 /usr/lib
或 /usr/local/lib
)。这样,程序在系统启动时就可以自动找到库了。
sudo cp libmyc.so /usr/local/lib/
./myprogram
注意:在复制库文件到系统目录之前,请确保拥有相应的权限,并且了解这可能对其他系统用户产生影响。
示例
假设您已经将 libmyc.so
放在了 /path/to/lib_test
目录中,并且您有一个 myprogram.c
文件,可以这样编译和运行它:
# 编译 myprogram.c 并链接到 libmyc.so
gcc -o myprogram myprogram.c -L/path/to/lib_test -lmyc
# 设置 LD_LIBRARY_PATH 环境变量(仅在当前终端会话中有效)
export LD_LIBRARY_PATH=/path/to/lib_test:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/home/zyb/study_code/file_sys/lib_test/user
/mylib/lib:$LD_LIBRARY_PATH
# 运行程序
./myprogram
或者,如果已经将库安装到了标准位置并更新了缓存,那么可以直接运行程序而无需设置 LD_LIBRARY_PATH
。
下面我们进行实验,我们编写如下用于构建并组织构建静态库libmyc.a
的makfile:
#形成动态库
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
%.o:%.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm -rf *.o libmyc.so mylib
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -rf *.h mylib/include
cp -rf *.so mylib/lib
该makefile
与上文构建静态库类似,不再赘述。
下面我们来观察动态库动态搜索和链接过程:
编译未链接库:
首先尝试编译main.c
但没有链接到libmyc.so
库。因此,链接器报告了在main.c
中引用了未定义的函数myAdd
和my_fopen
。
解决方案:在编译时添加-L
选项指定库文件的位置,并使用-l
选项指定库名(注意,库名不需要前缀lib
和后缀.so
)。
链接时未找到库:
在第二次尝试中,添加了-lmyc
但忘记添加-L ./mylib/lib/
来指定库文件的搜索路径。因此,链接器无法找到libmyc.so
。
解决方案:在编译命令中同时添加-L
和-l
选项。
运行时找不到库:
当尝试运行myexe
时,系统报告找不到libmyc.so
库。这是因为运行时链接器(动态链接器)没有在默认的库搜索路径中找到该库。
解决方案:
1、直接把动态库复制到默认的库搜索路径中。
2、添加到环境变量 LD_LIBRARY_PATH
中。
这个环境变量指定了动态链接器(如 ld-linux.so
)在标准位置(如 /lib
和 /usr/lib
)之外搜索动态库(如 .so
文件在 Linux 上)的目录列表。当运行一个程序,并且该程序依赖于某个动态库时,动态链接器会首先查看 LD_LIBRARY_PATH
中列出的目录来查找这个库。如果找到了,则使用这个库;如果没有找到,则继续搜索标准位置。
3、建立软链接
假设动态库位于 /path/to/your/library/libmyserver.so
,并且您想将它链接到系统的标准库目录(例如 /usr/lib
)中。您可以使用 ln
命令来创建软链接。
sudo ln -s /path/to/your/library/libmyserver.so /usr/lib/libmyserver.so
4、设置系统配置文件
/etc/ld.so.conf.d/
目录在 Linux 系统中用于存放动态链接器的配置文件,这些文件告诉系统在哪里可以找到动态库。当系统需要加载一个共享库时,它会查看 /etc/ld.so.conf
文件(这个文件通常包含了指向 /etc/ld.so.conf.d/
目录下所有配置文件的指令)以及 /etc/ld.so.conf.d/
下的所有 .conf
文件,以确定库文件的搜索路径。
如果有一个自定义的库文件路径,并且希望在系统启动或运行任何需要该库的程序时,动态链接器都能找到这个库,我们可以创建一个新的 .conf
文件在 /etc/ld.so.conf.d/
目录下,添加我的库文件所在的目录路径。
添加完成后,运行 ldconfig
命令以更新动态链接器的缓存。这个命令会读取 /etc/ld.so.conf
和 /etc/ld.so.conf.d/
下的所有配置文件,并构建动态链接器的缓存。
删除配置文件后,ldd myexe
就not found。
首先我们对它们的使用的特点进行对比:
综上所述,动静态库各有优缺点,适用于不同的场景和需求。在选择使用哪种库时,需要根据具体的项目需求和环境条件进行权衡和考虑。
那么若动态库和静态库同时存在,会发生什么呢?
我们可以分析出,如果同时有动态库和静态库,默认使用动态库。
若此时要使用静态链接,需要加上 -static
选项。
下面我们把动态库移走,观察只有静态库的情况下会发生什么?
此时,我们移走了动态库,只剩下了静态库。那么只能对该库进行静态链接,但是程序不一定整体是静态链接的。
如果只有动态库,默认只能动态链接。若静态链接,会报错。
如果同时提供动态库和静态库,gcc默认使用动态库。如果想使用静态链接。需要加 static
使用。如果只有静态库,那我们的可执行程序只能进行静态链接,但是程序不一定整体是静态链接的。如果只有动态库,默认只能动态链接,若非要静态链接,会发生链接报错。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。