【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.alibxxx.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文件格式,揭示静态链接的底层原理!

相关推荐
superman超哥2 小时前
仓颉代码内联策略深度解析
c语言·开发语言·c++·python·仓颉
prettyxian2 小时前
【linux】进程概念(2)Linux进程的生命密码:从fork到完全独立
linux·运维·服务器
ghujlhdrx2 小时前
FOC电机驱动自学记录系列(前言)一些想法的碎碎念
c语言
渡我白衣2 小时前
计算机组成原理(9):零拓展与符号拓展
c语言·汇编·人工智能·嵌入式硬件·网络协议·硬件工程·c
一分之二~2 小时前
回溯算法--全排列
c语言·数据结构·c++·算法·leetcode
松涛和鸣2 小时前
DAY37 Getting Started with UDP Network Programming
linux·c语言·网络·单片机·网络协议·udp
深信达沙箱2 小时前
常见数据泄露途径测试用例
服务器·安全·测试用例·源代码
热爱专研AI的学妹2 小时前
Coze-AI 智能体平台:工作流如何成为智能体的 “自动化引擎”?解锁零代码落地新范式
运维·数据结构·人工智能·自动化
梦想的旅途22 小时前
从 0 到 1:构建外部群自动化的全链路监控大屏
运维·自动化