先声明几点:
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
#ifndef LIB_H_
#define LIB_H_
void foobar(int i);
#endif
2、我们的库文件lib.c
#include
void foobar(int i)
{
printf("hellfrom%s,num:%d\n", __func__, i);
getchar();
}
3、我们的测试文件man.c
#include
#include "lib.h"
int main(void)
{
printf("hellofrom %s:\n", __func__);
foobar(250);
return 0;
}
程序很简单,就是在库文件中的函数foobar中输出一行字符串以及一个传递给它的参数。在主函数中同样输出一行字符串。
下文如果提到源代码名称,都是指上面的那些东东,这点就不再说明了。至于为何传递的参数的250呢?这是山人自己写的,你可以修改成其它值。
我们的静态库文件名称:libfoo.a
我们的动态库文件名称:libfoobar.so
同样的函数,使用库名称不同,是因为在同一目录下存在相同名称的库时,gcc会优先考虑动态库的。
实践
1、静态库的生成
Linux下编译成静态库很简单。使用如下命令
$ gcc -clib.c
$ ar cr libfoo.alib.o
其中第一行命令是生成lib.o目标文件。第二行使用ar命令生成libfoo.a静态库。注意,这里我们在文件名称上稍稍区别一下静态库与动态库。ar命令是创建、修改文档(archive),或者从文档中解压,c代表创建,r表示插入(带替换功能)。这些解释是从manar中翻译过来的,详细请参考man ar或者google。
Linux平台的库文件名遵循一定的约定,它们以lib开头,以.a或.so结尾,中间的就是库的名称,在使用gcc编译时直接使用-l选项指定库名称即可。比如这里的libfoo.a以及后面将会生成的libfoobar.so。
我们来看一下libfoo.a都包含了哪些符号
$ nmlibfoo.a
lib.o:
00000000 r __func__.1570
00000000 T foobar
U getchar
U printf
其实,它与nmlib.o输出效果是一样的,这可以从生成静态库的命令看出一点关系。注意,那个lib.o并不是山人写错,它是显示出来的信息,并没有修改过。
2、使用静态库
我们使用这个静态库来编译测试程序
$ gcc -o foomain.c ./libfoo.a
$ ./foo
hello from main:
hell from foobar,num:250
另外一个方法
$ gcc -ofoo-a main.c -L. -lfoo
$ ./foo-a
hello from main:
hell from foobar,num:250
-L选项表示查找库的目录,这里的-L.意思是在当前目录查找,-l选项是指定需要的库文件,上面提到一点,就是其后直接跟“库名称”,这里的libfoo.a遵循了Linux库命名的约定,因而直接使用-lfoo来指定。
上面两种方法的效果是一样的。
3、动态库的生成
动态库的生成同样简单,命令如下
$ gcc -clib.c
$ gcc -fPIC -shared -o libfoobar.solib.o
其中的lib.o也可以用lib.c代替,结果是一样的。
-f后面的PIC表示生成的库中符号是与位置无关的(PIC就是Position IndependentCode),关于PIC,可以参考这篇文章
Introduction toPosition Independent Code
-shared表示共享,共享库后缀名.so可以认为是shared object的简称。
4、使用动态库
同样地,使用动态库也有许多种方法。
比如
$ gcc -o barmain.c ./libfoobar.so
$ ./bar
hello from main:
hell from foobar,num:250
还有一种
$ gcc -obar-a main.c -L. -lfoobar
$ ./bar-a
./bar-a: error while loading sharedlibraries: libfoobar.so: cannot open shared object file: No suchfile or directory
编译没出现出错,但运行出错了,看提示信息,说不能加载libfoobar.so,因为没有那个目录。
原来,我们自己那个共享库不在系统默认的共享库路径中。有几种方法,一是将当前目录添加到共享库搜索目录;二是将我们自己的共享库放到系统默认的库目录中;三是暂时指定共享库的路径。显然,第一种方法和第二种不太可取,——就像将我们自己编写的头文件放到系统默认头文件目录那样。当然,在我们设计嵌入式根文件系统时,我们是可以将自定义的库放到系统目录中的。这里,我们使用第三种方法。执行以下命令
$ exportLD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./bar-a
hello from main:
hell from foobar,num:250
可以看到,程序能运行了。我们使用export临时指定当前目录为共享库目录。此时,我们使用ldd命令看一下该可执行文件共享库的信息
$ lddbar-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)表明了我们自己的共享库就在当前目录下。
然而,当我们切换到另一个终端,或者退出系统重新登陆,再次执行这个命令,得到如下信息
$ lddbar-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 => notfound,没有找到这个共享库。因为export仅对当前会话当前终端有效。将这个目录添加到系统的共享库目录是一种治根治标的方法,不过,我们需要根据实际情况作出选择。好,下面试一下第二种,也就是将我们的库文件放到系统库目录中,系统共享库搜索路径是由/etc/ld.so.conf这个文件指定的,不同版本的系统其内容亦不同。其实里面无非就是库的目录而已。这里,我们放到/lib目录下,之后,要记得使用ldconfig刷新共享库缓存/etc/ld.so.cache,注意需要使用root权限才能操作成功。
下面查看一下操作是否成功了
$ 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指定。
$ gcc -obar-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)
在《程序员的自我修养》中,还有一种方法,如下
$ gcc -obar-c main.c libfoobar.so -Xlinker -rpath .
$ ./bar-c
hello from main:
hell from foobar,num:250
我们的共享库由-rpath来指定(注意-rpath空格后面那个点,表示当前目录),这种效果跟第一种方法一样。
另外,如果-rpath指定的路径是绝对路径的话,那么生成的可执行文件放到任意目录中执行,都是可以的。CU上有帖子说-rpath指定的目录是已经写入可执行文件里面了,加载时,是使用-rpath指定的目录来加载共享库的。因此,如果使用当前目录的话,只要将共享库和可执行文件放到一起,就可以执行。经山人测试,这种说法是正确的。
我们使用ldd看一下这两种方法生成的可执行文件的共享库信息
$ lddbar
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)
但是,它们还是有区别的,具体的在此处不再深入了。
题外实践(下面内容纯粹是没事找事做,感兴趣的可以看看)
好了,实例显示完毕,下面再研究一下其它方面的东西。
上面所有例子都是动态链接的,现在我们使用静态链接
$ gcc-static -o foo-s main.c ./libfoo.a
$ ./foo-s
hello from main:
hell from foobar,num:250
我们在前面静态库编译时添加了-static选项。
我们使用ldd命令再次查看一下静态库生成的可执行文件的信息
$ lddfoo*
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都表明是动态的了,你非要显示静态的!开个玩笑,呵呵)。
我们再看一下它们占用的体积有多大
$ ll | grepfoo
-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多倍!
那么,我们能不能静态链接我们的动态库呢?
$ gcc-static -o bar-s main.c ./libfoobar.so
/usr/bin/ld: attempted staticlink of dynamic object `./libfoobar.so'
collect2: ld 返回 1
这是libfoobar.so放在当前目录的情况。我们将libfoobar.so复制一份到/lib目录中了,再执行一次
$ gcc-static -o bar-s main.c -lfoobar
/usr/bin/ld: cannot find-lfoobar
collect2: ld 返回 1
也不行。算了,这个不再研究了。
我们再来研究一下动态库与静态库的符号。
查看任意一个使用动态库链接库生成的可执行文件,可以发现里面的foobar是未定义的(未定义符号为U,倒数第4行)。
$ nmbar
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,表示在这个模块中已经定义了的)。
$ nmfoo
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.
我们再来看一下静态链接后的可执行文件(注意,该文件符号很多,这里删除了大量符号)
$ nm foo-s |grep foobar
08048288 T foobar
$ nm foo-s | grepprintf
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应该是这个样子的
$ cat lib.c
#include
void foobar(int i)
{
printf("hellfrom%s,num:%d\n", __func__, i);
getchar();
}
int hello(void)
{
printf("Thisfunction is not used!\n");
return 0;
}
这个函数中的字符串是可以使用strings命令读到的。
同样地,我们再次生成一个静态库和动态库,这里就省略了步骤了。
我们来看一下使用静态库生成的可执行文件
$ nmfoo-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稍微大一点
$ ll | grepfoo
-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命令看一下
$ stringsfoo-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等等网站上看人家以前讨论的帖子。这个过程虽然漫长,不过,乐于其中也不觉得有多么辛苦了。
转自http://www.latelee.org/programming-under-linux/108-library-on-linux.html