Linux:动静态库

发布于:2024-05-13 ⋅ 阅读:(164) ⋅ 点赞:(0)


链接

在C语言内部,会调用很多库函数,比如printfscanf等等。那么C语言要如何拿到这个函数,并调用它呢?这就涉及到链接的过程。

链接的过程,就是把可执行程序与众多库关联起来的过程,此时就可以调用外部的函数,使用外部的变量等等。

在链接到库时,库分为两种:动态库静态库。通过动态库实现的链接,叫做动态链接,通过静态库实现的链接叫做静态链接

先简单讲解下两个基本指令lddfile,可以用这两个指令来观察动静态库

ldd

ldd指令用来查看一个可执行文件动态链接了那些库

ldd 文件名
  • 使用ldd观察一个动态链接的文件:

在这里插入图片描述

动态库文件一般带有.so后缀

比如第二行的libc.so就是C语言的标准库,一个静态库的真实名称是:删掉头部的lib和尾部的.so。比如libc.so.6这个动态库的真实名称是c

  • 使用ldd观察一个静态连接的文件:

在这里插入图片描述

此时就会输出not a dynamic executable,即不是一个动态链接的可执行文件。


file

file指令用来查看一个可执行文件的链接情况

  • 使用file指令观察一个静态链接的文件:

在这里插入图片描述

statically linked字段就表示这是一个静态链接的程序。

  • 使用file指令观察一个动态链接的文件:

在这里插入图片描述

dynamically linked字段就表示这是一个动态链接的程序。


库的链接

静态链接

  • 静态链接:编译链接时,把库文件的代码全部拷贝到可执行文件中,静态库文件的后缀为.a

优点:只要形成了可执行文件,那么就脱离对库的依赖,可以自主运行,可移植性好
缺点:相同的资源拷贝多份,浪费资源,生成的文件比较大

如果我们想要生成静态链接的文件,在gcc时额外加上选项-static

gcc -static -o test.exe test.c 

这样test.exe文件就是一个静态链接的文件了。

现在我将test.c通过静态链接编译成sta-test.exe,并对其使用ldd

在这里插入图片描述

我们得到了一个sta-test.exe可执行文件,通过ldd可以知道这确实是一个动态链接的程序。

其有一个特点,那就是占用的空间非常大,因为静态链接把包含的头文件,都在文件内部拷贝了一份。


动态链接

  • 动态链接:在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,编译器会提供动态库的地址,当程序执行到指定的代码段,就会去动态库内部查找对应的内容。

优点:节省资源,整个操作系统所有使用动态库的程序,只需要一份库文件,内存中也只加载一份
缺点:一旦丢失,所有链接该库的程序都无法执行了

直接使用gcc,不加任何额外选项,就是动态链接:

gcc -o test.exe test.c 

也就是说:动态链接是编译器的默认行为

现在我将test.c通过静态链接编译成dyn-test.exe,并对其使用ldd

在这里插入图片描述

成功创建dyn-test.exe后,通过ldd可以看出这是一个动态链接程序。相比于刚刚静态链接的sta-test.exe,这个动态链接的文件明显小了很多。


库的封装

接下来我们看看库是如何封装出来的,也就是如何制作出动态库与静态库。

讲解这两个库的封装前,先来看看同时编译多个文件的方式:

当前目录下有以下文件:

在这里插入图片描述

现在要在test.c中使用myMath.cmyHello.c的函数,myMath.cmyHello.c如下:

myMath.c

#include "myMath.h"

void add(int x, int y)
{
    printf("%d + %d = %d\n", x, y, x + y);
}

void sub(int x, int y)
{
    printf("%d + %d = %d\n", x, y, x - y);
}

myHello.c

#include "myHello.h"

void HelloWorld()
{
    printf("Hello World!\n");
}

void HelloLinux()
{
    printf("Hello Linux!\n");
}

两个头文件myMath.hmyHello.h如下:

myMath.h

#pragma once
#include <stdio.h>

void add(int x, int y);
void sub(int x, int y);

myHello.h

#pragma once
#include <stdio.h>

void HelloWorld();
void HelloLinux();

简单来说就是要通过头文件.h,链接到两个.c文件内部的函数:

在这里插入图片描述

test.c中:

#include "myHello.h"
#include "myMath.h"

int main()
{
    add(3, 5);
    sub(100, 88);

    HelloLinux();
    HelloWorld();

    return 0;
}

首先引入了两个.h头文件,这样编译时就可以通过头文件内部的声明,到对应的.c文件中找到所需的函数了。

通过指令gcc -o test.exe test.c myMath.c myHello.c同时编译三个.c文件:

在这里插入图片描述

这样我们就实现了多个文件分开实现函数,最后让一个.c文件可以调用其它文件内部的函数了。

但是一般来说,我们不会直接把.c文件交给别人,因为.c内部是源代码,源代码是不能随意暴露的。

因为多个文件之间的相互,是发生在链接这个过程的,而链接的前一步文件状态是.o目标文件。因此我们可以先把.c通过编译变成.o文件,此后再进行链接,同样可以链接成功。

在这里插入图片描述

相比于给出.c文件,.o文件内部已经是二进制了,无法直接看到源代码,因此可以保护代码。

可以通过gcc-c选项将一个.c文件编译为.o文件:

在这里插入图片描述

将两个.o文件与test.c一起编译试试看:

在这里插入图片描述

通过指令gcc -o test.exe myMath.o myHello.o test.c我们成功把test.exe编译了出来,并成功执行了。

以上就是一个基本的多文件编译场景,但是大家不妨想一个问题,当引入的外部文件变多了,那这个gcc指令后面不是要不停的加更多的文件名?这会导致编译变得很麻烦,每次都要手动链接很多外部文件。

因此我们决定把所有的.o文件进行打包,此时这个包就叫做

在这里插入图片描述

现在我们就知道了是如何出现的了,那么我们要如何把这些文件打包为库呢?

在博客开头就说明过,库分为动态库和静态库,这两种库的封装是不同的,接下来一一讲解。


静态库

封装静态库需要用到指令ar,打包静态库指令为:

ar -rc 静态库名 文件名 
  • -rreplace,如果该库原先存在,则覆盖原先的库
  • -ccreate,如果该库原先不存在,则创建

通过指令把myHello.omyMath.o封装为静态库libMyC.a

在这里插入图片描述

此时当前目录下就多出了一个libMyC,a的静态库。

现在在gcc编译时,链接到静态库libMyC,a

gcc -o test.exe test.c -l MyC -L ./
  • -l:指明要连接的库,注意:libMyC.a的真实名称要去掉前面的lib和后缀.a
  • -L:指明连接到的库所处的路径

执行指令后,运行test.exe程序:

在这里插入图片描述

我们确实通过这个静态库成功编译了test.exe,最后也成功执行了test.exe,说明我们对库的封装是没问题的。


动态库

动态库的封装比较麻烦,从生成.o文件时,就要开始准备动态库。

如果想把.o文件封装为动态库,那么这个.o文件中需要有位置无关码,至于这个位置无关码的作用,在稍后会讲解。

gcc -c时,额外加上一个-fPIC选项,即可生成位置无关码,此时还会自动生成同名.a文件

当前目录下没有任何.o文件:

在这里插入图片描述

现在要通过-fPIC生成位置无关码,指令如下:

gcc -c -fPIC myMath.c
gcc -c -fPIC myHello.c

以上指令中,不需要通过-o来指定生成的文件名,因为会自动生成同名,o文件。

执行结果:

在这里插入图片描述

目录中多出两个.o文件,这两个文件就是带有位置无关码的目标文件了。

封装动态库需要用到gcc-shared选项:

gcc -shared -o libMyC.so myMath.o myHello.o

执行结果:

在这里插入图片描述

这条指令的功能就是:利用myHello.omyMath.o生成动态库libMyC.so

同样的,现在再次编译test.c

gcc -o test.exe test.c -l MyC -L ./

同样的,要通过-l来指定动态库,通过-L指定库的路径。

输出结果:在这里插入图片描述

可以看到,我们成功编译了test.exe出来,但是为什么无法执行呢?

使用dll查看一下test.exe的动态库:

在这里插入图片描述

其中libMyC.so显示为not found,为什么找不到呢?我们不是在编译时指明了动态库的路径是./即当前目录吗?

gcc -o test.exe test.c -l MyC -L ./时,是为编译器指明了动态库的位置,但是程序运行时,依然不知道动态库的位置,程序运行时只会去系统指定的目录下找动态库

解决方案就是,让这个静态库的路径进入到系统默认的库中。

  1. 修改环境变量LD_LIBRARY_PATH

环境变量LD_LIBRARY_PATH内部存储了一些路径,其用于辅助动态库查找,被记录到这个环境变量中的路径,都会变成默认的动态库查询路径。

我的当前路径为/home/box-he/CSDN/lib,执行指令:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/box-he/CSDN/lib

把当前路径写到环境变量中,现在就可以正常运行test.exe了:

在这里插入图片描述

  1. /lib64中创建软链接

/lib64是一个默认的动态库存储目录,在这个目录内部,创建一个指向自己的动态库libMyC.so的软链接,就可以把自己的动态库放进默认的动态库了。

我的当前路径为/home/box-he/CSDN/lib,执行指令:

sudo ln -s /home/box-he/CSDN/lib/libMyC.so /lib64/libMyC.so

此时在lib64内部就有一个libMyC.so的软链接了。

执行结果:

在这里插入图片描述

经过软链接后,在/lib64下就多出一个软链接文件,随后test.exe也可以正常执行了。

  1. 修改配置文件

在目录/etc/ld.so.conf.d/下,允许自定义用户级的配置文件,我们随便在这个目录下创建一个以.conf结尾的文件,然后再在文件内部写上动态库的路径即可,这个过程需要root权限。

先创建一个test.conf文件在该目录下:

在这里插入图片描述

在其内部写入动态库的路径:

在这里插入图片描述

我在内部写了/home/box-he/CSDN/lib/libMyC.so,随后test.exe就可以正常执行了。


库的加载

动态库与静态库的加载,符合以下规则:

  1. 如果同时提供动态库和静态库,默认使用动态库
  2. 如果只提供静态库,那么这个库使用静态库,其他库使用动态库
  3. 如果使用-static选项,所有库都尽量使用静态库
  4. 如果使用-static选项,只有静态库而没有动态库,会报错

接下来我们从操作系统角度再来看看库是如何加载的。

对于静态库而言,其简单粗暴地把静态库的内容拷贝到自己的代码段中,此后静态库的代码就相当于进程自己的代码

而对于动态库,这个过程就比较复杂了,接下来主要讲解动态库的加载。

动态库在整个内存中只加载一份,所有调用动态库的进程共享动态库

因此动态库也叫做共享库

先看看CPU是如何执行进程的代码的:

在这里插入图片描述

首先CPU的一个叫做PC指针的寄存器,会指向要被执行的代码地址,不过这个地址是虚拟地址。于是CPU拿着虚拟地址去找页表,然后得到对应的物理地址,从而访问到内存中的代码,进而执行。

也就是说,执行代码的过程,需要拿到代码的虚拟地址,再通过页表拿到物理地址,最后执行代码。

动态库内部本质也都是代码,那么问题就是,动态库代码的虚拟地址在哪里?动态库代码的物理地址在哪里?

虚拟地址是在编译的时候就产生的,也就是说在程序执行前,就已经分配好了虚拟地址。当一个进程被加载到内存中时,会带着自己的虚拟地址,代码和数据等一起进入,随后拿虚拟地址去初始化进程地址空间,页表等等。

页表也要去创建自己的虚拟地址到物理地址的映射关系。此时就同时有了物理地址和虚拟地址了。

动态库的代码不是直接通过虚拟地址到物理地址的映射来访问的,而是通过起始地址 + 偏移量的方式来访问的

在这里插入图片描述

在可执行程序的文件中,会给动态库预留一个起始地址的虚拟地址,随后通过页表去内存中找到那个动态库的物理地址,随后创建映射关系。而动态库内部会有很多函数,此时所有函数都以起始地址为标准,在可执行文件中存储的是偏移量。当要访问动态库的函数时,就根据动态库的起始地址与偏移量,来获取函数的物理地址,进而访问到函数。

因为动态库在内存中的位置是随机的,每次都可能不一样,而函数之间的偏移量是确定的,因此使用这种偏移量的方式,只需要确定动态库的起始地址,就知道整个动态库所有函数的地址。