前言
学习linux的库的制作与原理可以提高代码复用性,减少程序体积、加快编译,理解 Linux 程序的运行机制,方便模块化开发与协作,会做库、懂原理,你就能写出别人能直接用的“工具箱”,还能明白 Linux 在背后到底是怎么帮你把它装进程序里的。
一,库是什么?
- 库是已经编写好、经过验证并可复用的代码集合。在现实开发中,几乎每个程序都依赖于大量基础的底层库,不可能每位开发者都从零开始实现所有功能。因此,库的存在意义极为重要——它是软件开发高效运作的基石。
- 从本质上看,库是一种可执行代码的
二进制文件,能够被操作系统加载到内存中并直接运行。
根据链接方式的不同,库主要分为两类:静态库(Static Library):在编译时将库代码直接打包进可执行文件中。动态库(Dynamic Library / Shared Library):在运行时由操作系统按需加载,可供多个程序共享使用。
我们先看一下我们最常用的两个C和C++的库
- libc64:就是 64 位版本的 C/C++ 语言基础工具包,你写 C/C++ 程序时用到的很多函数,比如 printf、scanf、malloc,其实都藏在这个工具包里。
存放位置:
libc-2.17.so:就是 glibc 2.17 版本的核心动态库文件,也就是C标准库的具体实现文件。
[root@hcss-ecs-f59a lib64]# ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 2024 /lib64/libc-2.17.so
libc.a: 是C标准库的静态库文件,它和 libc.so(动态库)是同一个库的两种不同形式。
[root@hcss-ecs-f59a ~]# ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 2024 /lib64/libc.a
libstdc++.so.6:是 GNUC++标准库(libstdc++)的动态链接库文件,它是 Linux 系统中运行 C++ 程序的核心依赖库之一
[root@hcss-ecs-f59a ~]# ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Jul 26 2024 /lib64/libstdc++.so.6 libstdc++.so.6.0.19
libstdc++.a:是 GNUC++标准库(libstdc++)的静态版本,与动态库 libstdc++.so 不同,它在编译时会被直接链接到可执行文件中,而不是在运行时动态加载。
[root@hcss-ecs-f59a ~]# ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
-rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
一个简单的库制作
main.c
#include"file.h"
int main()
{
print("hello,world\n");
printf("%d\n",Add(100,200));
}
file.c
#include<stdio.h>
void print(const char* ch)
{
printf("%s",ch);
}
int Add(int a,int b)
{
return a+b;
}
file.h
#include<stdio.h>
void print(const char* ch);
int Add(int a,int b);
main.c:源文件 —> 包含 main() 函数,是程序的入口,调用其他文件中定义的函数。file.c:源文件 —> 实现具体的函数(如 print() 和 Add()),包含函数的具体逻辑。file.h:头文件 —> 声明函数(如 print() 和 Add()),告诉编译器这些函数的存在和格式。
二,静态库
-
静态库(.a)就是在编译链接阶段,把库里的代码直接拷贝进可执行文件里,运行时就不用再去找那个静态库了。 -
一个可执行程序可能会用到很多库,这些库有的是静态库,有的是动态库。默认情况下,编译会优先用动态库,只有找不到对应的动态
.so库时,才会用同名的静态库。我们也可以通过给gcc加个-static参数,强制让它只用静态库。
2-1 静态库的生成
my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE {
int flag; // 刷新方式
int fileno; // 文件描述符
char outbuffer[SIZE];
int cap;
int size;
// TODO
};
typedef struct IO_FILE mFILE;
mFILE * mfopen(const char * filename,const char * mode);
int mfwrite(const void * ptr, int num, mFILE * stream);
void mfflush(mFILE * stream);
void mfclose(mFILE * stream);
my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE * mfopen(const char * filename,
const char * mode) {
int fd = -1;
if (strcmp(mode, "r") == 0) {
fd = open(filename, O_RDONLY);
} else if (strcmp(mode, "w") == 0) {
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
} else if (strcmp(mode, "a") == 0) {
fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
}
if (fd < 0) return NULL;
mFILE * mf = (mFILE * ) malloc(sizeof(mFILE));
if (!mf) {
close(fd);
return NULL;
}
mf -> fileno = fd;
mf -> flag = FLUSH_LINE;
mf -> size = 0;
mf -> cap = SIZE;
return mf;
}
void mfflush(mFILE * stream) {
if (stream -> size > 0) {
// 写到内核文件的文件缓冲区中!
write(stream -> fileno, stream -> outbuffer, stream -> size);
// 刷新到外设
fsync(stream -> fileno);
stream -> size = 0;
}
}
int mfwrite(const void * ptr, int num, mFILE * stream) {
// 1. 拷贝
memcpy(stream -> outbuffer + stream -> size, ptr, num);
stream -> size += num;
// 2. 检测是否要刷新
if (stream -> flag == FLUSH_LINE && stream -> size > 0 && stream -
>
outbuffer[stream -> size - 1] == '\n') {
mfflush(stream);
}
return num;
}
void mfclose(mFILE * stream) {
if (stream -> size > 0) {
mfflush(stream);
}
close(stream -> fileno);
}
my_string.h
#pragma once
int my_strlen(const char * s);
my_string.c
#include "my_string.h"
int my_strlen(const char * s) {
const char * end = s;
while ( * end != '\0') end++;
return end - s;
}
Makefile
libmystdio.a:my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
.PHONY:clean
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/libc
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
ar 是 GNU 的归档工具,用来把多个文件打包成一个归档文件(比如 .a 静态库)。
r(replace):如果归档里已经有同名文件,就替换它。c(create):如果归档不存在,就新建一个。
[gch@hcss-ecs-f59a day8]$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Aug 11 17:17 2025 my_stdio.o
rw-rw-r-- 1000/1000 1272 Aug 11 17:17 2025 my_string.o
t: 列出静态库中的文件v:verbose 详细信息
2-2 静态库使用
main.c
#include"my_stdio.h"
#include"my_string.h"
#include<stdio.h>
int main()
{
const char* s="abcdefg";
printf("%s:%d\n",s,my_strlen(s));
mFILE *fp = mfopen("./log.txt","a");
if(fp == NULL)
{
return 1;
}
mfwrite(s,my_strlen(s),fp);
mfwrite(s,my_strlen(s),fp);
mfwrite(s,my_strlen(s),fp);
mfclose(fp);
return 0;
}
- 这个main.c和上面的文件我们都创建好了,我们执行make的结果如下
[gch@hcss-ecs-f59a day8]$ make
compling my_stdio.c to my_stdio.o ... done
compling my_string.c to my_string.o ... done
build my_stdio.o my_string.o to libmystdio.a ... done
[gch@hcss-ecs-f59a day8]$ ll
total 32
-rw-rw-r-- 1 gch gch 4374 Aug 11 17:17 libmystdio.a
-rw-rw-r-- 1 gch gch 334 Aug 11 17:34 main.c
-rw-rw-r-- 1 gch gch 375 Aug 11 14:17 makefile
-rw-rw-r-- 1 gch gch 1533 Aug 11 17:16 my_stdio.c
-rw-rw-r-- 1 gch gch 435 Aug 11 17:16 my_stdio.h
-rw-rw-r-- 1 gch gch 2848 Aug 11 17:17 my_stdio.o
-rw-rw-r-- 1 gch gch 138 Aug 11 17:17 my_string.c
-rw-rw-r-- 1 gch gch 44 Aug 11 17:17 my_string.h
-rw-rw-r-- 1 gch gch 1272 Aug 11 17:17 my_string.o
- 这里先是生成了
my_stdio.o和my_string.o,然后将它们打包形成静态库libmystdio.a
那我们的main.c要怎么使用这个静态库libmystdio.a呢,有以下几种情况
- 场景1:头文件和库文件安装到系统路径下
[gch@hcss-ecs-f59a day8]$ gcc main.c -lmystdio
- 场景2:头文件和库文件和我们自己的源文件在同一个路径下
[gch@hcss-ecs-f59a day8]$ $ gcc main.c -L. -lmystdio
- 场景3:头文件和库文件有自己的独立路径
gcc main.c -I头文件路径 -L库文件路径 -lmymath
- -lxxx 会去找名字是
libxxx.a(静态库)或libxxx.so(动态库)的文件,优先找动态库.so,找不到才会找静态库.a,如果要强制用静态库,可以加 -static, - -I 是 GCC 编译选项中用来指定头文件搜索路径的参数
- -L是 GCC 中的一个选项,用来 指定库文件的搜索路径。
这里我们的工作更符合场景2,所以我们用场景2来测试一下
[gch@hcss-ecs-f59a day8]$ gcc main.c -L. -lmystdio
[gch@hcss-ecs-f59a day8]$ ./a.out
abcdefg:7
[gch@hcss-ecs-f59a day8]$ cat log.txt
abcdefgabcdefgabcdefg
- 注意:这里的测试目标文件生成后,静态库删掉,程序照样可以运行,因为静态库是直接将代码导入
三,动态库
动态库(.so 文件)是程序运行时才加载进来的库,多个程序可以一起用同一份库代码。
怎么链接:跟动态库链接的程序里,不会把库的机器码都拷进去,只会记下“用到的函数入口地址在哪”。什么时候加载:程序一启动,操作系统里的“动态链接器”会从磁盘把需要的动态库读到内存,然后把函数地址补好,这就叫 动态链接。为什么省资源:因为一份动态库可以被好多程序一起用,内存和磁盘都只要放一份,可执行文件本身也能变得更小。
3-1 动态库的生成
Makefile
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
-fPIC:核心作用就是:让编译出来的目标文件可以在内存中的任意地址加载运行,这就是所谓的位置无关代码- 编译 动态库
(.so)时 → 必须加,否则链接会报错 - 静态库
(.a)不需要
- 编译 动态库
-shared: 表示生成共享库格式库名规则:libxxx.so
3-2 动态库使用
动态库也有三种使用场景,和上述静态库相同,这里就不再过多阐述了
- 我们以场景二为例:头文件和库文件和我们自己的源文件在同一个路径下
[gch@hcss-ecs-f59a day8]$ ll
total 24
-rw-rw-r-- 1 gch gch 334 Aug 11 17:34 main.c
-rw-rw-r-- 1 gch gch 320 Aug 11 19:39 makefile
-rw-rw-r-- 1 gch gch 1533 Aug 11 17:16 my_stdio.c
-rw-rw-r-- 1 gch gch 435 Aug 11 17:16 my_stdio.h
-rw-rw-r-- 1 gch gch 138 Aug 11 17:17 my_string.c
-rw-rw-r-- 1 gch gch 44 Aug 11 17:17 my_string.h
[gch@hcss-ecs-f59a day8]$ make
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared
[gch@hcss-ecs-f59a day8]$ gcc main.c -L. -lmystdio
[gch@hcss-ecs-f59a day8]$ ./a.out
abcdefg:7
[gch@hcss-ecs-f59a day8]$ cat log.txt
abcdefgabcdefgabcdefg
3-3 库运行搜索路径
我们观察一下a.out所依赖的库
[gch@hcss-ecs-f59a day8]$ ldd a.out
linux-vdso.so.1 => (0x00007ffea3dd0000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f6bf8018000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6bf85e8000)
为什么这里会出现libmystdio.so => not found?
-
gcc main.c -L. -lmystdio中的
-L.只是告诉编译器 在链接阶段 去当前目录找libmystdio.so,所以编译能顺利完成。 -
但程序运行时,动态链接器并不会参考编译时的
-L选项,而是只会去系统的默认库目录(如 /lib、/usr/lib、/usr/local/lib)寻找动态库。 -
如果运行时找不到,就会出现 libmystdio.so => not found 的情况。
解决方法:让运行时也能找到库,可以用以下方式之一:
- 修改环境变量
LD_LIBRARY_PATH
环境变量 LD_LIBRARY_PATH 里保存着一组目录路径。运行程序时,动态链接器会优先在这些路径中查找动态库。也就是说,把目录加入 LD_LIBRARY_PATH,就等于把它变成了运行时的额外默认库搜索路径。
我的当前路径为/working/HaoHao/day8,执行指令:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/working/HaoHao/day8
- 在
/lib64中创建软链接
/lib64 是系统默认的动态库搜索目录之一。如果在该目录下创建一个指向你自己动态库 libmystdio.so 的软链接,就相当于把你的库加入了系统的默认库路径,程序运行时就能自动找到它。
我的当前路径为/working/HaoHao/day8,执行指令:
[gch@hcss-ecs-f59a day8]$ sudo ln -s /working/HaoHao/day8/libmystdio.so /lib64/libmystdio.so
[gch@hcss-ecs-f59a day8]$ ls /lib64/libmystdio.so
/lib64/libmystdio.so
- 配置
/ etc/ld.so.conf.d/
在 /etc/ld.so.conf.d/ 目录下,可以放用户自定义的动态库路径配置文件。只需用 root 权限新建一个以 .conf 结尾的文件,里面写上你想添加的动态库路径即可。这样系统在运行 ldconfig 后,就会把这些路径加入到动态库搜索范围里。
[gch@hcss-ecs-f59a day8]$ ll /etc/ld.so.conf.d
total 20
-rw-r--r-- 1 root root 26 Jun 11 2024 bind-export-x86_64.conf
-r--r--r--. 1 root root 63 Aug 8 2019 kernel-3.10.0-1062.el7.x86_64.conf
-r--r--r-- 1 root root 63 Jun 4 2024 kernel-3.10.0-1160.119.1.el7.x86_64.conf
-rw-r--r-- 1 root root 17 Oct 2 2020 mariadb-x86_64.conf
-rw-r--r-- 1 root root 56 Aug 11 21:41 text.conf
[gch@hcss-ecs-f59a day8]$ ldconfig
四,目标文件
在 Windows 下,编译和链接这两个步骤基本都被 IDE 封装好了,我们平时一键构建,特别方便。但一旦出了问题,尤其是链接错误,很多人就懵了,不知道怎么下手。而在 Linux 下,我们之前学过怎么用 gcc 编译器手动完成这些步骤。

这里通过预处理,编译,汇编,将.c文件变为了.o文件当然我们可以直接跳过中间的步骤,直接将.c文件变为.o文件,示例如下
hello.c
#include<stdio.h>
void run();
int main()
{
printf("hello,world!\n");
run();
return 0;
}
code.c
#include<stdio.h>
void run()
{
printf("running...\n");
}
[gch@hcss-ecs-f59a day9]$ gcc -c code.c
[gch@hcss-ecs-f59a day9]$ gcc -c hello.c
[gch@hcss-ecs-f59a day9]$ ll
total 16
-rw-rw-r-- 1 gch gch 59 Aug 12 12:59 code.c
-rw-rw-r-- 1 gch gch 1496 Aug 12 13:00 code.o
-rw-rw-r-- 1 gch gch 94 Aug 12 12:55 hello.c
-rw-rw-r-- 1 gch gch 1560 Aug 12 13:00 hello.o
编译之后,会生成两个 .o 后缀的文件,我们叫它们目标文件。这里要注意,如果你改了其中一个源文件,只需要单独编译改动的那个文件,没必要整个工程都重新编译,省时间又高效。目标文件其实就是二进制文件,它的格式是 ELF,这是一种专门用来装机器码的文件格式。
用file命令辨识一下文件类型
[gch@hcss-ecs-f59a day9]$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
我们可以看到这里的hello.o文件是ELF格式
4598

被折叠的 条评论
为什么被折叠?



