静态库

有时候需要把一组代码编译成一个库,这个库在很多项目中都要用到,例如libc就是这样一个库,我们在不同的程序中都会用到libc中的库函数(例如printf),也会用到libc中的变量(例如以后要讲到的environ变量)。

考虑下面的目录结构:

1
2
3
4
5
6
7
8
9
10
11
$ tree
.
|-- main.c
`-- stack
|-- is_empty.c
|-- pop.c
|-- push.c
|-- stack.c
`-- stack.h

1 directory, 6 files

我们把stack.c、push.c、pop.c、is_empty.c编译成目标文件:

1
$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

然后打包成一个静态库libstack.a:

1
2
$ ar rs libstack.a stack.o push.o pop.o is_empty.o
ar: creating libstack.a

库文件名都是以lib开头的,静态库以.a作为后缀,表示Archive。ar命令类似于tar命令,起一个打包的作用,但是把目标文件打包成静态库只能用ar命令而不能用tar命令。选项r表示将后面的文件列表添加到文件包,如果文件包不存在就创建它,如果文件包中已有同名文件就替换成新的。s是专用于生成静态库的,表示为静态库创建索引,这个索引被链接器使用。ranlib命令也可以为静态库创建索引,以上命令等价于:

1
2
$ ar r libstack.a stack.o push.o pop.o is_empty.o
$ ranlib libstack.a

然后我们把libstack.a和main.c编译链接在一起:

1
$ gcc main.c -L. -lstack -Istack -o main

-L选项告诉编译器去哪里找需要的库文件,-L.表示在当前目录找。-lstack 告诉编译器要链接libstack库,-I选项告诉编译器去哪里找头文件。注意,即使库文件就在当前目录,编译器默认也不会去找的,所以-L.选项不能少。编译器默认会找的目录可以用-print-search-dirs选项查看:

1
2
3
4
$ gcc -print-search-dirs
install: /usr/lib/gcc/i486-linux-gnu/4.3.2/
programs: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/libexec/gcc/i486-linux-gnu/4.3.2/:/usr/libexec/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/bin/
libraries: =/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../i486-linux-gnu/4.3.2/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/:/lib/i486-linux-gnu/4.3.2/:/lib/../lib/:/usr/lib/i486-linux-gnu/4.3.2/:/usr/lib/../lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../i486-linux-gnu/lib/:/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../:/lib/:/usr/lib/

其中的libraries就是库文件的搜索路径列表,各路径之间用:号隔开。编译器会在这些搜索路径以及-L选项指定的路径中查找用-l选项指定的库,比如-lstack编译器会首先找有没有共享库libstack.so,如果有就链接它,如果没有就找有没有静态库libstack.a,如果有就链接它。所以编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。

共享库

编译、链接、运行

组成共享库的目标文件和一般的目标文件有所不同,在编译时要加-fPIC选项,例如:

1
$ gcc -c -fPIC stack/stack.c stack/push.c stack/pop.c stack/is_empty.c

-f后面跟一些编译选项,PIC是其中一种,表示生成位置无关代码(Position Independent Code)。

使用共享库之前需要先告诉gcc共享库所在的路径,否则会出现这样的错误:

1
2
3
$ gcc main.c -g -L. -lstack -Istack -o main
$ ./main
./main: error while loading shared libraries: libstack.so: cannot open shared object file: No such file or directory

由于指定了-L.选项,编译器可以在当前目录下找到libstack.so而运行时却说找不到libstack.so。那么运行时在哪些路径下找共享库呢?我们先用ldd命令查看可执行文件依赖于哪些共享库:

1
2
3
4
5
$ ldd main
linux-gate.so.1 => (0xb7f5c000)
libstack.so => not found
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7dcf000)
/lib/ld-linux.so.2 (0xb7f42000)

ldd模拟运行一遍main,在运行过程中做动态链接,从而得知这个可执行文件依赖于哪些共享库,每个共享库都在什么路径下,加载到进程地址空间的什么地址。/lib/ld-linux.so.2是动态链接器,它的路径是在编译链接时指定的,gcc在做链接时用-dynamic-linker指定动态链接器的路径,它也像其它共享库一样加载到进程的地址空间中。libc.so.6的路径/lib/tls/i686/cmov/libc.so.6是由动态链接器ld-linux.so.2在做动态链接时搜索到的,而libstack.so的路径没有找到。linux-gate.so.1这个共享库其实并不存在于文件系统中,它是由内核虚拟出来的共享库,所以它没有对应的路径,它负责处理系统调用。总之,共享库的搜索路径由动态链接器决定,从ld.so(8)的Man Page可以查到共享库路径的搜索顺序:

  1. 首先在环境变量LD_LIBRARY_PATH所记录的路径中查找。
  2. 然后从缓存文件/etc/ld.so.cache中查找。这个缓存文件由ldconfig命令读取配置文件/etc/ld.so.conf之后生成,稍后详细解释。
  3. 如果上述步骤都找不到,则到默认的系统路径中查找,先是/lib然后是/usr/lib。

最常用的解决方法是把libstack.so所在目录的绝对路径(比如/home/akaedu/somedir)添加到/etc/ld.so.conf中(该文件中每个路径占一行),然后运行ldconfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ sudo ldconfig -v
...
/home/akaedu/somedir:
libstack.so -> libstack.so
/lib:
libe2p.so.2 -> libe2p.so.2.3
libncursesw.so.5 -> libncursesw.so.5.6
...
/usr/lib:
libkdeinit_klauncher.so -> libkdeinit_klauncher.so
libv4l2.so.0 -> libv4l2.so.0
...
/usr/lib64:
/lib/tls: (hwcap: 0x8000000000000000)
/usr/lib/sse2: (hwcap: 0x0000000004000000)
...
/usr/lib/tls: (hwcap: 0x8000000000000000)
...
/usr/lib/i686: (hwcap: 0x0008000000000000)
/usr/lib/i586: (hwcap: 0x0004000000000000)
...
/usr/lib/i486: (hwcap: 0x0002000000000000)
...
/lib/tls/i686: (hwcap: 0x8008000000000000)
/usr/lib/i686/cmov: (hwcap: 0x0008000000008000)
...
/lib/tls/i686/cmov: (hwcap: 0x8008000000008000)

ldconfig命令除了处理/etc/ld.so.conf中配置的目录之外,还处理一些默认目录,如/lib、/usr/lib等,处理之后生成/etc/ld.so.cache缓存文件,动态链接器就从这个缓存中搜索共享库。hwcap是x86平台的Linux特有的一种机制,系统检测到当前平台是i686而不是i586或i486,所以在运行程序时使用i686的库,这样可以更好地发挥平台的性能,也可以利用一些新的指令,所以上面ldd命令的输出结果显示动态链接器搜索到的libc是/lib/tls/i686/cmov/libc.so.6,而不是/lib/libc.so.6。现在再用ldd命令查看,libstack.so就能找到了:

1
2
3
4
5
$ ldd main
linux-gate.so.1 => (0xb809c000)
libstack.so => /home/akaedu/somedir/libstack.so (0xb806a000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7f0c000)
/lib/ld-linux.so.2 (0xb8082000)

共享库的命名惯例

你可能已经注意到了,系统的共享库通常带有符号链接,例如:

1
2
3
4
5
6
7
8
9
10
11
12
$ ls -l  /lib
...
-rwxr-xr-x 1 root root 1315024 2009-01-09 22:10 libc-2.8.90.so
lrwxrwxrwx 1 root root 14 2008-07-04 05:58 libcap.so.1 -> libcap.so.1.10
-rw-r--r-- 1 root root 10316 2007-08-01 03:20 libcap.so.1.10
lrwxrwxrwx 1 root root 14 2008-11-01 08:55 libcap.so.2 -> libcap.so.2.10
-rw-r--r-- 1 root root 13792 2008-06-12 21:39 libcap.so.2.10
...
lrwxrwxrwx 1 root root 14 2009-01-13 09:28 libc.so.6 -> libc-2.8.90.so
...
$ ls -l /usr/lib/libc.so
-rw-r--r-- 1 root root 238 2009-01-09 21:59 /usr/lib/libc.so

按照共享库的命名惯例,每个共享库有三个文件名:real name、soname和linker name。

real name

真正的库文件(而不是符号链接)的名字是real name,包含完整的共享库版本号。例如上面的libcap.so.1.10、libc-2.8.90.so等。

soname

soname是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。例如上面的libcap.so.1和libcap.so.2是两个主版本号不同的libcap,有些应用程序依赖于libcap.so.1,有些应用程序依赖于libcap.so.2,但对于依赖libcap.so.1的应用程序来说,真正的库文件不管是libcap.so.1.10还是libcap.so.1.11都可以用,所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。注意libc的版本编号有一点特殊,libc-2.8.90.so的主版本号是6而不是2或2.8。

linker name

linker name仅在编译链接时使用,gcc的-L选项应该指定linker name所在的目录。有的linker name是库文件的一个符号链接,有的linker name是一段链接脚本。例如上面的libc.so就是一个linker name,它是一段链接脚本:

1
2
3
4
5
6
$ cat /usr/lib/libc.so
/* GNU ld script
Use the shared library, but some functions are only in
the static library, so try that secondarily. */
OUTPUT_FORMAT(elf32-i386)
GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a AS_NEEDED ( /lib/ld-linux.so.2 ) )

下面重新编译我们的libstack,指定它的soname:

1
$ gcc -shared -Wl,-soname,libstack.so.1 -o libstack.so.1.0 stack.o push.o pop.o is_empty.o

这样编译生成的库文件是libstack.so.1.0,是real name,但这个库文件中记录了它的soname是libstack.so.1:

1
2
3
4
5
6
7
$ readelf -a libstack.so.1.0
...
Dynamic section at offset 0xf10 contains 22 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000e (SONAME) Library soname: [libstack.so.1]
...

注意编译器只认 linker name 。可以先创建一个linker name的符号链接,然后再编译就没问题了:

1
2
$ ln -s libstack.so.1.0 libstack.so
$ gcc main.c -L. -lstack -Istack -o main

静态库 vs. 动态库

  1. 使用共享库可以大大节省内存。比如libc,系统中几乎所有的进程都映射libc到自己的进程地址空间,而libc的只读部分在物理内存中只需要存在一份,就可以被所有进程共享,这就是“共享库”这个名称的由来了。
  2. 使用共享库可以很方便地升级库文件而不需要重新编译应用程序。

深入阅读

  1. Libraries And Linking

Comments