静态库与动态库之间的那些事

什么是库

库是一种可执行代码的二进制形式,可以被操作系统载入到内存中执行,库根据链接不同分为静态库和动态库。

可执行文件生成的过程为:

c -> 预编译 -> 编译器 -> 汇编代码 -> 汇编器 -> 目标代码 -> 链接器 -> 可执行代码

  • 预编译:处理宏定义、删除注释、添加行号
  • 编译器:词法分析、语法分析、语义分析、代码优化
  • 汇编器:翻译成二进制
  • 链接器:合并符号表、符号解析、分配地址、重定位符号表

链接

将多个独立的模块(目标文件、静态库、动态库)链接在一起,建立全局符号表,然后再根据全局符号表修正所有的重定向表。重定向表中保留了当前目标文件中那些未知跳转地址的符号。

在编译a.o目标文件时,如果a库里面引用了其它库的符号,则在a.o的重定位表中该符号是没有对应的地址的,如果强行将a.o链接成可执行文件会出错。 此时就需要链接,假设b.o里面正好有该符号的实现,则将a.o和b.o链接,建立全局符号表,然后修正a.o中的符号对应的地址。

静态库

静态库被链接到可执行程序中采用的是静态链接。

静态链接

可执行程序在编译的时候,会将所有被链接的静态库中所有使用到的目标文件都拷贝到可执行文件中,并进行链接,重新修正所有的重定向表。

动态库

动态库被链接到可执行程序中采用的是动态链接。

动态链接

动态链接在编译的时候并不会将链接对象的内容拷贝到当前库中,仅仅在其中加入了所调用函数的描述信息用来重定位。直到链接对象被装载到内存中时,才会发生真正的链接,此时才会修正所有的重定向表,将目标文件中的未知符号地址修正。

静态装载

静态加载的方式通过在编译的时候加上 -l 来指定当前库所依赖的动态库,当可执行程序启动的时候,会自动将-l 依赖的动态库全部加载到内存中,此时加载进来的动态库会进行链接操作。

动态装载

想要动态加载的动态库,在编译时需要加上 -shared -fPIC来生成与位置无关的代码,生成的代码中使用的都是相对地址。加载使用dlopen函数以指定模式打开指定的动态库,通过dlopen来调用能够确保多个进程调用时内存中只存在一份。

动态加载就是在程序执行的时候,需要用到动态库的函数时,再加载该动态库。

JNI就是通过动态加载来实现的

具体场景分析

静态库引用静态库

假设有A、B、C、D四个静态库,其中A引用了C,B引用了D,可执行程序依赖了A、B。

  • 当A、B编译的时候,需要依赖C、D
  • 当可执行程序编译的时候,需要依赖A、B、C、D
  • 可执行程序编译完成后,会将A、B、C、D中所有使用到的目标文件copy到可执行文件中

所以如果当A、B、C、D中有重名的函数符号时,会被覆盖,第一个被链接的库中的重名函数会成效,而其他库中的会被丢弃。(因为在重定向表中一个符号只会有一个跳转地址)

静态库引用动态库

假设有A、B两个静态库,C、D两个动态库,其中A引用了C,B引用了D,可执行程序依赖了A、B。

  • 当A、B编译的时候,需要依赖C、D
  • 当可执行程序编译的时候,需要依赖A、B (因为A、B在编译的时候已经记录了引用的C、D库信息)
  • 可执行程序编译完成后,会将A、B所有使用到的目标文件copy到可执行文件中
  • 当可执行程序运行的时候,会将C、D库加载到内存中,并链接

所以如果当A、B、C、D中有重名的函数符号时,会被覆盖,第一个被链接的库中的重名函数会生效,而其他库中的会被丢弃。(因为在重定向表中一个符号只会有一个跳转地址)

动态库引用静态库

假设有A、B两个动态库,C、D两个静态库,其中A引用了C,B引用了D,可执行程序依赖了A、B。

  • 当A、B编译的时候,需要依赖C、D,此时C、D静态库会被触发链接,将库内被A、B引用的目标文件copy到A、B库中,并修正重定向表。
  • 当可执行程序编译的时候,需要依赖A、B(因为C、D已经包含到了A、B中)
  • 当可执行程序运行的时候,会将A、B库加载到内存中,并链接

所以如果A、B中有重名函数符号时,依旧是先链接的先生效,但是如果C、D中有重名函数时,由于在A、B中已经修正了重名名表,所以不会发生冲突。

动态库引用动态库

假设有A、B两个动态库,C、D两个动态库,其中A引用了C,B引用了D,可执行程序依赖了A、B。

  • 当A、B编译的时候,需要依赖C、D
  • 当可执行程序编译的时候,需要依赖A、B
  • 当可执行程序运行的时候,会将A、B、C、D库加载到内存中,并链接

动态库的重名覆盖是根据链接的顺序决定的,第一个被链接的库中的重名函数会生效。