Linux平台静态库、动态库的一些笔记 | 迟思堂工作室
A-A+

Linux平台静态库、动态库的一些笔记

2014-08-30 18:20 GNU/Linux程序 评论 2 条 阅读 5,245 次

李迟按:本文约在去年年底在我的csdn博客上发表,重新发表于此,只字未改。那时候很想对库有深一层的认识。并且知道了《程序员的自我修养--链接、装载与库》这本书。这本书如今已经看过一次了,收获不少,但仍需要再次阅读。

先声明几点:

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

/* lib.h */

#ifndef LIB_H_

#define LIB_H_

void foobar(int i);

#endif

 

2、我们的库文件lib.c

/*lib.c*/

#include <stdio.h>

void foobar(int i)

{

printf("hell from %s,num:%dn", __func__, i);

getchar();

}

3、我们的测试文件man.c

/*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下编译成静态库很简单。使用如下命令

$ 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都包含了哪些符号

$ nm libfoo.a

lib.o:

00000000 r __func__.1570

00000000 T foobar

U getchar

U printf

其实,它与nm lib.o输出效果是一样的,这可以从生成静态库的命令看出一点关系。注意,那个lib.o并不是山人写错,它是显示出来的信息,并没有修改过。

2、使用静态库

我们使用这个静态库来编译测试程序

$ gcc -o foo main.c ./libfoo.a

$ ./foo

hello from main:

hell from foobar,num:250

另外一个方法

$ 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、动态库的生成

动态库的生成同样简单,命令如下

$ 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、使用动态库

同样地,使用动态库也有许多种方法。

比如

$ gcc -o bar main.c ./libfoobar.so

$ ./bar

hello from main:

hell from foobar,num:250

还有一种

$ 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,因为没有那个目录。

原来,我们自己那个共享库不在系统默认的共享库路径中。有几种方法,一是将当前目录添加到共享库搜索目录;二是将我们自己的共享库放到系统默认的库目录中;三是暂时指定共享库的路径。显然,第一种方法和第二种不太可取,——就像将我们自己编写的头文件放到系统默认头文件目录那样。当然,在我们设计嵌入式根文件系统时,我们是可以将自定义的库放到系统目录中的。这里,我们使用第三种方法。执行以下命令

$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

$ ./bar-a

hello from main:

hell from foobar,num:250

可以看到,程序能运行了。我们使用export临时指定当前目录为共享库目录。此时,我们使用ldd命令看一下该可执行文件共享库的信息

$ 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)表明了我们自己的共享库就在当前目录下。

然而,当我们切换到另一个终端,或者退出系统重新登陆,再次执行这个命令,得到如下信息

$ 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权限才能操作成功。

下面查看一下操作是否成功了

$ 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 -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)

在《程序员的自我修养》中,还有一种方法,如下

$ 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看一下这两种方法生成的可执行文件的共享库信息

$ 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)

但是,它们还是有区别的,具体的在此处不再深入了。

题外实践(下面内容纯粹是没事找事做,感兴趣的可以看看)

好了,实例显示完毕,下面再研究一下其它方面的东西。

上面所有例子都是动态链接的,现在我们使用静态链接

$ gcc -static -o foo-s main.c ./libfoo.a

$ ./foo-s

hello from main:

hell from foobar,num:250

我们在前面静态库编译时添加了-static选项。

我们使用ldd命令再次查看一下静态库生成的可执行文件的信息

$ 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都表明是动态的了,你非要显示静态的!开个玩笑,呵呵)。

我们再看一下它们占用的体积有多大

$ 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多倍!

那么,我们能不能静态链接我们的动态库呢?

$ 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目录中了,再执行一次

$ gcc -static -o bar-s main.c -lfoobar

/usr/bin/ld: cannot find -lfoobar

collect2: ld 返回 1

也不行。算了,这个不再研究了。

我们再来研究一下动态库与静态库的符号。

查看任意一个使用动态库链接库生成的可执行文件,可以发现里面的foobar是未定义的(未定义符号为U,倒数第4行)。

$ 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,表示在这个模块中已经定义了的)。

$ 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.

我们再来看一下静态链接后的可执行文件(注意,该文件符号很多,这里删除了大量符号)

$ 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应该是这个样子的

$ 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命令读到的。

同样地,我们再次生成一个静态库和动态库,这里就省略了步骤了。

我们来看一下使用静态库生成的可执行文件

$ 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

puts@@GLIBC_2.0

可以看到,我们没有调用hello这个函数,但它依然是T!至于这个文件中其它的未定义的符号,是因为foo-b始终还是动态链接的!这点务必注意。

如果我们查看那几个foo文件,会发现,这个foo-b稍微大一点

$ 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命令看一下

$ 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等等网站上看人家以前讨论的帖子。这个过程虽然漫长,不过,乐于其中也不觉得有多么辛苦了。

木草山人



如果本文对阁下有帮助,不妨赞助笔者以输出更多好文章,谢谢!
donate




2 条留言  访客:2 条  博主:0 条

  1. andy

    请问怎么静态链接一个动态库。一般生成的动态库都链接有libc等等其他的库,如何静态链接一个动态库,这个动态库不在依赖其他库了。

    • 李迟

      你所说的这个似乎不能实现,我没研究过。如果是非系统的库,建议全部都编译成静态库,而系统级别的,都会在/lib等目录,用户不关心。

给我留言