linux静态库、动态库与静态编译

时间:2021-11-25 08:44:06

先声明几点:

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