Linux 动静态库深度解析:原理、制作与实战

在 Linux 开发中,动静态库是代码复用与工程化的核心基础。无论是系统自带的标准库,还是自定义的功能模块,都离不开库的封装与调用。本文将从原理、制作、使用三个维度,结合实战案例详细拆解 Linux 动静态库,帮助你彻底掌握这一关键技术。

1. 动静态库核心概念与区别

库本质是一组编译后的二进制代码集合,包含可复用的函数、变量等功能模块,避免重复开发,如printf 等非常常用的函数,我们总不可能每次写代码还要再写一遍printf是如何实现的吧?所以我们把那些非常常用的函数和变量等写入库中,每次使用时只需要引用库即可,大大提高了编程效率。

Linux 系统中动静态库的标识、特性差异显著,具体如下:

1.1 基本标识

静态库: 后缀为.a(如libc.a),Windows 系统对应.lib
**动态库:**后缀为.so(如libc.so.6),Windows 系统对应.dll

1.2 核心差异对比

其中我们最需要了解的差异点我们来详细说一下,静态库和动态库的加载其实就相当于是我们周末在学校指定学习计划,到休息时间要玩电脑游戏,既然要玩电脑游戏那我们总得有电脑吧?

静态库的加载其实就相当于我们把电脑带到学校里来,可以直接开玩,独立性很强不受外界影响,代入到程序中其实就是在编译时把库中我们要使用的函数、变量等直接在我们的程序中拷贝一份。那我们每次带电脑进学校其实负担是比较大的,显而易见的是静态库这样加载肯定会让我们的程序体积变得很大,所以我们程序编译链接其实一般是使用动态库链接的。

动态库的加载就好比我们想要玩电脑游戏,直接去学校附近的网吧玩就可以了,而我们自己只需要知道网吧在哪儿,就能直接去,再也不用辛苦的带电脑进学校了。代入到程序中,其实就是我们的程序只需要记录动态库在内存中的地址,等使用到其中的函数或变量时直接根据这个地址跳转过去,执行完毕后再回来,这样一来我们的程序体积就会大大减小了,但同时独立性就没有静态库加载那么强了,就好比我们要去网吧玩游戏,那万一网吧倒闭了,就什么都玩不了了。在程序中,如果动态库加载到内存中、记录动态库在内存中的地址等步骤出现了问题,那我们的程序也就会出问题了,这就是独立性较低。

1.3 底层本质

动静态库的本质都是.o目标文件的集合 ------ 将多个功能模块编译为目标文件后,通过工具打包而成。使用时只需提供头文件(声明功能)和库文件(实现功能),编译器通过链接过程关联到最终程序中。

一个 C/C++ 源文件(比如test.c)变成可执行程序(比如a.out),要走 4 个步骤,像 "流水线加工零件":

1. 预处理: 把#include的头文件 "粘贴" 进来、去掉注释、替换宏,输出test.i(还是文本)。
2. 编译: 把test.i翻译成汇编指令(比如mov、add),输出test.s(汇编代码)。
3. 汇编: 把汇编指令转成二进制机器码,输出test.o(目标文件,是 "半成品零件")。
**4. 链接:**把多个test.o(比如自己写的 + 系统库的)"拼" 成完整程序,输出a.out(可执行文件)。

我们来举个例子说明为什么需要库,假设你要搭两个 "积木模型":

模型 1: 用「方块 + 圆柱 + 三角」搭 "小房子"
**模型 2:**用「方块 + 圆柱 + 星星」搭 "小汽车"

如果每次搭都要单独找 "方块、圆柱",会很麻烦 ------不如把 "方块 + 圆柱" 打包成一个 "通用积木包",下次搭模型直接拿这个包,不用重复找零件。

对应到代码里:

• 你的代码:test1.c(方块)、test2.c(圆柱)、test3.c(三角)、main1.c(小房子逻辑)

• 编译后生成:test1.o(方块)、test2.o(圆柱)、test3.o(三角)、main1.o(小房子逻辑)

• 链接时要把这 4 个*.o拼起来,才能生成 "小房子程序"。

但如果要写第二个程序main2.c(小汽车逻辑),又需要test1.o(方块)、test2.o(圆柱)------重复拿这两个*.o太麻烦,不如把它们打包成 "库",下次直接用这个库,不用再找单个*.o。

库的本质:

不管静态库还是动态库,*本质都是 "一堆.o 目标文件的集合"**------ 就像 "把多个积木零件装在一个盒子里"。

用的时候:

• 你只需要拿到两个东西:
1. 头文件(.h): 告诉用户 "这个库里有什么零件"(比如 "方块怎么用、圆柱的尺寸"),对应库中函数 / 变量的声明。
**2. 库文件(.a/.so):**实际的 "零件"(*.o的打包体),对应函数 / 变量的实现。

• 编译时,编译器通过 "链接" 步骤,把库中的*.o和你的代码拼在一起,生成最终程序。

1.4 动静态库的查看

我们可以通过ldd 文件名 来查看一个可执行程序所依赖的库文件。其中的 libc.so.6就是该可执行程序所依赖的库文件,我们可以通过file 文件名命令来进一步查看 libc.so.6的文件类型:

通过上图观察,我们知道gcc/g++编译器默认都是动态链接的,这和我们上面所讲的吻合,如果想使用静态链接,需要在后面加一个-static。如果我们没有安装对应的静态库的话,可以使用以下指令安装:

bash 复制代码
sudo apt install glibc-static
sudo apt install libstdc++-static

2. 动静态库制作

2.1 准备工作

首先创建 4 个文件:2 个头文件(声明函数)+ 2 个实现文件(定义函数),如下:

add.h

cpp 复制代码
// add.h 用#pragma once避免头文件重复包含
#pragma once

int add(int x, int y);

add.c

cpp 复制代码
#include "add.h"

int add(int x, int y) 
{
    return x + y;  // 直接返回两数之和
}

sub.h

cpp 复制代码
#pragma once

int sub(int x, int y);

sub.c

cpp 复制代码
#include "sub.h"

int sub(int x, int y) 
{
    return x - y;  // 直接返回两数之差
}

2.2 静态库的制作

静态库在编译时会被完整嵌入可执行程序,运行时无需依赖外部文件,适合追求独立性的场景。

制作静态库(3 步)

静态库制作依赖 gcc(编译目标文件)和 ar(打包目标文件),步骤如下:

(1)编译生成目标文件(.o)

将add.c和sub.c编译为二进制目标文件(仅编译不链接):

执行后会生成 add.o 和 sub.o 两个文件,这是库的 "半成品零件"。

(2)打包为静态库(.a)

用 ar 工具的 -rc 选项(replace+create)将目标文件打包,库名必须以lib开头:

ar -rc: 若库文件不存在则创建,若已存在则替换旧目标文件

• **libmath.a:**静态库名(math是自定义库标识,可修改)

验证静态库内容

用ar -tv查看库中包含的目标文件,确认打包成功:

(3)组织头文件和生成的静态库

当我们将自己的库给别人使用的时候,通常要给两个文件夹:

• 一个文件夹存放头文件合集。我们将 .h 文件都放在 include 文件夹下。

• 一个文件夹放实现文件合集。我们已经将所有.o文件打包成 libmath.a了,将它放在 lib文件夹下。

最后,我们将 include 和 lib两个目录放在mathlib目录下,我们就可以将mathlib给别人使用了:

注:移动文件也可以用 mv 命令。

为了方便使用,我们可以写一个 Makefile 来一键使用:

bash 复制代码
# 1. 生成静态库:依赖add.o和sub.o
libmath.a:add.o sub.o
	ar -rc libmath.a $^

# 2. 通用规则:所有.c文件生成对应的.o文件
%.o:%.c
	gcc -c $^

# 3. 清理编译产物
.PHONY:clean
clean:
	rm -rf ./*.o mathlib

# 4. 发布库:生成mathlib目录(含include和lib)
.PHONY:output
output:
	# 创建mathlib下的include和lib子目录
	mkdir -p mathlib/include
	mkdir -p mathlib/lib
	# 复制所有头文件到include目录
	cp ./*.h mathlib/include
	# 移动静态库到lib目录
	mv ./*.a mathlib/lib

2.3 静态库的使用

创建测试程序 main.c,调用静态库中的 add 和 sub 函数,再编译运行。

cpp 复制代码
#include "add.h"
#include "sub.h"
#include <stdio.h>

int main() 
{
    int a = 10, b = 3;
    // 调用静态库中的函数
    printf("a + b = %d\n", add(a, b));  // 预期输出13
    printf("a - b = %d\n", sub(a, b));  // 预期输出7
    return 0;
}

在编译文件时,系统不知道我们声明的头文件和库文件在哪里,同时我们链接的库中可能还存在不同的库文件,需要指明要使用的是哪个头文件,于是使用 gcc命令需要包含以下三个选项:

-I: 指定头文件搜索路径。(大写i )

-L: 指定库文件搜索路径。

• **-l:**指明需要链接库文件路径下的哪一个库。(小写 L)

注:库名为去掉 lib前缀和.a后缀剩余的部分,同时这三个选项后加不加空格都可以正常执行。

编译main.c时,需通过-I、-L、-l指定头文件路径、库路径和库名,命令如下:

bash 复制代码
gcc main.c -I./mathlib/include -L./mathlib/lib -lmath -o main

参数解析:

-I./mathlib/include: 告诉编译器 "去mathlib/include目录找头文件(如add.h)";
-L./mathlib/lib: 告诉编译器 "去mathlib/lib目录找库文件(如libmath.a)";
-lmath: 指定要链接的库名(libmath.a省略lib前缀和.a后缀后为math);
**-o main:**指定生成的可执行文件名为main。

运行结果:

这里其实有一个疑问,为什么平时我们用系统中的库文件时不需要使用**-I、-L、-l**选项呢?其实也不难猜到,系统肯定有自己的默认的指定路径,平常会在默认指定路径下去找这些文件,那么如果我们将自己的头文件、库文件放在这些指定目录下,那么此时就只需要用 -l 指明链接的库即可:

bash 复制代码
# 复制头文件到系统头文件目录
sudo cp mathlib/include/* /usr/include/
# 复制静态库到系统库目录
sudo cp mathlib/lib/* /lib64/

但我们十分不推荐这种做法,因为该方案会污染系统文件,如我们不清楚如这些文件是否会覆盖系统原有文件,导致系统出现问题。

2.4 动态库的制作

动态库需生成 "位置无关码"(支持加载到任意内存地址),依赖gcc的-fPIC和-shared参数。

(1)编译位置无关目标文件(.o)

• **-fPIC:**生成位置无关码(Position Independent Code),确保动态库可加载到任意内存地址

(2)打包为动态库(.so)

这里我们不需要使用ar命令了,但要用 -shared选项生成动态库,库名同样以lib开头:

(3)组织头文件和生成的动态库

这一步和上面的一致,我们就不再重复了。

Makefile:

bash 复制代码
# 目标:生成动态库 libmath.so,依赖 add.o 和 sub.o 两个目标文件
libmath.so:add.o sub.o
	# -shared:指定生成动态库(共享库)格式
	# -o $@:$@ 代表目标文件(即 libmath.so),指定输出文件名
	# $^:代表所有依赖文件(即 add.o sub.o),将依赖的目标文件打包为动态库
	gcc -shared -o $@ $^

# 通用编译规则:将所有 .c 源文件编译为位置无关的 .o 目标文件
%.o:%.c
	# -fPIC:生成位置无关码(Position Independent Code),确保动态库可加载到任意内存地址
	# -c:仅编译不链接,生成目标文件(.o),不生成可执行程序
	# $<:代表当前依赖的 .c 源文件(如 add.c、sub.c)
	gcc -fPIC -c $<

# 伪目标:clean,用于清理编译产物(避免与实际文件重名)
.PHONY:clean
clean:
	# 删除所有 .o 目标文件和发布目录 mathlib
	rm -rf ./*.o mathlib

# 伪目标:output,用于生成规范的发布目录结构
.PHONY:output
output:
	# 创建 mathlib 目录及下属的 include(存头文件)和 lib(存动态库)子目录
	# -p:确保父目录不存在时自动创建,避免报错
	mkdir -p mathlib/include
	mkdir -p mathlib/lib
	# 复制所有 .h 头文件(add.h、sub.h)到 include 目录,供使用者引用
	cp ./*.h mathlib/include
	# 将生成的动态库(.so 文件)移动到 lib 目录,统一管理
	mv ./*.so mathlib/lib

2.5 动态库的使用

动态库的使用同样需要带三个选项:

bash 复制代码
gcc main.c -I./mathlib/include -L./mathlib/lib -lmath -o main

运行结果:

我们可以发现打开动态库失败,原因是找不到动态库所在位置,为什么会这样呢?我们不是指定目录了吗?

其实这是因为 -I(头文件路径)、-L(库路径)、-l(库名)这三个选项,仅在编译链接阶段生效------ 它们是告诉编译器 "去哪里找头文件、去哪里找库文件、要链接哪个库",确保编译能成功生成可执行程序。可执行程序生成后,编译期的-L等参数会失效,系统(动态链接器)会按照自己的规则搜索动态库,即查找它的默认搜索路径。

解决方法:

(1)复制动态库到系统默认路径

将动态库复制到/usr/lib64或/lib64(系统默认搜索路径),简单但可能污染系统目录

bash 复制代码
# 复制libmath.so到系统库目录
sudo cp ./mathlib/lib/libmath.so /lib64

# 更新缓存(可选,部分系统需执行)
sudo ldconfig

(2)临时设置环境变量

LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中,程序运行起来时就能找到对应的路径下的动态库。

通过LD_LIBRARY_PATH告诉动态链接器自定义库路径,仅当前终端生效,关闭重启终端后就失效了:

bash 复制代码
# 临时添加mathlib/lib到动态库搜索路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/nep/linux-learning/lesson21/mathlib/lib

(3)建立软链接

这个其实也很简单,就在在系统默认查找路径下( /lib64 或 /usr/lib64 )创建库的软链接,这样系统查找时也能找到我们的动态库所在位置:

bash 复制代码
# 创建软链接:sudo ln -s 动态库实际路径 系统默认库路径/软链接名
sudo ln -s /home/nep/linux-learning/lesson21/mathlib/lib/libmath.so /lib64/libmath.so

(4)配置系统全局路径

在系统中,/etc/ld.so.conf.d/是用于搜索动态库的路径。此路径下存放的全是后缀为.conf的配置文件,这些配置文件中所存放的内容都是动态库的路径,所以我们只要新建一个配置文件,将库文件路径写入其中,那么可执行程序运行时,系统也能够找到我们的库文件,同时这个方法是永久生效的,就算关闭重启终端也依然能够生效:

如果使用sudo echo命令还是会出现权限不足问题,所以我们加上tee 命令让我们能够成功写入。

结语

好好学习,天天向上!有任何问题请指正,谢谢观看!

相关推荐
云和数据.ChenGuang3 小时前
欧拉(openEuler)和CentOS
linux·运维·centos
qq_589568103 小时前
centos打开文件之后怎么退出 ,使用linux命令
linux·运维·centos
linuxxx1104 小时前
Cannot find a valid baseurl for repo: centos-sclo-rh/x86_64
linux·运维·centos
HIT_Weston4 小时前
69、【Ubuntu】【Hugo】搭建私人博客(三)
linux·运维·ubuntu
春日见4 小时前
眼在手上外参标定保姆级教学---离线手眼标定(vscode + opencv)
linux·运维·开发语言·人工智能·数码相机·计算机视觉·matlab
java小吕布5 小时前
CentOS 7 服务器性能监控实战指南
linux·服务器·centos
椰子今天很可爱6 小时前
仿照muduo库实现一个高并发服务器
linux·服务器·c++
yesyesyoucan6 小时前
安全工具集:一站式密码生成、文件加密与二维码生成解决方案
服务器·mysql·安全
小豆子范德萨6 小时前
cursor连接远程window服务器的WSL-ubuntu
运维·服务器·ubuntu