原文链接 http://t.cn/RPzyNB3
对于任何程序员来说,库都是不可缺少的工具.它们是使用已经存在的代码,编译并可以唯你使用的.它们通常提供一般性的功能,像可以存储任何数据的“链表”和“二叉树”,
或者某些特殊的功能,比如数据库服务(如MySQL).绝大多数软件项目包含若干组件,这些组件可能在之后的其它项目中会被用到,或者你构建项目的时候只是想要分出来而已.
当你有可重用或者逻辑上不相关的函数集,建立一个库是非常有帮助的,根据这些你就可以不必复制源代码到你当前的项目并同时重新编译它们,与此同时你可以保持你的程序
的不同模块功能不相交,而且改变任何一个不会影响到其它模块.一旦这个模块被编写和测试通过,你就可以一遍又一遍安全的重用它,同时可以节省时间和减少每次编译这个
模块到你的项目的困难.编译静态库是相当的简单.当编译的时候,我们几乎不会遇到问题,所以不在此处讨论这个问题.我将直接介绍动态库(一个似乎令绝大多数人困惑的问题).
在我们开始之前,浏览下面的这段简洁的有关即将看到的从源代码到运行程序的纲要可能是有帮助的:
1.预处理宏:这个阶段处理所有预编译指令.通常来说,每一行都以"#"符号开始,比如"#define"和"#include".
2.正常编译:一旦源代码预处理完成,然后会编译产生结果文件.很多人认为编译处理的整个过程才是编译.这个阶段常常被认为是“正常编译”.这个阶段会将后缀名为C的
文件转化为后缀名为O的对象文件.
3.连接:这个阶段的目标是将所有的对象文件和一些库连接到一起以便生成最终的程序.需要注意的是关于静态库,真正的库实际上被“复制”到了你的最终程序;然而对
于共享库,只有一个指向这个库的“引用”被“复制”到了你的程序.现在你有一个可以运行的完整程序了.你可以从终端的命令解释器运行它,“加载程序”会切换这个程
序到内存.
4.加载中:当你的程序开始运行的时候这个阶段会出现.你的程序会根据"引用"来读取共享库.所有被发现的“引用”会被重新解析到对应的库,这些库同时会被
映射到你的程序.第3步和第4步是共享库神奇和困惑的所在.
现在,看看我们(很简单)的例子.
foo.h:
#ifndef _FOO_H_
#define _FOO_H_
extern void foo(void);
#endif// _FOO_H_
foo.c:
#include
void foo(void)
{
puts("Hello,I'm a shared library.");
}
main.c:
#include
#include "foo.h"
int main(void)
{
puts("This is a shared library test.");
foo();
return 0;
}
文件"foo.h"定义了我们的库的接口,一个函数"foo()",文件"foo.c"包含这个函数的实现方法,另外文件"main.c"是使用我们的
库的一个"驱动程序".对于这个例子的目的,所以的事情都在 /home/username/foo
第1步:编译“位置无关”代码
我们需要编译我们的库的源代码到“位置无关”代码(PIC[1]):
$ gcc -c -Wall -Werror -fpic foo.c
第2步:从对象文件构建共享库
现在我们实际上需要将对象文件转化为共享库.我们将它命名为libfoo.so:
$ gcc -shared -o libfoo.so foo.o
第3步:用动态库连接
正如我们所见,这些实际上很简单.我们有了共享库.让我们编译我们的程序"main.c"和用libfoo连接它.我们称我们的最终的程序叫做"test".需要
注意的是"-lfoo"选项不是为了定位"foo.o",而是"libfoo.so".GCC假定所有的库文件名称都是以"lib"开始,以".so"
或者".a"(".so"是共享对象或者共享库,".a"是压缩格式,或者静态连接库)结束.
$ gcc -Wall -o test main.c -lfoo
/usr/bin/ld:cannot find -lfoo
collect2:ld returned 1 exit status
告诉GCC找共享库的地方在哪里
诶哟!连接器不知道库"libfoo"在哪里.GCC有一个默认的位置列表.但是我们的目录不在这个列表中.我们需要告诉GCC在哪里寻找"libfoo.so".我们
可以使用"-L"选项.在这个例子中,我们会使用到当前目录 /home/username/foo:
$ gcc -L/home/username/foo -Wall -o test main.c -lfoo
第4步:使库文件在运行时可用
很好,没有错误.现在让我们运行我们的程序:
$ ./test
./test:error while loading shared libraries:libfoo.so:cannot open shared object file:No such file or directory
诶哟!“加载程序”不能够找到共享库.我们不能安装它到标准位置,因此我们需要给“加载程序”一点提示.我们需要配置两个选项:我们这次可以使用环境变量LD_LIBRARY_PATH
或者使用"rpath".让我们先看看这个变量LD_LIBRARY_PATH:使用LD_LIBRARY_PATH
$ echo $LD_LIBRARY_PATH
这个变量里面可能什么也没有.让我们通过追加我们的当前工作目录到已经存在的变量LD_LIBRARY_PATH来修复:
$ LD_LIBRARY_PATH=/home/username/foo:$LD_LIBRARY_PATH
$ ./test
./test:error while loading shared libraries:libfoo.so:cannot open shared object file:No such file or directory
怎么回事?我们的目录已经在LD_LIBRARY_PATH里面了,但是我们导出它.在Linux环境下,如果你没有导出改变信息到环境变量,这些改变就不会被子进程继承.“加载程序”和
我们的程序"test"不能够继承我们作出的改变.谢天谢地,修复起来很容易:
$ exportLD_LIBRARY_PATH=/home/username/foo:$LD_LIBRARY_PATH
$ ./test
This is a shared library test.
Hello,I'm a shared library.
很好,正常工作了!变量LD_LIBRARY_PATH对于快速测试是很强大的,而且不需要拥有管理员权限.然而存在一个缺点,导出LD_LIBRARY_PATH变量意味着可能导致其它也依靠这
个变量的你要运行的程序出错,如果你在之后不能重设到先前的状态.
使用rpath
现在让我们试试"rpath"(首先,我们先清空LD_LIBRARY_PATH以便确信是"rpath"找到了我们的库)."rpath"或者运行"rpath"是
一种嵌入共享库到自己的可执行位置的途径,而不是依靠默认位置或者环境变量.我们在连接的阶段这样做就行.注意这个冗长的"-Wl,-rpath=/home/username/foo"
选项.这个"-Wl"选项部分可以发送逗号分隔符选项到连接程序,以便用我们的当前工作目录发送"-rpath"选项到连接程序.
$ unset LD_LIBRARY_PATH
$ gcc -L/home/username/foo -Wl,-rpath=/home/username/foo -Wall -o test main.c -lfoo
$ ./test
This is a shared library test.
Hello,I'm a shared library.
非常厉害,它正常工作了.使用"rpath"的方法是很强大的,因为每个程序使用的共享库位置列表是独立的, 例如不同的程序根据LD_LIBRARY_PATH变量在错误的
路径下寻找也没有关系.比较"rpath"和"LD_LIBRARY_PATH"然而"rpath"也存在一些缺点.首先,它需要共享库被安置在一个固定的位置,
以便所有使用你的程序的用户都可以访问那些库所在的位置.这意味着系统配置没有灵活性.其次,如果那些引用的库在一个网络文件系统(NFS)上挂载着,或者其它网络驱
动器,你可能会经历难以接受的延迟和糟糕的程序启动过程.使用"ldconfig"更改文件"ld.so"如果我们想要安装我们的库以便当前系统下的所有人
都可以使用它,怎么办?鉴于此,我们将需要管理员权限.你有两个原因需要它:首先,为了把库文件放到标准位置,可能是"/usr/lib"或者"/usr/local/lib",
这些位置一般用户无法执行写操作.其次,你需要修改"ld.so"配置文件和缓存.作为管理员,执行下面的操作:
# cp /home/username/foo/libfoo.so /usr/lib
# chmod 0755 /usr/lib/libfoo.so
现在这个文件已经在标准位置了,并且拥有了正确的权限,比如任何人可读.我们需要告诉“加载程序”它是可用的,以便让我们更新缓存:
# ldconfig
这样应该就可以创建一个指向我们的共享库的链接,同时也会更新缓存,以便立即可以使用.让我们仔细检查下:
$ ldconfig -p | grep foo
libfoo.so (libc6) => /usr/lib/libfoo.so
现在我们的库已经安装.在我们测试之前,我们必须清理一下东西:
再次清理我们的变量"LD_LIBRARY_PATH",只是以防万一:
$ unset LD_LIBRARY_PATH
重新连接我们的可执行程序.注意我们不需要引入"-L"选项,由于我们的库已经被存储在默认位置,我们不需要使用"rpath"选项:
$ gcc -Wall -o test main.c -lfoo
让我们确信我们使用的是"/usr/lib"实例化我们的库使用"ldd":
$ ldd test | grep foo
libfoo.so => /usr/lib/libfoo.so (0x00a42000)
很好,现在让我们来运行它:
$ ./test
This is a shared library test.
Hello,I'm a shared library.
一切都基本差不多了.我们已经讨论了如何编译共享库,如何连接它,和如何重新解析大部分“加载程序”使用共享库的问题和不同方法的优缺点.
--[notes]------------------------------------------------------
1.什么是“位置无关代码”,是指代码段内存位置无论在哪里都可以正常工作的代码.因为一些不同的程序都可以使用你的共享库的一个实例,这个库不能在一个固定地址
存储数据,由此那个库在内存中的位置会依据程序的不同而不同.
2.GCC首先搜索库文件在"/usr/local/lib",然后在"/usr/lib".以下是,它搜索库文件在指定路径根据参数"-L"选项,根据命令行指定的顺序.
3.默认GNU加载程序,"ld.so",需要库根据以下顺序:
1.在可执行文件的"DT_RPATH"域,除非存在"DT_RUNPATH"域.
2.在参数"LD_LIBRARY_PATH".如果可执行程序的因为安全原因设置setuid和setgid,则跳过.
3.在可执行程序的"DT_RUNPATH"域,除非setuid和setgid位被设置(为安全考虑).
4.在缓存文件"/etc/ld/so/cache"(禁用根据`-z nodeflib'连接选项)
5.默认路径"/lib",然后是"/usr/lib"(禁用根据`-z nodeflib'连接选项)
--[notes]------------------------------------------------------
感谢作者
英文原文链接:http://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html