Unit 系统编程手册-(41-42) 共享库基础
一、静态库 Vs 共享库 优缺点
静态库 | 共享库 |
---|---|
可靠,已经包含运行所需的全部库,与系统无关 | 运行之前需要确保相关共享库已经存在 |
加载速度更快 | 需要运行之前依次检索、加载所需的共享库,以及符号的重新定位 |
每次静态库改动,相关引用该库程序都需要重新编译 | 运行时动态加载,所以不需要重新编译 |
浪费磁盘,每被引用一次就会生成一次副本 | 运行是加载,所以不需要 |
浪费内存,每次运行都会在内存生成一次副本 | 只会在内存中生成一次副本 |
库更加简单 | |
编译时必须使用位置独立的代码,带来额外性能开销 |
二、静态库简单应用
2.1 创建、维护
使用 ar
命令进行
#创建
cc -g -c mod1.c mod2.c mod3.c
ar r libdemo.a mod1.o mod2.o mod3.o
#打印
ar tv libdemo.a
#删除
ar d libdemo.a mod3.o
2.2 链接
标准库:
/usr/lib 、/lib 、/usr/local/lib
#库当前目录
cc -g -o prog prog.o libdemo.a
#库位于标准库 使用 -l 去掉 .a
cc -g -o prog prog.o -ldemo
#指定路径 -L 文件路径 -l 库名称
cc -g -o prog prog.o -Lmydir -ldemo
三、共享库使用
3.1 创建
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o
#或则
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so
-fPIC
指定编译器生成位置独立的代码,因为链接的时候无法直到共享库位于内存的何处
#方式一: 是否使用 -fPIC 选项
nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_
#方式二: 若输出信息,则说明至少有一个目标模块没有使用 -fPIC 选项
objump --all-headers libfoo.so | grep TEXTREL
readelf -d libfoo.so | grep TEXTREL
3.2 共享库 soname
目的: 提供一层间接,使得程序能够运行时使用与链接时使用的库不同但兼容的共享库
# 1 创建
gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c
# 2 创建 soname: 将 libfoo.so 的 soname 设为 libbar.so
gcc -g -shared -Wl,-soname,libbar.so -o libfoo.so mod1.o mod2.o mod3.o
# 3 检查 soname: 确定一个既有共享库 soname,以下命令二选一
objump -p libfoo.so | grep SONAME
readelf -d libfoo.so | grep SONAME
# 4 soname 嵌入程序: 编译器检测到 soname 就将 soname 顶替 libfoo.so 嵌入程序中
gcc -g -Wall -o prog prog.c libfoo.so
# 5 创建 soname: 创建符号链接
ln -s libfoo.so libbar.so
3.3 共享库小工具
ldd
命令:显示一个程序所需共享库
objump
/readelf
命令:获取各类信息(包括反汇编二进制码),从可执行文件、库,显示 ELF 节头部信息nm 命令:列出目标库或可执行程序中定义的一组符号
nm -A /usr/lib/lib*.so 2> /dev/null | grep 'crypt$'
3.4 命名规则
真实库命名:libname.so.major_id.minor_id(次要版本号.补丁号) 如libdemo.1.0.1
soname 命名: libname.so.major_id 如 libdemo.so.1 -> libdemo.so.1.0.2
连接器命名:libname.so -> libdemo.so.1(指向 soname 常见,指向真实库也可)
3.5 安装共享库
方式一:(不推荐)LD_LIBRARY_PATH 所有用户都可以使用
方式二:标准库中
方式三:使用 ldconfig
ldconfig :解决库位置分散带来的加载缓慢和 soname 链接符号因为库迭代而失效
1 ldconfig 会依次检索 /etc/ld.so.conf中指定目录、/lib、/usr/lib 生成 /etc/ld.so.cache (ldconfig -p 打印)
2 检查每个库主要版本的最新次要版本(最大)找出嵌入的 soname,在同一目录创建相对符号链接(命名符合规范) -N 防止缓存重建 -X 阻止 soname 符号链接创建
方式四:目标文件中指定库检索 -rpath
1 强制指定
gcc -g -Wall -Wl,rpath,/home/mtk/pdir -o prog prog.c libdemo.so
DT_RPATH (优先级高于 LD_LIBRARY_PATH) / DT_RUNPATH (低于LD):
默认情况下,链接器会创建 DT_RPATH 标签,--enable-new-dtags
强制使用 DT_RUNPATH 标签
gcc -g -Wall -o prog prog.c -Wl,--enable-new-dtags -Wl,-rpath,/home/mtk/pdir/d1 -L/home/mtk/pdir/d1 -lx1
#这样是可执行文件中将包含两种标签,为了兼容老式动态链接器工作
2 不强制指定:使用 $ORIGIN(被解释成包含应用程序的目录),允许程序在任意目录中运行应用程序
gcc -Wl,-rpath,'$ORIGIN'/lib ...
3.6 运行时符号链接顺序
若主程序定义了与库相同的全局符号,将会覆盖库
gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o
-Bsymbolic 能够确保库自身调用与主程序相同的全局符号时,正确调用自身的符号
四、共享库高级特性
1 动态加载库
-
dlopen() 将共享库加载进调用进程的虚拟空间并增加该库的打开引用次数
-
dlerror() 错误信息
-
dlsym() handle 指向库及所依赖树的库中检索 symbol 的符号(函数或变量)
-
dlclose() 关闭共享库
-
dladdr() 获取与加载的符号相关的消息
2 支持回调(库调用主程序符号)
gcc -Wl,--export-dynamic main.c
使用 gcc -rdynamic / gcc -Wl -E / -Wl,–export-dynamic 含义一样
3 符号可见性
1 static 符号的可见性局限於单个源代码文件中
2 void __attribute__((visibility("hidden"))) fun(void) {}
对所有共享库不可见
4 链接器版本脚本
4.1 控制全局可见的符号
gcc -g -c -fPIC -Wall vis_com.c
gcc -g -shared -o vis.so vis_com.o -Wl,--version-script,vis.map
#vis.map 内容
VER_1 {
global:
vis_f1;
vis_f2;
local:
*;
};
#打印可见符号
readelf --syms --use-dynamic vis.so | grep vis_
4.2 符号版本化
也就是一个共享库提供同一个函数的多个版本
//cat sv_lib_v2.c
#include <stdio.h>
__asm__(".symver xyz_old,xyz@VER_1");
__asm__(".symver xyz_new,xyz@@VER_2"); //在.symver 指令中只能有一个 @@ 标记
//sv_v2.map
VER_1 {
global: xyz;
local: *;
};
VER_2 {
global: pqr;
}VER_1; //VER_2 依赖 VER_1
4.3 初始化和终止函数
-
老式 _init() 和 _fini(),需要指定
gcc -nostartfiles
也可以通过 -Wl,-init / -Wl,-fini 指定函数 -
使用 gcc ,函数名根据需要替换
void __attribute__((constructor)) some_name_load(void)
{
}
void __attribute__((destructor)) some_name_unload(void)
{
}
4.4 预加载共享库: LD_PRELOAD
LD_PRELOAD=libalt.so ./prog
这样的好处就是,本来 prog 会加载指定库的符号,使用预加载之后,预加载内存在的符号会覆盖原先的符号被主程序调用
4.5 监控动态链接器: LD_DEBUG
LD_DEBUG=libs date
根据关键字从动态链接器获得跟踪信息,以便清楚所检索的库