🌈 个人主页:Zfox_
🔥 系列专栏:Linux

一:🔥 动静态库基本原理

🦁 动静态库是可执行程序的“半成品”

所有的库本质就是可重定向二进制文件的集合,即目标文件的集合,其中包含了大量的执行方法

使用ldd 可执行程序命令查看可执行程序依赖的动静态库文件

二:🔥 认识动静态库

在 windows 中动态库以 .dll 为后缀,静态库以 .lib 为后缀。

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
  • 在Linux中动态库以.so为后缀,静态库以.a为后缀。库的命名有规范,一个库的名字去掉前缀lib,去掉后缀.so或者.a剩下的就是库的名字。例如libstdc++.so.6的库名字就是stdc++

  • 在使用gcc/g++生成可执行程序的时候,默认情况下生成的可执行程序就是动态链接动态库,如果想要静态链接静态库的话,需要使用 - static选项来静态编译生成可执行程序。

三:🔥 动静态库的优缺点

🦋 静态库

缺点

  • 浪费空间
  • 占磁盘空间,因为静态链接是将静态库中的代码加载到可执行程序中,所以静态编译的文件自己体积比较大。
  • 占内存空间,多个静态文件链接同一个静态库的时候,内存中会出现大量的重复代码

优点

  • 与库文件弱相关,不需要依赖库文件
  • 由于静态编译的时候,已经将静态库中的代码都放到可执行程序的正文代码中了,所以可执行程序本身是一个独立的文件

🦋 动态库

缺点

  • 与库文件强相关,必须依赖库文件
  • 如果将一个动态库文件从内存和磁盘中删掉,这个动态库链接的可执行程序就都不能运行了

优点

  • 节省空间
  • 节省内存空间,动态库文件在内存中只有一份,通过页表映射到不同进程的进程地址空间中的共享区共享。在静态链接的文件中,静态库的代码直接映射进了进程地址空间的正文代码中。

四:🔥 如何制作和使用动静态库?

🦁 我们在给被人静态库的时候,需要把 .a/.so 库文件和 .h 头文件给到对方。.a/.so 让别人可以调用库中的函数,.h 告诉别人库中有哪些函数可以被调用,相当于函数使用的说明书。

  • 在使用第三方库的时候,需要指明库的名称

🦁 假设有四个文件 my_stdio.c my_stdio.h my_string.c my_string.h,我们需要通过它们制作一个动静态库。

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

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;
   // 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 mclose(mFILE *stream);

my_stdio.c

#include "my_stdio.h"
#include <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_TRUNC | O_WRONLY, 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 mclose(mFILE *stream)
{
    if(stream->size > 0)
    {
        mfflush(stream);
        stream->size = 0;
    }
    close(stream->fileno);
}

my_string.h

#pragma once

int my_strlen(const char *s);

my_string.c

#include "my_string.h"

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

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"


# 静态库
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"

🦋 制作静态库

  1. 编译同名的目标文件
gcc -c my_stdio.c
gcc -c my_string.c
  1. 生成静态库
  • 生成一个叫做 mystdio 的静态库需要使用 ar 命令,其中 -r 选项表示 replace,-c 表示 create
ar -rc libmystdio.a my_stdio.o my_string.o
  • mystdio 静态库前后需要加上lib和.a,使用的时候需省略

🦋 使用静态库

🥝 1.安装到系统里面

sudo cp *.h /usr/include/
sudo cp libmystdio.a /lib64/
  • 当我们将头文件和库文件安装到系统里的时候 我们只需使用 -l 命令告诉 gcc 我们使用的库是什么即可 注意去掉 lib 和 .a

🥝 2.手动调用我们自己的静态库

调用的函数的库文件在哪

  • 使用 -L + 库文件的路径(告诉编译器,在我指明的路径下找)

调用的库的名称

  • 使用 -l + 库的名称
gcc main.c -o main -L. -lmystdio

在这里插入图片描述

🥝 3.使用带路径的库

.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"
  • 此时我们使用 makefile 将头文件和库文件分门别类放在 stdc 的目录下
root@hcss-ecs-a9ee:~/code/linux/112/lesson22/stdio# tree stdc
stdc
├── include
│   ├── my_stdio.h
│   └── my_string.h
└── lib
    └── libmystdio.a

2 directories, 3 files

💦 调用的函数的头文件在哪

  • 使用 -I + 头文件的路径

💦 调用的函数的库文件在哪

  • 使用 -L + 库文件的路径

💦 调用的库的名称

  • 使用 -l + 库的名称
gcc main.c -Istdc/include -Lstdc/lib -lmystdio

在这里插入图片描述

🦋 制作动态库

  1. 生成目标文件

在生成同名的目标文件的时候,需要加上-fPIC选项,即产生与位置无关码,这样就可以让可执行程序找到对应的动态库。

gcc -fPIC -c my_stdio.c
gcc -fPIC -c my_string.c
  1. 生成动态库
    在打包动态库的时候,需要加上 -shared 选项形成库文件
gcc -o libmystdio.so my_stdio.o my_string.o  -shared

🦋 使用动态库

🥝 1.安装到系统里面

sudo cp *.h /usr/include/
sudo cp libmystdio.so /lib64/
  • 当我们将头文件和库文件安装到系统里的时候 我们只需使用 -l 命令告诉 gcc 我们使用的库是什么即可 注意去掉 lib 和 .a
gcc main.c -lmystdio

在这里插入图片描述

🥝 2.手动调用我们自己的动态库

当我们有了头文件和库文件的时候不安装到系统里

调用的函数的库文件在哪

  • 使用 -L + 库文件的路径(告诉编译器,在我指明的路径下找)
    调用的库的名称
  • 使用 -l + 库的名称
gcc main.c -o main -L. -lmystdio

在这里插入图片描述

🥝 3.使用带路径的库

.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"
  • 此时我们使用 makefile 将头文件和库文件分门别类放在stdc的目录下
root@hcss-ecs-a9ee:~/code/linux/112/lesson22/stdio# tree stdc
stdc
├── include
│   ├── my_stdio.h
│   └── my_string.h
└── lib
    └── libmystdio.so

2 directories, 3 files

💦 调用的函数的头文件在哪

  • 使用 -I + 头文件的路径

💦 调用的函数的库文件在哪

  • 使用 -L + 库文件的路径

💦 调用的库的名称

  • 使用 -l + 库的名称
gcc main.c -Istdc/include -Lstdc/lib -lmystdio

在这里插入图片描述

gcc main.c -lmystdio

五:🔥 动态库报错

🦁 这里也是需要特别注意:当运行可执行文件时,我们会发现找不到动态库 。这也就是动态库的特别之处,上述步骤跟静态库操作几乎一样,这里开始就发生转折。

./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory

造成错误原因,在 gcc 下已形成可执行文件,程序编译已完成,执行文件的过程是和 gcc 无关,程序运行是操作系统管理,所以操作系统也需要找到动态库,总结就是:库文件没有在系统路径下,操作系统无法找到。

🦋 解决方法

Ubuntu 自身系统设定的相应的设置的原因,即其只在 /lib and /usr/lib 下搜索对应 的.so 文件,故需将对应 so 文件拷贝到对应路径。 centos则是 /lib/64

  1. 在 /lib/ 下建立软链接
sudo ln -s ~/code/linux/112/lesson22/stdio/libmystdio.so /lib/libmystdio.so
  1. 使用的时候需要导入.so动态库的路径 export LD_LIBRARY_PATH=
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库的绝对路径

这里有需要注意:添加环境变量后,默认只在本次登录有效,下次登录时无效(默认清理登录前一次添加环境变量)。

  • 在 ~/ 下 有一个 .bashrc 隐藏文件,我们只需要将命令添加进来即可永久保存
  1. 直接拷贝动态库到系统目录下 /lib/64 /usr/lib64

  2. /etc/ld.so.conf.d/ 目录下建立自己的 xxx.conf 的配置文件,把动态库的路径放进去,然后 ldconfig

  • ldconfig 命令用于更新共享库的缓存。你需要运行此命令以使新的配置文件生效。

六:🔥 动态库的加载

静态库不需要加载

这个过程,在磁盘中 main.c 和 lib.c-库,会先在磁盘中形成一段代码,然后对这段代码进行编译,编译的本质就是预处理,编译-查找错误,形成二进制代码,然后进行汇编形成二级制指令。在编译阶段的时候就已经形成了虚拟地址空间。在虚拟地址空间中,这段代码也就被存入代码区,这个是根据不同区的特性所决定的。当执行这段代码的时候,操作系统就会直接在代码区进行访问。

在这里插入图片描述

动态库加载

🦁 动态库加载的过程,在磁盘中有一个 my.exe (可执行)和 lib.so(动态库),在形成可执行之前,编译阶段时,我们用到了 fPIC :产生位置无关码。

gcc -c -fPIC my_add.c

在这个阶段,动态库会将指定的函数地址,写入到可执行文件中。这个地址可以理解成 my_add.c (地址) + 偏移地址。

形成好可执行文件之后,磁盘将可执行文件拷贝到内存中,内存通过页表映射到虚拟地址空间的代码区中,当 OS 执行程序时,扫描到 my_add.c 是需要调用动态库的时候,程序会停下来,OS 会再通过函数的地址,然后页表映射去内存到磁盘中找动态库中,找到后拷贝到内存,又通过页表映射到共享区中。OS 再去共享区调用该方法,然后向下执行程序。
在这里插入图片描述

七:🔥 共勉

以上就是我对 【Linux】动静态库:构建强大软件生态的基石 的理解,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉
在这里插入图片描述

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐