为什么我们需要"代码宝箱"?
想象一下,你正在建造一座宏伟的城堡(开发大型软件)。如果每块砖头都要自己烧制,每个窗户都要自己吹制玻璃,那将是一场噩梦!聪明的建筑师会使用预制件——这就是程序库的魔力所在。
程序库就像是软件开发者的"魔法宝箱",里面装满了各种现成的、经过千锤百炼的代码工具。它们让我们的开发效率提升了N个数量级
1.认识两种神奇的"宝箱"(什么是库)
库是写好的现有的,成熟的,可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个⼈的代码都从零开始,因此库的存在意义⾮同寻常。
1.1 静态库:随身携带的工具箱
特点:
-
后缀名:
.a
(Linux) /.lib
(Windows) -
就像你旅行时随身携带的工具箱,所有工具都打包进了你的行李(可执行文件)
-
优点:独立性强,不依赖外部环境
-
缺点:会让你的行李变重(程序体积增大)
1.2 动态库:共享的工具房
特点:
-
后缀名:
.so
(Linux) /.dll
(Windows) -
就像小区里的共享工具房,大家都可以去借用
-
优点:节省空间,多个程序可以共享
-
缺点:如果工具房被拆了(库文件缺失),大家就都用不了了
本质上来说库是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。库有两种:
- 静态库:.a [Linux]、.lib [windows]
- 动态库:.so [Linux]、.dll [windows]
// ubuntu 动态库
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rw-r--r-- 1 root root 2929592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so
$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a
// usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
$ ls -l /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so
lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -> ../../../../x86_64-linux-gnu/libstdc++.so.6
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a,这里要执行
// Centos7 静态库
// /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2165692 Jun 4 23:05 /lib64/libc-2.17.so
[whb@ite-alicloud ~]$ ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5185516 Jun 4 23:05 /lib64/libc.a
// C++
$ ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 -> libstdc++.so.6.0.19
$ ls -l /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
-rw-r--r-- 1 root root 2923366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
2.静态库
- 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。
- 一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在链接下找不到动态.so 的时候才会采用同名静态库。我们也可以使用 gcc 的 - static 强制设置链接静态库。
2.1打造自己的"静态宝箱"
准备"魔法原料"(源代码)
我们准备了两件"魔法物品":
-
文件IO魔法杖(my_stdio)
-
字符串长度测量仪(my_string)
// my_stdio.h
#ifndef _MY_STDIO_H
#define _MY_STDIO_H
#include <stdint.h>
#include <sys/stat.h>
#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;
};
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);
#endif
// my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <unistd.h>
#include <fcntl.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 == NULL)
{
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);
free(stream);
}
// my_string.h
#ifndef _MY_STRING_H
#define _MY_STRING_H
int my_strlen(const char *s);
#endif
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0') end++;
return end - s;
}
2.2 施展"静态宝箱"咒语(静态库的生成)
# 把原料加工成半成品(编译为.o文件)
gcc -c my_stdio.c my_string.c
# 用ar魔法把半成品装进宝箱
ar -rc libmystdio.a my_stdio.o my_string.o
// Makefile
libmystdio.a:my_stdio.o my_string.o
ar -rc $@ $^
@echo "build $@ to $^ ... done"
%.o:%c
gcc -c $<
@echo "compiling $< 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 -p my_stdio.h stdc/include
@cp -p my_string.h stdc/include
@cp -f a stdc/lib
@echo "output ... done"
- ar 是归档工具,rc 表示(replace and create)
🔍 小知识:可以用ar -tv
查看宝箱里有什么宝贝:
$ ar -tv libmystdio.a
rw-r--r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-r--r-- 1000/1000 1772 Oct 29 14:35 2024 my_string.o
- t:列出静态库中的文件
- v:verbose 详细信息
2.3 使用"静态宝箱"
// 任意目录下,新建
// main.c,引入头文件
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{
const char *s = "abcdefg";
printf("len:%d\n", my_strlen(s));
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL) return 1;
mfwrite(s, my_strlen(s), fp);
mfwrite("\n", my_strlen("\n"), fp);
mfclose(fp);
return 0;
}
编译命令就像念咒语:
// 场景1:头文件和库文件安装到系统路径下
$ gcc main.c -l mystudio
// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
$ gcc main.c -L -l mystudio
// 场景3:头文件和库文件在自己的独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -l mymath
- -L:指定库路径
- -I(大i):指定头文件搜索路径
- -l(小L):指定库名
- 测试目标文件生成后,静态库删除,程序照样可以运行
- 关于 - static 选项,稍后介绍
- 库文件名称和入库的名称:去掉前缀 lib,去掉后缀.so/.a,如 libc.so -> c
3.建造"动态共享工坊"(动态库)
- 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用的虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间
3.1 动态库生成
// Makefile
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared -fPIC
@echo "build $@ to $^ ... done"
%.o:%c
gcc -c $< -fPIC
@echo "compiling $< to $@ ... done"
.PHONY:clean
clean:
rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -p my_stdio.h stdc/include
@cp -p my_string.h stdc/include
@cp -f libmystdio.so stdc/lib
@echo "output ... done"
- shared: 表示生成共享库格式
- fPIC: 产生位置无关码(position independent code)
- 库名规则:libxx.so
3.2 使用"动态工坊"
// 场景1:头文件和库文件安装到系统路径下
$ gcc main.c -l mystudio
// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
$ gcc main.c -L. -l mystudio
// 场景3:头文件和库文件在自己的独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -l mymath
$ ldd ./a.out
linux-vdso.so.1 => (0x00007fff89d0c000)
libmystdio.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7c8c81d000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7c8cc07000)
3.3 解决"找不到工坊"的问题
运行使用动态库的时候系统找不到动态库,因为动态库也需要加载到内存,我们在编译的时候指定了路径,但是编译器知道,但是系统和加载器不知道。
$ ldd ./a.out
linux-vdso.so.1 => (0x00007fff4f30d000)
mylibc.so => unfound
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d3c39a000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2d3c784000)
有三种解决方法:
- 拷贝.so 文件到系统共享库位置,一般指 /lib/、/usr/lib/、/usr/local/lib、/lib64 或者开发的库路径等
- 向系统共享库路径下建立同名软连接
- 更改环境变量:LD_LIBRARY_PATH
1.把工坊搬到标准位置
sudo cp libmystdio.so /usr/local/lib
sudo ldconfig
2.把工坊搬到标准位置
sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so
3.临时指路
export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH