

**前引:**为什么同样的代码,打包成静态库和动态库后,编译出的可执行文件大小差异巨大?运行时又为何会出现 "找不到.so 文件" 的报错?这些问题的根源,在于静态链接与动态链接的底层机制差异。本文将结合编译原理,先讲清静态库(.a)与动态库(.so)的核心区别,再通过具体案例演示从库的创建到程序调用的每一个细节,帮你打通 "创建 - 链接 - 运行" 的任督二脉!
目录
【一】库的介绍
每种语言一般都有自己的底层库,库里面不仅封装了系统调用,还提供了代码复用,比如C++的容器,从而提高编程效率!
库本质上来说库是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏
库有两种: 静态库、动态库。下面我们再来学习和实现自己的库,掌握它们的调用原理!
【二】静态库实现
静态库:在程序编译链接阶段,会把库的代码链接到自己的可执行文件中,程序运行的时候将不再依赖静态库(静态库融入了你的可执行文件),可执行下面的指令转化为静态链接
cpp
gcc text.o -o hello_static -static
(1)打包二进制
我们知道,库里面是具体的方法实现,一般调用库都是声明头文件,也只给你头文件
现在,我们来一个 library.c 作为函数具体的方法实现,库文件是二进制文件,所以先打包成二进制

cpp
gcc -c library.c -o mylib.o

(2)形成静态库
用
ar工具(静态库打包工具)将.o文件打包成静态库静态库命名规则:通常以
lib开头,以.a结尾(比如libmath.a)
ar是打包命令r:如果库中已有同名文件,替换它c:如果库不存在,创建它s:为库生成索引(加速链接时的查找)
cpp
ar rcs libmyself.a mylib.o

(3)调用静态库(同一目录)
静态库我们已经实现完成为 libmyself.a
头文件为声明,通过头文件+静态库 ,即可以完成调用,例如:main.c 调用,头文件library.h


通过 gcc 执行下面选项,链接自己的静态库:
-L.:告诉编译器 "在当前目录(.)查找静态库";
-lmath :指定要链接的静态库(libmath.a 省略 lib 和 .a 后就是 math)
cpp
gcc main.c -o Hello(目标文件) -L. -lmyself(静态库)

(4)调用静态库(异目录)
我们是通过 main 函数调用 头文件+静态库 完成调用自己的静态库
我们每次执行的 main 函数工作目录由我们控制,那如果 头文件和静态库 是分离的呢?
解决方案:告诉编译器头文件和静态库的位置
告诉编译器头文件位置:
用 -I(大写的 i)参数,后面跟 "头文件所在的文件夹路径",例如:
math_lib.h 在 ./include(./ 表示 "当前目录"),就加 -I./include
告诉编译器静态库位置:
用 -L(大写的 l)参数,后面跟 "静态库所在的文件夹路径"。例如:
libmath.a 在 ./lib,就加 -L./lib
例如:我现在把头文件和静态库都放在另一个目录,然后在 username 目录下调用它们


调用情况:(./Path1表示当前目录下的Path1目录)
cpp
gcc main.c -o Hello -I./Path1 -L./Path1 -lmyself(必须指定库名)

【三】动态库实现
动态库:程序在运⾏的时候才去链接动态库的代码,而不是将库完全融入该可执行文件中
动态库命名规则:通常以 lib 开头,以 .so 结尾(比如 libmath.so)
步骤为两步:生成位置无关代码(PIC) 然后链接为共享库
(1)生成无关代码
动态库要求代码位置无关(即代码可以加载到内存的任意位置执行),需通过 -fPIC 选项编译:
-fPIC:生成位置无关代码(Position-Independent Code),动态库必需-c:只编译不链接,生成目标文件(.o)-o mylib.o:指定输出的目标文件名
cpp
gcc -fPIC -c library.c -o mylib.o

(2)链接为共享库
使用 -shared 选项将目标文件(.o)链接为动态库(.so):
-shared:指定生成动态库(共享库)-o libmylib.so:指定输出的动态库文件名
cpp
gcc -shared -o libmyself.so mylib.o

(3)调用动态库
-L.:-L指定动态库的搜索路径(.表示当前目录,若库在其他路径则替换为实际路径)-lmylib:-l指定要链接的动态库名称(规则:省略lib前缀和.so后缀)
cpp
gcc main.c -o Hello -L. -lmyself

【四】静/动态库精髓
下面我们来整理一下动/静态库的形成过程,然后讲重点内容(路径问题),方便快速复习:
静态库的形成:
(1)函数实现打包成二进制
cpp
gcc -c library.c -o mylib.o
(2)形成静态库
cpp
ar rcs libmyself.a mylib.o
动态库的形成:
(1)生成无关代码
cpp
gcc -fPIC -c library.c -o mylib.o
(2)链接为共享库
cpp
gcc -shared -o libmyself.so mylib.o
路径问题:
在形成静/动态库之后,每次编译为可执行文件都需要通过**.I** 或者 .L告诉编译器头文件、库的位置
这无非就是一个路径问题,那么有没有办法在每次编译时不使用这两个选项呢?有的兄弟有的
(1)默认搜索路径(推)
头文件可以直接干到系统的默认搜索路径下:即 /#include
cpp
# 假设头文件是 mylib.h
sudo cp mylib.h /usr/local/include/
自己的库可以直接干到系统的默认搜索路径下:即 /usr/local/lib
cpp
# 复制动态库和静态库到 /usr/local/lib
sudo cp libmylib.so libmylib.a /usr/local/lib/
(2)刷新动态库缓冲器
仅仅支持动态库,因为静态库是直接在编译的时候嵌入可执行文件,不需要二次调用
动态库复制到默认路径后,需让系统识别新库:
cpp
sudo ldconfig # 更新动态链接器的缓存列表
(3)环境变量
环境变量是用来告诉进程每次执行时,在哪里去找对应资源,所以环境变量也是可以的
头文件添加到环境变量:
cpp
# 假设头文件放在 ~/myinclude 目录
export C_INCLUDE_PATH=~/myinclude:$C_INCLUDE_PATH
(永久生效需写入 ~/.bashrc 或 ~/.zshrc)
库文件添加到环境变量:
cpp
# 假设库文件放在 ~/mylibs 目录
export LIBRARY_PATH=~/mylibs:$LIBRARY_PATH
(4)配置文件
仅支持动态库:
cpp
# 临时生效
export LD_LIBRARY_PATH=~/mylibs:$LD_LIBRARY_PATH
# 永久生效(推荐):将路径写入 /etc/ld.so.conf.d/
echo "~/mylibs" | sudo tee /etc/ld.so.conf.d/mylibs.conf
sudo ldconfig # 更新缓存
【五】make命令使用
上面我们已经熟悉了动/静态库的制作流程,现在我们直接在makefile文件里面来完成:

在makefile中,几乎所有的命名都是可以自己设置的,那么make指令是如何区分创建/删除的?
答案:依赖目标->依赖关系。make指令会去跟随依赖关系顺藤摸瓜执行一个个小的子关系,例如
只需要执行make all 指令,它就顺藤摸瓜执行对应的各种依赖关系!(注意有先后关系!)

效果展示:

【六】动态链接加载
大家有没有想过这样一个问题?动态库是需要放在共享区被多次被调用的,动态库内部的各种函数肯定有自己的虚拟地址,而如果外面的各种文件调用同一个动态库,也就是如下情况:

假设没有偏移量:那么A进程调用完之后,再次重新调用该动态库,就会重新给库生成新的存储地方,不能保证该库都被加载到原地,内部地址也不同,那么100个进程就会有100个相同的动态库
如果有偏移量:在形成动态库的"无关代码"时,就会给库里面的每个地址(比如一个函数、变量)相较于基地址形成偏移量,比如:变量 a 在相较于基地址 10 米处,那么每次只需要更改动态库的基地址就行了,而库内部的各个变量、函数的位置,在库形成的时候就确定了,这样才保证了效率
基地址好比动态库的"大门",偏移量就好比该变量、函数较于"大门"的方位,而不是较于整个库的
**因此:**动态库形成的时候给每个成员较于基地址分配不同的偏移量,库的位置与内部变量位置无关
