文章目录
-
- 库制作与原理(一):静态库与动态库的制作使用
- 一、什么是库
-
- [1.1 库的本质与作用](#1.1 库的本质与作用)
- [1.2 库的分类](#1.2 库的分类)
- 二、准备库代码
- 三、静态库的制作与使用
-
- [3.1 什么是静态库](#3.1 什么是静态库)
- [3.2 静态库的生成](#3.2 静态库的生成)
- [3.3 静态库的使用](#3.3 静态库的使用)
- 四、动态库的制作与使用
-
- [4.1 什么是动态库](#4.1 什么是动态库)
- [4.2 动态库的生成](#4.2 动态库的生成)
- [4.3 动态库的使用](#4.3 动态库的使用)
- 五、动态库运行时查找
-
- [5.1 问题分析](#5.1 问题分析)
- [5.2 解决方案](#5.2 解决方案)
-
- [5.2.1 方案1:拷贝到系统路径](#5.2.1 方案1:拷贝到系统路径)
- [5.2.2 方案2:创建软连接](#5.2.2 方案2:创建软连接)
- [5.2.3 方案3:设置LD_LIBRARY_PATH环境变量](#5.2.3 方案3:设置LD_LIBRARY_PATH环境变量)
- [5.2.4 方案4:配置/etc/ld.so.conf.d/(推荐)](#5.2.4 方案4:配置/etc/ld.so.conf.d/(推荐))
- [5.3 四种方案对比](#5.3 四种方案对比)
- 六、使用第三方库:ncurses示例
-
- [6.1 安装ncurses](#6.1 安装ncurses)
- [6.2 进度条示例程序](#6.2 进度条示例程序)
- 七、总结
库制作与原理(一):静态库与动态库的制作使用
💬 欢迎讨论:这是Linux系统编程系列的新篇章。之前我们深入学习了文件系统、进程管理等核心知识,现在我们将探索另一个重要主题:库。在实际开发中,我们每天都在使用各种库,但你真正理解库的本质吗?静态库和动态库有什么区别?为什么动态库运行时找不到?本篇将从零开始,带你深入理解Linux下库的制作与使用。
👍 点赞、收藏与分享:这篇文章包含了库的完整制作流程、动态库查找问题的4种解决方案,内容实用,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:本系列共3篇,本篇讲解库的基础,后续将深入ELF格式和动态链接原理。
一、什么是库
1.1 库的本质与作用
库是什么?
库是写好的、现有的、成熟的、可以复用的代码。本质上来说,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
为什么需要库?
现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始。比如:
- 输入输出功能(printf、scanf)
- 字符串处理(strlen、strcpy)
- 文件操作(fopen、fread)
- 网络通信(socket相关函数)
如果每个项目都要重新实现这些功能,开发效率将极其低下。库的存在让我们可以站在巨人的肩膀上编程。
1.2 库的分类
Linux下库分为两种:
| 库类型 | Linux扩展名 | Windows扩展名 | 链接方式 |
|---|---|---|---|
| 静态库 | .a | .lib | 编译时链接 |
| 动态库 | .so | .dll | 运行时链接 |
系统中的库文件实例:
bash
# Ubuntu系统
# C语言运行时库
ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 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
# C++标准库
ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l
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
bash
# CentOS系统
# C语言运行时库
ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so
ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 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
二、准备库代码
为了演示库的制作,我们先准备一些示例代码。这里使用之前封装过的文件I/O库代码:
c
// 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);
c
// 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)
{
memcpy(stream->outbuffer+stream->size, ptr, num);
stream->size += num;
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);
}
再准备一个字符串处理函数:
c
// my_string.h
#pragma once
int my_strlen(const char *s);
c
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0') end++;
return end - s;
}
三、静态库的制作与使用
3.1 什么是静态库
静态库(.a)的特点:
静态库是在程序编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
优点:
- 程序独立运行,不依赖外部库文件
- 运行速度快,无需动态加载
缺点:
- 可执行文件体积大
- 库更新后需要重新编译程序
- 多个程序使用同一库会造成内存浪费
3.2 静态库的生成
步骤一:编写Makefile
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/lib
tar -czf stdc.tgz stdc
@echo "output stdc ... done"
关键命令说明:
ar是GNU归档工具,用于创建静态库:
r: replace,替换库中已有的目标文件c: create,创建库文件
步骤二:执行make
bash
make
gcc -c my_stdio.c
compling my_stdio.c to my_stdio.o ... done
gcc -c my_string.c
compling my_string.c to my_string.o ... done
ar -rc libmystdio.a my_stdio.o my_string.o
build my_stdio.o my_string.o to libmystdio.a ... done
步骤三:查看静态库内容
bash
ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
参数说明:
t: 列出静态库中的文件v: verbose,显示详细信息
3.3 静态库的使用
创建测试程序:
c
// 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;
}
三种使用场景:
场景1:头文件和库文件安装到系统路径下
bash
# 安装头文件到系统目录
sudo cp *.h /usr/include/
# 安装库文件到系统目录
sudo cp libmystdio.a /usr/lib/
# 编译时直接指定库名
gcc main.c -lmystdio
场景2:头文件和库文件在当前目录
bash
gcc main.c -L. -lmystdio
参数说明:
-L.: 指定库文件搜索路径为当前目录-lmystdio: 链接libmystdio.a库(去掉前缀lib和后缀.a)
场景3:头文件和库文件在独立路径
bash
gcc main.c -I./stdc/include -L./stdc/lib -lmystdio
参数说明:
-I./stdc/include: 指定头文件搜索路径-L./stdc/lib: 指定库文件搜索路径-lmystdio: 指定要链接的库名
验证静态链接:
bash
./a.out
abcdefg: 7
cat log.txt
abcdefgabcdefgabcdefg
# 删除静态库后程序仍可运行
rm libmystdio.a
./a.out
abcdefg: 7
这证明静态库的代码已经被编译进可执行文件中!
四、动态库的制作与使用
4.1 什么是动态库
动态库(.so)的特点:
动态库是在程序运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
核心机制:
- 可执行文件仅包含函数入口地址的表,不包含函数机器码
- 程序开始运行前,操作系统从磁盘加载动态库到内存
- 这个过程称为动态链接(dynamic linking)
优点:
- 可执行文件体积小,节省磁盘空间
- 多个进程共享一份库代码,节省内存
- 库更新无需重新编译程序
- 支持代码的模块化和插件化
缺点:
- 运行时需要库文件存在
- 加载时间略长
- 版本管理较复杂
4.2 动态库的生成
步骤一:编写Makefile
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: 产生位置无关码(Position Independent Code)-shared: 表示生成共享库格式
什么是位置无关码(PIC)?
位置无关码是指代码可以被加载到内存的任意地址运行,不需要修改代码本身。这是通过相对地址和全局偏移表(GOT)实现的。
步骤二:执行make
bash
make
gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared
步骤三:查看动态库依赖
bash
ldd libmystdio.so
linux-vdso.so.1 => (0x00007fffacbbf000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
可以看到我们的动态库依赖了系统C库。
4.3 动态库的使用
编译链接方式与静态库相同:
bash
# 场景1:系统路径
gcc main.c -lmystdio
# 场景2:当前目录
gcc main.c -L. -lmystdio
# 场景3:独立路径
gcc main.c -I./stdc/include -L./stdc/lib -lmystdio
但运行时会遇到问题:
bash
./a.out
./a.out: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory
查看可执行文件依赖:
bash
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
libmystdio.so => not found 说明系统找不到我们的动态库!
五、动态库运行时查找
5.1 问题分析
为什么编译能通过,运行却失败?
- 编译时:
-L选项告诉编译器在哪里找库文件进行链接 - 运行时:系统需要在标准库路径中查找动态库
系统标准库路径:
/lib/lib64/usr/lib/usr/local/lib/etc/ld.so.conf配置的路径
我们的库不在这些路径中,所以运行失败!
5.2 解决方案
5.2.1 方案1:拷贝到系统路径
bash
sudo cp libmystdio.so /usr/lib/
./a.out
abcdefg: 7
优点 :永久生效,所有用户可用
缺点:需要root权限,污染系统环境
5.2.2 方案2:创建软连接
bash
sudo ln -s /home/user/mylib/libmystdio.so /usr/lib/libmystdio.so
./a.out
abcdefg: 7
优点 :不需要拷贝文件,节省空间
缺点:需要root权限,删除原文件会导致链接失效
5.2.3 方案3:设置LD_LIBRARY_PATH环境变量
bash
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./a.out
abcdefg: 7
# 查看动态库是否被找到
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => ./libmystdio.so (0x00007fa2aef30000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aeb00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
优点 :不需要root权限,灵活方便
缺点:仅对当前终端会话有效,关闭终端失效
永久生效方法:
bash
# 编辑用户配置文件
vim ~/.bashrc
# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/mylib
# 使配置生效
source ~/.bashrc
5.2.4 方案4:配置/etc/ld.so.conf.d/(推荐)
这是最推荐的专业做法!
步骤:
bash
# 1. 创建配置文件
sudo vim /etc/ld.so.conf.d/mylib.conf
# 2. 写入库路径
/home/user/mylib
# 3. 更新动态链接器缓存
sudo ldconfig
# 4. 验证
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => /home/user/mylib/libmystdio.so (0x00007fa2aef30000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aeb00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
# 5. 运行
./a.out
abcdefg: 7
原理:
ldconfig命令会:
- 读取
/etc/ld.so.conf及其包含的/etc/ld.so.conf.d/下所有配置 - 扫描配置中的路径,查找所有动态库
- 将结果缓存到
/etc/ld.so.cache文件 - 程序运行时,动态链接器优先从缓存中查找库
优点:
- 系统级配置,永久生效
- 性能最优(使用缓存)
- 所有用户共享
- 符合Linux标准做法
缺点:
- 需要root权限
5.3 四种方案对比
| 方案 | 权限要求 | 生效范围 | 是否永久 | 推荐度 |
|---|---|---|---|---|
| 拷贝到系统路径 | root | 系统全局 | ✅ | ⭐⭐ |
| 创建软连接 | root | 系统全局 | ✅ | ⭐⭐⭐ |
| LD_LIBRARY_PATH | 普通用户 | 当前用户 | ❌ | ⭐⭐⭐ |
| ldconfig配置 | root | 系统全局 | ✅ | ⭐⭐⭐⭐⭐ |
六、使用第三方库:ncurses示例
学会了自己制作库,我们也可以使用其他人提供的库。这里以ncurses图形库为例。
6.1 安装ncurses
bash
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev
6.2 进度条示例程序
c
// progress.c
#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
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;
int bar_y = 1;
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);
}
delwin(win);
endwin();
return 0;
}
编译运行:
bash
gcc progress.c -lncurses -o progress
./progress
你会看到一个漂亮的终端进度条!
这个示例展示了:
- 如何使用第三方库(
-lncurses) - ncurses库提供了丰富的终端UI功能
- 库让我们可以快速实现复杂功能
七、总结
本文深入讲解了Linux下库的制作与使用:
核心知识点:
-
库的分类
- 静态库(.a):编译时链接,独立运行
- 动态库(.so):运行时链接,共享使用
-
静态库制作
- 使用
ar -rc打包.o文件 - 通过
-L和-l选项链接 - 代码被编译进可执行文件
- 使用
-
动态库制作
- 使用
-fPIC生成位置无关码 - 使用
-shared生成共享库 - 支持多进程共享
- 使用
-
动态库查找
- 四种解决方案
- 推荐使用ldconfig配置
- 理解LD_LIBRARY_PATH环境变量
-
库命名规则
- 库文件名:
libxxx.a或libxxx.so - 链接时:
-lxxx(去掉前缀和后缀)
- 库文件名:
库文件处理流程图:
bash
源代码(.c)
↓ gcc -c
目标文件(.o)
↓
├─→ ar -rc → 静态库(.a) → gcc -L -l → 可执行文件
│ (库代码已包含)
│
└─→ gcc -fPIC -shared → 动态库(.so) → gcc -L -l → 可执行文件
(需要.so文件)
💡 思考题
- 为什么动态库需要
-fPIC编译选项?- 如果一个程序既有静态库又有动态库,优先链接哪个?
- 如何查看一个可执行文件依赖了哪些动态库?
- 多个进程使用同一个动态库,内存中有几份代码?
下一篇我们将深入ELF文件格式,揭示静态链接的底层原理!