Linux动静态库

静态库

将库的实现预编译成.o格式,然后将这些.o文件打包成.a文件,这份文件必须以lib开头,这就是静态库的实现。

下面代码演示建立并调用静态库

头文件static.h

cpp 复制代码
#include<stdio.h>
#include <stdlib.h>
extern int syserrno;
int mydiv(int a,int b);

源文件static.c

cpp 复制代码
#include"static.h"
//简单除法实现
int syserrno=0;

int mydiv(int a,int b){
    if(b==0){
        syserrno=-1;
        return -1;
    }
    return a/b;
}

makefile

cpp 复制代码
lib=libdiv.a
$(lib):static.o//这里是为了方便修改
	ar -rc $@ $^//将.o文件打包成lib

static.o:static.c
	gcc -c static.c

.PHONY:clean
clean:
	rm -rf *.o *.a lib
.PHONY:output
output://为头文件和静态库建立目录分别存放
	mkdir -p lib/include
	mkdir lib/divlib
	cp *.h lib/include
	cp *.a lib/divlib

调用make、make output后文件结构如下:


手动gcc完成静态库链接(也可写在makefile中,这里做演示)

gcc的选项解释

-I 的作用是在编译的时候告诉编译器头文件所在路径,这一步还没有链接

-L的作用是告诉链接器库的路径,所以-L后跟着库所在路径

-l为 链接器指定具体的库文件,因为库所在路径下可能有多个库。


静态链接的其它方式:

  • 将静态库安装到系统指定路径下,因为gcc默认在系统路径和当前路径下搜索库文件,其它路径找不到

  • 在系统路径下建立静态库的软链接


动态库

将.c带fPIC选项预编译为.o文件,然后带shared选项将.o文件编译为动态库文件

makefile动静态库整合版本

cpp 复制代码
libs=libdiv.a
libd=libadd.so

all:$(libs) $(libd)//这里是为了同时执行动静态库的实现

$(libs):static.o
	ar -rc $@ $^

static.o:static.c
	gcc -c $^

$(libd):dynamic.o
	gcc -shared -o $@ $^
	
dynamic.o:dynamic.c
	gcc -fPIC -c $^ -o $@

.PHONY:clean
clean:
	rm -rf *.o *.a *.so lib
.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/divlib
	cp *.h lib/include
	cp *.a lib/divlib
	cp *.so lib/divlib

动态库的编译同样要带-I-L-l选项告诉编译器库和头文件的位置

和静态库不同的是,静态库在编译后可直接被可执行程序加载,而动态库是运行程序时被加载,所以加载器也需要知道要链接的库的位置

让加载器找到动态库的方法

  1. 拷贝到系统默认的库路径 /lib64 /usr/lib64/

  2. 在系统默认的库路径 /lib64 /usr/lib64/下建立软连接

  3. 将自己的库所在的路径,添加到系统的环境变量LD_LIBRARY_PATH中

    cpp 复制代码
    export LD_LIBRARY_PATH=static_depot/lib/divlib:$LD_LIBRARY_PATH
  4. /etc/ld.so.conf.d 建立自己的动态库路径的配置文件,然后重新ldconfig即可

    cpp 复制代码
    sudo vim /etc/ld.so.conf.d/mylib.conf
    /home/doggy/mylib/
    sudo ldconfig

动态库是如何被多个进程加载的?

操作系统通过 inode 找到文件在磁盘上的数据块将数据从磁盘读入物理内存(物理页帧)页表负责把虚拟地址映射到那些物理页帧。

同理,动态库也是磁盘中的文件,当cpu执行调用库指令时,库被加载到内存中(因为已经链接过了加载器知道去哪找),并通过页表映射到虚拟地址空间的共享区中,库代码在共享区中运行完毕后,cpu回到正文部分继续执行指令。

动态库加载到内存后,如果又一进程调用该库,操作系统识别到内存已经加载了动态库,直接为该进程页表添加映射,实现动态库的复用,因此动态库比静态库更能节省空间。

动态库的代码段被所有进程共享(只读),但全局变量等数据段每个进程各自有独立副本,通过写时拷贝机制实现互不冲突


再谈虚拟地址空间

程序没有加载前的地址(虚拟地址)

程序编译好后有内置地址(虚拟地址),这种地址遵照程序的逻辑结构 ,上面存储着指令,每条指令都有自己的长度。cpu在设计时已经认识各种指令(cpu指令集),所以能够按顺序执行这些指令而不是把指令当做数据。虚拟地址和物理地址的最小寻址单元都是字节 ,地址宽度相同,所以两者之间可以建立映射。

程序加载后的地址

程序开始运行,指令被加载到内存,同时具虚拟地址和物理地址。

cpu如何执行到第一条指令?

虚拟地址有一个入口地址,cpu有个EIP寄存器专门用于存储虚拟地址,程序开始运行时,EIP存放的是入口地址,发现入口地址没和物理内存关联,此时触发缺页中断,页表将两种地址做映射。由此cpu根据虚拟地址找到物理地址。

所以cpu不是直接去内存中从上到下执行,而是跟着指令走,也就是跟着虚拟地址来走,因此代码加载是靠缺页中断按需进行的。

动态库的加载问题

动态库编译后不能产生固定的地址空间,因为动态库不知道自己内部的函数此后会被加载到虚拟地址空间的哪个位置。比方说,动态库中的某个函数要调用同库的另一个函数,它不能写死一个绝对地址,因为动态库在不同进程的虚拟地址空间中被映射到的位置不同,同一份指令必须对所有进程

都通用,所以必须用相对偏移。

而可执行程序不存在这个问题,因为每个进程拥有自己独立的虚拟地址空间,可执行程序每次加载到固定位置,所以地址可以直接写死。

因此制作动态库时要通过 -fPIC 编译 .c 生成 .o,让机器码中的地址引用全部变成相对偏移(相对于当前指令地址),而不是绝对地址。这样无论动态库被映射到哪个虚拟地址,指令都能正确找到目标。

静态库则不同,它在链接时就被复制进了可执行程序,地址在编译时就全部确定下来了,不需要 -fPIC。

相关推荐
Escalating_xu7 分钟前
【Linux 网络编程】五种 I/O 模型与非阻塞 I/O:从“等数据”到正确处理 EAGAIN
java·linux·网络
扶尔魔ocy33 分钟前
【QT入门到晋级】用开源的自定义控件开发
c++·qt·自定义控件
无言听风雨丶35 分钟前
Linux 容器本质探秘:Cgroup 与 Namespace 深度解析与实战
linux·运维·服务器·kubernetes
万联WANFLOW35 分钟前
CVE-2026-98432 等漏洞自查:AI 查出的 Linux、Windows 漏洞,运维怎么确认受不受影响
linux·运维·windows·网络安全
OPEN-F1 小时前
Ubuntu 自动登录后 GNOME 远程桌面连不上?一次登录钥匙环锁定的排查与永久修复
linux·运维·ubuntu
数字新视界1 小时前
机房动环监控系统在提升数据中心能效与安全中的应用探索
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
硅基手札1 小时前
【Linux 内核专栏 19】20 · 嵌入式子系统 — GPIO·pinctrl·clock·regulator
linux·运维·单片机·linux内核·嵌入式开发·调试定位
默|笙1 小时前
【Linux网络】传输层协议TCP(3)滑动窗口、流量控制
linux·网络·tcp/ip
akinggw2 小时前
libsherpacore - 基于 sherpa-onnx 的 C++ 语音交互静态库
c++·交互·语音识别
xiaoye-duck2 小时前
《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(中):全量响应实现和测试
网络·c++·语言模型·chatgpt