7. 库制作与原理

7. 库制作与原理(上)

📚 目录

  • [1. 什么是库](#1. 什么是库)
  • [2. 静态库](#2. 静态库)
    • [2.1 静态库生成](#2.1 静态库生成)
      • [2.1.1 静态库的常见命令](#2.1.1 静态库的常见命令)
      • [2.1.2 命令使用演示](#2.1.2 命令使用演示)
    • [2.2 静态库使用](#2.2 静态库使用)
  • [3. 动态库](#3. 动态库)
    • [3.1 动态库生成](#3.1 动态库生成)
    • [3.2 动态库使用](#3.2 动态库使用)
    • [3.3 库运行搜索路径](#3.3 库运行搜索路径)
      • [3.3.1 问题](#3.3.1 问题)
      • [3.3.2 四种通俗解决方案](#3.3.2 四种通俗解决方案)
  • [4. 使用外部库](#4. 使用外部库)

本节重点:

  • 动静态库的制作
  • 动静态库的使⽤
  • 动态库的查找
  • 可执⾏程序ELF格式
  • 可执⾏程序的加载过程
  • 虚拟地址空间和动态库加载的过程

1. 什么是库

库是写好的现有的,成熟的,可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库,不可能 每个⼈的代码都从零开始,因此库的存在意义⾮同寻常。

本质上来说库是⼀种可执⾏代码的⼆进制形式,可以被操作系统载⼊内存执⾏。库有两种:

  1. 静态库 .alinux、 .libwindows

  2. 动态库 .solinux、 .dllwindows

    c 复制代码
    // 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
    $ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a -l
    /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
    
    // 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
    $ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l
    -rw-r--r-- 1 root root 2932366 Sep 30  2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a

预备⼯作,准备好历史封装的libc代码,在任意新增"库⽂件

1.my_stdio.h

c 复制代码
// 这是头文件 my_stdio.h
// 作用:声明函数、定义结构体、定义常量,给 .c 文件使用

// 防止头文件被重复包含(重复 #include 会报错,加这个就不会)
#pragma once

// 定义缓冲区大小为 1024 字节
// 缓冲区就是我们自己模拟的一段内存,用来暂存要输出的数据
#define SIZE 1024

// 下面三个是"刷新策略",意思是:什么时候把缓冲区的数据真正写到文件里
#define FLUSH_NONE  0   // 从不自动刷新,只有手动调用 mfflush 才写
#define FLUSH_LINE  1   // 行缓冲:遇到换行符 \n 就自动写出去(类似 printf)
#define FLUSH_FULL  2   // 全缓冲:缓冲区满了才写出去

// 自定义的"文件结构体"
// 作用:模仿系统的 FILE,自己管理文件描述符、缓冲区、状态
struct IO_FILE
{
    int flag;           // 保存当前用哪种刷新策略(上面 0/1/2)
    int fileno;         // 文件描述符:系统给打开的文件分配的数字编号,用来标识文件
    char outbuffer[SIZE]; // 输出缓冲区:一段内存,暂存要写的数据
    int cap;            // capacity:缓冲区总容量(这里固定 1024)
    int size;           // 缓冲区当前已经存了多少字节数据
    // TODO 表示待实现、待扩展,这里先空着
};

// 给结构体起别名,以后写 mFILE 就等于 struct IO_FILE,更简洁
typedef struct IO_FILE mFILE;

// 函数声明:告诉编译器有这些函数,具体实现写在 .c 里

// 功能:打开文件
// 参数1 filename:要打开的文件名(字符串)
// 参数2 mode:打开模式,如 "r" 只读 "w" 只写 "a" 追加
// 返回值:成功返回自定义文件结构体指针,失败返回 NULL
mFILE *mfopen(const char *filename, const char *mode);

// 功能:往文件里写数据
// 参数1 ptr:要写入的数据的起始地址(指针)
// 参数2 num:要写多少个字节
// 参数3 stream:我们自己的 mFILE 结构体(里面有缓冲区、文件编号)
// 返回值:成功写入的字节数
int mfwrite(const void *ptr, int num, mFILE *stream);

// 功能:强制把缓冲区数据刷到文件里
// 参数 stream:自己的 mFILE 结构体
void mfflush(mFILE *stream);

// 功能:关闭文件
// 参数 stream:自己的 mFILE 结构体
void mfclose(mFILE *stream);

2.my_stdio.c

复制代码
// 实现文件:里面写函数具体怎么做

// 包含自己的头文件(拿到结构体、函数声明)
#include "my_stdio.h"

// 字符串比较函数 strcmp 用到
#include <string.h>

// malloc 动态内存分配用到
#include <stdlib.h>

// 文件状态相关结构体用到
#include <sys/stat.h>

// 系统类型定义用到
#include <sys/types.h>

// open 函数用到
#include <fcntl.h>

// close、write、fsync 系统调用用到
#include <unistd.h>

// 实现:打开文件
// filename:文件名
// mode:打开模式 "r"/"w"/"a"
mFILE *mfopen(const char *filename, const char *mode)
{
    // fd = file descriptor
    // 文件描述符,系统用来标识一个打开的文件
    // 初始值 -1 表示还没打开
    int fd = -1;

    // 如果模式是 "r" 只读
    if(strcmp(mode, "r") == 0)
    {
        // O_RDONLY = 只读打开
        // 成功返回 >=0 的数字,失败返回 -1
        fd = open(filename, O_RDONLY);
    }

    // 如果模式是 "w" 只写
    else if(strcmp(mode, "w") == 0)
    {
        // 三个宏用 | 组合,表示同时具备这三个属性
        // O_CREAT:文件不存在就创建
        // O_WRONLY:只写模式
        // O_TRUNC:如果文件已存在,清空原有内容
        // 0666 是文件权限(可读可写)
        fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
    }

    // 如果模式是 "a" 追加
    else if(strcmp(mode, "a") == 0)
    {
        // O_CREAT:不存在则创建
        // O_WRONLY:只写
        // O_APPEND:写入时永远追加到文件末尾,不会覆盖原有内容
        fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
    }

    // 如果打开失败(fd 是负数)
    if(fd < 0)
        return NULL;    // 返回空指针,表示打开失败

    // 给我们自己的 mFILE 结构体分配内存
    // malloc 从堆上申请一块和结构体一样大的内存
    mFILE *mf = (mFILE*)malloc(sizeof(mFILE));

    // 如果 malloc 失败(内存不足)
    if(!mf)
    {
        close(fd);      // 把刚才打开的文件关掉,避免资源泄漏
        return NULL;    // 返回失败
    }

    // 给结构体成员赋值
    mf->fileno = fd;        // 保存系统给的文件描述符
    mf->flag = FLUSH_LINE;  // 默认使用行缓冲(遇到\n刷新)
    mf->size = 0;           // 缓冲区一开始是空的
    mf->cap = SIZE;         // 缓冲区容量固定 1024

    return mf;  // 返回创建好的结构体指针
}

// 实现:刷新缓冲区,把数据真正写到文件
void mfflush(mFILE *stream)
{
    // 如果缓冲区里有数据
    if(stream->size > 0)
    {
        // write 是系统调用
        // 参数1:文件描述符
        // 参数2:数据地址(缓冲区起始位置)
        // 参数3:写多少字节
        // 作用:把数据从我们的缓冲区写到系统内核缓冲区
        write(stream->fileno, stream->outbuffer, stream->size);

        // fsync 强制把内核缓冲区的数据同步到磁盘/文件
        // 确保数据真正落盘,不会只停留在内存
        fsync(stream->fileno);

        // 刷新完,清空缓冲区已用大小
        stream->size = 0;
    }
}

// 实现:写数据到我们自己的缓冲区
int mfwrite(const void *ptr, int num, mFILE *stream)
{
    // memcpy:内存拷贝
    // 把 ptr 指向的数据,拷贝到我们的缓冲区里
    // 拷贝位置:缓冲区起始 + 已用大小(追加在后面)
    memcpy(stream->outbuffer + stream->size, ptr, num);

    // 拷贝完,更新已用大小
    stream->size += num;

    // 如果当前是行缓冲模式,并且缓冲区最后一个字符是换行 \n
    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);
}

3.my_string.h

复制代码
// 头文件 my_string.h
// 作用:声明自己实现的 strlen 函数

// 防止重复包含
#pragma once

// 函数声明:求字符串长度
// 参数 s:字符串起始地址
// 返回值:字符串长度(不包含 '\0')
int my_strlen(const char *s);

4.my_string.c

复制代码
#include "my_string.h"

// 实现:自己写的求字符串长度函数
int my_strlen(const char *s)
{
    // end 指针一开始指向字符串开头
    const char *end = s;

    // 一直往后走,直到遇到字符串结束符 '\0' 停止
    while(*end != '\0')
        end++;

    // 指针相减 = 两个地址之间的元素个数 = 字符串长度
    return end - s;
}

有些小伙伴可能看到:O_CREAT | O_WRONLY | O_TRUNC

、O_CREAT | O_WRONLY | O_APPEND就懵逼了。

  1. 我们先简单讲解一下这种 A|B|C 的阅读顺序:
  • 从左往右读,意思是 "同时具备这几个功能"
  • 你可以把 | 理解成 "并且"、"同时"、"加上"。
  1. 再来解释一下:
  • O_CREAT :文件不存在就创建
  • O_WRONLY :以只写方式打开
  • O_TRUNC :如果文件已经存在,清空原有内容
  • O_APPEND :写入时永远追加到文件末尾,不会覆盖原有内容
  1. 现在相信大家都明白了:
  • O_CREAT | O_WRONLY | O_TRUNC

    读作:创建文件 并且 只写 并且 清空原有内容

  • O_CREAT | O_WRONLY | O_APPEND

​ 读作:创建文件 并且 只写 并且 追加到末尾

2. 静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执⾏⽂件中 ,程序运⾏的时候将不再 需要静态库。
  • ⼀个可执⾏程序可能⽤到许多的库,这些库运⾏有的是静态库,有的是动态库,⽽我们的编译默 认为动态链接库 ,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使⽤gcc 的-static 强转设置链接静态库。

2.1 静态库⽣成

2.1.1静态库的常见命令:
  • ar 是 gnu 归档⼯具, rc 表⽰ (replace and create)

  • t: 列出静态库中的⽂件

  • v :verbose 详细信息

    目标:生成 libmystdio.a

    依赖:需要先有 my_stdio.o 和 my_string.o

    libmystdio.a:my_stdio.o my_string.o
    # ar 打包工具:把两个 .o 打包成静态库 .a
    # -r 替换已有文件,-c 不存在则创建
    # @ = 目标 libmystdio.a # ^ = 所有依赖 my_stdio.o my_string.o
    @ar -rc @ ^
    @echo "build ^ to @ ... done"

    模式规则:所有 .c 文件编译成 .o 文件

    %.o 表示任意xxx.o

    %.c 对应同名xxx.c

    %.o:%.c
    # gcc -c 只编译,不链接,生成 .o
    # < = 第一个依赖(即xxx.c) @gcc -c <
    @echo "compling < to @ ... done"

    伪目标:清理文件

    .PHONY:clean
    clean:
    # 删除所有 .a 静态库、.o 目标文件、stdc 目录等
    @rm -rf *.a .o stdc
    @echo "clean ... done"

    伪目标:打包输出(把头文件、库文件整理好)

    .PHONY:output
    output:
    # 创建目录 stdc/include 放头文件
    @mkdir -p stdc/include
    # 创建目录 stdc/lib 放静态库
    @mkdir -p stdc/lib
    # 复制所有 .h 到头文件目录
    @cp -f *.h stdc/include
    # 复制静态库到 lib 目录
    @cp -f *.a stdc/lib
    # 打包成压缩包 stdc.tgz
    @tar -czf stdc.tgz stdc
    @echo "output stdc ... done"

  • $@ = 当前目标

  • $^ = 所有依赖

  • $< = 第一个依赖

  • @命令:执行命令但不打印命令本身(更干净)

  • .PHONY:声明这不是文件,而是动作名(clean/output)

2.1.2命令使用演示

(1). 直接编译生成静态库:

在当前目录下执行:make

它会自动:

  1. 把 .c 编译成 .o
  2. 把 .o 打包成 libmystdio.a

(2). 查看静态库里面有什么

复制代码
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:显示详细信息

(3) 清理所有生成的文件

复制代码
make clean

会删除:

  • *.o
  • *.a
  • stdc/ 目录
  • stdc.tgz

(4)打包发布(给别人用)

复制代码
make output

会生成结构:

复制代码
stdc/
├── include/
│   ├── my_stdio.h
│   └── my_string.h
└── lib/
    └── libmystdio.a

并打包成 stdc.tgz

总结:

这个 Makefile 就是:编译 .c → 生成 .o → 打包成静态库 .a → 可清理、可打包发布

2.2静态库使⽤

main.c 测试代码

复制代码
// main.c:测试我们自制静态库 libmystdio.a 的测试程序
// 功能:1. 调用自制my_strlen计算字符串长度;2. 调用自制文件库往日志文件追加写入字符串3次

// 引入自制静态库的自定义文件操作头文件
#include "my_stdio.h"
// 引入自制静态库的自定义字符串函数头文件
#include "my_string.h"
// 引入系统标准库,用于printf控制台打印
#include <stdio.h>

int main()
{
    // 定义测试字符串常量
    const char *s = "abcdefg";
    // 调用自制my_strlen函数,打印字符串和它的长度
    printf("%s: %d\n", s, my_strlen(s));

    // 调用自制mfopen,以追加模式"a"打开 ./log.txt 日志文件,返回自定义mFILE文件指针
    mFILE *fp = mfopen("./log.txt", "a");
    // 文件打开失败判断:返回NULL代表打开出错,直接异常退出程序,返回错误码1
    if(fp == NULL) return 1;

    // 调用自制mfwrite,把字符串s写入自定义缓冲区,写入长度为字符串长度
    mfwrite(s, my_strlen(s), fp);
    // 重复写入第2次
    mfwrite(s, my_strlen(s), fp);
    // 重复写入第3次
    mfwrite(s, my_strlen(s), fp);

    // 关闭自定义文件流:内部会先刷新缓冲区,再关闭系统文件描述符,释放资源
    mfclose(fp);

    // 程序正常结束,返回0
    return 0;
}

三种链接静态库编译命令 + 逐行注释

gcc 编译三关键参数

  • -I路径:告诉编译器去哪里找 .h 头文件
  • -L路径:告诉链接器去哪里找 .a/.so 库文件
  • -l库名:指定要链接的库;库文件名规则:libxxx.a / libxxx.so,使用时只写中间xxx
  1. 静态库核心特性 编译链接阶段会把静态库的代码完整复制进最终可执行程序;生成程序后,原静态库文件可以删除,程序依然能独立运行。
  2. -static:强制全程静态链接(后续补充)
  3. Linux 下库文件统一命名规范: lib + 库本名 + .a(静态库) / .so(动态库) 使用 gcc 的 -l 参数链接库时: 要删掉开头lib、末尾.a/.so,只写中间的「库本名」

比如编译链接时不能写 -llibmystdio.a,正确写法:

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

// 场景1:静态库头文件(.h)、静态库(.a)已经安装到系统默认搜索路径
// -lmystdio:链接名为libmystdio.a的静态库,gcc识别规则:-lxxx 对应 libxxx.a
$ gcc main.c -lmystdio

// 场景2:头文件、静态库和当前main.c在同一个文件夹(当前目录.)
// -L . :指定库文件搜索路径为当前目录,让gcc能找到libmystdio.a
$ gcc main.c -L . -lmystdio

// 场景3:头文件、静态库存放在单独自定义文件夹,不在系统/当前目录
// -I头文件路径:指定头文件(.h)的搜索目录,解决#include "xxx.h"找不到文件问题
// -L库文件路径:指定静态库(.a)的搜索目录
// -lmystdio:链接libmystdio.a静态库
$ gcc main.c -I头文件路径 -L库文件路径 -lmystdio

解释:

(1)代码功能说明

main.c 是用来验证我们之前通过Makefile打包生成的静态库 libmystdio.a 的测试程序:

  1. 调用库内自定义字符串函数 my_strlen(),计算字符串长度并打印;
  2. 调用库内自定义文件操作函数 mfopen/mfwrite/mfclose,以追加模式打开日志文件,重复写入3遍测试字符串,完成自定义缓冲区IO读写。

(2)静态库链接编译参数含义

  1. -I:指定头文件搜索路径 ,编译器会去该目录查找#include引入的.h头文件;
  2. -L:指定库文件搜索路径 ,编译器链接阶段会去该目录查找.a静态库/.so动态库;
  3. -lxxx:链接名为libxxx.a的静态库,gcc自动补全前后缀,因此库libmystdio.a对应参数-lmystdio。

(3)三种编译场景区分

场景1:库安装至系统路径

系统自带头文件、库文件有默认搜索目录(如/usr/include、/usr/lib),把头文件复制到系统include、静态库复制到系统lib后,编译时无需额外-I、-L参数,直接-lmystdio即可链接。

场景2:库文件和源码同目录

头文件、libmystdio.a与main.c放在同一个文件夹,只需用-L .告诉编译器在当前目录查找静态库,无需-I(默认当前目录找头文件)。

场景3:库文件独立存放

头文件、静态库单独放在自定义文件夹(比如之前make output生成的stdc/include、stdc/lib),编译时必须同时用-I指定头文件目录、-L指定库目录,否则编译器会找不到头文件或静态库。

(4)完整使用流程串联

  1. 执行make,编译my_stdio.c、my_string.c生成.o目标文件,再打包出静态库libmystdio.a;
  2. 编写main.c,引入库的头文件,调用库内封装的函数;
  3. 根据头文件、静态库存放位置,选择对应gcc编译命令,链接静态库生成可执行程序;
  4. 运行生成的可执行文件,验证自定义库的文件读写、字符串计算功能。

3. 动态库

  • 动态库(.so):程序在运⾏的时候才去链接动态库的代码,多个程序共享使⽤库的代码。
  • ⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬ 标⽂件的整个机器码
  • 在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中, 这个过程称为动态链接(dynamiclinking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩,节省了磁盘空间。操作系统采 ⽤虚拟内存机制允许物理内存中的⼀份动态库被要⽤到该库的所有进程共⽤,节省了内存和磁盘空 间。

3.1动态库⽣成

复制代码
// Makefile:用于编译生成动态库 libmystdio.so
# 最终目标:生成动态库 libmystdio.so
# 依赖条件:必须先编译出 my_stdio.o、my_string.o 两个目标文件
libmystdio.so:my_stdio.o my_string.o
	# gcc -o $@ $^ -shared 生成动态库的核心命令
	# -o $@ :指定输出文件为当前目标 libmystdio.so
	# $^ :代表所有依赖文件 my_stdio.o my_string.o
	# -shared :gcc编译选项,作用是生成动态共享库 .so
	gcc -o $@ $^ -shared

# 模式匹配规则:所有 .c 源文件编译生成对应的 .o 目标文件
%.o:%.c
	# -fPIC :生成位置无关代码,这是制作动态库必须的参数
	# -c :只编译不链接,单独生成 .o 目标文件
	# $< :代表当前匹配到的 .c 源文件
	gcc -fPIC -c $<

# .PHONY:声明 clean 是伪目标,不是名为 clean 的文件
.PHONY:clean
# clean 清理规则:删除所有编译生成的中间文件与产物
clean:
	# 删除所有动态库*.so、目标文件*.o、stdc相关文件夹
	@rm -rf *.so *.o stdc*
	# 打印清理完成提示
	@echo "clean ... done"

# 声明 output 为伪目标,只是执行打包动作,不是文件
.PHONY:output
# output 打包规则:把头文件、动态库整理成标准发布包并压缩
output:
	# -p 目录不存在则创建,不会报错;创建存放头文件的目录 stdc/include
	@mkdir -p stdc/include
	# 创建存放动态库的目录 stdc/lib
	@mkdir -p stdc/lib
	# 复制所有头文件 .h 到发布包的 include 目录,-f 覆盖不提示
	@cp -f *.h stdc/include
	# 复制生成的动态库 .so 到发布包的 lib 目录
	@cp -f *.so stdc/lib
	# tar -czf 压缩命令,把stdc文件夹打包成 stdc.tgz 压缩包
	@tar -czf stdc.tgz stdc
	# 打印打包完成提示
	@echo "output stdc ... done"
  • shared:表⽰⽣成共享库格式
  • fPIC:产⽣位置⽆关码(positionindependentcode)
  • 库名规则:libxxx.so

3.2 动态库使⽤

复制代码
// 场景1:头文件和库文件安装到系统路径下
$ gcc main.c -lmystdio

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
$ gcc main.c -L. -lmystdio  // 从左到右搜索-L指定的目录

// 场景3:头文件和库文件有自己的独立路径
$ gcc main.c -I头文件路径 -L库文件路径 -lmystdio

// ldd命令:查看库或者可执行程序依赖哪些动态库
$ ldd libmystdio.so 
        linux-vdso.so.1 =>  (0x00007fffacbbf000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)

// 以场景2完整实操演示
// 1.查看当前目录全部文件
$ ll
total 24
-rwxr-xr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-r--r-- 1 whb whb  359 Oct 19 16:07 main.c
-rw-r--r-- 1 whb whb  447 Oct 29 14:50 my_stdio.h
-rw-r--r-- 1 whb whb  447 Oct 29 14:50 my_string.h

// 2.编译链接动态库生成可执行文件a.out
$ gcc main.c -L. -lmystdio

// 3.再次查看目录,多出可执行文件a.out
$ ll
total 36
-rwxr-xr-x 1 whb whb 8600 Oct 29 14:51 a.out
-rwxr-xr-x 1 whb whb 8592 Oct 29 14:50 libmystdio.so
-rw-r--r-- 1 whb whb  359 Oct 19 16:07 main.c
-rw-r--r-- 1 whb whb  447 Oct 29 14:50 my_stdio.h
-rw-r--r-- 1 whb whb  447 Oct 29 14:50 my_string.h

// 4.运行生成的可执行程序
$ ./a.out

3.3库运⾏搜索路径

3.3.1 问题

执行命令查看程序依赖

复制代码
$ ldd a.out
        linux-vdso.so.1 =>  (0x00007fff4d396000)
        libmystdio.so => not found  # 核心报错:找不到我们自己写的动态库
        libc.so.6 => /lib64/libc.so.6 (0x00007fa2ef300000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa2ef900000)

编译代码的时候我们用 -L. 告诉编译器「当前文件夹能找到 libmystdio.so」,编译成功了。 但运行程序 ./a.out 的时候,操作系统会重新去系统规定的固定文件夹里找动态库 ,不会自动识别当前目录的 .so 文件,于是就提示 not found,程序直接运行失败。

3.3.2 四种通俗解决方案
方案 1:直接把动态库复制到系统默认库目录

系统自带一套默认的库文件夹,操作系统运行程序时会自动去这里检索动态库。 常用系统库目录:/usr/lib、/usr/local/lib、/lib64 操作示例:

复制代码
cp libmystdio.so /usr/lib

优点:一劳永逸,所有程序运行时都能找到这个库; 缺点:需要 root 管理员权限,会污染系统自带库文件夹。

方案 2:在系统库目录创建软链接(快捷方式)

不用复制库文件本体,只在系统默认库文件夹创建一个指向我们 .so 文件的快捷方式,系统检索时顺着链接找到真实库文件。

方案 3:临时环境变量 LD_LIBRARY_PATH(临时生效,推荐测试用)

临时告诉操作系统:额外去某个自定义文件夹找动态库,只在当前终端窗口生效,关闭终端就失效。

方案 4:ldconfig 永久配置(永久生效,企业常用)
  1. 在 /etc/ld.so.conf.d/ 目录下新建一个配置文件(示例 bit.conf)

  2. 在文件里写入我们存放动态库的文件夹路径

    查看我们写好的库路径配置

    [root@localhost linux]# cat /etc/ld.so.conf.d/bit.conf
    /root/tools/linux

  3. 执行 ldconfig 命令,让系统重新加载所有库检索路径,配置永久生效

    [root@localhost linux]# ldconfig

优点:永久生效,所有终端、所有用户运行程序都能识别动态库; 缺点:需要 root 管理员权限。

编译阶段靠 -L 参数告诉编译器库在哪; 运行阶段操作系统不会看编译参数,只会走系统固定的库检索规则 ,找不到动态库就运行报错,上面 4 种方法都是用来让操作系统运行时能定位到 .so 文件。

4. 使⽤外部库

我们现在没接触过太多的库,唯⼀接触过的就是C、C++标准库,这⾥我们可以推荐⼀个好玩的图形 库:ncurses

复制代码
// 安装

// Centos
$ sudo yum install -y ncurses-devel

// ubuntu
$ sudo apt install -y libncurses-dev

系统中其实有很多库,它们通常由⼀组互相关联的⽤来完成某项常⻅⼯作的函数构成。⽐如⽤来处理 屏幕显⽰情况的函数(ncurses库)

推荐⼀篇不错的使⽤指南:ncurse编程指南_ncurses教程-CSDN博客

又是美好的一天,各位观众姥爷们,若有什么不懂的地方,欢迎在评论区留言哦,我会给出相应解释的。如有什么疏忽的细节,请点出,我会及时纠正。感谢观看。

相关推荐
Android系统攻城狮5 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_delay调用流程与实战(七十二)
linux·运维·服务器·gstreamer音视频·音视频进阶
快乐的韭菜根根5 小时前
ubuntu 安装slurm并配置
linux·运维·ubuntu
Navigator_Z6 小时前
LeetCode //C - 1289. Minimum Falling Path Sum II
c语言·算法·leetcode
himobrinehacken6 小时前
取模运算的汇编奥秘揭秘
c语言·安全
倒头就睡的小比特7 小时前
Linux基础指令3
linux·服务器
云计算练习生8 小时前
什么是内存映射(mmap)?文件和内存之间的桥梁
操作系统·内存·内存映射
梦帮科技8 小时前
Kali Linux Scapy 畸形报文构造学——深度解剖非法 TCP Flags 与反常序列号
linux·爬虫·神经网络·算法·数学建模·系统架构·模拟退火算法
半杯咖啡半行码9 小时前
Linux 驱动开发入门:内核模块编程基础
linux·服务器·网络
别动我齐刘海9 小时前
Linux 系统调试学习2
linux·运维·c++·人工智能·学习·机器学习·机器人
青岚_Nox9 小时前
C语言实战:带你手写一个带"计时功能"的猜数字小游戏
c语言