云计算百科
云计算领域专业知识百科平台

硬核拆解:Linux 静态库从原理到实战

本篇目标:

学会什么是库及库的作用,静态库的制作,静态库的使⽤,库的查找

一.什么是库

1.概念

简单来讲,库是写好的现有的,成熟的,可以复用的代码,我们可以把它想象成一个装备齐全的工具箱,假设我们正在造一辆汽车时,我们并不需要从零开始去炼钢、做轮胎、造发动机,而是直接从仓库里把现成的轮胎和发动机拿过来组装,那么库就是提供给程序员的就是这些现成的零部件。

作用:现实中每个程序都要依赖很多基础的底层库,就比如说我们常用的printf,如果没有了库,那么每个程序员都要自己写printf的实现,这就会导致每个人printf实现的都不相同,所以为了方便写代码,世界上的顶级程序员就联合起来,编写了库,这样以后,库将复杂的内部实现隐藏了起来,只暴露出简单易用的接口,我们就不需要库是如何实现的,只需要知道怎么调用接口即可

2.动静态库

本质上来说库是⼀种可执行代码的⼆进制形式,可以被操作系统载⼊内存执行,库有两种:静态库与动态库

2.1.静态库

后缀名: 在 Linux 下通常是 .a,在 Windows 下是 .lib。

工作原理: 在程序编译的链接阶段,编译器会把静态库中被调用的那部分代码,完完整整地拷贝一份到你最终生成的可执行程序中。

特点: 编译出来的程序体积比较大,但非常独立。即使把它拷到一台没有安装该库的电脑上,程序依然能完美运行。

2.2.动态库

后缀名: 在 Linux 下通常是 .so,在 Windows 下是 .dll。

工作原理: 在编译阶段,编译器并不会把代码拷贝进程序,而只是在程序里留下了一个标记,等到程序真正运行的时候,操作系统才会根据这个标记去系统里把动态库加载到内存中供程序调用。

特点: 生成的程序体积很小,而且多个程序可以共享内存中的同一个动态库(节省空间)。但缺点是,如果换了一台电脑运行,且那台电脑上缺失了对应的 .so 或 .dll 文件,程序就会报错无法启动。

3.有关库的操作

3.1.ldd

作用:查看可执行程序使用的库

例如:

ldd main_dynamic

输出示例:

linux-vdso.so.1 => (0x00007ffe3b1b5000)
libkongarray.so => /home/kong/my_project/libkongarray.so (0x00007f8a12345000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a11f54000)

解释;箭头 => 后面的路径,就是这个程序真正在使用的库的绝对位置!比如上面的输出明确告诉你,libc.so.6 藏在 /lib/x86_64-linux-gnu/ 目录下。

3.2.库的位置

我们要想要查看部分库的内容,可以用ls,例如:

/lib 或 /lib64里存放的是系统启动时核心库,如图所示:

补充:

1./usr/include 是 Linux 系统存放 C/C++ 头文件的地方,如图所示:

2.动静态库中,要不要包含main函数?

答案:不需要,因为主程序中我们也要包含main函数,如果库文件里也有一个 main 函数,就会导致main 函数被重复定义了。

4.代码

为了更好的讲解动静态库与原理,需要以一个代码的演示,来演示一下过程,代码如下:

在kong_array.h中,

#ifndef KONG_ARRAY_H
#define KONG_ARRAY_H

/*
* 数组工具库
* 包含常用的数组查找与排序功能
*/

// 获取数组中的最大值
int GetMax(int arr[], int size);

// 对数组进行冒泡排序(升序)
void BubbleSort(int arr[], int size);

// 打印数组元素
void PrintArray(int arr[], int size);

#endif

在kong_array.c中,

#include <stdio.h>
#include "kong_array.h"

// 实现获取最大值
int GetMax(int arr[], int size) {
if (size <= 0) return 0; // 边界情况简单处理

int max_val = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > max_val) {
max_val = arr[i];
}
}
return max_val;
}

// 实现冒泡排序
void BubbleSort(int arr[], int size) {
for (int i = 0; i < size – 1; i++) {
// 优化:如果某趟没有发生交换,说明已经有序
int flag = 0;
for (int j = 0; j < size – 1 – i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = 1;
}
}
if (flag == 0) break;
}
}

// 实现打印数组
void PrintArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\\n");
}

在main.c中,

#include <stdio.h>
#include "kong_array.h" // 引入自定义的数组工具库头文件

int main() {
// 定义一个测试数组
int my_arr[] = { 42, 15, 78, 9, 23, 56 };

// 在 C 语言中,通常用这种方式计算数组的元素个数
int size = sizeof(my_arr) / sizeof(my_arr[0]);

printf("1. 原始数组: ");
PrintArray(my_arr, size);

// 调用库函数找最大值
int max = GetMax(my_arr, size);
printf("2. 数组中的最大值是: %d\\n", max);

// 调用库函数进行排序
BubbleSort(my_arr, size);

printf("3. 排序后的数组: ");
PrintArray(my_arr, size);

return 0;
}

二.静态库

1.库的本质

当我们在第一次听到静态库、动态库、链接这些词时,可能总觉得它们是操作系统底层某种极其高深莫测的黑科技,都觉得像是在看天书,但今天,我想先帮你打破这个滤镜,告诉你一个极其简单粗暴的真相:所有的库(无论是 .a 还是 .so),本质上只不过是一个包含了一堆 .o(目标文件)的压缩包,下面以将写代码想象成一家汽车零件制造厂的例子来解释:

 我们敲下的每一行代码,对计算机来说都只是一堆纯文本的图纸,是看不懂的,因为计算机仅认识二进制,但是当我们使用编译器进行编译时,编译器就把图纸加工成了一个实实在在的机器码零件,(比如你写了 10 个源文件,就会生成 10 个独立的 .o 零件),但是如果你的项目有 100 个零件,每次让别人调用你的代码时,都要发送 100 个文件,不仅你崩溃,用的人也崩溃。 于是,我们使用打包工具,把这 100 个 .o 零件捆绑、压缩进了一个大箱子里。这个大箱子,就被我们尊称为库,有了库之后,事情就变得异常简单了。当我们在写主程序时,只需要写好自己的那几个 .o 零件,然后交给系统里的链接器(Linker)。链接器就像一位熟练的总装配工,它会把我们自己写的 .o 零件,与库这个大箱子里的零件完美地链接在一起,最终输出的,就是那个我们可以直接执行的终极产品——可执行程序。但是光有库文件还是不行的,因为编译器编译时,根本就不认识这些库,那么此时就需要头文件来告诉编译器是有这个库的,虽然它不知道具体代码在哪,但它相信后续的链接器能找到它。

2.代码演示

光听是很难懂得,下面以一个代码来演示一下:

目前我们的代码如下

现在我们先将所有的.c生成为.o文件,如图所示:

但是我并不直接将main.c编译为.o文件,而是先将kong_array.o打包成一个库,操作如下:

ar:归档工具,也就是打包器

r (replace):把 .o 文件插入到库包中。如果包里已经有了同名的 .o,就替换成最新的。

c (create):创建一个新的库文件

如图所示:

接下来讲两个选项:

-L告诉链接器(Linker),除了系统的默认目录之外,还要去哪个额外的文件夹里寻找库文件。

-l 作用是用来告诉链接器,你要链接具体哪一个名字的库文件。

代码演示:

我将main.c编译成.o文件后,尝试将main.o与libmyc.a进行链接,但系统提示找不到libmyc.a。其实问题很简单:我们应该链接的是myc库,而不是直接指定libmyc.a这个文件名。如图所示:

生成 main 可执行程序后,回顾之前的编译失败可能有以下原因:

答案:

使用小写的 -l 参数时,链接器会自动执行两个操作:

  • 在输入的名称前添加 lib 前缀
  • 在输入的名称后添加 .so(动态库)或 .a(静态库)后缀
  • 例如,若直接输入 -llibmyc.a,链接器处理后实际查找的文件名会变成:
    lib + libmyc.a + .a = liblibmyc.a.a

    显然系统中不存在这个文件,自然会导致 cannot find 错误。因此,库文件的核心名称需去掉 lib 前缀和 .a/.so 后缀。

    将库文件放在个人目录下存在共享不便的问题。为便于多人使用,我们需要将其移至公共位置。具体操作如下:

  • 将打包好的libmyc.a文件移至上级目录的mycodeh/lib路径(即../mycodeh/lib)
  • 将kong_array.h头文件移至上级目录的usr/include路径(即../mycodeh/usr/include)
  • 使用mv即可
  • 如图所示:

    这里再补充一个选项:-I决定了编译器去哪里找头文件 .h。

    此时我们在编译连接一下,如图所示:

    3.进一步理解库

    显而易见,这已经完美实现了我们的目标。不过有人可能会抱怨:"这也太冗长了吧!简直要命。"别急,这里有个更简洁的方案:我们先

    cp ../mycodeh/lib/libmyc.a /lib64

    cp ../mycodeh/usr/include/kong_array.h /usr/include

    将静态库 libmyc.a 复制到 /lib64 目录,并将头文件复制到 /usr/include 目录。随后,请删除当前目录下的其他冗余文件。如图所示,这是部署第三方库的标准操作流程。

    这样修改后,即使退出后重新进入,仍然可以正常编译链接。需要注意的是,为了避免与系统文件冲突或污染环境,请务必删除我们自行编写的第三方库的头文件和库文件。

    有人可能会问:为什么我们编写自己的库时需要手动链接,而写C/C++代码时却不需要显式链接标准库?原因在于gcc/g++编译器会自动识别并链接C/C++标准库。

    总结:库的本质就是一堆二进制零件的压缩包,写库是把图纸变成零件装箱,用库是拿着说明书(头文件)去召唤链接器,将要用的库和我们的目标文件进行总装。虽然静态库带来了独立运行的便利,但由于其在链接时将代码焊死在可执行文件中,当多个程序同时运行并链接同一个静态库时,会导致物理内存中存在多份相同的指令副本。在追求极高并发和内存利用率的场景下,这正是我们需要引入动态库(Shared Objects)并深入研究地址无关代码的根本诱因。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 硬核拆解:Linux 静态库从原理到实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!