【Linux】库制作与原理(一):静态库与动态库的制作使用

文章目录

库制作与原理(一):静态库与动态库的制作使用

💬 欢迎讨论:这是Linux系统编程系列的新篇章。之前我们深入学习了文件系统、进程管理等核心知识,现在我们将探索另一个重要主题:库。在实际开发中,我们每天都在使用各种库,但你真正理解库的本质吗?静态库和动态库有什么区别?为什么动态库运行时找不到?本篇将从零开始,带你深入理解Linux下库的制作与使用。

👍 点赞、收藏与分享:这篇文章包含了库的完整制作流程、动态库查找问题的4种解决方案,内容实用,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:本系列共3篇,本篇讲解库的基础,后续将深入ELF格式和动态链接原理。


一、什么是库

1.1 库的本质与作用

库是什么?

库是写好的、现有的、成熟的、可以复用的代码。本质上来说,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

为什么需要库?

现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始。比如:

  • 输入输出功能(printf、scanf)
  • 字符串处理(strlen、strcpy)
  • 文件操作(fopen、fread)
  • 网络通信(socket相关函数)

如果每个项目都要重新实现这些功能,开发效率将极其低下。库的存在让我们可以站在巨人的肩膀上编程。

1.2 库的分类

Linux下库分为两种:

库类型 Linux扩展名 Windows扩展名 链接方式
静态库 .a .lib 编译时链接
动态库 .so .dll 运行时链接

系统中的库文件实例:

bash 复制代码
# Ubuntu系统
# 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

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 /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
bash 复制代码
# CentOS系统
# C语言运行时库
ls /lib64/libc-2.17.so -l
-rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so

ls /lib64/libc.a -l
-rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a

# C++标准库
ls /lib64/libstdc++.so.6 -l
lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 -> libstdc++.so.6.0.19

二、准备库代码

为了演示库的制作,我们先准备一些示例代码。这里使用之前封装过的文件I/O库代码:

c 复制代码
// my_stdio.h
#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);
c 复制代码
// my_stdio.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)
{
    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);
}

再准备一个字符串处理函数:

c 复制代码
// my_string.h
#pragma once

int my_strlen(const char *s);
c 复制代码
// my_string.c
#include "my_string.h"

int my_strlen(const char *s)
{
    const char *end = s;
    while(*end != '\0') end++;
    return end - s;
}

三、静态库的制作与使用

3.1 什么是静态库

静态库(.a)的特点:

静态库是在程序编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。

优点:

  • 程序独立运行,不依赖外部库文件
  • 运行速度快,无需动态加载

缺点:

  • 可执行文件体积大
  • 库更新后需要重新编译程序
  • 多个程序使用同一库会造成内存浪费

3.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"

关键命令说明:

ar是GNU归档工具,用于创建静态库:

  • r: replace,替换库中已有的目标文件
  • c: create,创建库文件

步骤二:执行make

bash 复制代码
make

gcc -c my_stdio.c
compling my_stdio.c to my_stdio.o ... done
gcc -c my_string.c
compling my_string.c to my_string.o ... done
ar -rc libmystdio.a my_stdio.o my_string.o
build my_stdio.o my_string.o to libmystdio.a ... done

步骤三:查看静态库内容

bash 复制代码
ar -tv libmystdio.a

rw-rw-r-- 1000/1000   2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000   1272 Oct 29 14:35 2024 my_string.o

参数说明:

  • t: 列出静态库中的文件
  • v: verbose,显示详细信息

3.3 静态库的使用

创建测试程序:

c 复制代码
// main.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 复制代码
# 安装头文件到系统目录
sudo cp *.h /usr/include/

# 安装库文件到系统目录
sudo cp libmystdio.a /usr/lib/

# 编译时直接指定库名
gcc main.c -lmystdio

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

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

参数说明:

  • -L.: 指定库文件搜索路径为当前目录
  • -lmystdio: 链接libmystdio.a库(去掉前缀lib和后缀.a)

场景3:头文件和库文件在独立路径

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

参数说明:

  • -I./stdc/include: 指定头文件搜索路径
  • -L./stdc/lib: 指定库文件搜索路径
  • -lmystdio: 指定要链接的库名

验证静态链接:

bash 复制代码
./a.out
abcdefg: 7

cat log.txt
abcdefgabcdefgabcdefg

# 删除静态库后程序仍可运行
rm libmystdio.a
./a.out
abcdefg: 7

这证明静态库的代码已经被编译进可执行文件中!


四、动态库的制作与使用

4.1 什么是动态库

动态库(.so)的特点:

动态库是在程序运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

核心机制:

  • 可执行文件仅包含函数入口地址的表,不包含函数机器码
  • 程序开始运行前,操作系统从磁盘加载动态库到内存
  • 这个过程称为动态链接(dynamic linking)

优点:

  • 可执行文件体积小,节省磁盘空间
  • 多个进程共享一份库代码,节省内存
  • 库更新无需重新编译程序
  • 支持代码的模块化和插件化

缺点:

  • 运行时需要库文件存在
  • 加载时间略长
  • 版本管理较复杂

4.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"

关键编译选项:

  • -fPIC: 产生位置无关码(Position Independent Code)
  • -shared: 表示生成共享库格式

什么是位置无关码(PIC)?

位置无关码是指代码可以被加载到内存的任意地址运行,不需要修改代码本身。这是通过相对地址和全局偏移表(GOT)实现的。

步骤二:执行make

bash 复制代码
make

gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
gcc -o libmystdio.so my_stdio.o my_string.o -shared

步骤三:查看动态库依赖

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)

可以看到我们的动态库依赖了系统C库。

4.3 动态库的使用

编译链接方式与静态库相同:

bash 复制代码
# 场景1:系统路径
gcc main.c -lmystdio

# 场景2:当前目录
gcc main.c -L. -lmystdio

# 场景3:独立路径
gcc main.c -I./stdc/include -L./stdc/lib -lmystdio

但运行时会遇到问题:

bash 复制代码
./a.out
./a.out: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory

查看可执行文件依赖:

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)

libmystdio.so => not found 说明系统找不到我们的动态库!


五、动态库运行时查找

5.1 问题分析

为什么编译能通过,运行却失败?

  • 编译时:-L选项告诉编译器在哪里找库文件进行链接
  • 运行时:系统需要在标准库路径中查找动态库

系统标准库路径:

  • /lib
  • /lib64
  • /usr/lib
  • /usr/local/lib
  • /etc/ld.so.conf配置的路径

我们的库不在这些路径中,所以运行失败!

5.2 解决方案

5.2.1 方案1:拷贝到系统路径
bash 复制代码
sudo cp libmystdio.so /usr/lib/

./a.out
abcdefg: 7

优点 :永久生效,所有用户可用
缺点:需要root权限,污染系统环境

5.2.2 方案2:创建软连接
bash 复制代码
sudo ln -s /home/user/mylib/libmystdio.so /usr/lib/libmystdio.so

./a.out
abcdefg: 7

优点 :不需要拷贝文件,节省空间
缺点:需要root权限,删除原文件会导致链接失效

5.2.3 方案3:设置LD_LIBRARY_PATH环境变量
bash 复制代码
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.

./a.out
abcdefg: 7

# 查看动态库是否被找到
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => ./libmystdio.so (0x00007fa2aef30000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aeb00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)

优点 :不需要root权限,灵活方便
缺点:仅对当前终端会话有效,关闭终端失效

永久生效方法:

bash 复制代码
# 编辑用户配置文件
vim ~/.bashrc

# 添加以下内容
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/mylib

# 使配置生效
source ~/.bashrc
5.2.4 方案4:配置/etc/ld.so.conf.d/(推荐)

这是最推荐的专业做法!

步骤:

bash 复制代码
# 1. 创建配置文件
sudo vim /etc/ld.so.conf.d/mylib.conf

# 2. 写入库路径
/home/user/mylib

# 3. 更新动态链接器缓存
sudo ldconfig

# 4. 验证
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => /home/user/mylib/libmystdio.so (0x00007fa2aef30000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aeb00000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)

# 5. 运行
./a.out
abcdefg: 7

原理:

ldconfig命令会:

  1. 读取/etc/ld.so.conf及其包含的/etc/ld.so.conf.d/下所有配置
  2. 扫描配置中的路径,查找所有动态库
  3. 将结果缓存到/etc/ld.so.cache文件
  4. 程序运行时,动态链接器优先从缓存中查找库

优点:

  • 系统级配置,永久生效
  • 性能最优(使用缓存)
  • 所有用户共享
  • 符合Linux标准做法

缺点:

  • 需要root权限

5.3 四种方案对比

方案 权限要求 生效范围 是否永久 推荐度
拷贝到系统路径 root 系统全局 ✅ ⭐⭐
创建软连接 root 系统全局 ✅ ⭐⭐⭐
LD_LIBRARY_PATH 普通用户 当前用户 ❌ ⭐⭐⭐
ldconfig配置 root 系统全局 ✅ ⭐⭐⭐⭐⭐

六、使用第三方库:ncurses示例

学会了自己制作库,我们也可以使用其他人提供的库。这里以ncurses图形库为例。

6.1 安装ncurses

bash 复制代码
# CentOS
sudo yum install -y ncurses-devel

# Ubuntu
sudo apt install -y libncurses-dev

6.2 进度条示例程序

c 复制代码
// progress.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

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

编译运行:

bash 复制代码
gcc progress.c -lncurses -o progress

./progress

你会看到一个漂亮的终端进度条!

这个示例展示了:

  • 如何使用第三方库(-lncurses)
  • ncurses库提供了丰富的终端UI功能
  • 库让我们可以快速实现复杂功能

七、总结

本文深入讲解了Linux下库的制作与使用:

核心知识点:

  1. 库的分类

    • 静态库(.a):编译时链接,独立运行
    • 动态库(.so):运行时链接,共享使用
  2. 静态库制作

    • 使用ar -rc打包.o文件
    • 通过-L和-l选项链接
    • 代码被编译进可执行文件
  3. 动态库制作

    • 使用-fPIC生成位置无关码
    • 使用-shared生成共享库
    • 支持多进程共享
  4. 动态库查找

    • 四种解决方案
    • 推荐使用ldconfig配置
    • 理解LD_LIBRARY_PATH环境变量
  5. 库命名规则

    • 库文件名:libxxx.a或libxxx.so
    • 链接时:-lxxx(去掉前缀和后缀)

库文件处理流程图:

bash 复制代码
源代码(.c) 
    ↓ gcc -c
目标文件(.o)
    ↓
    ├─→ ar -rc → 静态库(.a) → gcc -L -l → 可执行文件
    │                                    (库代码已包含)
    │
    └─→ gcc -fPIC -shared → 动态库(.so) → gcc -L -l → 可执行文件
                                                      (需要.so文件)

💡 思考题

  1. 为什么动态库需要-fPIC编译选项?
  2. 如果一个程序既有静态库又有动态库,优先链接哪个?
  3. 如何查看一个可执行文件依赖了哪些动态库?
  4. 多个进程使用同一个动态库,内存中有几份代码?

下一篇我们将深入ELF文件格式,揭示静态链接的底层原理!

相关推荐
Decay__3 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
贞明电子5 小时前
工业三色报警灯怎么接入项目系统?开关量与RS485控制应用指南
运维·网络·微信公众平台·贞明电子
HideF6 小时前
Linux
linux·服务器·网络
万联WANFLOW6 小时前
海外网络故障怎么排查?Cloudflare 三季度断网报告解读与 mtr 实战
运维·网络·故障分析
听风说ml6 小时前
socket编程 【C语言】
c语言·udp·tcp·socket编程
学逆向的7 小时前
C 局部静态变量
c语言·开发语言·网络安全
Ruiery8 小时前
Linux 6.6内核内存管理深度解析(十三):slab/slub 分配器
linux·运维·服务器
乌恩大侠8 小时前
从 AI-RAN 到 AI Agent:基于 NVIDIA Nemotron 构建 5G/6G 智能网络故障诊断系统
服务器·人工智能·5g·o-ru·ai-ran·电信大模型
Decay__9 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
丰锋ff10 小时前
压缩命令笔记
linux