【Linux 系统篇(二十九)】文件(六):动静态库制作


大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇


Linux 系统篇(二十九) ------ 动静态库制作

文章目录

  • [Linux 系统篇(二十九) ------ 动静态库制作](#Linux 系统篇(二十九) —— 动静态库制作)
    • 写在前面:为什么我们需要库?
    • 一、准备工作:自己动手写一个"迷你标准库"
      • [1.1 迷你文件 IO 库:`my_stdio`](#1.1 迷你文件 IO 库:my_stdio)
      • [1.2 迷你字符串库:`my_string`](#1.2 迷你字符串库:my_string)
    • 二、静态库
      • [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 命令的作用?

  查看可执行文件或动态库的依赖关系,常用于排查"运行时找不到库"的问题。


结语:

  今天的内容到这里就结束了,希望你能有所收获~

干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _

相关推荐
%KT%1 小时前
ubuntu系统部署docker
运维·docker·容器
陈聪.1 小时前
01-项目一_企业级高可用Web集群实施手册
运维·云原生
随性而行3601 小时前
企业微信API调用案例:如何通过接口实现多账号统一管理与业务接入?
运维·微信·机器人·自动化·企业微信
额额额对了1 小时前
Linux 驱动学习笔记(部分)
linux·笔记·学习
guo_wen_qiang1 小时前
云服务器redis主从复制-哨兵模式
运维·服务器·redis
zhikouai1 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
运维小文2 小时前
apache nifi 2.90部署+运维手册
大数据·运维·数据分析·apache·nifi
2603_965148112 小时前
终极清单:构建自动化选品比价系统的所有组件与资源
大数据·运维·数据库·数据挖掘·自动化
可信时间戳2 小时前
原创作品如何做版权认证申请?先备材料,再挑路径
服务器·音视频·可信时间戳·权利卫士