新竹高于旧竹枝,全凭老干为扶持。 - 《新竹》(郑燮)
2024.8.25
目录
1、静态库和动态库简介
静态库(.a):
动态库(.so):
动态库和静态库的比较:
2、静态库的生成和使用(Static Library)
编写头文件声明库函数并编译生成目标文件(.o)
使用ar命令将目标文件打包成静态库
编译使用静态库的程序文件
使用makefile自动构建工具构建静态库
使用静态库
安装库
使用库
运行使用库的应用程序
注意事项
3、动态库的生成和使用(Dynamic Library)
编写头文件声明库函数并编译生成目标文件(.o)
编译源代码为对象文件:
使用gcc将目标文件打包成静态库打包成动态库:
编译使用动态库的程序并运行:
后记:理解程序的编译、执行与库函数调用机制
编译过程:从源代码到可执行文件
编译器的角色
动态库的加载与执行
静态库与动态库的区别
在Linux系统中,库(Library)是一种重要的组件技术,它封装了数据和函数供用户程序调用。库主要分为静态库和动态库两种类型,它们在编译、链接和运行时有不同的特点和使用方式。
1、静态库和动态库简介
静态库(.a):
-
- 静态库是一种编译时链接的库,它在编译期间将库中的代码复制到最终的可执行文件中。
- 这意味着静态库的代码在编译时就已经被包含在可执行文件中,运行时不再需要静态库文件。
- 优点是可执行文件独立,可以在没有静态库文件的情况下运行。
- 缺点是可执行文件体积较大,因为包含了库的所有代码。
动态库(.so):
-
- 动态库是一种运行时链接的库,它在程序运行时才被加载和链接。
- 动态库的代码不会被复制到可执行文件中,可执行文件只包含对动态库中函数的引用。
- 优点是可以被多个程序共享,节省了磁盘空间,并且可以方便地更新库而不需要重新编译程序。
- 缺点是程序运行时需要动态库文件,如果库文件缺失或版本不兼容,程序可能无法运行。
动态库和静态库的比较:
- 载入顺序:静态库在编译时链接,动态库在运行时链接。
- 大小与共享:静态库生成的可执行文件较大,不共享;动态库生成的可执行文件较小,可共享。
- 库函数调用:静态库直接包含代码,动态库在运行时解析函数地址。
选择使用静态库还是动态库取决于具体的应用场景和需求。静态库适合于不需要频繁更新,且对可执行文件大小不敏感的场景;而动态库适合于需要共享库代码,或者库更新频繁的场景。
2、静态库的生成和使用(Static Library)
静态库在编译时将库代码嵌入到可执行文件中,生成的可执行文件较大,但运行时不再需要依赖库文件。静态库的文件后缀名为.a
,例如libmylib.a
。
我们现在希望创建一个属于自己的库,实现加减乘除四个函数创建静态库的步骤大致包括:
编写头文件声明库函数并编译生成目标文件(.o
)
将add.c / sub.c / mul.c / div.c 通过gcc生成目标 .o 文件(gcc默认生成同名.o文件)
gcc -c add.c sub.c mul.c div.c
使用ar
命令将目标文件打包成静态库
ar -rcs libmymath.a add.o sub.o mul.o div.o
👋
ar
:静态库创建工具。
rcs
:选项的含义如下:
r
:替换已存在的库中的文件或插入新文件。
c
:创建库时不显示成员文件信息。
s
:创建索引,便于快速访问库中的成员文件。
libmymath.a
:你想要创建的静态库文件名。
add.o
和sub.o
:你想要包含在库中的目标文件。
编译使用静态库的程序文件
通过gcc
的-l
选项指定库名(不包括lib
前缀和.a
后缀),并使用-L
选项指定库文件的路径。
gcc -o myprogram main.c -L. -lmymath
使用makefile自动构建工具构建静态库
lib = libmymath.a
libdir = mymathlib
$(lib):add.o sub.o
ar -rcs $@ $^
add.o:add.c
gcc -c $^
sub.o:sub.c
gcc -c $^
mul.o:mul.c
gcc -c $^
div.o:div.c
gcc -c $^
.PHONY:clean
clean:
rm -rf *.o *.a
.PHONY:bag
bag:
mkdir -p lib/include
mkdir -p lib/$(libdir)
cp *.h lib/include
cp *.a lib/$(libdir)
打包完成后,将lib目录压缩即可发布属于自己的静态包~~
使用静态库
您提供的 Makefile 片段定义了一个名为 libmymath.a
的静态库的构建过程,以及一些清理和组织库文件的规则。要使用这个静态库,您需要遵循以下步骤:
测试程序:
#include<stdio.h>
#include<add.h>
#include<sub.h>
int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("a + b = %d\n", add(a,b));
printf("a - b = %d\n", sub(a,b));
return 0;
}
安装库
这将创建 lib/include
和 lib/mymathlib
目录,并将头文件(*.h
)复制到 lib/include
,将库文件(*.a
)复制到 lib/mymathlib
。
使用库
要使用这个库,您需要在编译您的应用程序时指定库的头文件路径和库文件路径。假设您的应用程序 main.c
使用了这个库,您可以这样编译它:
gcc -I./lib/include -L./lib/mymathlib -o main main.c -lmymath
这里的命令参数解释如下:
-I./lib/include
:告诉编译器在lib/include
目录下查找头文件。-L./lib/mymathlib
:告诉链接器在lib/mymathlib
目录下查找库文件。-o main
:指定输出的可执行文件名为main
。-lmymath
:告诉链接器链接名为mymath
的库(注意.a
后缀和lib
前缀被省略)。
运行使用库的应用程序
./main
注意事项
- 确保在编译和链接应用程序时,库文件和头文件的路径是正确的。
- 我们已经您将库安装到了系统的标准目录,不需要
-L
和-I
选项,因为编译器和链接器会自动在这些目录下查找,但是不建议将自己写的库放在标准目录(环境变量覆盖的)目录下。
3、动态库的生成和使用(Dynamic Library)
创建动态库是一个涉及编译和链接过程的任务,通常使用C语言进行。以下是创建包含add.c
和sub.c
两个函数文件的动态库的基本步骤:
编写头文件声明库函数并编译生成目标文件(.o
)
-
add.c
包含一个函数,比如int add(int a, int b)
,用于计算两个整数的和。sub.c
包含一个函数,比如int sub(int a, int b)
,用于计算两个整数的差。mul.c
包含一个函数,比如int mul(int a, int b)
,用于计算两个整数的乘积。div.c
包含一个函数,比如int div(int a, int b)
,用于计算两个整数的商。
编译源代码为对象文件:
使用编译器(如gcc)编译每个源文件,生成对应的对象文件(.o)。
gcc -c *.c
使用gcc将目标文件打包成静态库打包成动态库:
将编译后的对象文件链接成动态库。在Unix-like系统中,通常使用-shared
和-fPIC
(Position Independent Code)选项来创建动态库。
gcc -shared -o libmymath.so *.o
这里libmymath.so
是生成的动态库文件名,你可以根据需要更改它。
编译使用动态库的程序并运行:
编译时不需要链接动态库,只需在运行时加载。
gcc main.c -L 存放库的目录 -I存放头文件的目录 -lmymath -o main
可执行程序能够正常生成,但运行可执行程序时出现报错:
error while loading shared libraries:
libmymath.so: cannot open shared object file:
No such file or directory
原因是动态库必须在系统默认路径下存在库和头文件的软链接才能在运行时找到库和头文件中的定义。
建立软链接:
sudo ln -s /home/lhy/linux/05基础IO/动态库/dir/libmymath.so /lib/libmymath.so
sudo ln -s /home/lhy/linux/05基础IO/动态库/dir/mymath.h /usr/include/mymath.h
注意:不同系统默认存放库和头文件的路径不同,我这里是Ubuntu系统,库的路径是/lib
,头文件的默认路径是/usr/include
(当时上网找教程找的是centos系统的,路径和系统不同,后面才发现哈哈哈,大家注意避坑~~)
解决了软链接的问题,程序就能正常运行啦~~
后记:理解程序的编译、执行与库函数调用机制
编译过程:从源代码到可执行文件
程序的生命周期始于源代码,终于可执行文件。源代码是程序员用高级语言编写的指令集合。通过编译器,这些指令被转换成汇编语言,再进一步转换成机器码。在这一过程中,每个指令都被赋予了一个逻辑地址,这个地址是程序在内存中的虚拟地址。
逻辑地址是程序地址空间中的一个抽象概念,它与物理内存地址相对应,但并不直接映射。这种设计允许操作系统灵活地管理内存,实现虚拟内存技术。
编译器的角色
编译器不仅负责语言的转换,还负责生成程序的入口地址,这个地址存储在可执行文件的文件头中。它是程序执行的起点,也是CPU开始执行指令的地方。
CPU通过程序计数器(PC)寄存器来追踪当前需要执行的指令地址。当程序加载到内存后,操作系统会建立页表,将逻辑地址映射到物理地址。CPU使用这些映射来找到并执行指令。
动态库的加载与执行
动态库是程序执行中不可或缺的一部分。它们提供了程序所需的函数和资源,但是与传统的理解不同,动态库并不需要固定加载到内存的特定位置。
在动态库内部,不采用绝对编址,而是采用相对编址,对于动态库中的函数只需要知道其在库中的偏移量即可。这意味着库中的每个函数都有一个相对于库起始位置的偏移量。这样,无论库被加载到内存的哪个位置,只要加上这个偏移量,就能找到正确的函数地址。
这就是为什么,在使用gcc生成动态库的时候,需要加上-fPIC
选项用于生成位置无关代码,这对于动态库尤其重要。它允许动态库在内存中灵活加载,而不需要在程序启动时确定其位置。
静态库与动态库的区别
静态库在编译时被复制到可执行文件中,使用绝对编址。这意味着静态库的大小会直接影响可执行文件的大小。而动态库则在程序运行时按需加载,节省了磁盘空间和内存。