《静态库与动态库:从编译原理到实战调用,一篇文章讲透》

**前引:**为什么同样的代码,打包成静态库和动态库后,编译出的可执行文件大小差异巨大?运行时又为何会出现 "找不到.so 文件" 的报错?这些问题的根源,在于静态链接与动态链接的底层机制差异。本文将结合编译原理,先讲清静态库(.a)与动态库(.so)的核心区别,再通过具体案例演示从库的创建到程序调用的每一个细节,帮你打通 "创建 - 链接 - 运行" 的任督二脉!

目录

【一】库的介绍

【二】静态库实现

(1)打包二进制

(2)形成静态库

(3)调用静态库(同一目录)

(4)调用静态库(异目录)

【三】动态库实现

(1)生成无关代码

(2)链接为共享库

(3)调用动态库

【四】静/动态库精髓

静态库的形成:

动态库的形成:

路径问题:

(1)默认搜索路径(推)

(2)刷新动态库缓冲器

(3)环境变量

(4)配置文件

【五】make命令使用

【六】动态链接加载


【一】库的介绍

每种语言一般都有自己的底层库,库里面不仅封装了系统调用,还提供了代码复用,比如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 米处,那么每次只需要更改动态库的基地址就行了,而库内部的各个变量、函数的位置,在库形成的时候就确定了,这样才保证了效率

基地址好比动态库的"大门",偏移量就好比该变量、函数较于"大门"的方位,而不是较于整个库的

**因此:**动态库形成的时候给每个成员较于基地址分配不同的偏移量,库的位置与内部变量位置无关

相关推荐
不开心就吐槽2 小时前
linux安装kafka
linux·运维·kafka
孙同学要努力2 小时前
《Linux篇》进程等待(wait、waitpid)与进程程序替换(exec等接口)
linux·服务器·网络
Nan_Shu_6142 小时前
学习:JavaScript(1)
开发语言·javascript·学习·ecmascript
zhangx1234_3 小时前
C语言题目1
c语言·开发语言·数据结构
菜鸡儿齐3 小时前
ThreadLocal介绍
java·开发语言
国服第二切图仔3 小时前
Rust开发之自定义错误类型(实现Error trait)
开发语言·python·rust
柯衍ky3 小时前
Mac通过命令行开启ssh服务
运维·macos·ssh
_w_z_j_3 小时前
Linux----进程控制
linux·运维·服务器
雨中散步撒哈拉3 小时前
14、做中学 | 初二上期 Golang集合Map
开发语言·后端·golang