[TOC]

Linux 系统编程 动静态库

软硬链接(续)

在开始正式讲动静态库之前呢,我们先把文件系统里面介绍的软硬链接的知识再补充一下。

首先对于软连接, 之前我们说它是一个独立的文件,作用类似于快捷方式。这是他其中的一个用途。还有别的用途后续会提到,比较少用了。

对于硬链接,我们之前说他不是一个独立的文件,而是走的映射和引用计数来找到同一个文件的。

那么,问题来了, 普通文件可以有硬链接,这没问题,那么目录能不能有硬链接? 测试一下

image-20260531144618772

可以看到我们尝试给这个目录创建一个硬链接,报错了,告诉我们硬链接不允许给给一个目录。 顺带一提,软连接是可以给给目录的。

那么现在问题又来了,为什么硬链接不能给给目录? 再解释这个问题之前呢,我还要再抛出另一个问题,细心的同学或许猜到了我想说什么。

首先,软连接的文件属性 第一位是给 l,代表他是一个链接文件, 而硬链接则是直接拿的 假设叫 test.c 的 inode , 属性和 test.c 一模一样。 所以, 链接文件只有软连接。

但是,我们之前说的 . 和 .. 本质不就是对当前目录的硬链接吗?为什么 . 和 .. 可以是目录的硬链接? 这和我们上面的实验演示的,目录不能给硬链接不是矛盾的吗? 这就是我要说的第二个问题。

下面我们来解释一下, 其实这就是 OS 的一种只需州官放火不许百姓点灯的行为, 换句话说, . 和 .. 的硬链接不是我们自己创建的,而是我们每次创建目录自己带着的,除了我们,还有谁创建文件,那肯定操作系统喽。 所以, 给目录创建硬链接这个事,只允许操作系统自己干,而不允许用户手动干。 这个矛盾也就解释得通了。

但是,为什么要这样只需州官放火呢? 首先大家肯定听说过目录树这个概念,我们之前也讲过 dentry 树, 目录在 linux 里面是树状结构储存的, 思考一下,如果允许给目录创建硬链接,很容易会导致路径环的问题。

image-20260531150244655

可以看到如果我们自己把这个三级目录1 变成 一级目录1, 那当我们走到这个三级目录1 的时候,又回到了一级目录1, 这样不就死循环的成环了。 那肯定完蛋, 这还查找个什么劲呢?哈哈。

所以呢,基于成环这个事,也就不允许用户自己创建了。 那为什么 . 和 .. 可以呢? 因为他们名字特殊, 当查找的时候涉及到他们,一看到他们的名字 直接走一个 if 特殊处理就可以了,这是可控的。 顺带一提,当我们尝试删调 . 或者 .. 的时候是删不掉的。

既然这里给 . 和 .. 开个先例这么麻烦,为什么还要这么干? 主要是为了方便, 可以使用相对路径嘛。

还没有结束, 硬链接会成环,软连接不是也可以成环吗?为什么软连接可以, 答案就是软连接的文件类型是 l ,是个独立的文件, 这就是区分的点,一样的走特殊处理就好了, 遍历的时候当个普通文件展现。 硬链接直接拿属性和别人一模一样的区分不了。

还有一件事就是这个缓存的问题,为什么要缓存,主要还是因为CPU之和内存打交道,数据处理什么的都要在内存里面,从磁盘到内存加载很慢, 所以,我们就可以写数据的时候一批一批的写, 加载当前目录的时候,会加载它下面的一些文件,到缓存,因为大概率会用到,下次直接从缓存里面拿。 (再看看,有点拿不准了)


1. 库的制作和原理

为什么要有库,这个我们之前提到过不少次了,可以减少重复造轮子,而且像手机一样用起来很方便。

说点抽象的,库的本质是可执行的二进制形式。 不明白没关系,后面慢慢让你理解。

我们在刚刚开始学 linux 的时候,其实提到过库。 首先库,分为动态库和静态库两种,

.a / .lib 分别是 Linux 和 Windows 下静态库的后缀。 .so / .dll 分别是linux 和 Windows下动态库的后缀。

平时用windows用的多的话,有时玩点游戏经常会报什么 dll 缺失,其实就是 动态库没有或者被杀软干掉了。

我们常说的软件,就是可执行程序加上库, 库文件一定要存在才可以被使用。

1.1 静态库的制作

首先,我们先来聊聊静态库的制作,静态库好理解一些。 这里, 拉出来我们之前写的文件接口,做个简单的演示,没有也没关系。 首先,在此之前,我们编译链接形成可执行的时候, 基本都是 .c + .h 的形式,(以C语言为例)。

想象这样一个场景,

张三和李四呢要期末了,交作业C语言作业,老师让实现一个功能,结果张三没写。 张三就求助写了的李四,李四把他写好的

.c 源文件和 .h 头文件都给了张三。 张三只管写一个主程序调用就行了。 一开始没什么问题,老师只看上层调用,还看不出什么端倪。但是后来,老师要看源文件了,结果一看张三李四源文件一模一样,直接就露馅一起被惩罚。

后来,有了上一次的经验以后,张三这次又来找李四要,李四呢,思考思考,平时张三对他不错说实话,经常请他吃饭什么的,不给不好。 于是, 李四一拍脑袋, 想到了好招:

李四先把 .c 编译成 .o , gcc mystio.c -o mystdio.o , .o 文件我们说是二进制文件,打开以后全是乱码,老师也看不出来,但是张三也看不出来了。所以, 有了 .o 以后, 张三再把 函数声明的 .h 给给张三, 张三拿着这个头文件

.h 就知道怎么用了。 所以, 这里也能看出: 头文件间是对源文件的方法说明文档。

有了 .o 以后呢, 张三自己用的时候,就可以把所有 .o 全部 gcc *.o 全部链接在一起, 就可以生成对应的可执行。

走到这里,我们发现,只要有 .o 和 .h ,给别人就可以用。 由此哦我们也可以得出一个非常重要的结论: 所有的库,无论是动态库还是静态库, 本质都是源文件对应的 .o!!

再扩展一下,我们一般做项目可不止一个 .o , 要是我们有几千个 .o 怎么办? 库往往也是比较大的。

这个时候我们就可以给 .o 打个包。 注意 .h 是没法直接打包的,因为要阅读。 所以,这里也就引出了: 静态库就是 .o 打了个包。 但是这个打包是什么意思, tar ?肯定不是, 解压以后依然是几千个 .o。

linux里面有别的办法, 可以使 .o 打包以后能够直接被 gcc 识别。

ar 命令, Archive 的缩写, 意思是归档文件,打包。

1
ar -rc mylibc.a *.o

.a 就是静态库文件的后缀,本质是一种归档文件, 不需要使用者解包, 直接用 gcc 链接就好。

rc 是这个选项 replace create , 相当于是一路绿灯, .a 有同名了的就替换, 没有就产生。

还有一个重要的点是:

静态库一定要是 .a 结尾, 静态库一般是 lib*.a , 类似这样, 以lib开头, 但是静态库的名字是去掉lib, 去掉.a 的中间那一块,白菜芯。 libsu.a 名字就是 su 。 当然,有些库在 .a 后面还会 . 一些东西, 那是版本号之类的.我们不管.

那么现在再来一个问题, 库里面要不要 main 函数? 当然不要,我们之前说软件是 可执行 + 库, 都有 main 函数会链接冲突的. 但是如果你想设计一个不想被链接的库,加上main函数是个办法.

好,那么现在,张三拿到了李四打包发来的.a , 假设就叫 libmyc.a , 张三自己写的主函数在 usercode.o里面, 张三现在要开始链接了, (肯定要链接, 不链接找不到的) , 怎么链接? 首先告诉大家,指定库名字的选项是 -l

那怎么写? -llibmyc.a ? (这里这个 -l 和后面的库名字之间有没有空格都行) 不不不, 库的名字只有白菜芯, 去掉前缀和后缀. -lmyc.a 这样才对.

但是,有了这个也链接不到,为什么吗? 因为找不到库文件,gcc 默认去系统路径下找,而不是当前路径下, 所以,要再加一个选项 -L 然后指明当前路径.

需要注意的一点是,我们要链接任何非C/C++ 标准库, 都要加上 -l 和 -L 选项.

有了上面这种打包的办法以后,李四的名声越来越响了,越来越多的人都来找李四。 李四又开始动脑筋了,这么多人来找自己,自己一个一个发不得累死,而且解释起来也很麻烦。所以,李四现在开始定标准了, 把所有的 .h 和 .a 放到 include 目录下 mylib 文件夹, 然后 tar -czf ,把压缩包放啊都官网, 谁需要谁去下载,按这个目录标准来使用文件。

但是还有一个问题, 补充一下, 这样下载下来这个包的时候,之间链接 张三的usercode.o 找不到对应的头文件。

因为头文件要包含要么去系统路径下找,要么在用户当前路径下找。 但是现在的头文件放在李四规定的路径里面。 所有,这个时候,还有给头文件指定路径 -I + 路径, 指明在哪个路径下查头文件。

image-20260531194619017

通过上面这个例子,主要想说明的就是库的发布也是要有规范的,要放在统一的路径下。 并且,库在安装软件也是要被安装到系统中的。 但是,还有一个问题就是, 每次 gcc 编译的时候 -I -L -l 写一堆全链路,难受死了。 (有点小问题)

后面也是有办法解决,不着急。

centos7 下面的库的路径就是 user/include 和 /lib64 , 安装软件的同时要把相应的库也拷贝到这个里面。 安装本质不也是拷贝吗?记得安装要 sudo 一下。 但是有个问题捏, 放到这个系统路径下,还是第三方库,所以,链接的时候还要指定库的名字 -l。 总结一下就是库安装,就是把库放到指定的路径下。

我们之前提到过 ldd + 可执行, 可以看看当前可执行用哪个库

还需要注意的点,gcc g++ 默认就认识 C C++ 的库, 我们自己写的库他是不认识的。

那,为什么要把库文件放到规范的文件里面呢 /include /mylib 这样子, 其实是从设计者和使用者的角度出发的,这样放就是为了起到一个统一指示的作用,如果这个设计者愿意,也可以提供一个脚本,其实就是把对应路径下的库文件,拷贝到系统的库文件路径下面。 其实也就是一个对于设计者来说, 一个对于使用者来说。

比如举个例子 安装 vs2022 一开始只是在安装一个集成开发环境,第二部安装套件才是在安装库。

image-20260531195154284

这个时候我们就可以通过这个 makefile 做出库来,然后打包。 后续就可以发布了。


1.2 动态库

相比较于刚才的静态库,打包成动态库,是更加常见的。 其实,从打包方式上也可以看出端倪,打包动态库不需要使用别的什么工具,就用 gcc 就可以, gcc 亲儿子属于是。

要形成动态库,首先在 .c 形成 .o 的时候, 要加上一个 -fPIC 选项,意思是生成这个于位置无关码,后续我们会详细说明这个东西。

然后再 .o 链接的时候,加上一个 -shared 选项, 就可以形成一个动态库了。

动态库 是以 lib 开头 .so 结尾, 后面可能会再点一些版本之类的。

形成之后到底是不是一个动态库呢? 我们可以 file + 文件 来看一下文件的属性。

image-20260531201446215

同样的,我们在使用 makefile 的时候,只需要简单修改一下,动态库就出现 加上这个 -fPIC -shared .a 改 。so 就好了。

形成动态库以后我们要怎么编译呢? 依旧是 -I -L -l 三件套, 这样编译也就通过了。但是, 允许的时候会发现,依旧不行, 因为运行时还是找不到。

ldd 查看一下可执行程序依赖的库,看看发生肾磨石了, 结果发现对于的动态库 not found.

这个时候,大家或许有疑问,不是告诉 gcc 库的位置和名字吗?为什么呢还不行。 我们要明白gcc是编译器,编译时告诉他的以后让你通过编译,这合理。但是执行起来的时候不是 gcc 执行,而是操作系统去执行, 操作系统知道吗?不知道。

系统 != gcc 没有噶告诉系统,执行不起来。 就像学生跳转到红树林网吧。

那静态库怎么没有这个问题,因为静态库是链接的时候就把代码实现拷贝到了可执行程序里面, 形成可执行以后,可执行就不再依赖静态库了。 所以,动静态库虽然看起来制作上没什么大区别,但是使用是有区别的。

那怎么办呢?首先肯定要知道系统去哪里找动态库 centos7 下面是去 user/local/lib64 下面找动态库的。

所以第一个办法就是直接把我们自己动态库拷贝到这个路径下面, 这样我们可执行程序都不用再编译一遍,直接就能跑。

不想直接把库搞里头也可以在这个系统路径下整个软连接,链接到我的库就可以了。

我们在使用 yum 或者 apt 等包管理的时候,安装的时候动态库都是拷贝到系统路径下面的

但是,不想该系统怎么办? 还有一个办法就是走环境变量:

查一下环境变量的话会发现可能有一个环境变量叫 $LD_LIBRARY_PATH , 这个环境变量。 OS在找动态库的时候,除了查找上面我们提到过的系统路径,还会查找这个环境变量里面的路径。 这个环境变量呢有点时候有,有的时候又没有,而且很多时候虽然有也是空的, 所以这里我就需要自己导一下:

image-20260520163009356

就像这样。但是记住,这样导完了以后,是内存级的,一旦关了就没有了。 要想让他长期有效我们要去改一下配置文件,像是 bashrc bashprofile 这里就不展开说了,以前说到过。

但是,如果觉得导环境变量还是不够优雅怎么办? 其实还有一种办法。 在这个/etc路径下面有一个 /etc/ld.so.config/ 路径,翻译一下就是 加载 动态库 配置。 没错,我们改这里也是完全没有问题的。

我们在这个里面写一个配置文件,任意名字都行,后缀叫 .conf , 然后在这个配置文件里面写上我们要添加的找动态库的路径就好了。 但是如果只是这样的话,我们会发现再次运行还是找不到,没有起效。

其实这里还需要一步 sudo ldconfig 加载一下配置文件,然后就会生效了。

好的,到这里呢,差不多就告一段落了,还有一些补充问题,要交代清楚。

首先第一个,再把张三拉出来, 如果李四同时给了张三静态库和动态库,gcc 默认使用的是动态库。 如果非要使用静态库,在链接的时候加上 -static 选项, 就可以强制走静态库。

如果没有静态库加 -static 呢? 就会报错奥, 如果李四只提供静态库,那就只能是走静态库, ldd 发现也会有动态库 其实这里是C语言的动态库。

第二个就是在 linux 下, 默认安装的大部分都是动态库, 默认都是优先安装动态库,毕竟是亲儿子。 C C++ 默认是没有静态库的,想要自己装。

第三个就是库和应用程序是 1:n 的一个关系,不管是静态库和动态库都可以被多个可执行使用 后续随着发展,库不断更新会变得越来越大,会有新的东西集成到库里面,而且库里面老的东西也不会去掉。 就像python 一次更新差点死了。

还有一个就是我们之前用 vs2022, 这个不仅仅可以生成而可执行,也可以形成库,具体的不是重点就详细说了,问问ai。

对于这个工作来说呢,写库就是纯技术工作, 牛马中的牛马。怎么说呢,以后还是做一些 有技术又有业务的细分下来的工作会更好一些,还是要沾点业务会好。


*1.3 提一嘴一个好玩的库()

我们之前一直用的是 C/C++ 的库,很少接触到外部库,这里呢就是简单介绍一个好玩的图形库: ncurses

这一张纯介绍,不感兴趣不看也行。

首先是安装:

1
2
3
4
5
6
// 安装 
// Centos
$ sudo yum install -y ncurses-devel

// ubuntu
$ sudo apt install -y libncurses-dev

系统中其实有很多库,它们通常由⼀组互相关联的⽤来完成某项常⻅⼯作的函数构成。⽐如⽤来处理
屏幕显⽰情况的函数(ncurses库)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// 我们代码是我⽤⽂⼼⼀⾔⽣成并调试通过的,具体要什么功能,⼤家可以试着了解⼀下 
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.h>

#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的
宽度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 微秒(300毫秒)

int main() {
initscr();
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿⾊前景,⿊⾊背景
init_pair(2, COLOR_RED, COLOR_BLACK); /* 剩余部分(虽然⽤红⾊可能不太合适,
但为演⽰⽬的):红⾊背景 */
cbreak();
noecho();
curs_set(FALSE);

int max_y, max_x;
getmaxyx(stdscr, max_y, max_x);
int start_y = (max_y - WINDOW_HEIGHT) / 2;
int start_x = (max_x - WINDOW_WIDTH) / 2;

WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
box(win, 0, 0); // 加边框
wrefresh(win);

int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;

while (progress <= max_progress) {
werase(win); // 清除窗⼝内容

// 计算已完成的进度和剩余的进度
int completed = progress;
int remaining = max_progress - progress;

// 显⽰进度条
int bar_x = BORDER_PADDING + 1; // 进度条在窗⼝中的x坐标
int bar_y = 1; // 进度条在窗⼝中的y坐标(居中)

// 已完成部分
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++) {
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));

// 剩余部分(⽤背景⾊填充)
attron(A_BOLD | COLOR_PAIR(2)); // 加粗并设置背景⾊为红⾊(仅⽤于演⽰)
for (int i = completed; i < max_progress; i++) {
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(A_BOLD | COLOR_PAIR(2));

// 显⽰百分⽐
char percent_str[10];
snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) /
max_progress);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2; // 居中显⽰
mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);

wrefresh(win); // 刷新窗⼝以显⽰更新

// 增加进度
progress += PROGRESS_INCREMENT;

// 延迟⼀段时间
usleep(DELAY);
}

// 清理并退出ncurses模式
delwin(win);
endwin();
return 0;
}

代码什么效果呢大家感兴趣可以试试看, 是个进度条。

推荐⼀篇不错的使⽤指南:https://blog.csdn.net/bdn_nbd/article/details/134019142


2. 目标文件

这一章我们开个头,这一篇篇幅也差不多了。

我们从我们形成可执行的步骤开始讲起。首先,我们在windows下使用 vs2022 的时候,按一下 ctrl + f5 就自动生成可执行一步到位了。 但是当我们环境换到 linux 下的时候,形成可执行要我们自己用 gcc 来生成。

其实这个步骤我们也已经说过很多次了, 编译和链接。 其中,编译里面再细分为 预处理 编译 汇编

经过编译之后呢 linux 下会形成 .o 的目标文件 windows 下的目标文件是 。obj ,去 vs 当前目录下找也能找到。

.o 文件, 链接形成可执行。 其实,这里的 目标文件还有一个名字, 或者说叫全称: 可重定向/可重定位目标文件。两个都可以。 为什么叫这个名字,当我们讲完这一大节,就会明白了。

通过我们这一章的学习,我们也知道了库的本质就是 .o 文件打包。 其实不止是库,当我们要写多个模块的项目的时候, 不同的 .c 文件我们更偏向于先生成对应的 .o ,然后再形成可执行。

为什么喜欢先变成目标文件然后再链接呢,其实是为了降低后续修改的成本。比如,如果我们要把整个东西全部编译成一个 .o,有一个地方修改,所有文件都要重新编译。 但是分开 .o , 哪里改了,重新编哪一个文件就好,最后一起链接,。这就降低了修改的重新编译的成本。

其实, 动静态库, .o 还有 exe 文件,都是二进制文件, 他们是有固定格式的。 这个格式叫 ELF。

1
2
$ file hello.o 
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

所以,我们得出第一个结论:

虽然,我们不知道什么是 ELF 格式,但是,我们知道可执行程序是要被拆分成不同的模块的, 不是直接把代码和数据放进去。

这里,我们就要引出数据节的概念,section 意思就是节。 刚才我们说了 ELF 文件是分模块的, 其实就是把代码和数据分成了不同的节, 相同属性的内容放在同一个节。

我们可以通过 size + 文件名(code) 来看看每一个节的大小: 注意,每一个节可能占一个节或者几个节的大小,大小不固定的。

1
2
3
$ size code.o // 或者是 code.out 也行 (这是注释)
text data bss dec hex
3312 636 4 3952 f70

可以看到这个 code 里面的不同的节的名称和大小。 可看到里面有 text data bss

text 就是我们存代码的地方, data 是存数据的地方 , bss 是存未初始化数据的地方 准确说是未初始化全局变量 都是 0 ,后续我们详细说明。

当我们的 .o, 链接形成可执行的时候,其实就是把这些对应属性的节进行合并。 形成 exe。 没错, 链接的本质就是合并。 比如我们之前说的静态库,静态库的链接,就是二进制文件的合并。 链接器边读边解析,然后把对应的节写到可执行里面去。

这里我们要再拉出一个以前讲过的概念,叫虚拟地址空间。我们在讲这个虚拟地址空间的时候,说的是可执行加载到内存里面的时候,分什么栈, 堆, 代码段 , 数据段之类的。 但是这里是 section

所以每一个文件有不同的section, 加载到内存里面的时候, 像是我们之前提到的 text data 等 section 会进行相应的合并,然后形成我们之前说的 代码段 或者 数据段之类的。 segment 也就是段。 换句话说, 我们的可执行程序在加载到内存里面的时候是分段加载的。

回想一下,我们说虚拟地址空间 mm_struct 里面存着每个区域的begin 和 end , 这个数据从哪里来, 其实就是从 ELF文件里面来, 读取ELF 信息, 来初始化结构体。