Linux的动态库与静态库

发布于:2025-03-03 ⋅ 阅读:(19) ⋅ 点赞:(0)

目录

动静态库的基本原理

认识动静态库

 动静态库各自的特征

 静态库

动态库

动静态库与内存

静态库的加载方式

动态库的加载方式

加载到物理内存的细节

静态库的打包与使用

打包

使用

动态库的打包与使用

打包

使用


我以前写的一篇文章中就用网吧与在宿舍自己组装电脑解释了动静态库的区别,对其概念不清楚的可以看下面这篇文章。

Linux环境基础开发工具的使用(yum、vim、gcc、g++、gdb、make/Makefile)_linux gnu make之类-CSDN博客

动静态库的基本原理

动静态库的本质在于代码复用和模块化管理,所以我们可以称,动静态库是可执行程序的“半成品”

我们都知道,一堆源文件和头文件最终变为一个可执行程序需要经历以下四个步骤:

1. 预处理(Preprocessing)

  • 输入:源文件(.c)和头文件(.h)。

  • 输出:预处理后的文件(.i)。

  • 主要任务

    • 头文件展开:将 #include 指令替换为头文件的实际内容。

    • 宏替换:将所有的宏定义(#define)替换为对应的值。

    • 条件编译:根据条件编译指令(如 #if#ifdef)决定是否包含某些代码。

    • 去注释:删除所有的注释。

2. 编译(Compilation)

  • 输入:预处理后的文件(.i)。

  • 输出:汇编代码文件(.s)。

  • 主要任务

    • 词法分析:将源代码分解为 token(如关键字、标识符、运算符等)。

    • 语法分析:根据语法规则构建抽象语法树(AST)。

    • 语义分析:检查语义是否正确(如类型检查)。

    • 优化:对代码进行优化(如常量折叠、死代码消除)。

    • 生成汇编代码:将高级语言代码翻译成汇编指令。

3. 汇编(Assembly)

  • 输入:汇编代码文件(.s)。

  • 输出:目标文件(.o)。

  • 主要任务

    • 将汇编指令翻译成机器指令(二进制代码)。

    • 生成目标文件(.o),包含代码段、数据段、符号表等。

4. 链接(Linking)

  • 输入:目标文件(.o)和库文件(静态库 .a 或动态库 .so)。

  • 输出:可执行文件。

  • 主要任务

    • 符号解析:将每个目标文件中的符号(如函数名、变量名)绑定到具体的地址。

    • 地址重定位:根据符号的最终地址,修改目标文件中的引用。

    • 合并目标文件:将所有目标文件和库文件合并成一个可执行文件。

例如,用test1.c、test2.c、test3.c、test4.c以及main1.c形成可执行文件,我们需要先得到各个文件的目标文件test1.o、test2.o、test3.o、test4.o以及main1.o,然后再将这写目标文件链接起来,最终形成一个可执行程序。

如果我们在另一个项目当中也需要用到test1.c、test2.c、test3.c、test4.c和项目的main2.c或者main3.c分别形成可执行程序,那么可执行程序生成的步骤也是一样的。

注意:

一个可执行程序确实只能有一个入口点,也就是一个 main 函数。你提到的三个 .c 文件(test1.ctest2.cmain1.c)中,只有 main1.c 应该包含 main 函数,而其他文件(如 test1.c 和 test2.c)通常是一些辅助函数或模块的实现,它们会被 main 函数调用。 

但实际上,我们对于经常用到的源文件,比如上面三次都用到的test1.c、test2.c、test3.c、test4.c我们可以将它们的目标文件test1.o、test2.o、test3.o、test4.o进行打包,之后需要用到这四个目标文件时就可以之间链接这个包当中的目标文件了,而这个包实际上就可以称之为一个库。

实际上,所有库本质都是一堆目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的“半成品”。

认识动静态库

在Linux下创建文件编写以下代码,并生成可执行程序。

#include <stdio.h>

int main()
{
    printf("hello linux\n");
    return 0;
}

 这是最简单的代码,运行结果大家也都知道,就是hello linux。

下面我们就通过这份简单的代码来认识一下动静态库 

在这份代码当中,我们之所以可以通过调用printf来输出hello linux,主要原因是gcc/g++编译器在生成可执行程序时,将C标准库也链接进来了,也就是我们第一行#include <stdio.h>。 

再Linux下,我们可以通过指令,查看我们某个可执行程序所依赖的库文件

ldd 可执行程序名

这其中的libc.so.6就是该可执行程序所依赖的从C标准库库文件。剩下的两个分别是由内核提供的虚拟共享库,用于优化用户空间程序与内核之间的交互。和 Linux 系统的动态链接器,负责在程序启动时加载和链接动态库。

我们通过ls命令可以发现libc.so.6实际上只是一个软链接。 根据软连接的知识,我们得知libc.so.6这个软连接的源文件是libc-2.31.so

但实际上libc.so.6与libc-2.31.so是在同一目录下的,下面也给出了验证。

为了进一步了解,我们可以通过file 文件名命令来查看libc-2.31.so的文件类型。 

此时我们可以看到,libc-2.31.so实际上就是一个共享的目标文件库,准确来说,这还是一个动态库。 

  • 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
  • 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
  • 在 Linux 系统中,动态库的命名通常遵循以下规则:lib<库名>.so.<主版本号>.<次版本号>.<修订号>

这里可执行程序所依赖的libc.so.6实际上就是C动态库,更准确的来说可执行程序实际上依赖的是C动态库的一个软连接。当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字,也就是c.6。

而gcc/g++编译器默认都是动态链接的,若想进行静态链接,可以携带一个-static选项。

gcc -o test_s test.c -static

此时生成的可执行程序就是静态链接的了,可以明显发现静态链接生成的可执行程序的文件大小,比动态链接生成的可执行程序的文件大小要大得多。这也符合我们的猜想,也侧向证明了test是动态链接,test_s是静态链接。

静态链接生成的可执行程序并不依赖其他库文件,此时当我们使用ldd 文件名命令查看该可执行程序所依赖的库文件时就会看到以下信息。 

此外,当我们分别查看动静态链接生成的可执行程序的文件类型时,也可以看到它们分别是动态链接和静态链接的。

 动静态库各自的特征

 静态库

  • 定义:静态库在编译时直接嵌入到最终的可执行文件中。

  • 文件格式:通常为 .a(Unix/Linux)或 .lib(Windows)。

  • 优点

    • 独立性:不依赖外部库。

    • 性能:运行时无需加载,速度较快。

  • 缺点

    • 体积大:库代码被复制到每个可执行文件中。

    • 更新不便:库更新需重新编译整个程序。

动态库

  • 定义:动态库在程序运行时加载,可被多个程序共享。

  • 文件格式:通常为 .so(Unix/Linux)或 .dll(Windows)。

  • 优点

    • 节省空间:多个程序共享同一库。

    • 更新方便:更新库无需重新编译程序。

  • 缺点

    • 依赖性强:运行时需确保库存在且版本兼容。

    • 性能略低:运行时加载,稍有性能开销。

本质区别

  • 链接时机:静态库在编译时链接,动态库在运行时链接。

  • 内存占用:静态库增加可执行文件大小,动态库节省内存。

  • 更新维护:静态库更新需重新编译,动态库更新更灵活。

总结

  • 静态库:适合小型或独立程序。

  • 动态库:适合大型或需频繁更新的程序。

动静态库与内存

静态库的加载方式

  • 编译时:静态库的代码在编译时会被直接嵌入到可执行文件中。

  • 运行时

    • 静态库的代码已经包含在可执行文件中,因此不需要额外的加载操作

    • 当程序运行时,操作系统会直接将整个可执行文件(包括静态库的代码)加载到内存中。

    • 静态库的代码会成为可执行程序的一部分,占用程序的地址空间。

特点:

  • 独立性:可执行文件不依赖外部的库文件。

  • 内存占用:静态库的代码会被复制到每个使用它的可执行文件中,如果多个程序使用相同的静态库,会导致内存浪费。

  • 启动速度:由于不需要加载外部库,启动速度较快。


动态库的加载方式

  • 编译时:动态库的代码不会被嵌入到可执行文件中,而是记录动态库的符号信息和加载路径。

  • 运行时

    • 动态库的代码在程序运行时由操作系统的动态链接器(如 ld-linux.so)加载到内存中。

    • 动态库的代码会被映射到进程的地址空间中,但只有在需要时才会加载到物理内存(按需加载)。

    • 多个程序可以共享同一个动态库的代码,节省内存。

特点:

  • 依赖外部库:可执行文件依赖外部的动态库文件(如 libc.so.6)。

  • 内存占用:动态库的代码可以被多个程序共享,节省内存。

  • 启动速度:由于需要加载外部库,启动速度稍慢。

  • 更新方便:动态库可以独立更新,无需重新编译可执行文件。


加载到物理内存的细节

  • 静态库

    • 静态库的代码已经嵌入到可执行文件中,因此在程序加载时,静态库的代码会随着可执行文件一起加载到物理内存中。

    • 静态库的代码会占用程序的地址空间和物理内存。

  • 动态库

    • 动态库的代码在程序启动时并不会立即加载到物理内存中,而是通过内存映射(Memory Mapping)的方式映射到进程的地址空间中。

    • 只有当程序实际使用动态库中的代码时,操作系统才会将对应的代码加载到物理内存中(按需加载)。

    • 多个程序可以共享同一个动态库的物理内存页,从而节省内存。

假设我们有一个程序 myprogram,它依赖于一个库 libmylib

如果它依赖的是一个静态库,那么在编译的时候libmylib.a 的代码会被嵌入到 myprogram 中。

运行时myprogram 的代码和 libmylib.a 的代码一起加载到内存中。如果多个程序使用 libmylib.a,每个程序都会包含一份 libmylib.a 的代码。

但如果时动态库,libmylib.so 的代码不会被嵌入到 myprogram 中,而是记录依赖关系。

运行时操作系统会加载 libmylib.so 到内存中,并映射到 myprogram 的地址空间。如果多个程序使用 libmylib.so,它们可以共享同一份 libmylib.so 的代码。

静态库的打包与使用

为了更容易理解,下面自己实现一个动静态库的打包与使用,都以下面的四个文件为例,其中两个源文件add.csub.c,两个头文件add.hsub.h

add.h当中的内容如下:

#pragma once

extern int my_add(int x, int y);

add.c当中的内容如下:

#include "add.h"

int my_add(int x, int y)
{
	return x + y;
}

sub.h当中的内容如下:

#pragma once

extern int my_sub(int x, int y);

sub.c当中的内容如下:

#include "sub.h"

int my_sub(int x, int y)
{
	return x - y;
}

打包

第一步:让所有源文件生成对应的目标文件

gcc -c add.c
gcc -c sub.c

第二步:使用ar命令将所有目标文件打包为静态库 

ar命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。

  • -r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
  • -c(create):建立静态库文件。
ar -rc libcal.a add.o sub.o

此外,我们可以用ar命令的-t选项和-v选项查看静态库当中的文件。

  • -t:列出静态库中的文件。
  • -v(verbose):显示详细的信息。

 第三步:将头文件和生成的静态库组织起来

当我们把自己的库提供给他人使用时,通常需要提供两个关键文件夹:

  1. 头文件文件夹:包含所有公开的头文件(如 add.h 和 sub.h),供用户引用库中的函数声明和数据结构。

  2. 库文件文件夹:包含生成的库文件(如静态库 libcal.a 或动态库 libcal.so),供用户在链接时使用。

因此,在这里我们可以将add.hsub.h这两个头文件放到一个名为include的目录下,将生成的静态库文件libcal.a放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mathlib给别人使用了。

执行一下指令

mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include/
cp ./*.a mathlib/lib

然后,tree的目录如下 

使用Makefile简化上面操作 

当然,我们可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成静态库以及组织头文件和库文件时就可以一步到位了,不至于每次重新生成的时候都要敲这么多命令,这也体现了Makefile的强大。

代码如下:

myLib=libcal.a
CC=gcc

$(myLib): add.o sub.o
	ar -rc -o $(myLib) $^

%.o: %.c
	$(CC) -c $<

.PHONY: clean
clean:
	rm -f $(myLib) ./*.o

.PHONY: output
output:
	mkdir -p mathlib/include
	mkdir -p mathlib/lib
	cp ./*.h mathlib/include
	cp ./*.a mathlib/lib

 编写Makefile后,只需一个make就能生成所有源文件对应的目标文件进而生成静态库。

一个make output就能将头文件和静态库组织起来。

使用

创建源文件main.c,编写下面这段简单的程序来尝试使用我们打包好的静态库。

#include <stdio.h>
#include <add.h> // 引用我们刚才打包的静态库

int main()
{
	int x = 20;
	int y = 10;
	int z = my_add(x, y);
	printf("%d + %d = %d\n", x, y, z);
	return 0;
}

现在该目录下就只有main.c和我们刚才打包好的静态库。

方法一:使用选项

此时使用gcc编译main.c生成可执行程序时需要携带三个选项:

  • -I:指定头文件搜索路径。
  • -L:指定库文件搜索路径。
  • -l:指明需要链接库文件路径下的哪一个库。

 输入下面这段指令

gcc main.c -I./mathlib/include -L./mathlib/lib -lcal

此时就可以成功使用我们自己打包的库文件并生成可执行程序。 

补充一下:

  1. 因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
  2. 因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
  3. 实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
  4. -I,-L,-l这三个选项后面可以加空格,也可以不加空格。

 方法二:把头文件和库文件拷贝到系统路径下

既然编译器找不到我们的头文件和库文件,那么我们直接将头文件和库文件拷贝到系统所默认的路径下不就行了。 

指令:

sudo cp mathlib/include/* /usr/include/
sudo cp mathlib/lib/libcal.a /lib64/

需要注意的是,虽然已经将头文件和库文件拷贝到系统路径下,但当我们使用gcc编译main.c生成可执行程序时,还是需要指明需要链接库文件路径下的哪一个库。

指令:

gcc main.c -lcal

为什么之前使用gcc编译的时候没有指明过库名字? 

因为我们使用gcc(g++)编译的是C(C++)语言,而gcc就是用来编译C(C++)程序的,所以gcc编译的时候默认就找的是C(C++)库。

为什么需要使用 -l 选项

当你使用自定义库(如 libcal.a)时,编译器并不知道你需要链接哪个库。即使你指定了库文件的路径(使用 -L),链接器仍然需要知道具体的库名称。所以就要使用 -l 选项指定需要链接的库名称。链接器会根据 -l 指定的名称,在库文件搜索路径中查找对应的库文件。

小提示:
实际上我们拷贝头文件和库文件到系统路径下的过程,就是安装库的过程。但并不推荐将自己写的头文件和库文件拷贝到系统路径下,这样做会对系统文件造成污染。

动态库的打包与使用

打包

动态库的打包相对于静态库来说略有不同,但是大致还是一个道理都是打包。同样我们还用刚才的代码案例,四个文件。

第一步:让所有源文件生成对应的目标文件

但是这一步相对于静态库多了一个选项,要用源文件生成目标文件时需要携带-fPIC选项: 

  • -fPIC(position independent code):产生位置无关码。

指令:

gcc -fPIC -c add.c
gcc -fPIC -c sub.c

1. -fPIC 的作用

  • -fPIC:全称为 Position Independent Code(位置无关代码)。

  • 作用:告诉编译器生成与位置无关的代码。

    • 生成的代码中没有绝对地址,全部使用相对地址

    • 代码可以被加载到内存的任意位置,并且能够正确执行。


2. 为什么共享库需要 -fPIC

  • 共享库的特点

    • 共享库(.so)在运行时被加载到内存中,且加载的内存位置不固定。

    • 多个程序可以共享同一个共享库的代码段,从而节省内存。

  • 不加 -fPIC 的问题

    • 如果不加 -fPIC,共享库的代码段中会包含绝对地址。

    • 当共享库被加载到内存时,加载器需要根据实际加载的内存地址对代码段中的绝对地址进行重定位

    • 重定位会修改代码段的内容,导致每个使用该共享库的进程都需要维护一份独立的代码副本(因为加载的内存地址不同)。

    • 这会浪费内存,并且失去了共享库的意义。

  • 加 -fPIC 的好处

    • 生成的代码使用相对地址,不需要重定位。

    • 多个进程可以共享同一份代码段,节省内存。


3.  静态库不需要 -fPIC

  • 静态库的特点

    • 静态库(.a)在编译时直接嵌入到可执行文件中。

    • 可执行文件加载到内存时,静态库的代码段地址是固定的。

  • 为什么静态库不需要 -fPIC

    • 静态库的代码段地址在编译时就已经确定,不需要在运行时重定位。

    • 因此,静态库的代码可以使用绝对地址,不需要位置无关代码。


4. 不加 -fPIC 的共享库

  • 不加 -fPIC 的共享库

    • 如果不加 -fPIC 编译共享库,生成的代码会包含绝对地址。

    • 当共享库被加载到内存时,加载器需要根据实际加载的内存地址对代码段进行重定位。

    • 每个使用该共享库的进程都需要维护一份独立的代码副本(因为加载的内存地址不同)。

    • 这会浪费内存,并且失去了共享库的意义。

第二步:使用-shared选项将所有目标文件打包为动态库 

与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。 

指令:

gcc -shared -o libcal.so add.o sub.o

第三步:将头文件和生成的动态库组织起来 

与生成静态库时一样,为了方便别人使用,在这里我们可以将add.hsub.h这两个头文件放到一个名为include的目录下,将生成的动态库文件libcal.so放到一个名为lib的目录下,然后将这两个目录都放到mlib下,此时就可以将mlib给别人使用了。 

指令:

mkdir -p mlib/include
mkdir -p mlib/lib
cp ./*.h mlib/include/
cp ./*.so mlib/lib

最后tree一下,这就是我们打包好的动态库

使用Makefile 

当然,生成动态库也可以将上述所要执行的命令全部写到Makefile当中,后续当我们要生成动态库以及组织头文件和库文件时就可以一步到位了。 

编写Makefile后,只需一个make就能生成所有源文件对应的目标文件进而生成动态库。

一个make output就能将头文件和动态库组织起来。

使用

我们还是用刚才使用过的main.c来演示动态库的使用。

#include <stdio.h>
#include <add.h> // 引用我们刚才打包的动态库

int main()
{
	int x = 20;
	int y = 10;
	int z = my_add(x, y);
	printf("%d + %d = %d\n", x, y, z);
	return 0;
}

同样该目录下就只有main.c和我们刚才打包好的动态库。

说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I-L-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。

此时使用gcc编译main.c生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库。

指令:

gcc main.c -I./mlib/include -L./mlib/lib -lcal

与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。

 需要注意的是,我们在编译阶段使用-I-L-l这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁。但是,在编译完成后,生成的可执行程序与编译器无关。此时就编译器的工作完成后就交付给操作系统了。操作系统的动态链接器(如 ld-linux.so)负责加载和运行程序。但是操作系统就说,你光告诉了编译器,现在你又让我去工作,但是你不告诉我去哪?干什么?我给你干个迪奥啊。

同样我们可以使用 ldd 指令查看一下。也确实操作系统确实没有找到所连接的动态库。

解决该问题的方法有以下三个:

方法 1:将动态库路径添加到 LD_LIBRARY_PATH

LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。

然后正常gcc,就可以运行了。

方法 2:将动态库复制到系统的默认库路径:

运行指令:

sudo cp mlib/lib/libcal.so /usr/lib

需要注意的是,不同的系统,默认库路径不同。我的是Ubuntu 

 删除:

sudo rm /lib64/libcal.so

查看是否删除成功:

ls /lib64 | grep libcal.so