欢迎光临
我们一直在努力

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


目录

​编辑

前言

一、什么是库?—— 二进制的 “代码积木”

二、静态库 ——“打包带走” 的代码

2.1 准备源码 —— 造一个 “迷你标准库”

2.1.1 头文件:my_stdio.h

2.1.2 源文件:my_stdio.c

2.1.3 字符串处理:my_string.c

2.2 制作静态库 —— 把代码 “打包” 成.a 文件

2.2.1 编写 Makefile

2.2.2 关键命令解释

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

2.3.1 测试程序:main.c

2.3.2 编译链接 —— 三种场景

场景 1:头文件和库文件在系统路径

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

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

2.3.3 验证静态库的独立性

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

三、动态库 ——“共享使用” 的代码

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

3.1.1 编写 Makefile

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

3.2.1 编译测试程序

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

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

方案 2:设置环境变量 LD_LIBRARY_PATH

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

方案 4:配置 /etc/ld.so.conf.d

3.2.3 查看程序依赖的动态库

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

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

4.1 链接过程的区别 —— 静态链接是 “复制”,动态链接是 “记录”

4.2 内存加载的区别 —— 静态库是 “私有”,动态库是 “共享”

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

五、拓展:试试 ncurses 图形库

总结


前言

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


一、什么是库?—— 二进制的 “代码积木”

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

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

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

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

# 查看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++ 标准库同理,动态库通常是软链接形式,指向具体版本:

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 函数,类似标准库的FILE和fopen、fwrite等函数。

#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 系统调用open、write等。

#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。

#include "my_string.h"

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

        对应的头文件my_string.h:

#pragma once

int my_strlen(const char *s);

2.2 制作静态库 —— 把代码 “打包” 成.a 文件

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

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

2.2.1 编写 Makefile

# 目标:静态库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.o:ar是 GNU 归档工具,rc表示 “创建并替换”。libmystdio.a是静态库名,必须以lib开头,后缀是.a。
  • ar -tv libmystdio.a:查看静态库中的文件列表,t是列出内容,v是显示详细信息。

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

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

#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.h、my_string.h拷贝到/usr/include,把libmystdio.a拷贝到/usr/lib,可以直接编译:

gcc main.c -lmystdio -o main

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

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

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

gcc main.c -L. -lmystdio -o main

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

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

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

gcc main.c -I./include -L./lib -lmystdio -o main

2.3.3 验证静态库的独立性

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

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

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

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

优点:

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

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

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

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

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

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

    3.1.1 编写 Makefile

    # 目标:动态库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动态库:

    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,编译命令:

    gcc main.c -I. -L. -lmystdio -o main

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

    ./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(系统默认搜索路径):

    sudo cp libmystdio.so /usr/lib
    ./main
    # 正常运行

    方案 2:设置环境变量 LD_LIBRARY_PATH

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

    export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
    ./main
    # 正常运行

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

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

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

    sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so

    方案 4:配置 /etc/ld.so.conf.d

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

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

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

    3.2.3 查看程序依赖的动态库

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

    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 动态库的优缺点 —— 空间换时间的典范

    优点:

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

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

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

    4.1 链接过程的区别 —— 静态链接是 “复制”,动态链接是 “记录”

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

    4.2 内存加载的区别 —— 静态库是 “私有”,动态库是 “共享”

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

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

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

    五、拓展:试试 ncurses 图形库

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

            安装 ncurses 库:

    # 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++ 程序员从 “入门” 到 “进阶” 的必经之路。希望你能把今天学到的知识用到实际开发中,写出更优雅的代码~        

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


    赞(0)
    未经允许不得转载:171主机测评 » 【Linux系统编程】(二十七)手撕动静态库原理与实战:从底层逻辑到代码落地
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址