> 技术文档 > 【Linux】动态库与静态库:代码复用的利器

【Linux】动态库与静态库:代码复用的利器

52bc67966cad45eda96494d9b411954d.png

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 道阻且长,行则将至


目录

📚前言

📖编译的过程

🔖1.预处理

🔖2.编译 

🔖3.汇编

🔖4.链接

📚一、静态库

📖创建过程

🔖1.编写源文件

🔖2.编译源文件

🔖3.创建静态库

🔖4.链接静态库

📖特点

📚二、动态库

📖创建过程

🔖1.编写源文件 

🔖2.编译源文件

🔖3.创建动态库

🔖4.链接动态库

🔖5.运行程序

📖特点

📚对比 


📚前言

在程序开发中,库是代码复用的重要工具,无论是标准的C/C++库,还是为特定功能编写的自定义库,程序员都依赖这些库来节省时间,提高开发效率。在库的实现上,有两种常见的形式——动态库静态库。这两种形式在代码管理、性能、可维护行等方面都有所区别,本篇文章会一一介绍这两种库的实现方式以及各自的特点。

在正式开始介绍之前,我们需要清楚了解程序编译的具体过程,在这个基础上,才能更好地理解动静态库的原理和本质:

📖编译的过程

程序编译的过程一般可以分为以下几个主要步骤:预处理、编译、汇编和链接

🔖1.预处理

 指令:gcc -E

gcc -E main.c -o main.i

预处理阶段是编译过程的第一步,主要负责处理代码中的宏定义、头文件包含、条件编译等内容,主要工作如下:

① 宏替换:处理所有的 #define 宏,替换源代码中的宏定义。

② 头文件包含:处理所有 #include 语句,将头文件的内容插入到源代码中。 

③ 条件编译:处理 #if#ifdef 等条件编译指令,删除不符合条件的代码。 

④ 行号和文件信息:处理 #line 和其他预处理指令,帮助编译器跟踪源代码。 

预处理完成之后,生成一个扩展名为 .i 的文件,这个文件是已经经过预处理的源代码。

示例:

// main.c#include \"sum.h\"#define A 1#define B 2int main(){ sum(A,B); return 0;}
// main.iint sum(int a,int b); // 头文件被展开int main(){ sum(1,2); // 宏替换 return 0;}
🔖2.编译 

指令:gcc -S 

gcc -S main.c -o main.s

编译阶段编译阶段将预处理后的源代码转换成 汇编代码。具体过程如下:

① 语法分析:编译器检查源代码的语法是否符合编程语言的规则。

② 语义分析:分析代码中的语义,确保没有逻辑错误,如类型不匹配、未声明的变量等。

③ 生成中间表示(IR):编译器生成中间代码,通常是一种平台无关的表示,供后续优化和汇编使用。

④ 代码优化:编译器对中间表示进行优化,以提高生成的代码效率。优化包括删除冗余代码、循环优化、常量折叠等。

经过编译之后,输出的文件通常是 汇编代码文件.s 文件)。 

🔖3.汇编

指令:as(汇编器) 

as main.s -o main.o

汇编阶段将汇编代码转换成 机器码,即目标文件(.o.obj 文件)。具体步骤如下:

汇编:汇编器将汇编语言代码转换为机器语言(即二进制代码),生成目标文件。目标文件包含了程序的二进制表示,通常是与平台相关的代码。

② 符号表和调试信息:汇编过程会为目标文件生成符号表,符号表记录了程序中所有符号(如函数、变量等)的地址或偏移量。

汇编完成后,生成的目标文件中包含了程序的代码、数据以及符号表,但不包含可执行代码的完整结构,即不可执行

🔖4.链接

指令:gcc -o

gcc main.o -o my_program

链接阶段将多个目标文件和库文件组合成一个最终的可执行文件或动态库。链接过程包括: 

① 符号解析:链接器将不同目标文件中的符号(如函数和变量的引用)进行解析和连接。如果程序引用了外部符号(如函数或变量),链接器会查找并关联它们的定义。

② 库的处理:具体处理过程后面讲解

③ 地址重定位:链接器需要对程序中的所有符号进行重定位,确保每个符号的地址在内存中的位置正确。重定位是根据符号表完成的,确保程序中的函数和数据的地址指向正确的位置。

④ 生成可执行文件:链接器将所有的目标文件、库文件和系统依赖库链接在一起,生成最终的可执行文件(例如 Linux 中的 .out.exe 文件)。

链接完成后,最终的可执行文件就可以运行了。 


综上,一个程序从源文件到可执行文件需要经过四步,其中库的作用主要在链接阶段,那么库的内部到底是什么呢?

假设我们现在设计了一个用于计算两数只和的程序sum.c,现在另一个程序想要调用我们的方法,这个时候该怎么办呢。我们可以将函数的声明放到一个sum.h头文件中,作为调用的接口,这样其他程序通过头文件就可以调用到我们的sum方法了。

但是在调用的过程中,我们设计的程序的实现细节也被看到了(程序预处理阶段,头文件被展开),但是我们不想让别人看到,想要将具体实现隐藏起来,同时别人也能正常调用,该怎么办呢

我们可以把我们的sum.c文件先预编译成目标文件sum.o(内部是二进制代码),再交给调用者,这样实现的细节就被隐藏了,由于程序在链接阶段才会对函数进行解析,所以这样也并不影响调用,这个过程就是静态库的实现过程。

📚一、静态库

静态库是指在编译时将库文件中的代码直接嵌入到可执行文件中。也就是说,静态库在程序编译的链接阶段被引用并加入到目标文件中,最终生成的可执行文件会包含库的代码。通常,静态库的文件扩展名为 .a(在 Linux 下)或 .lib(在 Windows 下)。

📖创建过程

🔖1.编写源文件

例如:编写了一个简单的 sum.c 文件,包含了一个求和函数 sum()

// sum.cint sum(int a, int b) { return a + b;}
🔖2.编译源文件

使用 gcc 编译源文件 sum.c,生成目标文件 sum.o

gcc -c sum.c -o sum.o
🔖3.创建静态库

 使用 ar 工具将目标文件 sum.o 打包成静态库 libsum.a

ar rcs libsum.a sum.o

其中:

r:插入文件到库中。如果库中已经存在同名文件,r 会替换它。如果库不存在,r 会创建一个新的库。

c:创建库,如果库文件已经存在,则什么也不做。这个选项通常与 r 一起使用,用于确保在操作时创建库文件。

s:创建索引。这个选项会为库创建一个符号表(索引),使得链接器在链接时能够更快速地查找符号。

这样,libsum.a 就是我们生成的静态库,它包含了 sum.o 的内容。

🔖4.链接静态库

编写一个程序 main.c,在其中调用静态库中的 sum() 函数。

// main.c#include \"sum.h\"#include int main() { printf(\"Sum: %d\\n\", sum(3, 5)); return 0;}

然后,使用 gcc 链接静态库,并生成最终的可执行文件。 

gcc main.c -L. -l sum -o my_program

这条命令告诉编译器去当前目录查找名为 libsum.a 的静态库文件,并将其与 main.c 中的代码链接在一起,生成最终的可执行文件 my_program。 

📖特点

① 无外部依赖:静态库的代码在链接阶段直接嵌入到可执行文件中,因此,生成的可执行文件不依赖于外部的库文件,独立性强。

② 高性能:静态库不需要在运行时加载,因此在启动时性能较好。

③ 易于部署:由于可执行文件包含了所有需要的库代码,部署时无需考虑目标机器是否安装了相应的动态库。

④ 文件较大:因为每个程序都包含静态库的副本,所以最终生成的可执行文件较大,尤其是在多个程序使用相同库时。

⑤ 更新困难:如果库代码发生变化,必须重新编译所有使用该库的程序,因为静态库的代码已经在编译时嵌入到可执行文件中。

📚二、动态库

动态库与静态库不同,它是在程序运行时加载的,而不是在编译时将库代码嵌入到可执行文件中。动态库通常以 .so(Linux)或 .dll(Windows)为文件扩展名,允许多个程序在运行时共享同一份库代码。相比静态库,动态库具有更高的共享性、更小的磁盘占用和更灵活的更新机制。 

📖创建过程

🔖1.编写源文件 

还是编写一个简单的 sum.c 文件,包含一个求和函数 sum()

// sum.cint sum(int a, int b) { return a + b;}
🔖2.编译源文件

使用 gcc 编译源文件 sum.c,生成目标文件 sum.o。 

gcc -fPIC -c sum.c -o sum.o

这里的 -fPIC 参数用于生成位置无关代码(Position Independent Code),这是动态库的要求,保证库的代码在任何内存地址下都可以运行。 

🔖3.创建动态库

使用 gcc 将目标文件 sum.o 打包成动态库 libsum.so。 

gcc -shared -o libsum.so sum.o

这条命令使用 -shared 参数告诉编译器生成共享库文件 libsum.so。 

🔖4.链接动态库

编写一个程序 main.c,在其中调用动态库中的 sum() 函数。

// main.c#include \"sum.h\"#include int main() { printf(\"Sum: %d\\n\", sum(3, 5)); return 0;}

然后,使用 gcc 编译并链接程序时,指定动态库的位置,并与动态库 libsum.so 链接。 

gcc main.c -L. -l sum -o my_program

这条命令告诉编译器去当前目录查找名为 libsum.so 的动态库,并在链接时将其包含进来。生成的可执行文件 my_program 并不包含库代码,只有一个对动态库的引用。 

🔖5.运行程序

在运行程序时,操作系统会加载 libsum.so 动态库。 

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../my_program

通过设置 LD_LIBRARY_PATH 环境变量,告诉操作系统在当前目录查找动态库文件 libsum.so。 

📖特点

① 共享性:多个程序可以共享同一个动态库的代码,这减少了磁盘空间和内存的占用。在多个程序使用同一个库时,只有一个副本会被加载到内存中。

② 灵活性:动态库可以在程序运行时被加载,因此可以更灵活地更新和替换库的实现,而不需要重新编译依赖该库的所有程序。

③ 启动开销:由于动态库在运行时才加载,程序在启动时需要一些时间来进行符号解析和库加载,可能会导致启动时间略有增加。

④ 外部依赖:程序在运行时依赖动态库,如果缺少所需的动态库文件或库的版本不匹配,程序会无法运行。

📚对比 

特点 静态库 动态库 编译与链接 在编译时将库代码嵌入到可执行文件中 在程序运行时通过动态链接器加载库代码 链接速度 较快 较慢 共享性 每个程序都有独立的库副本 多个程序共享同一份库代码 内存占用 每个程序都占用一份库代码的内存 只有一份库代码驻留内存,共享使用 兼容性 更新时需重新编译所有依赖该库的程序 更新时只需替换动态库文件 部署与维护 无需额外的外部库文件,部署简单 确保目标机器上存在正确版本的动态库 性能 性能较好 需要加载库,影响启动速度 文件拓展名 .a(Linux)、.lib(Windows) .so(Linux)、.dll(Windows)

以上就是【动态库与静态库:代码复用的利器】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力!