先声明几点:
1、操作系统:linux(fc9)、编译器:gcc-4.3.0、编辑器:包括但不限于emacs、vim。这些无理由也不应造成限制。
2、生成的可执行文件名称比较有规律,仅仅是为了演示的方便。比如使用静态库生成的是foo,不同的生成方法得到的可执行文件可能会是foo-a、foo-b……,而使用动态库生成的是foobar,可能会是foobar-a、foobar-b……,等等。
3、木草山人正在看的那本《程序员的自我修养——链接、装载与库》只看了前面部分,而且还只看第一遍,很多知识不牢固。因此不会深入讲述原理性的东西,比如静态库与动态库的优点与缺点,它们是怎么加载的。此外也不涉及共享库版本、兼容性以及SO-NAME,等等——很多时候,我们不必要追根问底,特别是在计算机领域中。
4、示例程序仅为演示程序,不代表实践中的操作、编写方法。——比如使用编译器的-g、-O选项,等等。但却具有实践指导意义。
5、山人尽量保证文章属实,但限于自身见识和水平,即使在自己的机器上亲自实践一次,辛辛苦苦的粘贴运行结果,但不敢保证100%正确。大家体谅一下。
背景代码:
1、我们的库的头文件lib.h
1 2 3 4 5 |
/* lib.h */ #ifndef LIB_H_ #define LIB_H_ void foobar(int i); #endif |
2、我们的库文件lib.c
1 2 3 4 5 6 7 |
/*lib.c*/ #include <stdio.h> void foobar(int i) { printf(“hell from %s,num:%dn”, __func__, i); getchar(); } |
3、我们的测试文件man.c
1 2 3 4 5 6 7 8 9 |
/*main.c*/ #include <stdio.h> #include “lib.h” int main(void) { printf(“hello from %s:n”, __func__); foobar(250); return 0; } |
程序很简单,就是在库文件中的函数foobar中输出一行字符串以及一个传递给它的参数。在主函数中同样输出一行字符串。
下文如果提到源代码名称,都是指上面的那些东东,这点就不再说明了。至于为何传递的参数的250呢?这是山人自己写的,你可以修改成其它值。
我们的静态库文件名称:libfoo.a
我们的动态库文件名称:libfoobar.so
同样的函数,使用库名称不同,是因为在同一目录下存在相同名称的库时,gcc会优先考虑动态库的。
实践
1、静态库的生成
Linux下编译成静态库很简单。使用如下命令
1 2 |
$ gcc -c lib.c $ ar cr libfoo.a lib.o |
其中第一行命令是生成lib.o目标文件。第二行使用ar命令生成libfoo.a静态库。注意,这里我们在文件名称上稍稍区别一下静态库与动态库。ar命令是创建、修改文档(archive),或者从文档中解压,c代表创建,r表示插入(带替换功能)。这些解释是从man ar中翻译过来的,详细请参考man ar或者google。
Linux平台的库文件名遵循一定的约定,它们以lib开头,以.a或.so结尾,中间的就是库的名称,在使用gcc编译时直接使用-l选项指定库名称即可。比如这里的libfoo.a以及后面将会生成的libfoobar.so。
我们来看一下libfoo.a都包含了哪些符号
1 2 3 4 5 6 |
$ nm libfoo.a lib.o: 00000000 r __func__.1570 00000000 T foobar U getchar U printf |
其实,它与nm lib.o输出效果是一样的,这可以从生成静态库的命令看出一点关系。注意,那个lib.o并不是山人写错,它是显示出来的信息,并没有修改过。
2、使用静态库
我们使用这个静态库来编译测试程序
1 2 3 4 |
$ gcc -o foo main.c ./libfoo.a $ ./foo hello from main: hell from foobar,num:250 |
另外一个方法
1 2 3 4 |
$ gcc -o foo-a main.c -L. -lfoo $ ./foo-a hello from main: hell from foobar,num:250 |
-L选项表示查找库的目录,这里的-L.意思是在当前目录查找,-l选项是指定需要的库文件,上面提到一点,就是其后直接跟“库名称”,这里的libfoo.a遵循了Linux库命名的约定,因而直接使用-lfoo来指定。
上面两种方法的效果是一样的。
3、动态库的生成
动态库的生成同样简单,命令如下
1 2 |
$ gcc -c lib.c $ gcc -fPIC -shared -o libfoobar.so lib.o |
其中的lib.o也可以用lib.c代替,结果是一样的。
-f后面的PIC表示生成的库中符号是与位置无关的(PIC就是Position Independent Code),关于PIC,可以参考这篇文章
Introduction to Position Independent Code
-shared表示共享,共享库后缀名.so可以认为是shared object的简称。
4、使用动态库
同样地,使用动态库也有许多种方法。
比如
1 2 3 4 |
$ gcc -o bar main.c ./libfoobar.so $ ./bar hello from main: hell from foobar,num:250 |
还有一种
1 2 3 |
$ gcc -o bar-a main.c -L. -lfoobar $ ./bar-a ./bar-a: error while loading shared libraries: libfoobar.so: cannot open shared object file: No such file or directory |
编译没出现出错,但运行出错了,看提示信息,说不能加载libfoobar.so,因为没有那个目录。
原来,我们自己那个共享库不在系统默认的共享库路径中。有几种方法,一是将当前目录添加到共享库搜索目录;二是将我们自己的共享库放到系统默认的库目录中;三是暂时指定共享库的路径。显然,第一种方法和第二种不太可取,——就像将我们自己编写的头文件放到系统默认头文件目录那样。当然,在我们设计嵌入式根文件系统时,我们是可以将自定义的库放到系统目录中的。这里,我们使用第三种方法。执行以下命令
1 2 3 4 |
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH $ ./bar-a hello from main: hell from foobar,num:250 |
可以看到,程序能运行了。我们使用export临时指定当前目录为共享库目录。此时,我们使用ldd命令看一下该可执行文件共享库的信息
1 2 3 4 5 |
$ ldd bar-a linux-gate.so.1 => (0x00658000) libfoobar.so => ./libfoobar.so (0x004ec000) libc.so.6 => /lib/libc.so.6 (0x00110000) /lib/ld-linux.so.2 (0x0038b000) |
其中第二行libfoobar.so => ./libfoobar.so (0x004ec000)表明了我们自己的共享库就在当前目录下。
然而,当我们切换到另一个终端,或者退出系统重新登陆,再次执行这个命令,得到如下信息
1 2 3 4 5 |
$ ldd bar-a linux-gate.so.1 => (0x001e8000) libfoobar.so => not found libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) |
我们清楚地看到libfoobar.so => not found,没有找到这个共享库。因为export仅对当前会话当前终端有效。将这个目录添加到系统的共享库目录是一种治根治标的方法,不过,我们需要根据实际情况作出选择。好,下面试一下第二种,也就是将我们的库文件放到系统库目录中,系统共享库搜索路径是由/etc/ld.so.conf这个文件指定的,不同版本的系统其内容亦不同。其实里面无非就是库的目录而已。这里,我们放到/lib目录下,之后,要记得使用ldconfig刷新共享库缓存/etc/ld.so.cache,注意需要使用root权限才能操作成功。
下面查看一下操作是否成功了
1 2 3 |
$ strings /etc/ld.so.cache | grep foobar libfoobar.so /lib/libfoobar.so |
一切正常,已经找到libfoobar.so文件了。
(这里插一下题外话,很多同志在编译跟qt相关的程序(或者编译qt)时会遇到各种莫名其妙的错误,其中不乏库版本不对应这种错误,我们可以大胆猜测,这跟这里提及到的ld.so.conf有关,因此,我们遇到这种错误时,不妨先查看ld.so.conf文件是否是我们需要的路径)
下面是编译、运行的过程。注意,我们的共享库不是系统默认的库,需要使用-l来指定共享库名称。其它如线程库也不是Linux默认的,需要用-lpthread指定。
1 2 3 4 5 6 7 8 9 |
$ gcc -o bar-b main.c -lfoobar $ ./bar-b hello from main: hell from foobar,num:250 $ ldd bar-b linux-gate.so.1 => (0x0072d000) libfoobar.so => /lib/libfoobar.so (0x00834000) libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) |
在《程序员的自我修养》中,还有一种方法,如下
1 2 3 4 |
$ gcc -o bar-c main.c libfoobar.so -Xlinker -rpath . $ ./bar-c hello from main: hell from foobar,num:250 |
我们的共享库由-rpath来指定(注意-rpath空格后面那个点,表示当前目录),这种效果跟第一种方法一样。
另外,如果-rpath指定的路径是绝对路径的话,那么生成的可执行文件放到任意目录中执行,都是可以的。CU上有帖子说-rpath指定的目录是已经写入可执行文件里面了,加载时,是使用-rpath指定的目录来加载共享库的。因此,如果使用当前目录的话,只要将共享库和可执行文件放到一起,就可以执行。经山人测试,这种说法是正确的。
我们使用ldd看一下这两种方法生成的可执行文件的共享库信息
1 2 3 4 5 6 7 8 9 10 |
$ ldd bar linux-gate.so.1 => (0x00693000) ./libfoobar.so (0x00b63000) libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) $ ldd bar-c linux-gate.so.1 => (0x007cf000) libfoobar.so => ./libfoobar.so (0x00a65000) libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) |
但是,它们还是有区别的,具体的在此处不再深入了。
题外实践(下面内容纯粹是没事找事做,感兴趣的可以看看)
好了,实例显示完毕,下面再研究一下其它方面的东西。
上面所有例子都是动态链接的,现在我们使用静态链接
1 2 3 4 |
$ gcc -static -o foo-s main.c ./libfoo.a $ ./foo-s hello from main: hell from foobar,num:250 |
我们在前面静态库编译时添加了-static选项。
我们使用ldd命令再次查看一下静态库生成的可执行文件的信息
1 2 3 4 5 6 7 8 9 10 11 |
$ ldd foo* foo: linux-gate.so.1 => (0x00828000) libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) foo-a: linux-gate.so.1 => (0x00ffa000) libc.so.6 => /lib/libc.so.6 (0x003ab000) /lib/ld-linux.so.2 (0x0038b000) foo-s: 不是动态可执行文件 |
细心的你应该注意到,我们生成的可执行文件中,凡是foo都是由静态库libfoo.a生成的,凡是bar都是libfoobar.so生成的。大家能体会到山人的良苦用心吧?
前面两个是动态链接的,因而能查看与之相关的动态库信息,而最后一行,就是静态链接的foo-s,就显示不出来了。
注意,这里没有提示关于libfoo.a的信息(似乎是废话,人家ldd都表明是动态的了,你非要显示静态的!开个玩笑,呵呵)。
我们再看一下它们占用的体积有多大
1 2 3 4 |
$ ll | grep foo -rwxr-xr-x 1 xxx xxx 5.1K 12-16 14:40 foo -rwxr-xr-x 1 xxx xxx 5.1K 12-16 14:41 foo-a -rwxr-xr-x 1 xxx xxx 549K 12-16 15:21 foo-s |
最后一行是我们静态链接得到的可执行文件,可以看到,它灰常的大,是动态链接的100多倍!
那么,我们能不能静态链接我们的动态库呢?
1 2 3 |
$ gcc -static -o bar-s main.c ./libfoobar.so /usr/bin/ld: attempted static link of dynamic object `./libfoobar.so’ collect2: ld 返回 1 |
这是libfoobar.so放在当前目录的情况。我们将libfoobar.so复制一份到/lib目录中了,再执行一次
1 2 3 |
$ gcc -static -o bar-s main.c -lfoobar /usr/bin/ld: cannot find -lfoobar collect2: ld 返回 1 |
也不行。算了,这个不再研究了。
我们再来研究一下动态库与静态库的符号。
查看任意一个使用动态库链接库生成的可执行文件,可以发现里面的foobar是未定义的(未定义符号为U,倒数第4行)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ nm bar 08049674 d _DYNAMIC 08049748 d _GLOBAL_OFFSET_TABLE_ 080485cc R _IO_stdin_used …… 08048384 T _init 08048410 T _start 08049768 b completed.5699 08049764 W data_start 0804976c b dtor_idx.5701 U foobar 080484a0 t frame_dummy 080484c4 T main U printf@@GLIBC_2.0 |
而使用静态库生成的可执行文件中的foobar是全局TEXT符号(倒数第5行,符号为T,表示在这个模块中已经定义了的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ nm foo 080495e0 d _DYNAMIC 080496ac d _GLOBAL_OFFSET_TABLE_ 0804851c R _IO_stdin_used …… 080482b4 T _init 08048340 T _start 080496cc b completed.5699 080496c8 W data_start 080496d0 b dtor_idx.5701 08048434 T foobar 080483d0 t frame_dummy U getchar@@GLIBC_2.0 080483f4 T main U printf@@GLIBC_2. |
我们再来看一下静态链接后的可执行文件(注意,该文件符号很多,这里删除了大量符号)
1 2 3 4 5 6 7 8 9 10 11 12 |
$ nm foo-s | grep foobar 08048288 T foobar $ nm foo-s | grep printf 0805ea90 W _IO_fprintf 08048df0 T _IO_printf …… 08055860 t buffered_vfprintf 0805eac0 T dprintf 0805ea90 T fprintf 08048df0 T printf 08055400 t printf_unknown 08055df0 T vfprintf |
细心的你同样会注意到,在foo(动态链接)可执行文件中,printf函数是未定义的,而foo-s(静态链接)可执行文件中,printf却是已经定义了的。前者,需要动态地加载一些必要的库函数,因为这些函数不在那个可执行文件中,而静态链接却包含了许多函数,因此,不需要再加载了。知道这点后,我们在看许多资料,看到说静态链接占用很多体积,如何不好……时,应该有点感性认识了吧?
我们也可以再做个实验来说明这一点。
我们在lib.c中再添加一个不调用它的函数:Hello。而我们的测试程序main.c没有作修改。
现在lib.c应该是这个样子的
1 2 3 4 5 6 7 8 9 10 11 12 |
$ cat lib.c #include <stdio.h> void foobar(int i) { printf(“hell from %s,num:%dn”, __func__, i); getchar(); } int hello(void) { printf(“This function is not used!n”); return 0; } |
这个函数中的字符串是可以使用strings命令读到的。
同样地,我们再次生成一个静态库和动态库,这里就省略了步骤了。
我们来看一下使用静态库生成的可执行文件
1 2 3 4 5 6 7 8 9 10 11 12 |
$ nm foo-b 08049648 d _DYNAMIC 08049714 d _GLOBAL_OFFSET_TABLE_ 0804856c R _IO_stdin_used …… 08048464 T foobar 08048400 t frame_dummy U getchar@@GLIBC_2.0 0804848c T hello 08048424 T main U printf@@GLIBC_2.0 U puts@@GLIBC_2.0 |
可以看到,我们没有调用hello这个函数,但它依然是T!至于这个文件中其它的未定义的符号,是因为foo-b始终还是动态链接的!这点务必注意。
如果我们查看那几个foo文件,会发现,这个foo-b稍微大一点
1 2 3 4 5 |
$ ll | grep foo -rwxr-xr-x 1 xxxx xxxx 5.1K 12-16 14:40 foo -rwxr-xr-x 1 xxxx xxxx 5.1K 12-16 14:41 foo-a -rwxr-xr-x 1 xxxx xxxx 5.3K 12-17 08:31 foo-b -rwxr-xr-x 1 xxxx xxxx 549K 12-16 15:21 foo-s |
不为什么,它已经包含了hello这个函数了。不信,再使用strings命令看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ strings foo-b /lib/ld-linux.so.2 __gmon_start__ libc.so.6 _IO_stdin_used puts printf getchar __libc_start_main …… main hello from %s: foobar hell from %s,num:%d This function is not used! |
最后一行就是我们未调用的的那个函数打印的。
另外,我们使用同样的方法查看使用动态库生成的可执行文件,会发现,它与前面的那几个并没有发生变化。但是,libfoobar.so中也已经有了hello这个函数了(这句也是废话,大家直接无视之)。
再次声明:
山人初出茅庐,对许多专业术语使用得不是很恰当,有些理解也是牵强附会,连贯性不好。还请大家见谅并批评指正,谢谢大家!
PS:写这种文章,吃力又不讨好,很辛苦的!山人花费大量时间,不仅要在linux下测试,还需要参考很多资料,比如到javaeye、CU、CSDN等等网站上看人家以前讨论的帖子。这个过程虽然漫长,不过,乐于其中也不觉得有多么辛苦了。
发表评论
要发表评论,您必须先登录。