【Linux系统编程】(二十七)手撕动静态库原理与实战:从底层逻辑到代码落地


目录

​编辑

前言

[一、什么是库?------ 二进制的 "代码积木"](#一、什么是库?—— 二进制的 “代码积木”)

[二、静态库 ------"打包带走" 的代码](#二、静态库 ——“打包带走” 的代码)

[2.1 准备源码 ------ 造一个 "迷你标准库"](#2.1 准备源码 —— 造一个 “迷你标准库”)

[2.1.1 头文件:my_stdio.h](#2.1.1 头文件:my_stdio.h)

[2.1.2 源文件:my_stdio.c](#2.1.2 源文件:my_stdio.c)

[2.1.3 字符串处理:my_string.c](#2.1.3 字符串处理:my_string.c)

[2.2 制作静态库 ------ 把代码 "打包" 成.a 文件](#2.2 制作静态库 —— 把代码 “打包” 成.a 文件)

[2.2.1 编写 Makefile](#2.2.1 编写 Makefile)

[2.2.2 关键命令解释](#2.2.2 关键命令解释)

[2.3 使用静态库 ------ 链接成可执行程序](#2.3 使用静态库 —— 链接成可执行程序)

[2.3.1 测试程序:main.c](#2.3.1 测试程序:main.c)

[2.3.2 编译链接 ------ 三种场景](#2.3.2 编译链接 —— 三种场景)

[场景 1:头文件和库文件在系统路径](#场景 1:头文件和库文件在系统路径)

[场景 2:头文件和库文件在当前目录](#场景 2:头文件和库文件在当前目录)

[场景 3:头文件和库文件在自定义路径](#场景 3:头文件和库文件在自定义路径)

[2.3.3 验证静态库的独立性](#2.3.3 验证静态库的独立性)

[2.4 静态库的优缺点 ------ 优点缺点同样明显](#2.4 静态库的优缺点 —— 优点缺点同样明显)

[三、动态库 ------"共享使用" 的代码](#三、动态库 ——“共享使用” 的代码)

[3.1 制作动态库 ------ 生成.so 文件](#3.1 制作动态库 —— 生成.so 文件)

[3.1.1 编写 Makefile](#3.1.1 编写 Makefile)

[3.2 使用动态库 ------ 编译链接 + 运行时加载](#3.2 使用动态库 —— 编译链接 + 运行时加载)

[3.2.1 编译测试程序](#3.2.1 编译测试程序)

[3.2.2 解决动态库找不到的问题 ------ 四种方案](#3.2.2 解决动态库找不到的问题 —— 四种方案)

[方案 1:拷贝动态库到系统路径](#方案 1:拷贝动态库到系统路径)

[方案 2:设置环境变量 LD_LIBRARY_PATH](#方案 2:设置环境变量 LD_LIBRARY_PATH)

[方案 3:添加软链接到系统路径](#方案 3:添加软链接到系统路径)

[方案 4:配置 /etc/ld.so.conf.d](#方案 4:配置 /etc/ld.so.conf.d)

[3.2.3 查看程序依赖的动态库](#3.2.3 查看程序依赖的动态库)

[3.3 动态库的优缺点 ------ 空间换时间的典范](#3.3 动态库的优缺点 —— 空间换时间的典范)

[四、动静态库的核心区别 ------ 底层逻辑大揭秘](#四、动静态库的核心区别 —— 底层逻辑大揭秘)

[4.1 链接过程的区别 ------ 静态链接是 "复制",动态链接是 "记录"](#4.1 链接过程的区别 —— 静态链接是 “复制”,动态链接是 “记录”)

[4.2 内存加载的区别 ------ 静态库是 "私有",动态库是 "共享"](#4.2 内存加载的区别 —— 静态库是 “私有”,动态库是 “共享”)

[4.3 直观对比 ------ 一张表看懂区别](#4.3 直观对比 —— 一张表看懂区别)

[五、拓展:试试 ncurses 图形库](#五、拓展:试试 ncurses 图形库)

总结


前言

在 C/C++ 开发的世界里,库就像是程序员的 "神兵利器"。它把成熟、可复用的代码打包成二进制文件,让我们不用重复造轮子,直接站在巨人的肩膀上开发。你写的每一个printf、每一次字符串操作,其实都在和库打交道。今天我们就彻底撕开动静态库的神秘面纱,从原理剖析实战操作,手把手带你搞懂它们的区别、制作方法和底层逻辑。下面就让我们正式开始吧!


一、什么是库?------ 二进制的 "代码积木"

首先我们要明确:库是可复用代码的二进制形式,能被操作系统载入内存执行。它就像乐高积木,不同的积木块(库函数)可以组合出各种复杂的程序(模型)。

从类型上看,库主要分为两大类:

  • 静态库 :Linux 下后缀是.a,Windows 下是.lib。程序编译链接时,会把库的代码直接复制到可执行文件中。程序运行时,完全不依赖静态库文件。
  • 动态库 :Linux 下后缀是.so,Windows 下是.dll。程序编译时只记录函数入口地址,运行时才去加载库文件并调用函数。多个程序可以共享同一个动态库,实现 "一份代码,多处使用"。

我们可以用ls命令直观查看系统中的动静态库。比如 Linux 系统下的 C 标准库:

bash 复制代码
# 查看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

# 查看C标准静态库
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 -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

接下来,我们用一个自定义的mystdio库(模拟标准 IO 功能)作为案例,一步步演示动静态库的制作和使用。

二、静态库 ------"打包带走" 的代码

静态库的核心特点是编译时嵌入,运行时独立。就像你点外卖时把餐具打包带走,吃饭的时候不需要再回餐馆拿 ------ 程序一旦编译完成,静态库就可以删除,完全不影响运行。

2.1 准备源码 ------ 造一个 "迷你标准库"

我们先写两个基础文件:my_stdio.h(头文件,声明函数)和my_stdio.c(源文件,实现函数),再加上一个字符串处理文件my_string.c

2.1.1 头文件:my_stdio.h

这个头文件定义了模拟的文件结构体和 IO 函数,类似标准库的FILEfopenfwrite等函数。

cpp 复制代码
#pragma once

#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2

// 模拟标准库的FILE结构体
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);

2.1.2 源文件:my_stdio.c

实现文件的打开、写入、刷新和关闭功能,底层调用 Linux 系统调用openwrite等。

cpp 复制代码
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

// 打开文件,模拟fopen
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;
}

// 刷新缓冲区,模拟fflush
void mfflush(mFILE *stream)
{
    if (stream->size > 0)
    {
        write(stream->fileno, stream->outbuffer, stream->size);
        fsync(stream->fileno); // 强制写入磁盘
        stream->size = 0;
    }
}

// 写入数据,模拟fwrite
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;
}

// 关闭文件,模拟fclose
void mfclose(mFILE *stream)
{
    if (stream->size > 0)
    {
        mfflush(stream);
    }
    close(stream->fileno);
    free(stream);
}

2.1.3 字符串处理:my_string.c

实现一个简单的my_strlen函数,模拟标准库的strlen

cpp 复制代码
#include "my_string.h"

// 计算字符串长度
int my_strlen(const char *s)
{
    const char *end = s;
    while (*end != '\0') end++;
    return end - s;
}

对应的头文件my_string.h

cpp 复制代码
#pragma once

int my_strlen(const char *s);

2.2 制作静态库 ------ 把代码 "打包" 成.a 文件

制作静态库的步骤很简单:编译生成.o 目标文件 → 用 ar 工具打包成.a 文件

我们可以写一个**Makefile**来自动化这个过程,避免手动敲命令。

2.2.1 编写 Makefile

bash 复制代码
# 目标:静态库libmystdio.a
libmystdio.a: my_stdio.o my_string.o
	@ar -rc $@ $^
	@echo "build $^ to $@ ... done"

# 编译生成.o文件(-c表示只编译不链接)
%.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"

2.2.2 关键命令解释

  • *gcc -c .c:把.c文件编译成.o目标文件,不进行链接
  • ar -rc libmystdio.a my_stdio.o my_string.oar是 GNU 归档工具,rc表示 "创建并替换"。libmystdio.a是静态库名,必须以lib开头,后缀是.a
  • ar -tv libmystdio.a:查看静态库中的文件列表,t是列出内容,v是显示详细信息。

执行**make**命令,就能生成libmystdio.a静态库:

cpp 复制代码
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

2.3 使用静态库 ------ 链接成可执行程序

静态库制作完成后,我们写一个main.c来测试它的功能。

2.3.1 测试程序:main.c

cpp 复制代码
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>

int main()
{
    const char *s = "hello static library!\n";
    // 测试my_strlen
    printf("%s: length = %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;
}

2.3.2 编译链接 ------ 三种场景

链接静态库的核心是告诉编译器头文件在哪库文件在哪库名是什么。对应的 gcc 参数是:

  • -I:指定头文件搜索路径
  • -L:指定库文件搜索路径
  • -l:指定库名(去掉lib前缀和.a后缀)
场景 1:头文件和库文件在系统路径

如果把my_stdio.hmy_string.h拷贝到/usr/include,把libmystdio.a拷贝到/usr/lib,可以直接编译:

bash 复制代码
gcc main.c -lmystdio -o main

-lmystdio就是链接libmystdio.a,编译器会自动去系统路径找。

场景 2:头文件和库文件在当前目录

头文件、库文件和main.c在同一文件夹下:

bash 复制代码
gcc main.c -L. -lmystdio -o main

**-L.**表示库文件在当前目录(.代表当前路径)。

场景 3:头文件和库文件在自定义路径

如果头文件在./include,库文件在./lib

bash 复制代码
gcc main.c -I./include -L./lib -lmystdio -o main

2.3.3 验证静态库的独立性

编译生成可执行文件main后,我们删除静态库,再运行程序:

bash 复制代码
rm -f libmystdio.a
./main
# 输出:
# hello static library!
# : length = 22
cat log.txt
# 输出3行hello static library!

程序正常运行!这就是静态库的优势 ------编译后脱离库文件,可独立运行

2.4 静态库的优缺点 ------ 优点缺点同样明显

优点

  1. 运行独立:可执行文件包含了库的代码,不需要依赖外部文件。
  2. 运行速度快:没有运行时的动态链接开销,执行效率高。
  3. 部署方便:发给别人时只需要一个可执行文件,不需要附带库。

缺点

  1. 可执行文件体积大 :每个程序都会复制一份库代码,多个程序会造成冗余。比如 10 个程序都用libmystdio.a,就会有 10 份相同的代码。
  2. 更新麻烦:如果库有 bug 需要修复,所有使用该库的程序都要重新编译链接。

三、动态库 ------"共享使用" 的代码

动态库的核心特点是编译时记录地址,运行时加载共享。就像你去餐馆吃饭,餐具是餐馆提供的,多个顾客共享同一套餐具 ------ 多个程序可以共享同一个动态库,大大节省内存和磁盘空间。

3.1 制作动态库 ------ 生成.so 文件

动态库的制作和静态库类似,但需要两个关键参数:-fPIC-shared

  • -fPIC:生成位置无关代码(Position Independent Code)。动态库加载到内存的地址是不固定的,PIC 保证代码在任意地址都能正常运行。
  • -shared:生成共享库(动态库)。

3.1.1 编写 Makefile

bash 复制代码
# 目标:动态库libmystdio.so
libmystdio.so: my_stdio.o my_string.o
	gcc -o $@ $^ -shared
	@echo "build $^ to $@ ... done"

# 编译生成位置无关的.o文件
%.o: %.c
	gcc -fPIC -c $<
	@echo "compling $< 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 -f *.h stdc/include
	@cp -f *.so stdc/lib
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"

执行**make**命令,生成libmystdio.so动态库:

bash 复制代码
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.so ... done

3.2 使用动态库 ------ 编译链接 + 运行时加载

动态库的编译链接命令和静态库完全一样,但运行时需要让系统找到动态库文件。

3.2.1 编译测试程序

还是用之前的main.c,编译命令:

bash 复制代码
gcc main.c -I. -L. -lmystdio -o main

编译成功后,直接运行./main会报错:

bash 复制代码
./main
# 输出:
# ./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory

原因是系统找不到动态库文件 。Linux 下动态库的搜索路径是有优先级的,我们需要把libmystdio.so加入搜索路径。

3.2.2 解决动态库找不到的问题 ------ 四种方案

方案 1:拷贝动态库到系统路径

libmystdio.so拷贝到/usr/lib/lib64(系统默认搜索路径):

bash 复制代码
sudo cp libmystdio.so /usr/lib
./main
# 正常运行
方案 2:设置环境变量 LD_LIBRARY_PATH

临时添加库路径,关闭终端后失效:

bash 复制代码
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
./main
# 正常运行

如果想永久生效,可以把这行命令写入~/.bashrc~/.zshrc,然后source ~/.bashrc

方案 3:添加软链接到系统路径

和方案 1 类似,但用软链接,方便更新库:

bash 复制代码
sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so
方案 4:配置 /etc/ld.so.conf.d

创建自定义配置文件,添加库路径:

bash 复制代码
sudo echo $(pwd) > /etc/ld.so.conf.d/mystdio.conf
sudo ldconfig # 更新缓存

这种方式适合长期使用的自定义库。

3.2.3 查看程序依赖的动态库

用**ldd**命令可以查看可执行程序依赖的所有动态库:

bash 复制代码
ldd main
# 输出:
# linux-vdso.so.1 =>  (0x00007fffacbbf000)
# libmystdio.so => ./libmystdio.so (0x00007f8917335000)
# libc.so.6 => /lib64/libc.so.6 (0x00007f8916f67000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)

可以看到main依赖libmystdio.so和系统的libc.so.6

3.3 动态库的优缺点 ------ 空间换时间的典范

优点

  1. 节省空间:多个程序共享一个动态库,磁盘和内存占用大大减少。比如 10 个程序用同一个动态库,内存中只需要一份库代码。
  2. 更新方便:修复库的 bug 后,不需要重新编译程序,直接替换动态库文件即可。
  3. 版本灵活:可以同时存在多个版本的动态库,满足不同程序的需求。

缺点

  1. 运行依赖库:程序运行时必须找到对应的动态库,否则无法启动。
  2. 运行时有开销:动态链接需要在运行时解析函数地址,比静态库多了一点性能损耗(现代系统中这个损耗可以忽略不计)。
  3. 部署麻烦:发给别人时需要附带动态库文件,或者配置库路径。

四、动静态库的核心区别 ------ 底层逻辑大揭秘

看到这里,你可能已经会用动静态库了,但还想知道为什么静态库编译后不依赖,动态库运行时才加载 ?这就需要从链接过程内存加载两个层面来理解。

4.1 链接过程的区别 ------ 静态链接是 "复制",动态链接是 "记录"

  • 静态链接 :编译器在链接阶段,会把静态库中用到的函数代码直接复制到可执行文件中。链接完成后,可执行文件包含了所有需要的代码,和静态库彻底没关系。
  • 动态链接 :编译器在链接阶段,不复制代码,只在可执行文件中记录函数的 "入口地址" 和 "依赖的动态库名"。程序运行时,操作系统的动态链接器会根据这些信息,找到并加载动态库,然后把函数地址替换成实际的内存地址。

4.2 内存加载的区别 ------ 静态库是 "私有",动态库是 "共享"

  • 静态库:每个程序加载时,都会把自己的可执行文件(包含静态库代码)加载到内存,不同程序的静态库代码是独立的,互不干扰。
  • 动态库 :动态库被加载到内存后,会被所有依赖它的程序共享。操作系统用虚拟内存机制,让多个进程映射到同一份物理内存的动态库代码,大大节省内存。

4.3 直观对比 ------ 一张表看懂区别

特性 静态库(.a) 动态库(.so)
链接方式 编译时复制代码到可执行文件 编译时记录地址,运行时解析
运行依赖 不依赖库文件,可独立运行 必须依赖库文件,否则无法启动
可执行文件体积 大(包含库代码) 小(不包含库代码)
内存占用 高(每个程序一份代码) 低(多个程序共享一份代码)
更新维护 需重新编译程序 直接替换库文件即可
性能 略高(无运行时链接开销) 略低(有运行时链接开销)

五、拓展:试试 ncurses 图形库

最后给大家推荐一个好玩的图形库 ------ncurses,它可以用来制作终端界面的图形程序,比如进度条、菜单、游戏等。

安装 ncurses 库:

bash 复制代码
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev

这里推荐一篇不错的使用指南:https://blog.csdn.net/bdn_nbd/article/details/134019142

感兴趣的同学可以自己试试写一个简单的进度条程序来体验一下。


总结

库是代码复用的核心,也是大型项目模块化开发的基础。掌握动静态库的使用,是 C/C++ 程序员从 "入门" 到 "进阶" 的必经之路。希望你能把今天学到的知识用到实际开发中,写出更优雅的代码~

到这里,动静态库的原理和使用就讲完了。从源码到制作,从使用到底层逻辑,希望这篇文章能帮你彻底搞懂动静态库。如果觉得有用,欢迎点赞、收藏、转发!有问题欢迎在评论区交流~


相关推荐
南烟斋..2 小时前
Linux设备驱动开发完全指南:从启动流程到Platform驱动模型
linux·驱动开发·uboot
天才奇男子10 小时前
HAProxy高级功能全解析
linux·运维·服务器·微服务·云原生
学嵌入式的小杨同学10 小时前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
酥暮沐11 小时前
iscsi部署网络存储
linux·网络·存储·iscsi
❀͜͡傀儡师11 小时前
centos 7部署dns服务器
linux·服务器·centos·dns
Dying.Light11 小时前
Linux部署问题
linux·运维·服务器
S190111 小时前
Linux的常用指令
linux·运维·服务器
小义_12 小时前
【RH134知识点问答题】第7章 管理基本存储
linux·运维·服务器
梁洪飞12 小时前
内核的schedule和SMP多核处理器启动协议
linux·arm开发·嵌入式硬件·arm