【Linux系统】17 库制作与原理(上)

目录

前言

本节学习重点

[一、静态库 .a](#一、静态库 .a)

[1.1 静态库生成步骤](#1.1 静态库生成步骤)

[Makefile 完整脚本](#Makefile 完整脚本)

[1.2 静态库的三种使用场景](#1.2 静态库的三种使用场景)

[1.3 静态库优缺点](#1.3 静态库优缺点)

[二、动态库 .so(共享库)](#二、动态库 .so(共享库))

[2.1 动态库制作](#2.1 动态库制作)

[2.2 动态库链接使用](#2.2 动态库链接使用)

[2.3 动态库运行时查找的 4 种解决方案](#2.3 动态库运行时查找的 4 种解决方案)

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

[方案 2:软链接](#方案 2:软链接)

[方案 3:环境变量 LD_LIBRARY_PATH(开发最常用)](#方案 3:环境变量 LD_LIBRARY_PATH(开发最常用))

[方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效)](#方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效))

[2.4 动态库优缺点](#2.4 动态库优缺点)

[补充小拓展:ncurses 第三方库演示](#补充小拓展:ncurses 第三方库演示)

上篇小结


前言

本节学习重点

  1. 什么是库,库的分类
  2. 静态库.a 制作、Makefile 编写、使用方式
  3. 动态库.so 制作、Makefile 编写、使用方式
  4. 动态库查找的 4 种解决方案
  5. 实操演示,常见报错排查

库是什么:库是写好的、成熟的、可以复用的二进制代码。软件开发不可能所有代码从零开始,大量底层功能都封装在库中。本质是可执行二进制,操作系统可以加载进内存运行。

Linux 与 Windows 库文件对比:

表格

系统 静态库后缀 动态库后缀
Linux .a .so
Windows .lib .dll

系统自带库示例(CentOS/Ubuntu)

cpp 复制代码
# CentOS C标准库
ls /lib64/libc.a
ls /lib64/libc-2.17.so

# Ubuntu C标准库
ls -l /lib/x86_64-linux-gnu/libc.a
ls -l /lib/x86_64-linux-gnu/libc-2.31.so

我们自己模拟简易 C 库,准备下面 4 个源文件,作为制作库的素材。

my_stdio.h

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

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>

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);
}

my_string.h

cpp 复制代码
#pragma once
int my_strlen(const char *s);

my_string.c

cpp 复制代码
#include "my_string.h"
int my_strlen(const char *s)
{
    const char *end = s;
    while(*end != '\0')end++;
    return end - s;
}

一、静态库 .a

核心定义:静态库在编译链接阶段,把库里面目标文件代码拷贝复制到可执行程序内部;程序运行的时候,不再依赖静态库文件本身。

gcc 默认优先链接动态库;如果同一个目录同时存在同名.so和.a,优先选.so。使用-static强制编译器使用静态链接。

1.1 静态库生成步骤

  1. 把.c源文件编译生成目标文件*.o ,gcc -c,只编译不链接。
  2. 使用ar归档工具,把若干.o打包成静态库libxxx.a。

ar是 GNU 归档工具:

  • -r replace:替换 / 新增目标文件进库
  • -c create:创建库文件
  • -t:查看库里面包含哪些.o文件
  • -v verbose,显示详细过程
Makefile 完整脚本
cpp 复制代码
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"

执行 make,生成libmystdio.a。 查看库内包含的目标文件:

复制代码
ar -tv libmystdio.a

注意库命名规范:必须以lib开头,后缀.a;链接时-lmystdio,自动去掉前缀lib和后缀.a。

1.2 静态库的三种使用场景

测试代码main.c

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

表格

场景 命令 说明
头文件、库安装到系统路径 gcc main.c -lmystdio 系统自动搜索系统头文件、系统库路径
头文件、库在当前目录 gcc main.c -L. -lmystdio -L. 指定库搜索路径为当前目录
头文件、库独立自定义路径 gcc main.c -I./include -L./lib -lmystdio -I 指定头文件搜索路径;-L指定库搜索路径

参数说明

  • -I:指定头文件搜索路径
  • -L:指定库文件搜索路径
  • -lxxx:链接名字为libxxx的库

✅静态库特性验证:链接完成后,把 libmystdio.a 删掉,程序依旧可以正常运行,代码已经拷贝进可执行程序。

1.3 静态库优缺点

✅优点

  1. 运行时不依赖库文件,程序移植简单,拷贝 exe 直接运行;
  2. 运行时不需要动态链接,启动速度快。

❌缺点

  1. 可执行文件体积大;
  2. 多个程序使用同一个静态库,内存会有多份库代码副本,内存浪费;
  3. 如果库升级,所有程序必须重新编译链接,不能直接替换库文件。

二、动态库 .so(共享库)

核心定义:动态库不会把代码拷贝进可执行程序;链接阶段只记录依赖信息,等到程序运行时,操作系统才把动态库加载映射到进程虚拟地址空间;物理内存中一份动态库可以被多个进程共享。

可执行文件中,只保存函数的入口引用表,没有库函数完整机器码。运行时由操作系统把磁盘上.so加载到内存,这个过程叫动态链接。

2.1 动态库制作

关键编译选项:

  1. -fPIC:生成位置无关代码(Position Independent Code),动态库必须。代码全部使用相对寻址,可以加载到虚拟地址空间任意位置。
  2. -shared:生成共享库(动态库)。

Makefile 脚本

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

注意:生成.o目标文件必须加-fPIC,不能省略!

2.2 动态库链接使用

同样使用上面的main.c测试代码,链接命令和静态库几乎一模一样:

cpp 复制代码
# 当前目录下有libmystdio.so
gcc main.c -L. -lmystdio -o app

链接成功生成app可执行程序,但是直接执行 ./app 会报错 :error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory。

🔔区分两个阶段路径:

  1. 编译链接阶段 :-L告诉编译器去哪里找库,编译时生效;
  2. 程序运行阶段 :操作系统动态链接器ld-linux.so会去系统预设路径找.so,此时-L参数已经失效。

使用ldd命令,查看可执行程序依赖哪些动态库:

复制代码
ldd app

输出示例,可以看到我们自己写的库显示not found。

cpp 复制代码
linux-vdso.so.1 =>  (0x00007fffacbbf000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)

2.3 动态库运行时查找的 4 种解决方案

方案 1:拷贝动态库到系统库目录

系统库目录:/usr/lib、/usr/local/lib、/lib64

cpp 复制代码
sudo cp libmystdio.so /usr/lib
./app

缺点:污染系统库,不推荐开发环境频繁使用。

方案 2:软链接

把我们库所在路径创建软链接到系统库目录。

cpp 复制代码
sudo ln -s /home/xxx/code/libmystdio.so /usr/lib/libmystdio.so
方案 3:环境变量 LD_LIBRARY_PATH(开发最常用)

把动态库所在目录添加到环境变量LD_LIBRARY_PATH,动态链接器优先读取这个环境变量。

cpp 复制代码
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
./app

注意:仅当前终端会话生效;新开终端环境变量失效。适合开发调试。

方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效)
  1. 在/etc/ld.so.conf.d/目录新建配置文件,例如mybit.conf,写入动态库所在绝对路径。
cpp 复制代码
sudo vim /etc/ld.so.conf.d/mybit.conf
# 文件内写入:/home/xxx/code
  1. 执行ldconfig,更新系统动态库缓存/etc/ld.so.cache

    sudo ldconfig

原理:动态链接器优先读取缓存文件ld.so.cache,缓存保存所有可用动态库路径。修改配置文件后必须执行 ldconfig 刷新缓存,否则不生效。

2.4 动态库优缺点

✅优点

  1. 磁盘节省空间,多个程序共用同一份.so文件;
  2. 物理内存只加载一份库,多进程共享,节约内存;
  3. 库升级只替换.so文件,不需要重新编译应用程序。

❌缺点

  1. 运行依赖.so库文件,部署必须同步拷贝库,否则程序直接无法运行;
  2. 程序启动需要动态链接做符号解析,启动会有少量开销。

补充小拓展:ncurses 第三方库演示

系统存在大量现成库,举例 ncurses 字符图形库。 安装:

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

测试 demo

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

编译链接:gcc main.c -lncurses -o bar


上篇小结

  1. 静态库.a:编译链接期拷贝目标文件代码到可执行程序;运行不需要库文件;ar -rc打包;
  2. 动态库.so:运行时加载,多进程共享内存;编译加-fPIC -shared;
  3. 动态库运行找不到库四种解决方案:拷贝系统目录、软链接、LD_LIBRARY_PATH环境变量、/etc/ld.so.conf.d + ldconfig;
  4. 编译阶段找库看-L;运行阶段找库看动态链接器ld-linux.so。
相关推荐
wzg20161 小时前
ubuntu 离线安装 docker-compose
linux·ubuntu·docker
Shadow(⊙o⊙)1 小时前
仿muduo实现高并发服务器——EchoServer完成
运维·服务器
行走的陀螺仪1 小时前
Claude Code 从入门到实战(三):Hooks、Skills、SubAgents 高级扩展
linux·编辑器·vim
Brilliantwxx2 小时前
【STM32】 内部 Flash 的 HAL 库函数详解与自定义封装实战
linux·服务器·网络
时间售卖铺2 小时前
Docker镜像构建全解析:从原理到实战优化
运维·docker·云原生·容器
西索斯coding2 小时前
qwen3.8-max-0902 和 minimax-m3 改真实仓库差多少?2026 年 50 个缺陷复现实测框架
java·服务器·网络·ai
小豆庄2 小时前
会议录屏忘打码后的技术复盘:本地自动化隐私脱敏方案
运维·opencv·目标检测·自动化·视频·纹理贴图
码途行者2 小时前
Linux 服务器 Golang 安装及环境变量配置
linux·运维·服务器
叼烟扛炮2 小时前
Linux 学习笔记(四十七):TCP 状态机与 tcpdump 抓包排雷实战
linux·网络·笔记·学习·tcp/ip·tcpdump