【Linux】动静态库
目录
- 1. 静态库
- 1.1 生成静态库
- 1.2 静态链接
- 1.2.1 带选项编译链接
- 1.2.2 安装库到系统库路径
- 1.2.3 系统库路径下建立软链接
- 2. 动态库
- 2.1 生成动态库
- 2.2 动态链接
- 2.3 动态库的加载方式
- 2.3.1 拷贝到系统默认的库路径
- 2.3.2 建立软链接
- 2.3.3 环境变量法
- 2.3.3 建立动态库路径配置文件
- 2.4 动静态库的区别
在 linux 系统中,形如 libXXX.a 的文件称为静态库,对应是的静态链接;形如 libYYY.so 为动态库,对应的是动态链接,也是 gcc/g++ 的默认编译行为。但是对于动静态库 和 动静态链接,远远不只这些。
1. 静态库
1.1 生成静态库
// mymath.h 文件
#pragma once
#include<stdio.h>
extern int myerrno;
int add(int x, int y);
int sub(int x, int y);
int mul(int x, int y);
int div(int x, int y);
// mymath.c 文件
#include "mymath.h"
int myerrno = 0;
int add(int x, int y) { return x + y; }
int sub(int x, int y) { return x - y; }
int mul(int x, int y) { return x * y; }
int div(int x, int y) { if(y == 0){ myerrno = 1; return -1; } return x / y;
}
将我们自己编写的方法提供给其它文件,有两种途径:
- 提供源代码,在其它文件下包含该头文件,编译时,将方法实现一同编译
- 把源代码打包成库,最终提供库 + 头文件‘
lib=libmymath.a # 静态库的名称# ar --- 生成静态库(把所有 .o 文件打包形成 .a 文件)
# -rc --- replace 和 create 的意思,如果目标文件存在则替换,不存在则创建。
$(lib):mymath.o ar -rc $@ $^ # 我们 gcc 多文件编译时,就是把每个文件编译为 .o 文件,最后链接起来形成可执行程序
# 而所谓静态链接,无非就是先将源代码都编译为 .o 文件,把所有 .o 文件打包,命令为形如 libXXX.a 的文件
# 将来只需要将我们的 main.c 之类的源文件编译为 .o 文件,然后与 libXXX.a 结合,这就是静态链接!
mymath.o:mymath.c gcc -c $^ .PHONY:clean
clean: rm -rf *.o *.a lib # 发布库
.PHONY:output
output: mkdir -p lib/include mkdir -p lib/mymathlib cp *.h lib/include cp *.a lib/mymathlib
这是因为找不到头文件,编译时,编译器会在系统头文件 和 当前路径下查找头文件,但这两个地方都没有 mymath.h。
1.2 静态链接
1.2.1 带选项编译链接
gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath
// -I 指定头文件所在目录
// -L 指定方法的实现所在目录
// -l 指定方法的实现所在目录中具体的库文件(一般不带空格)
// 库的文件名为 libmymath.h,但库的真实名字为 mymath(即去除lib前缀和文件格式后缀)
关于 gcc/g++ 的默认编译行为都是动态链接的,但是当我们没有动态库,只有静态库的时候,即便编译选项不带 -static
,也只能是静态链接(因为只能静态链接啊,没有动态库啊,至少在编译时,需要用到的库,是静态链接的)。也可以理解为 -static
选项只是一个给编译器建议性的选项,在有相应的静态库时,编译器会听取建议。
1.2.2 安装库到系统库路径
静态链接也可以不用在编译时带上 -I
和 -L
选项,如果直接把库文件拷贝到系统路径下,那么也可以实现编译时的静态链接,但是依旧需要带上 -l
选项指明具体要链接的库文件。(该做法一般不推荐)
1.2.3 系统库路径下建立软链接
#include "myinc/mymath.h" // 建立了软链接后,头文件指向软链接即可
int main()
{ int ret = div(10, 0); printf("10 / 0 = %d, errno = %d\n", ret, myerrno); return 0;
}
但是无论是哪种链接方式,都需要在编译时指明特定的库文件,要不然编译器无法得知使用哪些静态库进行链接。
软链接的其中一个应用场景就是如此,可以在系统路径下通过建立软链接,快速定位各种库文件,而不再需要将整个库拷贝到系统路径下。
2. 动态库
2.1 生成动态库
// myprint.h
#pragma once
#include<stdio.h>
void Print();
// myprint.c
#include "myprint.h"
void Print()
{printf("This is a dynamic-linking test!\n");printf("This is a dynamic-linking test!\n");printf("This is a dynamic-linking test!\n");
}
// mylog.h
#pragma once
#include<stdio.h>
void Log(const char* msg);
// myprint.c
#include "mylog.h"
void Log(const char* msg) { printf("Warning: %s\n", msg); }
生成动态库的核心思想与静态库是一致的,都是把各源文件先编译成 .o 文件,然后把所有 .o 文件打包,最后用户把自己的源文件编译为 .o 文件,再与库文件进行链接就行了。因此在链接之前,无论动静态库,在链接之前都需要先编译为 .o 文件。
静态库是通过命令 rc 打包生成的,但动态库并不直接通过系统指令来打包,动态库的生成一系列工作是内嵌在 gcc/g++ 编译器当中,可以理解动态库/动态链接与编译器的关系是最亲近的,这也就能够进一步理解为什么编译器的默认链接都是动态链接了。
- shared: 表示生成共享库格式
- fPIC:产生位置无关码(position independent code)
- 库名规则:libxxx.so
gcc -fPIC -c mylog.c # 先带上 -fPIC 生成 .o 文件
gcc -fPIC -c myprint.c
gcc -shared -o libmymethod.so *.o # 正常 -o 选项编译为生成可执行程序,加上 -shared 指明不直接生成可执行,而是生成一个共享库
静态库提供源代码的(二进制文件数据),当用户的可执行程序使用到静态库的数据时,把静态库的源代码拷贝到用户的可执行程序,拷贝完成后,用户的源文件编译就全部完成,跟静态库就再也没关系了,而将来可执行程序运行起来变为进程,静态库是不需要被加载到内存的,因为需要的静态库的数据,已经拷贝到可执行程序里面了。
而动态库它并不像静态库一样,直接拷贝的形式,当可执行程序执行时,需要访问动态库的数据,就需要跳转到动态库去执行,动态库要被执行,就注定了要被加载到内存中。有了可执行权限作为前提,库文件才能快速的被系统加载到内存中(不排除没有 x 权限的文件也会被系统加载)。总而言之,给动态库带上 x 权限,也是一种 “动态库需要被执行” 的含义,虽然动态库里面没有 main 函数,但是它提供的方式是将来用户的可执行程序可能要访问的,也算是用户可执行程序的一部分了,因此也算是用户的可执行程序的一部分(动态库不是不能被执行,只是不能被单独执行!!!)。
# Makefile
dy-lib=libmymethod.so
static-lib=libmymath.a# 同时生成多个目标文件
.PHONY:all
all: $(dy-lib) $(static-lib)$(static-lib):mymath.oar -rc $@ $^
mymath.o:mymath.cgcc -c $^$(dy-lib):myprint.o mylog.ogcc -shared -o $@ $^ # -shared 生成共享库
myprint.o:myprint.cgcc -fPIC -c $^ # -fPIC 产生位置无关码
mylog.o:mylog.cgcc -fPIC -c $^.PHONY:clean
clean:rm -rf *.o *.a mylib.PHONY:output
output:mkdir -p mylib/includemkdir -p mylib/libcp *.h mylib/includecp *.a mylib/libcp *.so mylib/lib
2.2 动态链接
// main.c
#include "mylog.h"
#include "myprint.h"
int main()
{Print();Log("This is a log message!\n");return 0;
}
与静态链接一样,编译时直接带上 -I
确定头文件路径,-L
确定库文件路径,-l
确定库文件。
gcc main.c -I mylib/include/ -L mylib/lib/ -lmymethod
编译成功了,也即动态链接这个过程并没有问题,但是当程序运行起来之后,却显示 “file not found” 这样的字眼,我们不说已经通过各种选项,告诉编译器头文件、库文件在哪了吗??为什么还会找不到呢??
其实,编译器它确实找到了,但是动态库它不像静态库啊,编译链接完就没它啥事了。当程序运行起来,变为进程,该进程的代码推进时,如果调用了动态库中的数据(即方法),那么操作系统(加载器)就需要去执行动态库的方法,但是加载器不知道动态库在哪啊!
想要搞清楚动态库加载的问题,就需要先清楚那为什么诸如 libc.so.6 这样的系统中的动态库可以被正常加载。系统指令在执行时不需要携带路径,而我们自己的 exe 程序需要路径执行,这是因为环境变量的存在。而编译器有自己默认的搜索头文件、库文件的路径,操作系统同样也有一套自己默认的搜索动态库路径,这些路径都是操作系统内置好的,所以要想让动态库顺利被加载,就需要解决系统的搜索问题!
2.3 动态库的加载方式
关于动态库加载的办法,有以下四种。
2.3.1 拷贝到系统默认的库路径
将动态库拷贝到系统路径的 /lib64 或者 /usr/lib64 即可解决动态库的加载问题(与静态库相似,因此不多言)
2.3.2 建立软链接
sudo ln -s /home/outlier/linux/mylib/test/mylib/lib/libmymethod.so /lib64/libmymethod.so
在动态链接之后,由于加载器找不到动态库的问题,在系统默认库路径下建立一下动态库的软链接,甚至都不需要重新编译链接,该动态库就能够被系统找到了。
2.3.3 环境变量法
- LD_LIBRARY_PATH:搜索用户自定义的库路径
将自己的库所在路径添加到系统环境变量的 LD_LIBRARY_PATH 中,同样可以解决动态库加载的问题!
echo $LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx # 追加环境变量的搜索路径(会话周期性)
2.3.3 建立动态库路径配置文件
cd /etc/ld.so.conf.d
touch mylib.conf
echo "/home/xxx/xxx/xxx/xxx/mylib/lib" > mylib.conf # 写入路径
ldconfig # 加载配置文件
系统维护动态库的配置文件,文件内存储的是动态库的路径,只要在 /etc/ld.so.conf.d 这个目录下建立一个 .conf 的配置文件,把动态库的路径写入配置文件,那么系统就能够找到动态库的路径。
动态库路径的配置文件不关注文件名,但需要注意的是,如果存在多个动态库 且 位于同一路径下,可以不需要建立多个路径配置文件,但如果不在同一个路径下,则需要建立多个配置文件来导入多个路径(一个配置文件一个路径)。
实际开发中,企业用的各种第三方库,都是很成熟的库了,因此都采用直接按照到系统库路径下的方式加载动态库,这种方法只是我们平时学习时不使用(因为我们的库不成熟,很简陋,按照到库路径下,有点污染库的意思哈哈哈)。
2.4 动静态库的区别
#include "mymath.h"
#include "mylog.h"
#include "myprint.h"
int main()
{int ret = add(1, 1);printf("1 + 1 = %d, errno = %d\n", ret, myerrno);Print();Log("This is a log message!\n");return 0;
}
程序编译完后,静态库被删除了,程序不会受到任何影响,因为需要用到的静态库的内容,已经在编译链接时拷贝到自己的可执行程序中了;但是动态库被删除了,程序就运行不起来了,这就是动静态库最本质的区别,动态库并不做拷贝行为,而是在程序运行起来之后,在调用动态库的地方,转而运行动态库的内容,如今动态库没了,程序也就自然运行不起来了。
并且我们可以看到的是,动态库只有一个,但是当多个可执行程序依旧可以正常运行,这也就说明了,动态库是被多个程序所共享的!因此动态库也称为共享库。即便有10个可执行程序,只要调用的是这个动态库的内容,那么在操作系统中,只需要加载这一个动态库,也只加载一次即可,并不需要因为有多个可执行程序而加载多次动态库。
系统指令也可以很好的印证上述论点,诸如 ls、pwd 等指令都用到了系统库 lic.so.6,但系统中就只有这一个 lic.so.6 动态库,并没有存在多份,但各指令依旧正常运行,因为它们都是共享同一个动态库的。
所以,动态库在系统中加载之后,会被所有进程共享! 所有进程共享一个库,系统中重复的代码就减少了,同时也可以大大节省内存。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!