
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(二十九) ------ 动静态库制作
文章目录
- [Linux 系统篇(二十九) ------ 动静态库制作](#Linux 系统篇(二十九) —— 动静态库制作)
-
- 写在前面:为什么我们需要库?
- 一、准备工作:自己动手写一个"迷你标准库"
-
- [1.1 迷你文件 IO 库:`my_stdio`](#1.1 迷你文件 IO 库:
my_stdio) - [1.2 迷你字符串库:`my_string`](#1.2 迷你字符串库:
my_string)
- [1.1 迷你文件 IO 库:`my_stdio`](#1.1 迷你文件 IO 库:
- 二、静态库
-
- [2.1 什么是静态库?](#2.1 什么是静态库?)
- [2.2 静态库的生成](#2.2 静态库的生成)
- [2.3 静态库的使用](#2.3 静态库的使用)
- [2.4 关于 `-static` 选项](#2.4 关于
-static选项)
- 三、动态库
-
- [3.1 什么是动态库?](#3.1 什么是动态库?)
- [3.2 动态库的生成](#3.2 动态库的生成)
- [3.3 动态库的使用](#3.3 动态库的使用)
- [3.4 问题来了:运行时找不到库!](#3.4 问题来了:运行时找不到库!)
- [3.5 解决方案(四种)](#3.5 解决方案(四种))
- 四、动静态链接核心结论
- [五、进阶:使用外部库------以 ncurses 为例](#五、进阶:使用外部库——以 ncurses 为例)
-
- [5.1 安装](#5.1 安装)
- [5.2 一个进度条示例](#5.2 一个进度条示例)
- 七、面试问题速查
- 结语:
写在前面:为什么我们需要库?
如果你每次写 C 语言程序,都要自己实现一遍 printf、malloc、strlen......那该多痛苦?现实中,每个程序都要依赖大量底层的、成熟的、可复用的代码。如果每个人都从零开始写,那软件开发根本不可能有今天的规模。
库(Library),就是这些"写好的、现成的、可以复用的代码"的集合。
它的本质其实很朴素:库就是一堆 .o 目标文件打包在一起形成的二进制文件 。它不需要包含 main 函数(因为它是给别人用的,不是自己跑的),可以被操作系统加载到内存中执行。
在 Linux 下,库分为两种:
| 类型 | Linux 后缀 | Windows 后缀 | 链接时机 |
|---|---|---|---|
| 静态库 | .a |
.lib |
编译链接时 |
| 动态库 | .so |
.dll |
程序运行时 |
你可以在 Ubuntu 下看到系统自带的 C 标准库:
bash
# 动态库
$ 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++ 也类似:
bash
$ 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
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
/usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
关键认知 :库与应用程序的比例是 1 : n。一个库可以被无数个程序共同使用。
一、准备工作:自己动手写一个"迷你标准库"
为了把动静态库的制作过程讲清楚,我们不能光看系统库,得自己造一个。下面我们来封装一套"历史遗留"的迷你 libc,包含两个模块:
1.1 迷你文件 IO 库:my_stdio
头文件 my_stdio.h:
c
#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:
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);
}
1.2 迷你字符串库:my_string
头文件 my_string.h:
c
#pragma once
int my_strlen(const char *s);
源文件 my_string.c:
c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while (*end != '\0') end++;
return end - s;
}
现在,我们手里有了 my_stdio.c 和 my_string.c,接下来就要把它们变成库。
二、静态库
2.1 什么是静态库?
静态库(.a)的核心特点是:程序在编译链接的时候,把库的代码直接链接到可执行文件中 。程序运行时,不再需要静态库。
2.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"
这里的关键命令是:
bash
ar -rc libmystdio.a my_stdio.o my_string.o
ar是 GNU 归档工具(archive)。-r表示 replace,替换已存在的文件。-c表示 create,创建归档文件。
问题 :
ar和tar有什么区别?tar是用来打包文件的,保留文件属性,常用于备份;ar是用来打包目标文件形成静态库的,它不压缩,只是简单归档。
生成之后,可以用 ar -tv 查看静态库内容:

t:列出静态库中的文件。v:verbose,显示详细信息。
2.3 静态库的使用
写一个测试程序 main.c:
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
$ gcc main.c -lmystdio
此时编译器会在系统默认路径(如 /usr/lib、/usr/local/lib)下查找 libmystdio.a。
场景 2:头文件和库文件和源文件在同一路径下
bash
$ gcc main.c -L. -lmystdio
-L.:指定库搜索路径为当前目录。-lmystdio:指定库名。
场景 3:头文件和库文件有独立路径
bash
$ gcc main.c -I头文件路径 -L库文件路径 -lmystdio
-I:指定头文件搜索路径。-L:指定库文件搜索路径。-l:指定库名。
库名规则 :
libmystdio.a去掉前缀lib,去掉后缀.a,剩下的mystdio就是库名。所以链接时写-lmystdio。
关键验证:静态库生成目标文件后,把静态库删掉,程序照样可以运行!因为代码已经被"焊死"在可执行文件里了。
bash
$ rm libmystdio.a
$ ./a.out
abcdefg: 7
程序依然跑得好好的。
2.4 关于 -static 选项
默认情况下,gcc 优先使用动态库 。只有在找不到动态 .so 时,才会采用同名的静态库。如果你想强制静态链接,可以加 -static:
bash
$ gcc main.c -L. -lmystdio -static
但注意:一旦使用 -static,就必须存在对应的静态库。如果系统只有动态库,没有静态库,那静态链接就会失败。
三、动态库
3.1 什么是动态库?
动态库(.so)的核心特点是:程序在运行时才去链接动态库的代码,多个程序共享使用库的代码。
与动态库链接的可执行文件,仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的动态库中复制到内存中,这个过程称为 动态链接(dynamic linking)。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制,允许物理内存中的一份动态库被所有用到该库的进程共用,节省了内存和磁盘空间。
3.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"
两个关键选项:
-shared:表示生成共享库格式。-fPIC:产生位置无关码(position independent code)。
为什么要
-fPIC?动态库在运行时被加载到内存的哪个位置是不确定的。如果代码中使用了绝对地址,那么加载到不同位置就会出错。
-fPIC让代码使用相对地址,这样无论加载到哪里都能正确执行。
3.3 动态库的使用
使用方式与静态库类似:
bash
# 场景 1:安装到系统路径
$ gcc main.c -lmystdio
# 场景 2:同目录
$ gcc main.c -L. -lmystdio
# 场景 3:独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -lmystdio
可以用 ldd 查看库或可执行程序的依赖:
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)
3.4 问题来了:运行时找不到库!
假设我们在场·景 2 下编译:
bash
$ gcc main.c -L. -lmystdio
$ ./a.out
结果报错:
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)
为什么编译能过,运行却找不到?
根本原因在于:
- 编译时 :我们告诉的是 gcc 去哪里找库(
-L和-l),这仅限于链接阶段。- 运行时 :系统(动态链接器) 不知道去哪里找这个动态库。系统 ≠ gcc。系统有自己的查找路径规则。
3.5 解决方案(四种)
方案 1:拷贝 .so 文件到系统共享库路径下
bash
$ sudo cp libmystdio.so /usr/lib/
一般指 /usr/lib、/usr/local/lib、/lib64 等路径。
方案 2:向系统共享库路径下建立同名软连接
bash
$ sudo ln -s /path/to/libmystdio.so /usr/lib/libmystdio.so
方案 3:更改环境变量 LD_LIBRARY_PATH
bash
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/your/lib
注意:
LD_LIBRARY_PATH默认通常为空。这种方式只在当前终端会话有效,关闭终端就失效了。
方案 4:配置文件 + ldconfig
在 /etc/ld.so.conf.d/ 目录下新建一个 .conf 文件,写入库所在路径:
bash
$ sudo vim /etc/ld.so.conf.d/bit.conf
#将动态库所在的绝对路径直接粘贴进文件中并保存
/root/tools/linux
然后执行:
bash
$ sudo ldconfig
ldconfig 会重新加载库搜索路径,使配置生效。
四、动静态链接核心结论
经过上面的实践,我们可以总结出几条重要的结论:
结论 1 :gcc/g++ 默认使用动态库!
结论 2 :在 Linux 系统下,默认情况安装的大部分库,优先安装的是动态库!
结论 3 :库与应用程序的比例 = 1 : n。
结论 4:VS(Visual Studio)不仅形成可执行程序,也能形成动静态库。
结论 5 :非静态链接(默认)只能通过加 -static 选项强制静态链接。
- 一旦使用
-static,就必须存在对应的静态库。 - 如果系统中只存在静态库,没有动态库,可执行程序对于该库只能进行静态链接。
结论 6:静态库在链接时被"焊死"进可执行文件,运行时不需要库文件;动态库在运行时才被加载,可执行文件只保留函数入口地址表。
结论 7 :编译时 -L 和 -l 只告诉 gcc 去哪里找库;运行时系统有自己的查找路径规则,需要单独配置。
五、进阶:使用外部库------以 ncurses 为例
我们现在接触过的库主要是 C/C++ 标准库。为了让大家感受一下"外部库"的使用,这里推荐一个好玩的图形库:ncurses。
5.1 安装
bash
# CentOS
$ sudo yum install -y ncurses-devel
# Ubuntu
$ sudo apt install -y libncurses-dev
系统中其实有很多库,它们通常由一组互相关联的、用来完成某项常见工作的函数构成。比如用来处理屏幕显示情况的函数(ncurses 库)。
5.2 一个进度条示例
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 // 微秒(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;
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;
}
编译时需要链接 ncurses 库:
bash
$ gcc progress.c -lncurses -o progress
$ ./progress

这个例子展示了外部库的典型使用流程:安装库 → 包含头文件 → 链接库 → 运行程序。
七、面试问题速查
Q1:静态库和动态库的区别?
| 维度 | 静态库 | 动态库 |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 可执行文件大小 | 大 | 小 |
| 运行时依赖 | 不需要 | 需要 |
| 内存共享 | 不共享 | 共享 |
| 更新库 | 需重新编译 | 只需替换库文件 |
| 加载速度 | 快 | 稍慢 |
Q2:为什么动态库需要 -fPIC?
因为动态库加载到内存的位置不确定,-fPIC 生成位置无关码,使用相对地址,保证加载到任何位置都能正确执行。
Q3:-L 和 -l 的区别?
-L:指定库的搜索路径(去哪里找)。-l:指定库名(找什么库)。
Q4:运行时找不到动态库怎么办?
四种方案:拷贝到系统路径、建立软连接、配置 LD_LIBRARY_PATH、配置 /etc/ld.so.conf.d/ + ldconfig。
Q5:ar 命令的 -r 和 -c 是什么意思?
-r:replace,替换已存在的文件。-c:create,创建归档文件。
Q6:ldd 命令的作用?
查看可执行文件或动态库的依赖关系,常用于排查"运行时找不到库"的问题。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _