[Linux系统编程——Lesson18.动静态库]

目录

前言

一、🤔什么是库

二. 🧐初识动静态库

三、🔥编写一个自己的库

3.1 🍕静态库

3-1-1🐕打包静态库

1️⃣首先需要将所有的.c源文件都编译为目标文件。

2️⃣使用ar指令将所有目标文件打包为静态库。

3️⃣将打包成的组织静态库与头文件组织起来

3-1-2  🐈‍⬛使用静态库

1️⃣拷贝到系统目录

2️⃣指定库的路径

3-2 🍔动态库

3-2-1🦌打包动态库

1️⃣生成所有源文件对应的目标文件。

①核心定义

②核心解决的问题

③实现机制:相对编址 vs 绝对编址

④编译层面的控制(以 GCC 为例)

⑤使用场景差异

⑥核心优势

总结

2️⃣使用 gcc 的 -shared 选项将所有目标文件打包为一个动态库。

3️⃣组织头文件和动态库文件。

3-2-2 🐎 动态库的使用

1️⃣将动态库放到系统路径下,即放到/usr/lib64路径下:类似静态库的操作:

总结来说:动态链接的 “延迟绑定” 本质

2️⃣修改环境变量LD_LIBRARY_PATH,该环境变量是专门用来存储用户自定义库路径的:

3️⃣在/etc/ld.so.conf.d目录中,创建一个自己的动态库路径配置文件以.conf结尾,创建好后使用ldconfig复用:

①将路径存放在.conf文件中。

四. 🕵️‍♀️动态库与地址空间

1️⃣动态库在进程虚拟地址空间的映射位置及原理

①映射位置:共享区的核心作用

2️⃣动态库映射与调用的完整流程

3️⃣关键技术支撑:位置无关码(PIC)

4️⃣总结

五、🧐共享区空间如何分配

5.1 🍟执行程序形成后的地址

六、🔥程序加载到内存中的地址分配与动态加载机制

1️⃣内存地址分配的基本单位:页帧

2️⃣虚拟地址与物理地址的映射:页表的作用

3️⃣大型程序的动态加载:按需加载策略

4️⃣缺页中断:动态加载的触发机制

总结

七、📖动态库的地址定位机制:位置无关码与偏移量的作用

1️⃣动态库的 “动态” 本质:加载地址不固定

2️⃣地址定位的核心:起始位置 + 偏移量

3️⃣为什么需要位置无关码(PIC)?

总结

八、动态库🆚静态库

1️⃣核心定义与工作原理

1.静态库(Static Library)

2. 动态库(Dynamic Library)

2️⃣关键区别对比

3️⃣优缺点分析

静态库的优缺点

动态库的优缺点

4️⃣适用场景

静态库适用场景

动态库适用场景

总结

九、补充

结束语:


前言

        在 C++ 开发中,我们常常会用到标准库中的 vector、string 等容器,或是调用 Boost、Qt 等第三方库提供的复杂功能。这些现成的组件让我们得以快速实现需求,无需从零编写基础逻辑。但你或许会好奇🧐:这些库中的类和函数并未出现在我们的源代码里,它们是如何与我们的代码协同工作的?我们没有手动实现这些功能,编译器和 linker 是怎样找到并调用它们的?当项目中引入多个库时,又如何避免冲突并准确关联到所需的那一个🤔?

接下来,我将围绕这些问题,详细的解释动静态库是什么,如何制作动静态库,以及如何使用动静态库。

一、🤔什么是库

        在学习生涯中,我们总是能谈到库,例如C语言标准库、C++标准库。那么到底什么是库呢❓

在计算机科学中,术语“库”通常指的是库文件(Library),它是一组预编译的、可重用的代码和资源的集合,用于支持软件开发。库的目的是为开发人员提供一组常用的功能,以便在应用程序中进行调用,从而避免重复编写相同的代码。

在日常开发过程中,我们如果想要在自己的代码中使用别人实现的库或方法,有什么办法吗❓

  • 直接将别人的源代码拷贝到自己的文件中,在进行调用。

那么如果别人不愿意将源代码公开,但是也希望别人使用自己写的库,怎么办???

  • 学过C语言都知道,一个可执行程序生成需要经历四个阶段:预处理、编译、汇编、链接。头文件在预处理阶段被引入,库文件在链接阶段被引入。
  • 其中代码经过编译后会生成汇编代码,此时我们依旧是可以看懂汇编代码的,而经过汇编之后就会生成二进制目标文件,而我们是无法阅读二进制文件的。所以根据这个思路,我们能不能将一个代码经过预处理,编译,汇编后再发送给别人使用?
  • 答案当然是可以的,只要我们有别人的头文件,并且其中声明了调用方法,编译器就会认为有这些方法不会报错,只要在链接时编译器能够找到这些方法即可,关于编译器如何找到在后文会详细介绍。

以上就是动静态库形成的全过程:所以动静态库就是一个已经编译好的源代码文件,通过链接库使得我们可以使用其中的方法。

库中有main函数吗?下面会告诉你答案:

  • 当有多个不同的源文件中的main函数调用这些功能函数时,每次都要重新对这几个函数重复预处理、编译、汇编操作,各自生成.o文件,然后再和调用功能函数的源文件(一般是main函数)生成的.o,最后才生成可执行程序😰。
  • 这样会有很多重复的操作所以一般将这些常用的函数所在的.cpp文件预处理、编译、汇编生成的多个.o文件打包在一起,称之为库而事实上我们经常使用的<stdio.h><iostream>、使用各种STL容器包含的头文件都是这么做的。
  • 由此可以见,➡️库的本质若干个目标文件(.o文件)的集合。➡️每个.o文件都包含了由源码编译生成的二进制代码,以供调用。
  • 严格地说,库并不是可执行程序的半成品。库是一组预先编译好的目标文件(.o文件)的集合,它们可以被链接到可执行程序中,以提供某些功能库中的目标文件包含了机器语言代码,但它们并不能直接运行。要生成一个可执行程序,需要将库中的目标文件与其他目标文件(如含有main函数的目标文件)链接在一起,然后由链接器生成一个可执行文件。

由此我们可以得出核心点总结🕵️‍♀️:

库的本质若干目标文件(.o/.obj)的集合,这些目标文件是通过对功能函数源码进行预处理、编译、汇编后得到的二进制代码文件。

解决的问题避免多个源文件调用相同功能函数时,重复进行预处理、编译、汇编操作,减少冗余工作,提高开发效率。

与可执行程序的关系库本身不可直接运行,需通过链接器与其他目标文件(如含 main 函数的目标文件)链接后,才能生成可执行程序。

常见案例 <stdio.h><iostream>以及 STL 容器对应的头文件,其背后实际依赖的功能实现往往以库的形式存在,我们在编译链接时会隐式或显式地将这些库链接到程序中。

库可以分为两大类:

  • Windows中:静态库以.lib结尾,动态库以.dll结尾;

  1. Linux中:静态库以.a结尾,动态库以.so结尾。

  • 静态库(Static Library) 静态库编译时链接到应用程序中,形成一个独立的可执行文件。在程序运行时,静态库的代码被完全复制到应用程序中,因此应用程序不再依赖于原始的库文件。静态库的文件扩展名通常是.a(在Unix/Linux系统中).lib(在Windows系统中)
  • 动态库(Dynamic Library) 动态库运行时加载到内存中多个应用程序可以共享同一个动态库的实例。这可以减小应用程序的大小,因为动态库的代码只需要存在一份,而且可以在运行时更新。动态库的文件扩展名通常是.so(在Unix/Linux系统中)或.dll(在Windows系统中)

二. 🧐初识动静态库

接下来我们用一个简单的例子初步了解动静态库

#include <stdio.h>
int main()
{
    printf("Hello World!\n");
    return 0;
}

Makefile

mylib : mylib.c
	gcc -o $@ $^
.PHONY : clean
clean :
	rm -f mylib
  • 编译生成可执行程序mylib。这个程序能够成功调用库函数printf归功于gcc编译器在生成可执行程序时,将C标准库也链接进可执行程序中。

通过指令ldd filename查看可执行程序依赖的库文件

[llz@VM-12-2-ubuntu]$ ldd mylib
        linux-vdso.so.1 =>  (0x00007ffea9bfb000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fc6231d9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc6235a7000)

ldd 是一个命令,它用于打印程序或库文件所依赖的共享库列表。它不是一个可执行程序,而只是一个 shell 脚本。

  • 其中,libc.so.6就是这个可执行程序依赖的库文件,通过ll指令查看这个该路径下这个库文件的属性:

  • 表明它其实是软链接到同目录下的libc-2.17.so文件,通过file指令,查看该文件的文件类型:

如果一个库文件是 shared object,那么它是一种特殊的目标文件,可以在程序运行时被加载(链接)进来。在 Linux 下,动态链接库(shared object file,共享对象文件)的文件后缀为 .so。使用动态链接库的优点是:程序的可执行文件更小,便于程序的模块化以及更新,同时,有效内存的使用效率更高。
也就是说:这个libc-2.17.so是一个动态库

去掉前缀和后缀,剩下的就是库的名字。在这里,libc.so.6实际上是C语言的动态库,库名是c

libc.so.6 glibc 的软链接。glibc 是 GNU 发布的 libc 库,即 C 运行库。

值得一提的是🕵️‍♀️:默认情况下,gcc/g++ 采用动态链接的方式链接第三方库。例如,当你指定 -lpng 时,链接程序就会去找 libpng.so。不过,gcc/g++ 提供了一个 -static 参数,可以改变默认链接方式。如:

gcc -static mylib.c -o mylib-s

其中mylib-s静态链接版本生成的可执行程序,可见,动态链接生成的可执行程序的大小比静态链接的小不少。

使用ldd指令试着查看它是否有依赖的其他库文件:

[llz@VM-12-2-ubuntu]$ ldd mylib-s
        not a dynamic executable

说明静态链接生成的可执行程序不依赖其他库文件,同样地,用file指令查看它的文件类型:

三、🔥编写一个自己的库

3.1 🍕静态库

为了更深入的理解库运作的原理,我们尝试自己写一个库🧐。

接下来,我们将实现一个简单的加减运算的程序,并将给程序的源代码与头文件进行打包。

以下面四个文件和一个main.c文件为例,演示其打包为库的过程。

//Add.h:

#pragma once

extern int Add(int a, int b);
//Add.c:

int Add(int a, int b)
{
    return a + b;
}
//Print.h:

#pragma once

extern void Print(const char* str);
//Print.c:

#include <stdio.h>

void Print(const char* str)
{
    printf("%s", str);
}

extern 是 C 语言中的一个关键字,它可以置于变量或函数前,用来说明“此变量/函数是在别处定义的,要在此处引用”。它的作用是提示编译器遇到此变量或函数时,在其他模块中寻找其定义。

//main.c:

#include <stdio.h>
#include "Print.h"
#include "Add.h"
int main()
{
    int res = Add(1, 2);
    Print("Hello World!\n");
    printf("%d\n", res);
    return 0;
3-1-1🐕打包静态库
1️⃣首先需要将所有的.c源文件都编译为目标文件。
gcc -c Add.c
gcc -c Print.c
  • -c选项告诉gcc只编译源代码,但不进行链接。这会生成一个目标文件(通常以.o结尾),该文件包含了编译后的代码,但还不能直接运行。你可以使用-c选项来编译多个源文件,然后再使用链接器将它们链接成一个可执行文件或共享库。

生成目标文件

如果我们只把.o.h文件给别人,别人能用吗

再将main.c编译

gcc main.c -c

然后,将main.o和其他.o文件链接以后生成的文件就是可执行程序

gcc Add.o Print.o main.o -o libtest.out

运行:

通过上面的例子我们知道:需要将生成的所有目标文件和main.o文件链接才能生成可执行程序,但是除了main.o之外的.o文件都太分散了,用起来很麻烦(当然可以通过Makefile简化步骤),给别人使用也不太方便,还容易缺失,所以将它们打包。而将目标文件打包的结果就是一个静态库

2️⃣使用ar指令将所有目标文件打包为静态库

ar 命令 GNU Binutils 的一员,可以用来创建、修改静态库,也可以从静态库中提取单个模块。它可以将一个或多个指定的文件并入单个写成 ar 压缩文档格式的压缩文档文件

常用参数

  • -r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
  • -c(create):建立静态库文件。
  • -t:列出静态库中的文件。
  • -v(verbose):显示详细的信息。
  • 语法:ar [选项] [库名] [依赖文件]

例如,将Add.o和 Print.o打包

ar -rc libtest.a Add.o Print.o

-t-v选项查看静态库中的文件及信息:

ar -tv libtest.a

3️⃣将打包成的组织静态库与头文件组织起来

静态库的本质是多个目标文件(.o)的二进制归档,仅包含函数 / 变量的实现代码,但编译器在编译用户代码(如main.c)时,需要先通过声明确认函数 / 变量的 “接口格式”(如参数类型、返回值类型、变量类型等),否则无法判断调用语法是否合法。

头文件核心作用就是提供这些 “接口声明”,二者的关系可类比为:

  • 静态库(.a:相当于 “工具箱”,里面装着已经做好的工具(实现代码);
  • 头文件(.h相当于 “工具箱说明书”,标注了每个工具的名称、用法(接口声明)。

缺少头文件编译器会因 “不知道工具怎么用” 而报错⚠️undefined reference to XXX或 “隐式声明函数” 警告)若仅提供头文件而缺少静态库,则链接阶段会因 “找不到工具本身” 而失败。

头文件和函数的实现分离是为了提高代码的可维护性和可重用性头文件中只包含函数和变量的声明,而不包含具体的实现。这样,当我们需要修改函数的实现时,只需要修改对应的源文件,而不需要修改头文件。同时,由于头文件只包含声明,因此可以被多个源文件共享。这样,当我们需要在多个源文件中使用同一个函数时,只需要在每个源文件中包含对应的头文件即可。

注意⚠️:在Linux下,静态库的命名必须是:lib开头,.a结尾中间才是静态库的名称

  • 组织静态库和头文件的方法有很多种。一种常见的方法是将静态库文件(.a文件)和头文件放在同一个目录下。
  • 在使用静态库时,需要在程序中包含对应的头文件,并在编译时指定静态库的位置。这样,编译器就能够找到静态库中的函数和变量,并将它们链接到程序中。
  • 例如,将所有的头文件(.h)放在一个名为include的目录下,将生成的静态库文件(.a)放在一个名为lib的目录下。然后将这两个目录都放在名为libtest的目录下,这个libtest就可以作为一个第三方库被使用。

创建好目录以后,通过tree指令查看目录结构:

Makefile打包

make指令既然可以执行编译和删除指令,自然也能执行打包目标文件生成静态库和组织头文件与库文件的指令。

libtest.a : Add.o Print.o # 静态库依赖的目标文件
	ar -rc libtest.a Add.o Print.o # 打包
Add.o : Add.c # .o文件依赖的源文件
	gcc -c Add.c -o Add.o
Print.o : Print.c
	gcc -c Print.c -o Print.o

.PHONY : mylib # 组织头文件和库文件
mylib:
	mkdir -p mylib/lib
	mkdir -p mylib/include
	cp -rf *.h mylib/include
	cp -rf *.a mylib/lib

.PHONY : clean
clean : 
	rm -rf *.o mylib libtest.a

make生成目标文件,然后打包生成静态库:

make mylib组织静态库文件和头文件:

查看mylib的目录结构:

3-1-2  🐈‍⬛使用静态库

仍然使用一开始就写好的main.c,让它和含有头文件、静态库文件libtest共处同一目录libuse下才能调用库中写好的函数。

试着在该目录下直接编译main.c

这个错误说明 gcc 没有找到头文件对应的库文件,下面的操作是让 gcc 看到导入的第三方库。

使用打包好的静态库有三种方法:

  • 将库文件放到系统路径下。
  • 使用 gcc -L 选项指定链接库的搜索路径,例如 gcc main.c -o main -L./ -lchild 。
  • 设置存放链接库时搜索路径的环境变量,将当前库文件所在的路径添加进去,例如 export LIBRARY_PATH=$LIBRARY_PATH:. 。

下面将介绍前两种方法。

1️⃣拷贝到系统目录
  • 系统库目录 /usr/lib64 或 /usr/lib ;
  • 系统头文件目录/usr/include

将生成的静态库文件和头文件分别添加到系统目录下 :

[xy@xy libuse]$ sudo cp mylib/include/* /usr/include
[xy@xy libuse]$ sudo cp mylib/lib/* /usr/lib

试着编译:

这是链接时错误,两个函数没有引用,说明编译器已经找到头文件了。

即使我们将头文件和库文件拷贝到系统目录下,但是 gcc 在编译main.c时,还需要显式地说明要链接的库在哪一路径下。通过-l库名选项让 gcc 知道这是一个第三方库,而库名就是libtest.a去掉前缀和后缀剩下的部分,即test

gcc main.c -ltest

这是因为gcc在默认路径下是链接C/C++的库,它只知道哪些是内置的库,而不知道第三方库的存在。

运行程序:

但是将头文件和库文件添加到系统目录下是非常不推荐的🙅,因为这样会污染系统库目录,而这就是安装库的过程。

2️⃣指定库的路径

gcc 编译 main.c 链接库时,需要由以下三个选项定位文件

  1. -I指定头文件搜索路径。
  2. -L指定库文件搜索路径。
  3. -l指明需要链接库文件路径下的哪一个库。

我们只要显式地给 gcc 指明第三方库的路径即可完成链接

gcc main.c -I./mylib/include -L./mylib/lib -ltest

三个选项后,空格可加可不加。

这个mylib文件相当于一个第三方库,只要调用了include里的头文件,就可以间接调用lib里已经被编译成二进制编码的函数。

🧐静态库必须整个拷贝到可执行程序中,所以由静态库链接生成的可执行程序一般比动态库链接的大,这种程序采用的是绝对编址它们在当前程序的进程地址空间中的地址是固定的。

3-2 🍔动态库

3-2-1🦌打包动态库

依然使用之前的四个文件和一个main.c文件示例。

1️⃣生成所有源文件对应的目标文件。

gcc 需要增加-fPIC选项(position independent code)位置无关码。

gcc -fPIC -c Add.c
gcc -fPIC -c Print.c

这里我们要先了解一下位置无关码🤔

①核心定义

位置无关代码(PIC)是一种机器代码,其执行不依赖于加载到内存中的绝对地址,可在任意内存位置运行,无需修改代码中的地址引用。

②核心解决的问题

动态库的共享复用共享库(动态库)需被多个进程同时使用,但每个进程可能将其加载到不同的内存地址。若代码依赖绝对地址,每次加载需重新定位(修改代码中的地址),导致:

  • 每个进程需维护一份独立的代码副本(无法共享),浪费内存;
  • 程序启动时增加重定位开销,延长启动时间。PIC 通过相对地址引用避免上述问题,使多个进程可共享同一份代码段。
③实现机制:相对编址 vs 绝对编址
  • 绝对编址代码中直接使用内存绝对地址(如静态库链接时确定)若代码加载到其他地址,引用会失效,必须重新修改地址(重定位)
  • 相对编址PIC 所有地址引用均基于 “当前指令地址” 的偏移量(相对位置),而非固定绝对地址。例如:
    • 访问同一段代码中的函数 / 数据时,通过 “当前指令地址 + 固定偏移” 计算目标地址,与整体加载位置无关。
    • 类比 “房车家具”:房车整体位置可变,但家具间的相对位置固定,无论房车停在哪,内部物品的相对访问方式不变。
④编译层面的控制(以 GCC 为例)
  • -fPIC 选项编译阶段通过 -fPIC 告知编译器生成位置无关代码,强制所有地址引用使用相对偏移,避免绝对地址。这是动态库(.so)的必需选项,确保其可被多个进程共享。

  • 不使用 -fPIC 的后果生成的动态库仍可运行,但加载时需针对每个进程的加载地址进行重定位(修改代码段内容),导致:

    • 每个进程必须保留一份独立的代码副本(无法共享),失去动态库的内存优势;
    • 重定位操作增加启动时间和内存开销。
⑤使用场景差异
  • 动态库(.so必须用 -fPIC 编译否则无法实现多进程代码共享,违背动态库设计初衷。
  • 静态库(.a无需 -fPIC因静态库在链接时会与主程序合并,最终生成的可执行文件中地址已被确定为绝对地址(随主程序加载位置固定)
  • 可执行文件:通常无需 PIC(依赖绝对地址),但部分场景(如动态加载代码)可能需要。
⑥核心优势
  • 内存高效:共享库代码段仅需加载到内存一次,映射到多个进程的虚拟地址空间,节省大量 RAM。
  • 启动快速:避免加载时的重定位操作,减少程序启动延迟。
  • 灵活性:代码可在内存任意位置执行,适配动态加载、地址空间随机化(ASLR)等安全机制。
总结

PIC 动态链接技术的核心基础,通过相对编址实现代码的 “位置无关性”,解决了共享库在多进程环境下的高效复用问题。其设计理念简洁而关键:放弃绝对地址依赖,通过相对关系保证代码在任意位置的正确性,这一特性使其成为现代操作系统中动态库不可或缺的技术。

2️⃣使用 gcc 的 -shared 选项所有目标文件打包为一个动态库。
gcc -shared -o libtest.so Add.o Print.o

其中,在选项-shared后面的是要生成动态库的名称,在它之后是动态库依赖的目标文件

通过指令readelf -S可以查看库的部分详细,如偏移量 offset

注意💡:动态库使用相对地址,是由偏移量和某个参照点找到库的

3️⃣组织头文件和动态库文件。

同样地,所有的头文件(.h)放在一个名为include的目录下,将生成的动态库文件(.so)放在一个名为lib的目录下。然后将这两个目录都放在名为libtest的目录下,这个libtest就可以作为一个第三方库被使用。

使用Makefile打包:

  • 将编译、打包等指令用Makefile保存。在这里,可以通过Makefile先后生成动静态库
.PHONY:all
all:libtest.so

libtest.so:Print.o Add.o
	gcc -shared Print.o Add.o -o libtest.so
Print.o:Print.c
	gcc -c -fPIC Print.c -o Print.o
Add.o:Add.c
	gcc -c -fPIC Add.c -o Add.o

.PHONY:output
output:
	mkdir -p mylib/lib
	mkdir -p mylib/include
	cp -rf *.h mylib/include
	cp -rf *.so mylib/lib

.PHONY : clean
clean : 
	rm -rf *.o mylib *.a *.so output
  • 此处将打包的操作命名为output

3-2-2 🐎 动态库的使用

对于动态库,即使显式地提示 gcc main.c 中调用了第三方库中的函数,也会因为找不到库而编译错误。

  • 例如,仍然使用 gcc 的三个选项说明编译 main.c 需要的库文件和头文件,以及应该链接哪个库。注意,此时的工作目录依然是:

gcc main.c -I./mylib/include -L./mylib/lib -ltest

这样就能生成可执行程序:

经过编译,打包后,形成的动态库有x权限,这也就意味着其可以执行,而静态库是没有x权限的,这就与这两种库的区别有关了。

不同于静态库,这里动态库生成的可执行程序并不能运行。

这种情况已经不是在编译的时候找不到动态库了,而是运行时。

通过ldd指令查看可执行程序依赖的动态库的信息

libtest.so => not found,这说明系统无法找到动态库文件

在前面我们说过,动态库要加载到内存中,动态库与可执行程序有关联的,那么找不到动态库是不是因为动态库没有加载到内存中

是的,确实是这样的。在编译阶段,要让编译器找到动态库的位置,在运行时要让动态链接器也能找到动态库,那么怎么让动态链接器找到动态库❓

在Linux下,有以下几种使用第三方动态库的方法(解决以上问题):

  • 使用ldconfig指令
  • 设置LD_LIBRARY_PATH环境变量;
  • 编译时指定路径
  • 也可以使用pkg-config命令来导入第三方库文件。

在这里,主要介绍前三种方式。

1️⃣动态库放到系统路径下,即放到/usr/lib64路径下:
类似静态库的操作:
sudo cp mylib/lib/libtest.so /lib64

现在这个动态库就被找到了。

但是,为什么只要将动态库的.so文件拷贝到系统目录下,这个可执行程序就可以被链接到动态库呢❓不应该重新编译链接一次吗🤔

  • 编译链接时,只需要记录需要链接文件的编号运行程序时才会进行真正的“链接”,所以称为“动态链接”。因此,只要将动态库的.so文件拷贝到系统目录下,这个可执行程序就可以被链接到动态库,而不需要重新编译链接。
  • 也就是说,编译器只负责生成一个main.c对应的二进制编码文件,而链接的工作要等到运行程序时才会进行链接,所以生成可执行程序以后就没有编译器的事了。
总结来说:动态链接的 “延迟绑定” 本质
  • 编译链接生成 “半成品” 可执行文件,仅记录依赖关系,不涉及实际地址计算。
  • 运行阶段由动态链接器完成库的查找、加载和地址绑定,依赖系统路径和库文件本身。

这正是动态库 “灵活复用” 的核心同一个可执行文件可在不同环境中链接不同位置的同版本库,无需重新编译,极大降低了程序分发和维护的成本。

缺点⚠️:同样地,将动态库的.so文件拷贝到系统目录下也可能会污染系统库目录。

2️⃣修改环境变量LD_LIBRARY_PATH,该环境变量专门用来存储用户自定义库路径的

LD_LIBRARY_PATH程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量中告诉系统程序依赖的动态库所在的路径

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xy/Linux/libtest/libtest/mylib/lib

现在,程序也可以运行了。

ldd查看依赖库信息

注意要用:隔开,否则会覆盖原来的环境变量。但是这个方法是临时的,因为这个环境变量是内存级别的环境变量,机器会在下次登录时清理

3️⃣/etc/ld.so.conf.d目录中,创建一个自己的动态库路径配置文件以.conf结尾,创建好后使用ldconfig复用
  • /etc/ld.so.conf.d/目录下的文件用来指定动态库搜索路径。这些文件被包含在/etc/ld.so.conf文件中,ldconfig命令会在默认搜寻目录(/lib和/usr/lib)以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索可共享的动态链接库,并创建出动态装入程序(ld.so)所需的连接和缓存文件。
  • 这些.conf文件中存储的都是各种文件的路径,只要将我们写的第三方库的路径保存在一个.conf文件中,程序运行时在就会通过它链接到它依赖的动态库。

①将路径存放在.conf文件中。
echo /home/xy/Linux/libtest/libtest/mylib/lib > libtest.conf

这样,当前目录下就会出现刚才创建的文件:

②将.conf文件拷贝到/etc/ld.so.conf.d/下。

sudo cp libtest.conf /etc/ld.so.conf.d/

ldd一下:

系统还是没有找到a.out依赖的动态库,原因是此时的系统的数据库还未更新,使用命令ldconfig更新配置文件

sudo ldconfig

ldd一下:

成功链接,运行一下:

注意💡:在实际开发过程中,我们大多数会直接采用第一种安装库的方式来使用别人提供的库。

动态库在进程运行的时候,会被加载到内存中,并且被所有程序共享,都可以去动态库中执行代码,那么这个过程具体是如何实现的呢

下面我将围绕这一话题,聊一下动态库与进程地址空间的位置关系。

四. 🕵️‍♀️动态库与地址空间

一个进程是有自己的进程地址空间的,通过页表与物理内存建立联系

如果一个程序要调用动态库中的方法,那么毫无疑问也要将动态库的物理地址映射到进程的虚拟地址中,那么映射到虚拟地址的哪一个位置呢

1️⃣动态库在进程虚拟地址空间的映射位置及原理

当程序需要调用动态库中的方法时,动态库物理地址会映射到进程虚拟地址空间共享区(也称为共享内存区),而非代码区、堆区或栈区。这一设计是实现动态库 “共享” 特性核心

①映射位置:共享区的核心作用

共享区进程虚拟地址空间中专门用于加载共享资源(如动态库、共享内存段)的区域,位于堆区与内核空间之间(Linux 系统中典型地址范围靠近 1GB 内核空间下方)。将动态库映射到共享区的关键目的是

  • 实现 “一次加载,多进程共享”
    动态库被加载到物理内存后,操作系统不会为每个调用它的进程复制一份物理内存数据,而是让多个进程的共享区通过页表映射到同一块物理内存。例如,多个进程调用 libc.so(C 标准库动态库)时,所有进程的共享区都会指向物理内存中同一份 libc.so 数据,极大节省内存资源
  • 避免地址冲突
    共享区地址空间独立于进程的代码区(存放自身指令)堆区(动态内存分配)和栈区(函数调用栈)动态库加载到此处不会与进程自身的代码或数据地址重叠,确保程序运行稳定。

2️⃣动态库映射与调用的完整流程

当进程执行到调用动态库方法的指令时,操作系统会按以下步骤完成映射与调用:

①检查动态库是否已加载到物理内存

  • 若动态库未加载:触发缺页中断操作系统动态库从磁盘读取到物理内存,并记录该动态库的物理地址。
  • 若动态库已加载直接复用已有的物理内存,无需重复读取磁盘,提升效率。

②建立共享区与物理内存的映射
操作系统进程的页表中添加一条映射关系将进程共享区的某一段虚拟地址,关联到动态库所在的物理内存地址。

  • 注意⚠️:不同进程映射动态库时共享区虚拟地址可能不同(因各进程共享区空闲空间不同),但最终都会指向同一块物理内存

③进程跳转执行动态库方法
进程执行自身代码时,若遇到调用动态库方法的指令(如 call 指令),会从代码区跳转到共享区的对应虚拟地址(动态库方法的入口),执行完成后再跳转回原代码区继续执行,整个过程对进程透明,如同调用自身代码。

3️⃣关键技术支撑:位置无关码(PIC)

动态库能灵活映射到共享区任意位置,依赖于编译时生成的位置无关码(Position-Independent Code,PIC),这也是制作动态库时需添加 -fPIC 选项(如 gcc -fPIC -c add.c)的原因

  • 位置无关码不使用绝对地址编址,而是通过 “动态库起始地址 + 函数偏移量” 的方式定位方法
    例如,动态库中 
    add 函数的地址以 “相对于动态库加载起始位置的偏移量 0x123” 存储,无论动态库被映射到共享区的哪个虚拟地址,只要找到起始地址,加上偏移量就能准确定位 add 函数,避免因加载位置变化导致地址失效

4️⃣总结

动态库映射到进程虚拟地址空间共享区,是操作系统平衡 “内存效率” 与 “调用灵活性” 的关键设计:

  • 从资源角度:通过物理内存共享,减少重复加载,节省内存开销;
  • 从执行角度:借助位置无关码和页表映射,实现动态库的灵活加载与透明调用,确保多进程同时使用动态库时的稳定性。

五、🧐共享区空间如何分配

使用objdump -d+ 二进制目标文件可以进行反汇编,以下是将test.o进行反汇编的截取代码

  • 在汇编中我们可以看到一条call指令,这就是在调用我们动态链接的add()函数,前面是call的地址,注意是后8位,也就是00000000,这里的地址是虚拟地址。此处的00000000并不是add()的地址,因为要进行动态链接编译器才知道方法的实现在哪,此处表
  • 显示地址还没有分配,会在连接时进行分配。在连接的过程中就会将此处的地址进程初始化。

如图是可执行程序的汇编代码:

思考🤔动态库是在程序运行的时候才映射到虚拟内存中的,那么形成的可执行程序中会call地址来找动态库的虚拟地址位置,动态库都还没有加载进来他怎么知道地址在哪的???难道是在还没有加载之前就将共享区中所有位置的地址都进程分配好了

对于后面的猜想是不现实的,因为共享区中的地址是在动态变化的,如果一个动态库调用完了,就可以将这块共享区的空出来了,再让后面使用如果一开始就分配好势必会造成空间上的浪费

下面我将围绕这一问题介绍一个程序形成的全过程。

5.1 🍟执行程序形成后的地址

实际上在程序还没有加载到内存之前,程序内部就已经存在地址了,这并不难理解,因为上面的汇编代码中要进行call地址操作,那么上面的可执行程序一定分配了地址。

编译的时候,gcc就已经将地址进行了分配,这些地址就是从00 00 00 00 开始向上生长的,就是线性地址,文件中的内容依据ELF格式进行排列:
 

也就是说,编译器在编译代码的时候也会考虑操作系统将进程加载到内存中的操作gcc将可执行程序的地址按照线性地址进行排列,这使得操作系统在设置进程虚拟地址空间的时候更方便。

六、🔥程序加载到内存中的地址分配与动态加载机制

程序加载到内存时地址管理操作系统内存虚拟化的核心环节,涉及物理地址与虚拟地址的映射动态加载策略缺页中断机制

1️⃣内存地址分配的基本单位:页帧

内存物理地址空间页帧(Page Frame) 最小分配单位(典型大小为 4KB、2MB 等),每个页帧对应一段连续的物理内存,拥有唯一的物理地址当程序被加载到内存时,其代码和数据会被分割成与页帧大小匹配的块(称为 “页”),分别存储在不同的页帧中,因此程序的代码和数据天然具备了离散的物理地址。

2️⃣虚拟地址与物理地址的映射:页表的作用

编译器在编译程序时,会为代码和数据分配线性地址(虚拟地址)但这些地址并非直接对应物理内存位置程序加载到内存后,操作系统通过页表建立虚拟地址与物理地址的映射关系:

  • 页表中记录了 “程序虚拟地址页” 与 “内存物理页帧” 的对应关系。
  • CPU 执行程序时,会使用虚拟地址访问内存,通过页表查询得到实际物理地址,最终访问物理内存中的数据或指令。

3️⃣大型程序的动态加载:按需加载策略

对于大型程序,操作系统不会一次性将所有代码和数据加载到内存,而是采用 “一边执行,一边加载” 的按需加载策略,原因如下:

  • 避免空间浪费程序执行具有局部性(如先执行初始化代码,后执行功能模块),未执行的代码或暂时不用的数据无需占用内存。
  • 提高内存利用率有限的物理内存可同时服务多个程序,仅加载当前需要的页,减少内存竞争。

4️⃣缺页中断:动态加载的触发机制

CPU 使用虚拟地址访问内存时若页表中未找到对应的物理地址(即该页尚未加载到内存),会触发缺页中断,流程如下:

  • ①CPU 暂停当前程序执行,将控制权转移给操作系统的缺页中断处理程序。
  • ②操作系统查找该虚拟页对应的代码或数据在磁盘中的位置(如可执行文件、动态库文件)。
  • ③操作系统在物理内存中分配空闲页帧,将磁盘中的数据加载到该页帧。
  • ④更新页表,建立 “虚拟地址页” 与 “新分配的物理页帧” 的映射关系。
  • ⑤中断处理结束,CPU 恢复程序执行,重新访问该虚拟地址(此时已能找到物理地址)。

总结

程序加载到内存地址管理核心 “虚拟地址 - 物理地址映射”“按需加载”

  • 物理地址内存页帧天然决定,虚拟地址编译器分配页表负责建立两者的映射
  • 大型程序通过缺页中断实现动态加载,仅在需要时将代码 / 数据加载到内存,既节省空间又提高内存利用率,是现代操作系统高效运行的关键机制。

七、📖动态库的地址定位机制:位置无关码与偏移量的作用

动态库在运行时加载的灵活性(可被映射到共享区任意位置),与其地址定位机制密切相关核心问题是❓:程序如何在动态库加载位置不固定的情况下,准确找到要调用的函数?这一过程依赖于位置无关码(PIC) 和偏移量编址:

1️⃣动态库的 “动态” 本质:加载地址不固定

动态库与静态库的关键区别之一加载时机静态库编译时被合并到程序中地址在编译期就已确定;而动态库在程序运行时才被加载到内存,其在进程共享区的虚拟地址无法预先固定,原因包括:

  • 不同进程的共享区空闲空间不同,动态库需适配当前进程的地址空间布局;
  • 多个动态库可能存在地址冲突,需动态调整加载位置。

因此,动态库必须支持 “在任意地址加载后仍能正常工作”,这一特性由位置无关码(Position-Independent Code,PIC) 实现。

2️⃣地址定位的核心:起始位置 + 偏移量

动态库被加载到共享区后,程序通过 “动态库起始地址 + 函数偏移量” 的方式定位目标函数,具体流程如下:

①动态库编译时:生成偏移量信息
编译动态库时,通过 -fPIC 选项生成位置无关码。此时,动态库内部的函数、变量地址均以 “相对于动态库起始位置的偏移量” 记录,而非绝对地址。例如:

  • 动态库 libtest.so 中,add 函数的地址被记录为 “相对于库起始位置的偏移量 0x200”,sub 函数为偏移量 0x300。
  • 这些偏移量在编译时确定,与动态库最终加载到内存的位置无关。

②动态库加载时:确定起始虚拟地址
操作系统动态库加载到进程共享区的某一空闲位置后,会记录该动态库的起始虚拟地址(如 0x7f000000)。此时,动态库所有函数实际虚拟地址可通过 “起始地址 + 偏移量” 计算得出:

  • add 函数实际地址 = 0x7f000000 + 0x200 = 0x7f000200;
  • sub 函数实际地址 = 0x7f000000 + 0x300 = 0x7f000300。

③程序调用时:通过偏移量跳转
程序中调用动态库函数的 call 指令,本质上是通过偏移量实现的。编译器在编译程序时,已记录了目标函数相对于动态库起始位置的偏移量;当动态库加载后操作系统将 “起始地址 + 偏移量” 的计算结果填入程序的跳转表(如全局偏移表 GOT),最终 call 指令会跳转到该计算出的实际地址。

3️⃣为什么需要位置无关码(PIC)?

动态库不使用 PIC(即使用绝对地址编址),当它被加载到与编译期预设地址不同的位置时所有函数、变量的绝对地址都会失效(出现 “地址错位”),导致程序崩溃。而 PIC 通过偏移量编址,彻底消除了对固定加载地址的依赖,确保动态库在任意位置加载后,内部指令和外部调用都能正常工作。

总结

动态库地址定位机制 “灵活性” “正确性” 的平衡:

  • 动态库可被加载到共享区任意位置,解决了地址冲突和多进程适配问题
  • 依赖位置无关码(PIC)和 “起始地址 + 偏移量” 的计算方式,确保程序能准确找到动态库中的目标函数,实现跨进程的高效复用。这也是动态库编译时必须添加 -fPIC 选项的根本原因。

八、动态库🆚静态库

动态库(Dynamic Library)静态库(Static Library)是程序链接中两种核心的代码复用方式,它们在工作原理、优缺点和适用场景上有显著差异。

1️⃣核心定义与工作原理

1.静态库(Static Library)
  • 本质:一组目标文件(.o)的归档文件(通常以 .a 为后缀,Windows 中为 .lib),包含编译后的二进制代码。
  • 链接过程:编译时,链接器会将静态库中被程序引用的代码 完整复制 到可执行文件中。最终生成的可执行文件包含程序本身和所有依赖的静态库代码,是一个独立的二进制文件。例如:gcc main.c libxxx.a -o main 会将 libxxx.a 中用到的函数直接嵌入 main 中。
2. 动态库(Dynamic Library)
  • 本质:编译后的二进制代码(通常以 .so 为后缀,Windows 中为 .dll,macOS 中为 .dylib),可被多个程序共享。
  • 链接过程:编译时,链接器仅在可执行文件中记录 依赖的动态库名称和符号信息,不复制库代码;程序运行时,由动态链接器(如 ld-linux)负责将动态库加载到内存,并完成地址绑定(动态链接)。例如:gcc main.c -lxxx -o main 生成的 main 仅记录依赖 libxxx.so,运行时才加载该库。

2️⃣关键区别对比

静态库(.a/.lib)动态库(.so/.dll/.dylib)
可执行文件大小较大(包含库代码副本)较小(仅记录依赖信息)
内存占用多个程序使用同一库时,各自保留副本,内存占用高多个程序共享同一份库代码(内存中仅加载一次),占用低
加载速度启动快(无需运行时加载库)启动稍慢(需动态链接器查找、加载库并绑定地址)
更新维护库更新后,所有依赖程序需重新编译链接才能生效库更新后,只需替换动态库文件,依赖程序无需重新编译
编译链接时机编译时完成链接(静态链接)运行时完成链接(动态链接)
地址依赖依赖绝对地址(链接时确定)依赖相对地址(PIC 技术,加载位置可变)
分发复杂度简单(仅需分发可执行文件)复杂(需确保目标系统存在对应动态库,或随程序打包)

3️⃣优缺点分析

静态库的优缺点
  • 优点

    1. 可执行文件独立,不依赖外部库,分发方便(如单文件工具)。
    2. 程序启动快,无需运行时加载和链接库。
    3. 兼容性好,无需担心目标系统缺少特定版本的动态库。
  • 缺点

    1. 可执行文件体积大,浪费磁盘空间。
    2. 多个程序共用同一库时,内存中存在多份副本,浪费内存。
    3. 库更新后,所有依赖程序必须重新编译,维护成本高。
动态库的优缺点
  • 优点

    1. 可执行文件体积小,节省磁盘空间。
    2. 多个程序共享同一份库代码,大幅节省内存(尤其适合系统级库,如 libc.so)。
    3. 库更新方便,替换 .so 文件即可,无需重新编译依赖程序(如修复漏洞时)。
  • 缺点

    1. 程序启动依赖动态库,若系统中缺少对应库或版本不兼容,会运行失败(“找不到库” 错误)。
    2. 启动速度略慢(动态链接耗时)。
    3. 分发时需额外处理动态库依赖(如打包库文件或要求用户预先安装)。

4️⃣适用场景

静态库适用场景
  • 编写小型工具或独立程序,希望单文件分发(如命令行工具)。
  • 对程序启动速度要求极高,且库代码体积小(如嵌入式设备)。
  • 依赖的库版本稳定,极少更新(避免频繁重新编译)。
动态库适用场景
  • 系统级库(如 libclibstdc++),需被大量程序共享,节省内存。
  • 频繁更新的库(如插件、模块),需在不重新编译主程序的情况下升级。
  • 大型项目,拆分模块为动态库可减少可执行文件体积,便于团队协作开发。

总结

静态库 “将代码复制进程序”,追求独立性和启动速度代价是资源占用和维护成本动态库 “程序运行时共享代码”,追求资源效率和更新灵活性代价是依赖管理复杂度。两者没有绝对优劣,需根据具体场景(如程序大小、更新频率、资源限制等)选择。实际开发中,系统级库通常为动态库,而特定功能模块可能用静态库以简化分发。

九、补充

  • gcc 默认优先链接动态库:
    当同时存在静态库和动态库文件时,GCC 默认会优先链接动态库。例如执行命令gcc -o myprogram main.c -L. -lmylib,若当前目录下同时存在libmylib.solibmylib.a,则优先使用动态库libmylib.so进行链接。若要强制 GCC 链接静态库,可以使用-static选项,或者直接指定静态库文件的全称,如gcc -o myprogram main.c libmylib.a。另外,也可以使用-Wl,-Bdynamic-Wl,-Bstatic选项在命令行中切换链接首选项,-Wl,-Bdynamic告知链接器优先使用动态库,-Wl,-Bstatic则反之。
  • 动态库可分批加载到内存:
    动态库在程序运行时才被加载到内存,可实现进程之间资源共享。其加载方式分为隐式加载和显式加载两种,可实现分批加载2。具体如下:
    • 隐式加载:程序启动时,所有依赖的动态库会被一次性加载到内存,如程序依赖libA.solibB.so,在 ELF 文件的.dynamic段中会列出这两个库,动态链接器在进程初始化阶段就会将它们全部加载。不过,即使库在启动时全部加载,库内的函数符号也可能延迟解析,如在 RTLD_LAZY 模式下,符号会在首次调用时才绑定地址。
    • 显式加载:通过dlopen()函数实现,严格按代码执行顺序加载。比如程序先调用dlopen("libX.so")再调用dlopen("libY.so"),则实际加载时机就是这两个函数调用发生时

结束语:

以上是我对于【Linux文件系统】动静态库制作与原理的理解,

感谢您的三连支持!!!

评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值