【Linux指南】动静态库系列(四):动态库制作与使用:-fPIC、-shared 和 .so 文件
文章目录
上一篇我们完成了静态库 .a 的制作和使用。静态库的特点是:链接阶段把库代码合并进可执行程序,运行时不再依赖 .a 文件。
动态库 .so 的思路不同:编译链接阶段只记录依赖关系,程序运行时再由动态链接器加载库代码。它更灵活、更节省空间,也是 Linux 系统中更常见的链接方式。
这篇文章先把动态库做出来,并理解 -fPIC、-shared、-I、-L、-l 的作用。运行期找不到 .so 的问题,下一篇会专门展开。
一、动态库的本质
动态库在 Linux 下通常以 .so 结尾,命名规则是:
libxxx.so
例如:
libc.so
libm.so
libpthread.so
libmyc.so
动态库的特点是:
程序运行时才把库加载到内存中,多个程序可以共享同一份库代码。
静态库和动态库第一层区别如下:
| 对比项 | 静态库 .a |
动态库 .so |
|---|---|---|
| 链接时机 | 编译链接时合并进程序 | 编译链接时记录依赖,运行时加载 |
| 可执行文件体积 | 较大 | 较小 |
| 运行时依赖 | 不依赖 .a |
依赖 .so 可被找到 |
| 多进程共享 | 不方便共享 | 可以共享库代码段 |
| 库更新 | 程序需重新链接 | 更灵活 |
动态库是现代 Linux 程序默认更常见的方式。
二、准备库代码
我们继续使用前面类似的示例。
目录结构:
shared_lib_demo/
├── include/
│ ├── my_string.h
│ └── my_math.h
├── src/
│ ├── my_string.c
│ └── my_math.c
└── test/
└── main.c
include/my_string.h:
#pragma once
int my_strlen(const char *s);
src/my_string.c:
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while (*end) {
end++;
}
return end - s;
}
include/my_math.h:
#pragma once
int add(int x, int y);
int sub(int x, int y);
src/my_math.c:
#include "my_math.h"
int add(int x, int y)
{
return x + y;
}
int sub(int x, int y)
{
return x - y;
}
test/main.c:
#include <stdio.h>
#include "my_string.h"
#include "my_math.h"
int main()
{
printf("len=%d\n", my_strlen("dynamic"));
printf("add=%d\n", add(1, 2));
printf("sub=%d\n", sub(9, 3));
return 0;
}
三、第一步:使用 -fPIC 生成位置无关目标文件
动态库的目标文件通常要使用 -fPIC 编译:
gcc -I./include -fPIC -c src/my_string.c -o my_string.o
gcc -I./include -fPIC -c src/my_math.c -o my_math.o
PIC 是 Position Independent Code,意思是位置无关代码。
为什么动态库需要位置无关代码?
因为动态库不是固定加载到某个地址。不同进程的地址空间使用情况不同,同一个 .so 可能被映射到不同虚拟地址区域。
如果动态库内部代码写死了绝对地址,那么换一个加载位置就可能无法正常运行。
所以动态库更希望使用:
相对地址 + 运行时修正
后面讲 GOT/PIC 时会深入解释。现在先记住:
制作动态库时,库源码一般先用 -fPIC 编译成位置无关目标文件。
四、第二步:使用 -shared 生成 .so
有了位置无关目标文件后,使用 -shared 生成动态库:
gcc -shared -o libmyc.so my_string.o my_math.o
这里的 -shared 表示生成共享库。
生成结果:
libmyc.so
可以使用 file 查看类型:
file libmyc.so
你会看到它是 ELF 共享目标文件,类似:
ELF 64-bit LSB shared object
这说明 .so 本质上也是 ELF 文件。后面讲 ELF 时会系统展开。
五、动态库不需要 ar
制作静态库时我们使用:
ar rcs libmyc.a my_string.o my_math.o
因为静态库是 .o 的归档包。
但制作动态库不使用 ar,而是直接由 gcc -shared 链接生成:
gcc -shared -o libmyc.so my_string.o my_math.o
原因是动态库不是简单归档包,它本身是一个可以被动态链接器加载的共享目标文件。
六、使用动态库编译程序
现在我们用 test/main.c 链接动态库:
gcc test/main.c -I./include -L. -lmyc -o main
含义仍然是:
-I./include:找头文件
-L.:找库文件
-lmyc:链接 libmyc.so 或 libmyc.a
如果当前目录中只有 libmyc.so,链接器会使用这个动态库。
如果同时有:
libmyc.so
libmyc.a
通常默认优先选择动态库。
七、使用 ldd 查看动态依赖
编译完成后,可以查看程序依赖哪些动态库:
ldd ./main
可能看到类似输出:
linux-vdso.so.1
libmyc.so => not found
libc.so.6 => /lib64/libc.so.6
/lib64/ld-linux-x86-64.so.2
这里有一个非常重要的点:
编译成功,不代表运行时一定能找到动态库。
链接阶段 gcc 通过 -L. 找到了 libmyc.so,所以可执行程序生成成功。
但运行阶段负责找库的是动态链接器,而不是刚才的 gcc 命令。动态链接器不一定会搜索当前目录,所以可能出现 not found。
这个问题下一篇专门讲。
八、临时运行方式:LD_LIBRARY_PATH
为了先验证动态库确实可用,可以临时设置环境变量:
export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH
./main
如果程序能正常输出,说明动态库本身没问题,只是运行期搜索路径没配置好。
但注意,LD_LIBRARY_PATH 通常是临时方案。关闭当前 shell 后,这个设置可能失效。
九、动态库和静态库同时存在时如何选择
假设当前目录下同时有:
libmyc.so
libmyc.a
执行:
gcc test/main.c -I./include -L. -lmyc -o main
通常会优先链接动态库。
如果希望强制静态链接,可以尝试:
gcc test/main.c -I./include -L. -static -lmyc -o main_static
但要注意:
-static会影响整体链接方式,不只是你自己的库。- 系统必须存在所需库的静态版本。
- 一些系统默认不安装完整静态库,因此可能链接失败。
所以初学阶段不要滥用 -static,先明确自己要验证什么。
十、动态库的优点
动态库的优点主要有三个。
1. 节省磁盘空间
可执行文件不需要包含完整库代码,只记录依赖关系和必要的链接信息,因此体积更小。
2. 节省内存
多个进程使用同一个动态库时,库的只读代码段可以在物理内存中共享。
例如多个程序都依赖 libc.so,系统没有必要为每个程序都在物理内存中保存一份完整 libc 代码。
3. 方便升级维护
如果动态库保持接口兼容,更新 .so 后,依赖它的程序可能不需要重新编译。
这也是系统库和大型项目普遍使用动态库的重要原因。
十一、动态库的缺点
动态库也有明显缺点。
1. 运行环境必须准备好
如果目标机器没有对应 .so,程序就会运行失败。
2. 版本可能不匹配
如果程序需要 libxxx.so.1,但系统只有不兼容的 libxxx.so.2,也可能失败。
3. 排错链路更长
静态库问题通常集中在编译链接阶段。动态库还多了运行期加载阶段,所以会出现:
编译通过,运行失败
这也是初学者最容易困惑的地方。
十二、动态库 Makefile 示例
可以用 Makefile 自动化构建动态库:
CC=gcc
CFLAGS=-I./include -Wall -fPIC
LIB=libmyc.so
OBJS=my_string.o my_math.o
$(LIB): $(OBJS)
$(CC) -shared -o $@ $^
%.o: src/%.c
$(CC) $(CFLAGS) -c $< -o $@
main: $(LIB)
$(CC) test/main.c -I./include -L. -lmyc -o main
clean:
rm -f *.o *.so main
output: $(LIB)
mkdir -p output/include output/lib
cp include/*.h output/include
cp $(LIB) output/lib
tar -czf myc-shared.tgz output
.PHONY: clean output
构建动态库:
make
构建测试程序:
make main
十三、静态库和动态库制作流程对比
| 步骤 | 静态库 | 动态库 |
|---|---|---|
编译 .o |
gcc -c xxx.c |
gcc -fPIC -c xxx.c |
| 生成库 | ar rcs libxxx.a *.o |
gcc -shared -o libxxx.so *.o |
| 使用方式 | gcc main.c -L. -lxxx |
gcc main.c -L. -lxxx |
| 运行期是否依赖库文件 | 不依赖 .a |
依赖 .so |
| 常见问题 | 编译链接错误 | 编译链接错误 + 运行期找库错误 |
可以看到,使用命令表面上很像,但背后的运行机制完全不同。
十四、总结
动态库 .so 的制作核心流程是:
.c -> -fPIC 生成 .o -> -shared 生成 .so -> 程序链接并在运行时加载
核心命令是:
gcc -fPIC -c xxx.c -o xxx.o
gcc -shared -o libxxx.so xxx.o
gcc main.c -I头文件路径 -L库文件路径 -lxxx -o main
这一篇我们已经能做出动态库,并能让程序在编译阶段链接它。但动态库真正麻烦的地方在运行阶段:为什么 gcc 明明找到了库,程序运行时却提示 libxxx.so => not found?
下一篇就专门解决这个问题。
欢迎来到FlagOS开发社区,这里是一个汇聚了AI开发者、数据科学家、机器学习爱好者以及业界专家的活力平台。我们致力于成为业内领先的Triton技术交流与应用分享的殿堂,为推动人工智能技术的普及与深化应用贡献力量。
更多推荐



所有评论(0)