Linux-【动静态库】

库 = 预先写好、可直接复用的二进制代码。我们写的 C/C++ 程序,几乎都离不开系统库(libc、libstdc++),自己封装工具代码做成库,能实现:

  • 代码复用,不用重复造轮子
  • 二进制发布,不暴露源码
  • 模块化开发,大型项目必备

一、什么是库

库是写好的 , 现有的 、 成熟的 、 可以复用的代码的二进制形式 。 每个程序都要依赖很多底层库 , 比如C标准库 libc , C++标准库 libstdc++ 。 库的存在极大地提高了开发效率和代码复用性 。

本质上 , 库是一种可执行代码的二进制形式 , 可以被操作系统载入内存执行 。 根据链接时机不同 , 库分为两种:

  • 静态库 :在Linux中后缀为.a(archive),在Windows中为 .lib。程序在编译链接时,会将库中的代码直接拷贝到最终的可执行文件中。因此,程序运行时不再需要静态库。

  • 动态库 :在Linux中后缀为 .so(shared object),在Windows中为 .dll。程序在运行时才去链接动态库的代码,多个程序可以共享同一个动态库。

核心区分动静态库:

  • 静态库 :编译时复制代码到可执行文件 → 运行不需要库
  • 动态库 :编译时只记地址 → 运行必须依赖库

可以看到,系统同时提供了静态库(.a)和动态库(.so)。默认情况下,gcc优先使用动态链接,只有当动态库不存在时才会考虑静态库,或者使用 -static 选项强制静态链接。

情景解释: 如果突然某一天,电脑报错 , 出现dll(window下的动态库)缺失 , 可能是由于软件更新 或者 电脑的杀毒软件更新毒源 , 病毒库 ,把dll文件看成病毒, 干掉了

demo:

为了深入理解,我们来实现一个迷你的标准I/O库,包含文件打开、写入、刷新、关闭等功能,以及一个字符串长度函数。这个示例将贯穿全文。

mystdio.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;          // 当前缓冲区中数据大小
    // TODO 可扩展
};

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

mystdio.c:

复制代码
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "mystdio.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);
    free(stream);
}

mystring.h:

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

my_string.c:

复制代码
#include "mystring.h"

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

usercode.c

复制代码
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
#include "mystring.h"
#include <stdio.h>

int main()
{
    MYFILE * filep = MyFopen("./log.txt","a");
    if(!filep)
    {
        printf("fopen error!\n");
        return 1;
    }

    int cnt = 10;
    while(cnt--)
    {
        char *msg = (char*)"helllo myfile!!!";
        MyFwrite(filep,msg,strlen(msg));
        MyFflush(filep);
        printf("buffer:%s\n",filep->outbuffer);
        sleep(1);
    }

    MyClose(filep);//FILE *fp

    const char* str = "hello bit\n";
    printf("strlen: %d\n",my_strlen(str));
    return 0;
}

以上代码模拟了标准I/O库的缓冲机制和文件操作。我们将以此为基础制作静态库和动态库。

如果现在,我写了很大的一个工程项目 , 里面有很多个 .c 和 .h文件 , 别人想要购买我这个项目 , 但是我希望隐藏源代码 (.c) 文件 , 仅提供给别人 (.o) 文件 和接口说明(.h) ,供他人使用 。引出我们接下来讲的动静态库!

1. 目标文件(.o文件)
通过 gcc -c将.c源文件预处理、编译、汇编生成,包含机器代码但未链接。
2. 头文件(.h文件)
本质是方法声明文档,包含函数原型、结构体定义等,供调用者参考。
3. 静态库(.a文件)
将多个.o文件打包为单个归档文件(.a),便于分发和管理。

二、静态库

  • 静态库本质上是一个归档文件,它将多个目标文件(.o)打包成一个文件 ,并建立了索引以便链接器快速查找。
  • .a静态库 :**本质是一种归档文件,不需要使用者解包,**而用gcc/g++直接进行链接即可!!!!
  • 一个可执行程序可能用到很多库 , 这些库运行有的是静态库 , 有的是动态库,而我们的编译的时候默认是动态库 , 只有在该库下找不到动态库 (以.so )的时候,才会采用同名的静态库!!!我们也可以使用gcc的**-static** 强转设置链接静态库
  • 编译链接时,把库中代码直接拷贝进可执行文件 。
    • 优点:运行不依赖库,移植方便。
    • 缺点:可执行文件大,多个程序复用浪费磁盘 / 内存。

生成静态库的步骤:

  1. 编译源文件为目标文件 :gcc -c my_stdio.c mystring.c

  2. 使用 ar 工具归档 :ar -rc libmyc.a

2.1 静态库生成

Makefile:

复制代码
libmyc.a: mystudio.o mystring.o
	ar -rc $@ $^

mystudio.o: mystudio.c
	gcc -c $<

mystring.o: mystring.c
	gcc -c $<

.PHONY: output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.a lib/mylib
	tar czf lib.tgz lib

.PHONY: clean
clean:
	rm -f *.o libmyc.a lib.tgz
	rm -rf lib

逐行解释Makefile:

  • libmyc.a: mystudio.o mystring.o:目标 libmyc.a依赖于两个 .o 文件。

  • ar -rc $@ $^:ar 是归档工具,-r 表示替换或插入文件 ,-c 表示创建归档 。$@ 是目标文件名(libmyc.a),$^ 是所有依赖文件(mystudio.o mystring.o)。@ 前缀表示不显示命令本身。

  • %.o: %.c:模式规则,任何 .o 文件都由对应的 .c 文件通过 gcc -c 生成。

  • clean 伪目标:删除所有生成的文件。

  • output 伪目标:创建目录结构,将头文件和静态库拷贝到 lib/include 和 lib/mylib,然后打包成 lib.tgz,方便分发。

执行 make 后,会生成 libmyc.a。我们可以用 ar -tv 查看库中包含的目标文件:

2.2 静态库使用

库的名称 :去掉前缀 lib + 后缀 .a (静态库) 或者 .so (动态库), 剩下的就是 库的名称

  • 通过静态库 , 我们开发者在给别人可执行项目的时候 ,就可以不给源码,给相对应的.o 文件以及 .h 文件即可!
  • 测试目标文件生成后 , 静态库删掉,程序照样可以运行 , ,因为库代码已经被整合到可执行文件中。
  • 关于 -static 选项:如果我们希望强制使用静态链接(即使系统中有动态库)
  • 静态链接的可执行文件,体积更大,但不依赖任何外部动态库。

三、动态库

动态库在程序运行时才被加载 ,多个程序可以共享内存中的同一份库代码,节省内存和磁盘空间。

3.1 动态库生成

复制代码
libmyc.so: mystudio.o mystring.o
	gcc -shared -o $@ $^

mystudio.o: mystudio.c
	gcc -fPIC -c $<

mystring.o: mystring.c
	gcc -fPIC -c $<

.PHONY: output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.so lib/mylib
	tar czf lib.tgz lib

.PHONY: clean
clean:
	rm -f *.o libmyc.so lib.tgz
	rm -rf lib
  • libmyc.so :将 mystudio.o 和 mystring.o 打包为动态共享库,-shared 选项指定生成动态库。
  • .o 目标 :**编译 .c 文件时添加 -fPIC 选项,生成位置无关代码,**这是生成动态库的必要条件。
  • output 伪目标 :创建目录结构,拷贝头文件和动态库,最后将整个 lib 目录打包为 lib.tgz。
  • clean 伪目标:清理所有编译生成的中间文件、动态库、压缩包和目录。

3.2 动态库使用

3.3 库运行搜索路径

3.3.1 问题

可执行程序都摆在这儿了,为什么又打不开 ??? 凭啥???

ldd 可执行程序 : 查看可执行程序关联了那个库!!!

  • 你告诉谁了 ? 你只告诉了gcc !但是系统不知道,不知道去哪里找动态库(运行时错误) ;
  • 编译时 :你用 -L ./lib/mylib/告诉了 gcc 链接器库文件在哪里,所以能成功生成可执行文件。
  • 运行时 :系统动态链接器(ld-linux.so)不知道这个路径,它只会去系统默认库路径(如 /lib、/usr/lib)和 LD_LIBRARY_PATH 里找,找不到 libmyc.so 就会报错。

简单说:-L 只管编译链接,不管运行时加载。

  • 静态库没有这个问题 , 为什么? 静态库链接的时候,是把程序拷贝到可执行程序里,一旦可执行程序创建成功,就不再依赖静态库了;只要编译成功,就一定能运行

3.3.2 解决方案

方法1:将库文件拷贝到系统库目录(需要root权限)

方法2:在系统库目录下建立软链接

取消软连接

方法3:设置环境变量 LD_LIBRARY_PATH

这种方法只对当前终端有效,适合临时测试。

复制代码
$ export LD_LIBRARY_PATH=.   # 将当前目录加入搜索路径
$ ./a.out

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/zs/linux/lesson23/zs/lib/mylib/

  • 用 export 命令设置的环境变量,只在当前终端会话(进程)中生效 ,属于临时生效。
  • 一旦关闭终端,这个环境变量就会被系统回收,下次打开新终端时,LD_LIBRARY_PATH 会恢复为空或默认值,程序就又找不到动态库了。
  • LD_LIBRARY_PATH 是内存级的临时环境变量,仅当前终端进程可见,关闭终端后配置就会丢失。
  • 想要有效,就需要写到配置文件里

方法4:修改 /etc/ld.so.conf 配置文件

在 /etc/ld.so.conf.d/ 下新建一个配置文件,如 my.conf,内容为库所在目录的绝对路径,然后执行 sudo ldconfig。

复制代码
$ sudo sh -c "echo /absolute/path/to/lib > /etc/ld.so.conf.d/my.conf"
$ sudo ldconfig

3.4 动态库的优点

  • 节省磁盘空间:多个程序共享同一份库文件,而不是每个程序都拷贝一份。

  • 节省内存:操作系统通过虚拟内存机制,物理内存中只保留一份动态库,被所有使用该库的进程共享。

  • 易于更新:只需替换库文件,无需重新链接所有程序(前提是接口保持不变)。

3.5 相关知识补充

【库里面能不能不动态库和静态库都同时存在 】

可以,在linux系统下,默认情况安装的大部分库,都是动态库

  • 如果非要使用静态链接 , 使用 --static 选项;
  • 如果只存在静态库 ,可执行程序就只能使用静态链接了!!!
  • 库 : 应用程序 = 1 : n
  • vs 不仅仅可以形成可执行程序 , 也能形成动静态库

四、使用外部库

实际开发中我们经常使用第三方库,比如图形库 ncurses。下面以一个简单的进度条程序为例,展示如何使用外部库。

4.1 安装ncurses开发包

在CentOS上

复制代码
$ sudo yum install -y ncurses-devel

在Ubuntu上:

复制代码
$ sudo apt install -y libncurses-dev

4.2 示例程序:进度条

以下代码使用ncurses库在终端显示一个动态进度条

复制代码
#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();                      // 初始化ncurses模式
    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_y = 1;
        int bar_x = BORDER_PADDING + 1;

        // 显示已完成部分(绿色)
        attron(COLOR_PAIR(1));
        for (int i = 0; i < completed; i++) {
            mvwprintw(win, bar_y, bar_x + i, "#");
        }
        attroff(COLOR_PAIR(1));

        // 显示剩余部分(红色背景,但只打印空格模拟)
        attron(COLOR_PAIR(2));
        for (int i = completed; i < max_progress; i++) {
            mvwprintw(win, bar_y, bar_x + i, " ");
        }
        attroff(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 - 2, percent_x, percent_str);

        wrefresh(win);                // 刷新窗口

        progress += PROGRESS_INCREMENT;
        usleep(DELAY);                // 延迟
    }

    // 清理并退出ncurses模式
    delwin(win);
    endwin();
    return 0;
}

编译时需要链接ncurses库:

复制代码
$ gcc progress.c -lncurses -o progress
$ ./progress

使用外部库的一般步骤:

  1. 安装开发包(通常包含头文件和库文件)。

  2. 在代码中包含相应头文件。

  3. 编译时用 -l 指定库名(如 -lncurses),必要时用 -L 指定库路径。

五、从源文件到目标文件

当我们执行 gcc -c hello.c 时,会生成一个目标文件 hello.o。目标文件是二进制格式,但还不是可执行文件,它包含了编译后的机器指令和数据,但其中的外部符号地址尚未确定。

查看目标文件的类型:

复制代码
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

这表明 hello.o 是一个ELF格式的可重定位文件(relocatable file)。

把 .c 编译形成 .o 文件 , 与其他文件没有任何关系 , 只有在链接的时候会和其他文件形成关系;如果存在一个 .c 文件出错 , 修改之后重新生成 .o 文件 , 只会修改同名的 .o 文件,不影响其他的 .o 文件 !!!!

-> 以较小的成本,把代码进行重新编译!!!!

相关推荐
律宏阔43 分钟前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔43 分钟前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴44 分钟前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千1 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei371 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei371 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei371 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh1 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW1 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲1 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js