1. 动静态库的制作与使用
1.1 什么是库
库是写好的、现有的、成熟的、可复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在有很大的意义。
本质上说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
- 静态库:.a[Linux]、.lib[Windows]
- 动态库:.so[Linux]、.dll[Windows]
预先准备制作库的代码,如下:
// 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;
};
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;
}
使用库的代码:
// main.c
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main() {
// 1.打印s及其长度
// 2.创建文件并往文件中写入三次s的信息
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;
}
后续演示库的使用如下:
- 场景1:头文件和库文件安装到系统路径下
gcc main.c -lmystdio
- 场景2:头文件和库文件以及自己的源文件在同一个路径下
gcc main.c -L. -lmystdio
- 场景3:头文件和库文件有自己独立路径
gcc main.c -I头文件路径 -L库文件路径 -lmystdio
其中:
- -L:指定库路径
- -I(大写i):指定头文件搜索路径
- -l(小写L):指定当前链接用此库名
- 库文件名称和引入库的名称:去掉前缀 lib ,去掉后缀 .so ,.a ,如: libc.so -> c
1.2 静态库
- 静态库(.a),程序在编译链接时把库的代码链接到可执行文件中,相当于将静态库拷贝至可执行文件中,则程序运行时将不再需要静态库
- 编译默认使用动态链接库,只有找不到动态库时才会使用同名静态库。当然也可以使用gcc的
-static
强制设置使用静态库
1.2.1 制作
需使用ar
命令,选项:
- rc:表示 replace 和 create
- t:列出静态库中的文件
- v:verbose 列出详细信息
以Makefile形式给出制作方式:
# 生成静态库
libmystdio.a:my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done" # 打印提示信息
# 生成.o文件
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
# 清理
.PHONY:clean
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
# 创建新路径,并将.h和.a 拷贝至新路径
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
1.2.2 使用
场景1:
运行:
错误演示:
场景2:
场景3:
先创建目录:
运行:
以场景3为例,将静态库删除后,再次运行程序:
依旧能运行,这是因为程序在编译链接时,将静态库给全部拷贝进了程序文本中,这样,即使静态库删除,程序照样可以运行
1.3 动态库
- 动态库(.so),程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码
- 一个与动态库链接的可执行文件仅仅包含它用到的函数入⼝地址的一个表,而不是外部函数所在目标文件的整个机器码
- 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
- 动态库可以在多个程序间共享,所以动态链接使得可执行⽂件更小,节省了磁盘空间。操作系统采⽤虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共⽤,节省了内存和磁盘空间
1.3.1 制作
需使用gcc
命令,选项:
- -shared:表示生成动态库
- fPIC:产生位置无关码(了解)
- 库名规则:libxxx.so
以Makefile形式给出制作方式:
# 生成动态库
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared
# 生成.o文件
%.o:%.c
gcc -fPIC -c $<
# 清理
.PHONY:clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
# 创建新路径,并将 .h和.so 拷贝至新路径
.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"
1.3.2 使用
使用方法就和静态库的类似,这里只演示场景3
错误演示:
分析:
- 使用静态库链接之前就说过,其在编译链接时相当于把库给拷贝进了可执行文件中,所以在运行时,不需要指明库的路径,因为其可执行文件内本来就有库
- 而使用动态库链接,其不需要将库的内容拷贝进可执行文件中,是在运行时,与其他程序一起共享动态库的内容,所以即使编译的时候指定了库的路径,但是运行时,编译器仍不知道去哪里找动态库的路径,于是运行时便会报找不到库的错误
解决:
- 拷贝 .so 文件到系统共享库路径下,一般指
/usr/lib
、/usr/local/lib
、/lib64
- 向系统共享库路径下建立同名软链接
- 更改环境变量:
LD_LIBRARY_PATH
- ldconfig方案:配置
/etc/ld.so.conf.d/
,然后使用ldconfig
更新
这里使用环境变量的方法来解决:
2. 生成可执行程序的过程
以C文件为例,采用gcc编译器,格式为:
gcc [选项] 要编译的文件 [选项] [目标文件]
后续均以hello.c文件来说明:
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
2.1 预处理
- 指头文件展开,宏定义替换,条件编译和去注释等
- 对应格式:gcc –E hello.c –o hello.i
生成hello.i文件:
vim hello.i:
可见预处理后的文件会显得很臃肿
2.2 编译(生成汇编)
- 在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的⼯作,在检查无误后,gcc把代码翻译成汇编语言
- 对应格式:
gcc -S hello.i -o hello.s
生成hello.s文件:
vim hello.s:
此时生成的文件还是我们人类能看懂的(汇编语言),这个文件相对于hello.i文件体积小些
2.3 汇编(将汇编代码生成机器可识别代码)
- 汇编阶段是把编译阶段⽣成的“.s”文件转成目标文件
- 对应格式:
gcc –c hello.s –o hello.o
生成hello.o:
vim hello.o:
此时生成的文件只能去由计算机去读了
2.4 链接(生成可执行文件或库文件)
- 这个阶段其实就是今天的主题,在成功编译之后,就进入了链接阶段,此阶段是让前一个阶段生成的目标文件 (.o)去与其内部关联的动静态库作链接
- 对应格式:
gcc hello.o -o hello
生成可执行文件hello:
vim hello :
2.5 规则怪谈
- 预处理、编译、汇编对应的选项是
ESc
,对应生成的文件是iso
,关联想到键盘最上角的Esc键和苹果操作系统ios,再记住一点,S对应s即可,然后把ios中o和s换位置。其实iso是国际标准化组织的简称,单纯记这个组织简称也是可以的 - 即:
①-E 生成.i
②-S 生成.s
③-c 生成.o
3. 目标文件与ELF文件(了解)
3.1 目标文件
- 编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,而在Linux,是通过命令行的形式来完成这一系列操作的,正如下图过程所示:
- 可以看到,在编译之后会生成扩展名为 .o 的文件,它们被称作目标文件。要注意的是如果我们修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF ,是对二进制代码的一种封装,采用
filie hello.o
命令来查看
3.2 ELF文件
3.2.1 常见ELF文件
有常见四种ELF文件:
- 可重定位文件(Relocatable File) :即xxx.o文件。包含适合于与其他目标文件链接来创建可执行文件或者共享⽬标文件的代码和数据
- 可执行⽂件(Executable File) :即可执行程序
- 共享目标文件(Shared Object File) :即xxx.so文件
- 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发
3.2.2 ELF文件组成
由四部分组成:
- ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分
- 程序表头(Program header table) :列举了所有有效的段(segments)和他们的属性
- 节头表(Section header table) :包含对节(sections)的描述
- 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据
最常见的节:
- 代码节(.text):用于保存机器指令,是程序的主要执行部分
- 数据节(.data):保存已初始化的全局变量和局部静态变量
3.3 ELF可执行文件的加载过程
- ⼀个ELF会有多种不同的节(Section),在加载到内存的时候,也会进行节合并,形成段(segment)
- 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等
- 这样,即便是不同的节,在加载到内存中,可能会以段的形式,加载到一起
如下图:
4. 动静态链接
通常使用静态库会采用静态链接方式,使用动态库会采用动态链接方式。
4.1 静态链接
静态链接是指在程序编译链接阶段,将程序所依赖的库(如静态库.a或.lib)或目标文件(.o文件)的代码直接复制到最终的可执行文件中,生成一个完全独立的、自包含的二进制文件。
4.1.1 特点:
-
- 代码被直接嵌入可执行文件
-
- 生成的可执行文件较大
-
- 运行时无需加载外部库
-
- 适用于独立部署
4.1.2 优缺点
优点:
-
- 独立运行:不依赖外部库,适合单文件部署
-
- 性能稍高:无需运行时加载库,启动略快
-
- 版本稳定:不受系统库版本变化影响
缺点:
-
- 可执行文件较大:多个程序无法共享同一份库代码,浪费磁盘和内存
-
- 更新困难:如果库有更新,必须重新编译整个程序
4.1.3 总结
静态链接的核心特点时库代码被直接复制到可执行文件,使得程序运行时不再依赖外部库文件。适用于独立部署、嵌入式开发或避免依赖问题的场景,但会导致可执行文件变大。
4.2 动态链接
动态链接是指在程序运行时(而非编译时)加载所需的共享库(如.so、.dll或.dylib),而不是将库代码直接嵌入可执行文件。
4.2.1 特点
-
- 库代码不嵌入可执行文件
-
- 运行时加载
-
- 共享内存中的库代码
-
- 可独立更新库
4.2.2 优缺点
优点:
-
- 节省磁盘和内存:多个程序共享一份库代码
-
- 便于更新:修复漏洞或升级库时,只需替换动态库文件
-
- 支持插件机制:程序可动态加载模块(如浏览器的插件)
缺点:
-
- 依赖管理复杂:缺少库或版本不兼容会导致程序无法运行
-
- 轻微性能开销:首次加载库时需要解析符号
4.2.3 总结
动态链接的核心是运行时加载共享库,而非编译时嵌入代码。其能够节省资源、便于更新、支持插件。但其依赖管理复杂,需确保库版本兼容。适用于操作系统、大型软件、插件系统等。
4.3 静态链接 VS 动态链接
如果需要完全独立的开发(如嵌入式系统),则静态链接更合适;若追求灵活性和资源共享,动态链接是更好的选择。