亲爱的读者朋友们😃,此文开启知识盛宴与思想碰撞🎉。
快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。
在 Linux 系统编程中,动静态库是重要的组成部分,理解它们的原理、制作方法和使用技巧,对于开发者来说至关重要。
目录
一、动静态库基础概念
二、静态库的原理与操作
2.1 原理剖析
2.2 制作流程
2.3 使用方法及路径问题
2.4 errno 的作用
2.5 库的安装
三、动态库的原理与操作
3.1 原理与命令
3.2 动静态库分离实践
3.3 动态库加载问题
3.4 解决加载问题的方法
3.5 ncurses 库介绍
四、动态加载的底层原理
4.1 动态库加载机制
4.2 进程地址空间详解
4.3 动态库地址处理
一、动静态库基础概念
库是已编写好、可复用的代码,以二进制形式存在,能被操作系统载入内存执行,分为静态库和动态库。
- 静态库(.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.oar -rc $@ $^# 这是一个目标规则,目标是生成目标文件 mymath.o
# 此规则表明,生成 mymath.o 依赖于 mymath.c 文件
mymath.o:mymath.c# 使用 gcc 编译器将 mymath.c 文件编译成目标文件 mymath.o# -c 选项表示只进行编译,不进行链接# $^ 代表所有的依赖文件,即 mymath.cgcc -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.oar -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.ogcc -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】