动态库实战:使用 gcc -shared 创建动态库
说实话,动态库这东西,我刚开始学的时候也觉得挺玄乎的。不就是个 .so 文件嘛,编译时加个 -shared 就完事了?后来踩了坑才明白,这里面的门道可不少。今天咱们就动手做一遍,把动态库的创建和位置无关代码(PIC)这事儿彻底搞明白。
一个最简单的动态库
先写个计算器模块,就两个函数:加法和乘法。代码很简单,但够用。
// calc.h
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int mul(int a, int b);
#endif
// calc.c
#include "calc.h"
int add(int a, int b) {
return a + b;
}
int mul(int a, int b) {
return a * b;
}
嗯,这里要注意:头文件里的函数声明前面,我习惯不加任何修饰。动态库的导出符号,默认就是全部可见的。如果你想控制哪些符号对外暴露,那是另一个话题,咱们后面再聊。
编译成动态库
关键命令来了:
gcc -shared -fPIC -o libcalc.so calc.c
这条命令干了三件事:
- -shared:告诉编译器,我要生成动态库,不是可执行文件
- -fPIC:生成位置无关代码,这是动态库的灵魂
- -o libcalc.so:输出文件名,约定俗成用 lib 开头,.so 结尾
你可能会问:不加 -fPIC 行不行?我试过,在某些平台上确实能编译通过。但千万别这么干!
位置无关代码(PIC)到底是个啥?
说白了,PIC 就是让代码里的地址引用不依赖固定的内存位置。你想想看,动态库被加载时,它的基地址是随机的——这是地址空间布局随机化(ASLR)的要求。如果代码里写死了某个变量的地址,加载到别处不就废了?
PIC 的实现方式,核心就是「间接跳转」。看个例子:
// 非 PIC 版本(绝对地址)
mov eax, [0x4000a0] // 直接访问固定地址
// PIC 版本(相对地址)
lea rbx, [rip + offset] // 通过当前指令指针 + 偏移量访问
mov eax, [rbx]
第二种写法,不管代码被加载到哪个地址,rip 始终指向当前指令,加上固定的偏移量,就能正确找到数据。这就是「位置无关」的精髓。
用动态库写个测试程序
库写好了,得验证一下能不能用:
// main.c
#include <stdio.h>
#include "calc.h"
int main() {
int x = 10, y = 20;
printf("add(%d, %d) = %d\n", x, y, add(x, y));
printf("mul(%d, %d) = %d\n", x, y, mul(x, y));
return 0;
}
gcc -o test main.c -L. -lcalc
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./test
这里有个小细节:-L. 告诉链接器在当前目录找库,-lcalc 链接 libcalc.so。运行时还得设置 LD_LIBRARY_PATH,否则系统找不到 .so 文件。
验证 PIC 的效果
咱们用工具看看动态库到底是不是 PIC 的:
readelf -d libcalc.so | grep TEXTREL
如果这条命令没输出,说明没有文本重定位,代码是纯 PIC 的。如果输出了 TEXTREL 条目,那就危险了——意味着代码段里有需要重定位的绝对地址。
再深入一点,看看符号表:
nm -D libcalc.so
输出里会看到 add 和 mul 函数,前面有个 T 标记,表示它们是导出的全局符号。
PIC 的性能代价
说实话,PIC 不是免费的午餐。每次访问全局变量或调用外部函数,都要多一次间接寻址。我做过测试,在 x86_64 上,PIC 版本比非 PIC 版本慢大约 5%-10%。
但这点性能损失,跟 ASLR 带来的安全性提升相比,完全值得。再说了,现代 CPU 的分支预测和缓存机制,已经把这部分开销压得很低了。
本章知识体系
下面这张图,把动态库创建和 PIC 的核心逻辑串起来了:
从这张图能看出来,整个流程的核心就是 PIC。没有它,动态库就像没有地基的房子——看着挺完整,一加载就塌。
避坑指南
- 我曾经犯过的错:在 Makefile 里只写了 -shared,忘了 -fPIC。结果在 x86 上跑得好好的,换到 ARM 上就段错误。后来我养成了习惯,写动态库的编译规则时,先把 -fPIC 写上,再补其他参数。
- 另一个坑:链接时用了 -static 还想着用动态库。这两个是互斥的,-static 会让链接器忽略所有 .so 文件。
- 还有:动态库的依赖链。如果你的 libcalc.so 依赖了 libmath.so,那链接 main.c 时也得把 libmath.so 带上,否则运行时符号找不到。
好了,动态库的创建和 PIC 原理,咱们就聊到这儿。动手试一遍,比看十遍文章都管用。记住那个命令:gcc -shared -fPIC -o libxxx.so xxx.c,以后写动态库,这就是你的起手式。
可可精品资料库