Contents

Compiler:动态和静态库

本文采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。

Dynamic And Static Libraries

区别

静态库和动态库最本质的区别就是:该库是否被编译进目标(程序)内部。

静态(函数)库

简介

一般扩展名为(.a或.lib),这类的函数库通常扩展名为libxxx.a或xxx.lib 。
这类库在编译的时候会直接整合到目标程序中,所以利用静态函数库编译成的文件会比较大,这类函数库最大的优点就是编译成功的可执行文件可以独立运行,而不再需要向外部要求读取函数库的内容;但是从升级难易度来看明显没有优势,如果函数库更新,需要重新编译。

使用静态库的好处

1,模块化,分工合作 2,避免少量改动经常导致大量的重复编译连接 3,也可以重用,注意不是共享使用

静态库生成

libmy_static_a.c :

1
2
3
4
5
6
7
#include <stdlib.h>
#include <time.h>

int getRandInt(){
   srand(time(NULL)); 
   return rand() % 10;
}

libmy_static_a.b :

1
2
3
4
5
6
#include <stdio.h>

void printInteger(int *inValue){
    
     printf("Got integer: %d\n", (*inValue));
}

makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CFLAGS =-Wall -Werror -Wl,-rpath,$(shell pwd) 
LIBS = -L. -lmy_shared

libmy_static.a: libmy_static_a.o libmy_static_b.o
	ar -rsv libmy_static.a libmy_static_a.o libmy_static_b.o

libmy_static_a.o: libmy_static_a.c
	cc -c libmy_static_a.c -o libmy_static_a.o $(CFLAGS)

libmy_static_b.o: libmy_static_b.c
	cc -c libmy_static_b.c -o libmy_static_b.o $(CFLAGS)

.PHONY: clean
clean:
	rm *.o
	rm *.a

编译生成文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdio.h>

// forward declaration from my_lib.a
int getRandInt();
void printInteger(int *inValue);

int main(){

    printf("Press Enter to repeat\n\n");
    do{
        int n = getRandInt();
        printInteger(&n);

    } while (getchar() == '\n');
   
    return 0;
}

makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
OBJS = main.o
 
main: $(OBJS)
	gcc $(OBJS) -o main.out ./libmy_static.a
 
main.o: main.c
	gcc -c  *.c *.h  -L -llibmy_static

clean:
	rm *.o *.h.gch

扩展

静态库是多个目标(object)文件的归档(archive)(ar)。这些目标文件通常是 ELF 格式的。ELF 是 可执行可链接格式(Executable and Linkable Format) 的简写,它与多个操作系统兼容。file 命令的输出可以告诉你静态库 libmy_static.a 是 ar 格式的归档文件类型。

1
2
zyh@zyh main (master) $ file libmy_static.a 
libmy_static.a: current ar archive

使用 ar -t,你可以看到归档文件的内部。它展示了两个目标文件:

1
2
3
zyh@zyh main (master) $  ar -t libmy_static.a 
libmy_static_a.o
libmy_static_b.o

你可以用 ax -x <archive-file> 命令来提取归档文件的文件。被提出的都是 ELF 格式的目标文件:

1
zyh@zyh main (master) $ ar -x libmy_static.a
1
2
3
zyh@zyh main (master) $ file libmy_static_a.o libmy_static_a.o
libmy_static_a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
libmy_static_a.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

动态函数库

简介

动态函数库的扩展名一般为(.so或.dll),这类函数库通常名为libxxx.so或xxx.dll 。
与静态函数库被整个捕捉到程序中不同,动态函数库在编译的时候,在程序里只有一个“指向”的位置而已,也就是说当可执行文件需要使用到函数库的机制时,程序才会去读取函数库来使用;也就是说可执行文件无法单独运行。这样从产品功能升级角度方便升级,只要替换对应动态库即可,不必重新编译整个可执行文件。

动态库使用有如下好处:

1,使用动态库,可以将最终可执行文件体积缩小
2,使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
3,使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。

应用

libmy_shared.c

1
2
3
4
5
6
7
8
int negateIfOdd(int inValue){

    if (inValue % 2 == 0){
        return inValue * -1;
    } else {
        return inValue;
    }
}

makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
libmy_shared.so: libmy_shared.o
	cc -shared -o libmy_shared.so libmy_shared.o
libmy_shared.o: libmy_shared.c
	cc -c -fPIC libmy_shared.c -o libmy_shared.o
	

.PHONY: clean
clean:
	rm *.o
	rm *.so

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

// forward declaration from my_static.a
int getRandInt();
void printInteger(int *inValue);
// forward declaration from my_shared.so
int negateIfOdd(int inValue);

int main(){

    printf("Press Enter to repeat\n\n");
    do{
        int n = getRandInt();
        n = negateIfOdd(n);
        printInteger(&n);

    } while (getchar() == '\n');
   
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
OBJS = main.o
 
main: $(OBJS)
	gcc $(OBJS) -o main.out ./libmy_static.a ./libmy_shared.so

main.o: main.c
	cc -c main.c $(CFLAGS)

.PHONY: clean
clean:
	rm *.o

扩展

共享库是 Linux 系统中依赖管理的最常用方法。这些共享库在应用启动前被载入内存,当多个应用都需要同一个库时,这个库在系统中只会被加载一次。这个特性减少了应用的内存占用。

另外一个值得注意的地方是,当一个共享库的 bug 被修复后,所有引用了这个库的应用都会受益。但这也意味着,如果一个 bug 还没被发现,那所有相关的应用都会遭受这个 bug 影响(如果这个应用使用了受影响的部分)。

当一个应用需要某个特定版本的库,但是链接器(linker)只知道某个不兼容版本的位置,对于初学者来说这个问题非常棘手。在这个场景下,你必须帮助链接器找到正确版本的路径。

尽管这不是一个每天都会遇到的问题,但是理解动态链接的原理总是有助于你修复类似的问题。

幸运的是,动态链接的机制其实非常简洁明了。

ldd

1
2
3
4
5
zyh@zyh main (master) $ ldd main.out 
        linux-vdso.so.1 (0x00007ffd857b6000)
        ./libmy_shared.so (0x00007fb8688ac000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb8684bb000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fb868cb0000)

可以注意到 libmy_shared.so 库是代码仓库的一部分,但是没有被找到。这是因为负责在应用启动之前将所有依赖加载进内存的动态链接器没有在它搜索的标准路径下找到这个库。

对新手来说,与常用库(例如 bizp2)版本不兼容相关的问题往往十分令人困惑。一种方法是把该仓库的路径加入到环境变量 LD_LIBRARY_PATH 中来告诉链接器去哪里找到正确的版本。在本例中,正确的版本就在这个目录下,所以你可以导出它至环境变量:

1
2
$ LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH

现在动态链接器知道去哪找库了,应用也可以执行了。你可以再次执行 ldd 去调用动态链接器,它会检查应用的依赖然后加载进内存。

想知道哪个链接器被调用了,你可以用 file 命令:

1
2
zyh@zyh main (master) $ file main.out 
main.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=657eb92801a69848419bc6c293aa753d114926cc, not stripped

链接器 /lib64/ld-linux-x86–64.so.2 是一个指向 ld-2.30.so 的软链接,它也是我的 Linux 发行版的默认链接器:

1
$ file/lib64/ld-linux-x86-64.so.2/lib64/ld-linux-x86-64.so.2: symbolic link to ld-2.31.so

回头看看 ldd 命令的输出,你还可以看到(在 libmy_shared.so 边上)每个依赖都以一个数字结尾(例如 /lib64/libc.so.6)。共享对象的常见命名格式为:

1
libXYZ.so.<MAJOR>.<MINOR>

在我的系统中,libc.so.6 也是指向同一目录下的共享对象 libc-2.31.so 的软链接。

1
$ file/lib64/libc.so.6/lib64/libc.so.6: symbolic link to libc-2.31.so

如果你正在面对一个应用因为加载库的版本不对导致无法启动的问题,有很大可能你可以通过检查整理这些软链接或者确定正确的搜索路径来解决这个问题。

动态加载

动态加载的意思是一个库(例如一个 .so 文件)在程序的运行时被加载。这是使用某种特定的编程方法实现的。

当一个应用使用可以在运行时改变的插件时,就会使用动态加载。

动态加载器:ld.so

在 Linux 系统中,你几乎总是正在跟共享库打交道,所以必须有个机制来检测一个应用的依赖并将其加载进内存中。

ld.so 按以下顺序在这些地方寻找共享对象:

  1. 应用的绝对路径或相对路径下(用 GCC 编译器的 -rpath 选项硬编码的)

  2. 环境变量 LD_LIBRARY_PATH

  3. /etc/ld.so.cache 文件

需要记住的是,将一个库加到系统库归档 /usr/lib64 中需要管理员权限。你可以手动拷贝 libmy_shared.so 至库归档中来让应用可以运行,而避免设置 LD_LIBRARY_PATH

1
unset LD_LIBRARY_PATHsudocp libmy_shared.so /usr/lib64/

当你运行 ldd 时,你现在可以看到归档库的路径被展示出来:

1
$ ldd my_app        linux-vdso.so.1(0x00007ffe82fab000)        libmy_shared.so => /lib64/libmy_shared.so (0x00007f0a963e0000)        libc.so.6=> /lib64/libc.so.6(0x00007f0a96216000)        /lib64/ld-linux-x86-64.so.2(0x00007f0a96401000)

在编译时定制共享库

如果你想你的应用使用你的共享库,你可以在编译时指定一个绝对或相对路径。

编辑 makefile(第 10 行)然后通过 make -B 来重新编译程序。然后 ldd 输出显示 libmy_shared.so 和它的绝对路径一起被列出来了。

把这个:

1
CFLAGS =-Wall-Werror-Wl,-rpath,$(shell pwd)

改成这个(记得修改用户名):

1
CFLAGS =/home/stephan/library_sample/libmy_shared.so

然后重新编译:

1
$ make

确认下它正在使用你设定的绝对路径,你可以在输出的第二行看到:

1
$ ldd my_app    linux-vdso.so.1(0x00007ffe143ed000)        libmy_shared.so => /lib64/libmy_shared.so (0x00007fe50926d000)        /home/stephan/library_sample/libmy_shared.so (0x00007fe509268000)        libc.so.6=> /lib64/libc.so.6(0x00007fe50909e000)        /lib64/ld-linux-x86-64.so.2(0x00007fe50928e000)

这是个不错的例子,但是如果你在编写给其他人用的库,它是怎样工作的呢?新库的路径可以通过写入 /etc/ld.so.conf 或是在 /etc/ld.so.conf.d/ 目录下创建一个包含路径的 <library-name>.conf 文件来注册至系统。之后,你必须执行 ldconfig 命令来覆写 ld.so.cache 文件。这一步有时候在你装了携带特殊的共享库的程序来说是不可省略的。

怎样处理多种架构

通常来说,32 位和 64 位版本的应用有不同的库。下面列表展示了不同 Linux 发行版库的标准路径:

红帽家族

◈ 32 位:/usr/lib

◈ 64 位:/usr/lib64

Debian 家族

◈ 32 位:/usr/lib/i386-linux-gnu

◈ 64 位:/usr/lib/x86_64-linux-gnu

Arch Linux 家族

◈ 32 位:/usr/lib32

◈ 64 位:/usr/lib64

FreeBSD(技术上来说不算 Linux 发行版)

◈ 32 位:/usr/lib32

◈ 64 位:/usr/lib

总结

从产品化的角度,发布的算法库或功能库尽量使动态库,这样方便更新和升级,不必重新编译整个可执行文件,只需新版本动态库替换掉旧动态库即可。
从函数库集成的角度,若要将发布的所有子库(不止一个)集成为一个动态库向外提供接口,那么就需要将所有子库编译为静态库,这样所有子库就可以全部编译进目标动态库中,由最终的一个集成库向外提供功能。