【Linux】库制作与原理

文章目录

  • 前言
  • [一. 什么是库](#一. 什么是库)
  • [二. 静态库](#二. 静态库)
    • [2.1 静态库的制作](#2.1 静态库的制作)
    • [2.2 静态库的使用](#2.2 静态库的使用)
  • [三. 动态库](#三. 动态库)
    • [3.1 动态库的制作](#3.1 动态库的制作)
    • [3.2 动态库的使用](#3.2 动态库的使用)
    • [3.3 库运行搜索路径](#3.3 库运行搜索路径)
  • [四. 目标文件](#四. 目标文件)
  • [五. ELF文件](#五. ELF文件)
    • [5.1 ELF文件组成](#5.1 ELF文件组成)
    • [5.2 ELF形成可执行程序](#5.2 ELF形成可执行程序)
    • [5.3 ELF可执行文件的加载](#5.3 ELF可执行文件的加载)
  • [六. 链接与加载](#六. 链接与加载)
    • [6.1 静态链接](#6.1 静态链接)
    • [6.2 ELF加载与进程地址空间](#6.2 ELF加载与进程地址空间)
    • [6.3 动态链接](#6.3 动态链接)
    • [6.4 总结](#6.4 总结)
  • 最后

前言

在上一篇文章中,我们详细介绍了文件系统的内容,那么本篇文章将带大家详细讲解库制作与原理的内容,接下来一起看看吧!


一. 什么是库

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

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:

  • 静态库:.a[Linux].lib[windows]
  • 动态库:.so[Linux].dll[windows]

库的命名规则:

库的命名都是以 lib 开头,.a/.so 为后缀;去掉前缀 lib 和后缀 .a/.so 剩下的部分才是库的名字。

例如 C 标准库 libc.so,去掉前缀和后缀,c 就是库的名字。

我们知道gcc/g++在编译时默认使用的是动态链接,若想要进行静态链接就要带-static选项。

  • 静态链接,本质上就是程序在编译链接时,将静态库的内容链接到可执行文件中,这样可执行程序在执行时就不会再依赖库;但是静态链接的可执行文件会比较大。
  • 动态链接:本质上就是程序在编译链接时,在可执行文件和动态库之间建立某种关联,这样可执行程序在执行时就会依赖动态库。

二. 静态库

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

2.1 静态库的制作

接下来我们尝试一下制作静态库:

mystdio.h

c 复制代码
#pragma once                                                                             
    
#include <stdio.h>    
    
#define MAX 1024    
#define NONE_FLUSH (1<<0)    
#define LINE_FLUSH (1<<1)    
#define FULL_FLUSH (1<<2)    
    
typedef struct IO_FILE    
{    
    int fileno;    
    int flag;    
    char outbuffer[MAX];    
    int bufferlen;    
    int flush_method;    
}MyFile;    
    
MyFile* MyFopen(const char* path, const char* mode);    
void MyFclose(MyFile*);    
int MyFwrite(MyFile* , void* str, int len);    
ssize_t MyFread(void* str, size_t len, MyFile*);    
void MyFFlush(MyFile*);

mystdio.c

c 复制代码
#include "mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

static MyFile* BuyFile(int fd, int flag)
{
    MyFile* f = (MyFile*)malloc(sizeof(MyFile));
    if(f == NULL) return NULL;
    f->bufferlen = 0;
    f->fileno = fd;
    f->flag = flag;
    f->flush_method = LINE_FLUSH;
    memset(f->outbuffer, 0, sizeof(f->outbuffer));
    return f;
}

MyFile* MyFopen(const char* path, const char* mode)
{
    int fd = -1;
    int flag = 0;
    if(strcmp(mode, "w") == 0)
    {
        flag = O_CREAT | O_WRONLY | O_TRUNC;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flag = O_CREAT | O_WRONLY | O_APPEND;
        fd = open(path, flag, 0666);
    }
    else if(strcmp(mode, "r") == 0)                                                                                                                                                 
    {
        flag = O_RDWR;
        fd = open(path, flag);    
    }
    if(fd < 0) return NULL;
    return BuyFile(fd, flag);
}                                                                                                                                                                                   
void MyFclose(MyFile* file)
{
    if(file->fileno < 0) return;
    MyFFlush(file);
    close(file->fileno);
    free(file);
}
int MyFwrite(MyFile* file, void* str, int len)
{
    // 1. 拷贝
    memcpy(file->outbuffer+file->bufferlen, str, len);
    file->bufferlen+=len;
    // 2.尝试判断是否满足刷新条件
    if((file->flush_method & LINE_FLUSH) && file->outbuffer[file->bufferlen-1] == '\n')
    {
        MyFFlush(file);
    }
    return 0;
}
ssize_t MyFread(void* str, size_t len, MyFile* file)
{
    return read(file->fileno, str, len);
}
void MyFFlush(MyFile* file)
{
    if(file->bufferlen <= 0) return;
    // 把数据从用户拷贝到内核文件缓冲区中
    int n = write(file->fileno, file->outbuffer, file->bufferlen);
    (void)n;
    fsync(file->fileno);
    file->bufferlen = 0;
}

mystring.h和mystring.c

c 复制代码
// mystring.h
#pragma once    
    
int my_strlen(const char* s);

// mystring.c
#include "mystring.h"    
                                                                                                                                                                                    
int my_strlen(const char* s)    
{    
    const char* start = s;    
    while(*s)    
    {    
        ++s;    
    }    
    return s-start;    
}

静态库是.o文件的归档文件,所以在制作库时,要将所有的.c文件编译形成.o文件

有了.o文件,现在就要将这些.o文件进行归档形成静态库;

这里就要使用指令ar -rc(其中argnu归档工具,-rc表示replacecreate

  • t:列出静态库中的文件
  • v:verbose 详细信息

2.2 静态库的使用

usercode.c

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

int main()    
{    
    MyFile* filep = MyFopen("./log.txt", "w");    
    if(!filep)    
    {    
        printf("fopen error!\n");    
        return 1;    
    }    
    
    int cnt = 10;    
    while(cnt--)    
    {    
        char* msg = (char*)"hello myfile!!!\n";    
        MyFwrite(filep, msg, strlen(msg));    
        MyFFlush(filep);    
        printf("buffer: %s", filep->outbuffer);    
        sleep(1);    
    }    
    const char* s = "hello myfile!!!\n";    
    printf("strlen: %d\n", my_strlen(s));    
    MyFclose(filep);    
    return 0;                                                                                                                                                                       
}

我们直接运行是不行的,找不到指定的函数。

所以我们要指明要链接哪一个库,还要指明链接库的路径。

  • -L:指定库的路径
  • -l:指定库名
powershell 复制代码
gcc 源文件 -L库的路径 -l库名

如果我们的头文件不在当前路径下怎么办?

我们把静态库和头文件分别放在./bin/lib./bin/include路径下

在编译时gcc在当前路径下找不到头文件,就会报错:

  • -I:指定头文件搜索路径

解决办法:

  1. 在gcc编译时带上-I选项,指明头文件的路径。
  2. 在源文件引用头文件时,指明路径:#include "./bin/include/mystdio.h"#include "./bin/include/mystring.h"

三. 动态库

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

3.1 动态库的制作

生成动态库,也是要先将所有的.c文件编译形成.o文件,与生成静态库不同的是,生成动态库在编译形成.o文件是需要带-fPIC选项,产生位置无关码。

其次,就是将这些.o文件链接形成动态库,使用的是gcc,-shared选项

powershell 复制代码
gcc -o libmyc.so *.o -shared

3.2 动态库的使用

我们像链接静态库一样去链接动态库。

编译链接形成了可执行程序a.out,为什么在运行时它找不到动态库libmyc.so了呢?

通过ldd查看a.out可执行程序依赖的库,可以发现确实找不到libmyc.so库。

因为gcc编译的-L选项指明libmyc.so的路径,这是告诉gcc我们要链接的库在哪,但是操作系统在执行程序并不知道我们的动态库在哪里了;

因为这里是动态链接,在可执行程序执行时,操作系统就会去找动态库libmyc.so,就会发现它找不到这个库。

3.3 库运行搜索路径

动态链接我们自己的库,在可执行程序运行时,系统却找不到我们的库,那如何解决这一问题呢?

  1. 拷贝 .so 文件到系统共享库路径下,一般指 /usr/lib/usr/local/lib/lib64 等等
  1. 向系统共享库路径下建立同名软链接
  1. 更改环境变量: LD_LIBRARY_PATH
  1. ldconfig方案:配置/etc/ld.so.conf.d/ldconfig更新


最后不要忘了ldconfig更新(需要提权),这样操作系统才能找到指定的动态库。

四. 目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。

c 复制代码
// hello.c
#include<stdio.h>
void run();
int main() {
	printf("hello world!\n");
	run();
	return 0;
}
// code.c
#include<stdio.h>
void run() {
	printf("running...\n");
}

在一个源文件 hello.c 里便简单输出"hello world!",并且调用一个run函数,而这个函数被定义在另一个原文件 code.c 中。这里我们就可以调用 gcc -c 来分别编译这两个源文件。

可以看到,在编译之后会生成两个扩展名为 .o 的文件,它们被称作目标文件 。要注意的是如果我们修改了一个源文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制的文件,文件的格式是 ELF ,是对二进制代码的一种封装。

生成可执行程序code

五. ELF文件

5.1 ELF文件组成

要理解编译链链接的细节,我们不得不了解一下ELF文件。其实有以下四种文件其实都是ELF文件:

  • 可重定位文件 (Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创
    建可执行文件或者共享目标文件的代码和数据。
  • 可执行文件(Executable File) :即可执行程序。
  • 共享目标文件(Shared Object File) :即 xxx.so文件。
  • 内核转储(core dumps),存放当前进程的执行上下文,用于dump信号触发。

一个ELF文件由以下四部分组成:

  • ELF头 (ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。readelf -h可以查看一个ELF文件的ELF Header

  • 程序头表 (Program header table):列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。我们可以使用readelf -l查看ELF文件的程序头表。

  • 节头表 (Section header table):包含对节(sections)的描述。我们可以使用readelf -S来查看一个ELF文件的节头表。

  • (Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

最常见的节:

  • 代码节(.text):用于保存机器指令,是程序的主要执行部分。
  • 数据节(.data):保存已初始化的全局变量和局部静态变量。

简单了解一下.bss节:我们知道未初始化的全局变量它默认就是0,这是为什么呢?

如果是初始化的全局变量就要记录下来类型和数值;这些存储在.data中;那未初始化的全局变量它需要存储数值吗?

答案是不需要,这些数值默认都是0,所以未初始化的全局变量就存储在.bss节,只存储类型和个数,这样在程序加载到内存时,再对.bss节进行展开并初始化成0。

5.2 ELF形成可执行程序

.o文件,.so文件以及可执行文件都是ELF文件,那我们的可执行程序(文件)如何形成的呢?

因为.o文件的格式都是ELF,所以所有的.o文件形成可执行程序时,只需要将所有节(section)进行合并形成一个大的数据节;也就是形成一个大的ELF格式的文件。

5.3 ELF可执行文件的加载

  • 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
  • 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。
  • 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起。
  • 很显然,这个合并工作也已经在形成 ELF 的时候,合并方式已经确定了,具体合并原则被记录在了 ELF 的 程序头表(Program header table) 中。

可以看到我们的.text代码段和.rodata只读数据段是被合并到一个segment的;

.got.data.bss段这些可读可写的数据是合并到一个segment的。

  • .text节 :是保存了程序代码指令的代码节。
  • .data节 :保存了初始化的全局变量和局部静态变量等数据。
  • .rodata节 :保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。
  • .BSS节 :为未初始化的全局变量和局部静态变量预留位置
  • .symtab节 : Symbol Table 符号表,就是源码里面那些函数名、变量名和代码的对应关系。
  • .got .plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。对于GOT的理解,我们后面会说。

为什么要将section合并成为segment

答案就是为了提高空间利用率,在内存空间中也是以4KB 为基本单位的,也就是说我们只需要1KB 的内存空间,在申请内存空间时也是申请4KB的空间

  • Section合并的主要原因是为了减少页面碎片,提高内存使用效率 。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
  • 此外,操作系统在加载程序时,会将具有相同属性的Section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。

六. 链接与加载

6.1 静态链接

无论是自己的 .o ,还是静态库中的 .o ,本质都是把.o文件进行链接的过程,所以:研究静态链接,本质就是研究 .o 是如何链接的。

c 复制代码
// hello.c
#include<stdio.h>
void run();
int main() {
	printf("hello world!\n");
	run();
	return 0;
}
// code.c
#include<stdio.h>
void run() {
	printf("running...\n");
}

我们将生成的hello.ocode.o文件进行反汇编,看一下汇编代码。

objdump -d 命令将部分代码进行反汇编:



我们可以看到这里的call指令,它们分别对应之前调用的printfrun函数,但是你会发现它们的跳转地址都被设成了0。那这是为什么呢?

其实就是在编译 hello.c 的时候,编译器是完全不知道 printfrun 函数的存在的,比如它们位于内存的哪个区块,代码长什么样都是不知道的。因此,编译器只能将这两个函数的跳转地址先暂时设为0。

接下来我们再对可执行程序main.exe进行反汇编生成汇编代码:


可以看到callq调用函数时就有了函数地址

当多个.o文件进行链接时,编译器就找到了这些函数的地址,并且将0修改为具体的地址。

readelf -s可以查看ELF文件的符号表。


readelf -s main.exe

这里前面的数字(13)表示run函数被合并到了哪一个segment中。

所以链接其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括我们之前提到的地址修正,当所有模块组合在一起之后,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址,所以.o目标文件也叫做可重定位目标文件。这其实就是静态链接的过程。

6.2 ELF加载与进程地址空间

一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用"平坦模式"进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编之后的代码。

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们认为起始地址是0。也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编址了

虚拟地址空间mm_structvm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start, end]等范围数据,另外在用详细地址,填充页表。

所以:虚拟地址机制,不光光OS要支持,编译器也要支持

重新理解进程虚拟地址空间

ELF 在被编译好之后,会把自己未来程序的入口地址记录在ELF headerEntry字段中:

根据readelf -h 可以查看Entry point address的值:

这样CPU就可以拿到程序入口的地址了,拿到这个地址再查看页表找到对应的物理地址,就可以开始执行程序了。

我们的程序在运行时加载动态库,那进程如何看到我们的动态库呢?

在可执行程序中,依赖哪些库,就要加载哪些库;而要加载这些库就要先找到这些库。

所以进程就可以根据库的路径找到这些库,然后将这些库加载到内存中;此外也要将库映射到进程的地址空间中

动态库也被称为共享目标文件,也就是说动态库可以被多个进程共享的。

库映射到进程的地址空间中,这样多个进程就共享同一个库了;而在内存中库文件就只存在一份


6.3 动态链接

动态链接其实远比静态链接要常用得多。比如我们查看下 main.exe 这个可执行程序依赖的动态库,会发现它就用到了一个C动态链接库:

这里的 libc.so 是C语言的运行时库,里面提供了常用的标准输入输出文件字符串处理等等这些功

能。

那为什么编译器默认不使用静态链接呢?静态链接会将编译产生的所有目标文件,连同用到的各种库,合并形成一个独立的可执行文件,它不需要额外的依赖就可以运行。照理来说应该更加方便才对是吧?

静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。随着软件复杂度的提升,我们的操作系统也越来越臃肿,不同的软件就有可能都包含了相同的功能和代码,显然会浪费大量的硬盘空间。

这个时候,动态链接的优势就体现出来了,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态链接库,等到程序运行的时候再将它们加载到内存,这样不但可以节省空间,因为同一个模块在内存中只需要保留一份副本,可以被不同的进程所共享。

动态链接到底是如何工作的??

首先要交代一个结论,动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

我们的可执行程序被编译器动了手脚

在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是 _start ,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。

在 _start 函数中,会执行一系列初始化操作,这些操作包括:

  1. 设置堆栈:为程序创建一个初始的堆栈环境。
  2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
  3. 动态链接 :这是关键的一步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址
  4. 调用 __libc_start_main :一旦动态链接完成, _start 函数会调用__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
  5. 调用 main 函数:最后, __libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
  6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调用 _exit 函数来终止程序。

动态链接器

  • 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
  • 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

环境变量和配置文件

  • Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。
  • 这些路径会被动态链接器在加载动态库时搜索。

缓存文件

  • 为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。
  • 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先
    搜索这个缓存文件。

我们的程序怎么和库具体映射起来的

  • 动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的。
  • 让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进
    行跳转访问的,所以需要把动态库映射到进程的地址空间中。

我们的程序怎么进行库函数调用

  • 库已经被我们映射到了当前进程的地址空间中
  • 库的虚拟起始地址我们也已经知道了
  • 库中每一个方法的偏移量地址我们也知道
  • 所有:访问库中任意方法,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法
  • 而且:整个调用过程,是从代码区跳转到共享区,调用完毕在返回到代码区,整个过程完全在进程地址空间中进行的

全局偏移量表GOT(global offset table)

我们知道代码区是只读的,那如何修改呢?代码区是不能修改的

动态链接采用的做法是在 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。


  1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表
  2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。
  3. 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
  4. 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC = 相对编址 + GOT

PLT是什么?

由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定 ,或者也叫PLT(过程连接表(Procedure Linkage Table))。与其在程序一开始就对所有函数进行重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。

思路是:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次

调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。

总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,它虽然牺牲了一定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。

6.4 总结

  • 静态链接的出现,提高了程序的模块化水平。对于一个大的项目,不同的人可以独立地测试和开发自己的模块。通过静态链接,生成最终的可执行文件。
  • 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件,其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
  • 动态链接实际上将链接的整个过程推迟到了程序加载的时候 。比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进行调用(运行重定位,也叫做动态地址重定位)。

最后

本篇关于库制作与原理的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

相关推荐
Paper_Love2 小时前
RK3568-linux升级用户程序
linux
2gexmxy2 小时前
FTP服务器搭建详解(Linux、Windows)
linux·centos·ftp
松涛和鸣2 小时前
28、Linux文件IO与标准IO详解:从概念到实战
linux·网络·数据结构·算法·链表·list
修己xj2 小时前
外网下载内网部署:Yum离线升级Linux软件包
linux
嵌入式小能手2 小时前
飞凌嵌入式ElfBoard-文件I/O的深入学习之I/O多路复用
linux·服务器·学习
墨北x2 小时前
2025 年江西省职业院校技能大赛(中职组)《网络建设与运维》赛项样题
运维·网络
Konwledging2 小时前
Linux memblock
linux
一条咸鱼¥¥¥2 小时前
【运维经验】ESXi虚拟机磁盘设置精简置备,需要回收所占用未能释放的空间
运维
测试人社区—小叶子3 小时前
边缘计算与AI:下一代智能应用的核心架构
运维·网络·人工智能·python·架构·边缘计算