亲爱的读者朋友们😃,此文开启知识盛宴与思想碰撞🎉。
快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。
在 Linux 系统编程中,动静态库是重要的组成部分,理解它们的原理、制作方法和使用技巧,对于开发者来说至关重要。
目录
一、动静态库基础概念
库是已编写好、可复用的代码,以二进制形式存在,能被操作系统载入内存执行,分为静态库和动态库。
- 静态库(.a)在程序编译链接时,代码被链接到可执行文件中(因此一般文件内存会更大),程序运行时不再依赖静态库;
- 动态库(.so)即 “Shared Object” 的缩写,在程序运行时才链接代码,多个程序可共享使用。
📌注意:
- 一个可执行程序可能用到许多的库,这些库运行时有的是静态库,有的是动态库。我们的编译默认使用动态链接库,只有在该库下找不到动态
.so
文件时,才会采用同名静态库。我们也可以使用gcc
的-static
选项强制设置链接静态库。
gcc -static main.c -o main_static
二、静态库的原理与操作
2.1 原理剖析
静态库的原理是将相关源文件编译为.o 目标文件,再打包成 libXXX.a 文件。
当主程序的 main.c 编译为 main.o 时,会自动与静态库链接形成可执行程序。在这个过程中,ar 命令用于生成静态库,如ar -rc libmymath.a add.o sub.o
,其中-rc
选项表示若目标静态库文件存在则替换,不存在则创建。
📌解释:
ar -rc libmymath.a add.o sub.o
这条命令的作用是,把add.o
和sub.o
这两个目标文件添加到libmymath.a
静态库中。如果libmymath.a
库文件不存在,就会创建一个新的;如果已经存在,就把add.o
和sub.o
插入到库中,若库中已有同名文件,会用新文件替换旧文件。而且,在创建或更新库文件的过程中不会显示提示信息。
2.2 制作流程
以制作一个简单的数学库为例,先编写源文件,如 mymath.c 定义数学运算函数,mymath.h 声明函数。然后编写 Makefile 文件,内容如下:
# 定义一个变量 lib,其值为静态库的文件名 libmymath.a
lib=libmymath.a
# 这是一个目标规则,目标是生成静态库 libmymath.a
# 此规则表明,生成 libmymath.a 依赖于 mymath.o 文件
$(lib):mymath.o
# 使用 ar 命令将 mymath.o 文件打包成静态库 libmymath.a
# $@ 代表目标文件,即 libmymath.a
# $^ 代表所有的依赖文件,即 mymath.o
ar -rc $@ $^
# 这是一个目标规则,目标是生成目标文件 mymath.o
# 此规则表明,生成 mymath.o 依赖于 mymath.c 文件
mymath.o:mymath.c
# 使用 gcc 编译器将 mymath.c 文件编译成目标文件 mymath.o
# -c 选项表示只进行编译,不进行链接
# $^ 代表所有的依赖文件,即 mymath.c
gcc -c $^
# .PHONY 声明了一些伪目标,伪目标不是真正的文件,而是一个动作
# 这里声明了 clean 为伪目标,即使当前目录下存在名为 clean 的文件,make 也会执行 clean 对应的命令
.PHONY:clean
# 这是一个伪目标规则,用于清理生成的中间文件和静态库文件
# 执行 make clean 命令时,会删除当前目录下所有的 .o 文件和 .a 文件
clean:
rm -f *.o *.a
执行 Makefile 文件,就能生成静态库。之后可将库文件和头文件整理到相应目录,方便分发使用。
2.3 使用方法及路径问题
⭐ 使用静态库时,需注意头文件和库文件的路径设置。若头文件路径未指定,gcc 默认在系统路径(如 /usr/include)和当前目录查找。可通过-I
选项指定头文件搜索路径,如gcc main.c -I./lib/include
。对于库文件,gcc 默认在系统路径(如 /lib64、/usr/lib)和当前目录查找,若找不到,需用-L
选项指定库路径,如gcc main.c -L./lib/mymathlib
。此外,还需用-l
选项指明要链接的库名(去掉前缀 lib 和后缀.a),如gcc main.c -I./lib/include -L./lib/mymathlib -lmymath
。
-I
: 指定头文件搜索路径-L
: 指定库路径-l
: 指定库名- 测试目标文件生成后,静态库删掉,程序照样可以运行
- 库文件名称和引入库的名称:去掉前缀
lib
,去掉后缀.so
、.a
,如:libc.so
->c
2.4 errno 的作用
库中常提供全局变量 errno 用于指示运行时错误。如在数学库的除法函数中,当除数为 0 时,可设置 errno 表示错误,方便调用者判断结果正确性及获取错误原因。
2.5 库的安装
库的安装本质上是将库文件和头文件放置到系统可查找的路径下,可通过拷贝文件或建立软连接实现。如将头文件拷贝到 /usr/include,库文件拷贝到 /lib64 ,之后编译时只需用-l
选项指定库名即可。但不建议随意将第三方库安装到系统路径,以免污染系统库。
三、动态库的原理与操作
3.1 原理与命令
动态库的制作同样先将源文件编译为.o 文件,但编译时需添加-fPIC
选项,用于生成位置无关码,使库能在虚拟内存的任意位置加载。生成动态库使用gcc -shared -o libmymethod.so *.o
命令,-shared
选项告诉 gcc 生成共享库。
📌解释:
- 假设当前目录下有
add.o
和sub.o
两个目标文件,执行gcc -shared -o libmymethod.so *.o
命令后,gcc
会将add.o
和sub.o
链接成一个名为libmymethod.so
的动态链接库。
3.2 动静态库分离实践
在实际开发中,可将动静态库分离管理。通过编写 Makefile 文件,分别生成静态库和动态库,如:
# 定义动态库变量,值为动态库文件名libmymethod.so
dy-lib=libmymethod.so
# 定义静态库变量,值为静态库文件名libmymath.a
static-lib=libmymath.a
# 声明all为伪目标,伪目标不是真正的文件,用于定义一组操作
.PHONY:all
# all目标,依赖于动态库和静态库的生成,执行make或make all时会构建这两个库
all: $(dy-lib) $(static-lib)
# 构建静态库libmymath.a的规则,依赖于mymath.o文件
$(static-lib):mymath.o
# 使用ar工具将mymath.o打包成静态库libmymath.a
# $@代表目标文件,即libmymath.a
# $^代表所有的依赖文件,即mymath.o
ar -rc $@ $^
# 从mymath.c生成mymath.o的规则
mymath.o:mymath.c
# 使用gcc编译器将mymath.c编译成目标文件mymath.o
# -c选项表示只编译不链接
gcc -c $^
# 构建动态库libmymethod.so的规则,依赖于mylog.o和myprint.o文件
$(dy-lib):mylog.o myprint.o
# 使用gcc编译器生成共享库(动态库)libmymethod.so
# -shared选项用于生成共享库
# $@代表目标文件,即libmymethod.so
# $^代表所有的依赖文件,即mylog.o和myprint.o
gcc -shared -o $@ $^
# 从mylog.c生成mylog.o的规则
mylog.o:mylog.c
# 使用gcc编译器将mylog.c编译成目标文件mylog.o,并生成位置无关码
# -fPIC选项用于生成位置无关码,使生成的目标文件可用于创建共享库
gcc -fPIC -c $^
# 从myprint.c生成myprint.o的规则
myprint.o:myprint.c
# 使用gcc编译器将myprint.c编译成目标文件myprint.o,并生成位置无关码
# -fPIC选项用于生成位置无关码,使生成的目标文件可用于创建共享库
gcc -fPIC -c $^
# 声明clean为伪目标,用于清理生成的文件
.PHONY:clean
# clean目标,执行make clean时会删除所有的.o、.a、.so文件以及mylib目录
clean:
rm -rf *.o *.a *.so mylib
# 声明output为伪目标,用于整理和输出库文件及头文件
.PHONY:output
# output目标,执行make output时会创建mylib目录结构,并将头文件、静态库和动态库文件复制到相应位置
output:
# 创建mylib/include目录,如果目录已存在则不报错
mkdir -p mylib/include
# 创建mylib/lib目录,如果目录已存在则不报错
mkdir -p mylib/lib
# 将所有.h头文件复制到mylib/include目录
cp *.h mylib/include
# 将所有.a静态库文件复制到mylib/lib目录
cp *.a mylib/lib
# 将所有.so动态库文件复制到mylib/lib目录
cp *.so mylib/lib
这样可清晰管理不同类型的库,提高项目的可维护性。
3.3 动态库加载问题
生成可执行程序后,运行时可能出现找不到动态库的情况,如这是因为系统加载器在运行时找不到动态库路径,即使编译时指定了库路径,运行时也需重新告知系统。
3.4 解决加载问题的方法
- 解决动态库加载问题有多种方法。可将动态库拷贝到系统默认库路径(如 /usr/lib64);
- 也可在系统默认库路径建立软连接,如
sudo ln -s /home/whb/108/Lesson24/test/mylib/lib/libmymethod.so /lib64/libmymethod.so
;- 还能将库所在路径添加到环境变量 LD_LIBRARY_PATH 中,但该方法在关闭 shell 后失效,若想长久生效,需将环境变量写入系统启动配置文件(如~/.bash_profile);
- 另外,可在 /etc/ld.so.conf.d 目录下创建配置文件,添加动态库路径后执行 ldconfig 重新加载。
3.5 ncurses 库介绍
ncurses 是基于终端的图形库,可用于控制终端界面。在 CentOS 系统中,可通过
sudo yum install ncurses-devel
命令安装,安装后开发者能利用其丰富功能创建交互式终端应用程序,提升用户体验。
四、动态加载的底层原理
4.1 动态库加载机制
当 CPU 执行代码时,若调用库函数,会先在共享区查找。若库文件未加载进内存,会触发缺页中断,系统将动态库文件加载进来,并建立与页表的映射关系。此后,程序在进程地址空间中执行。系统会管理多个动态库的加载情况,确保程序正确运行。
进程如何看到动态库的:
进程间如何共享库的:
4.2 进程地址空间详解
程序编译好后,内部已有地址概念,采用平坦模式编址(0 - 4GB),此时的地址为逻辑地址,也是虚拟地址。编译形成可执行程序时,会生成存储各段地址的表,表头地址即 main 函数地址,CPU 从该地址开始执行。执行过程中,CPU 读取的指令可能包含数据或虚拟地址。若虚拟地址在页表中无映射关系,会引发缺页中断,将所需内容加载进内存并建立映射。
4.3 动态库地址处理
由于动态库可能有多个,难以保证每个库都加载到固定位置,因此采用相对编址方式。gcc 的-fPIC
选项让编译器用偏移量对库中函数编址,库加载时,通过起始地址加偏移量的方式找到库函数,实现库在虚拟内存任意位置的加载!
动静态库的各个环节紧密相连,就像一条完整的 “生产线”,每个环节都不可或缺,共同支撑着 Linux 系统编程的高效运行🚀。
欢迎关注我👉【A charmer】