当前位置: 首页 > news >正文

Linux动静态库制作与原理

什么是库

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

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

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

静态库

・静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。

・⼀个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so 的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库。

静态库的制作

在本篇文章中使用只使用下面的.c和.h代码进行操作

#include "mytring.h"// 计算字符串长度
size_t my_strlen(const char *str) {const char *s = str;while (*s) s++;return s - str;
}// 字符串复制
char *my_strcpy(char *dest, const char *src) {char *d = dest;while ((*d++ = *src++));return dest;
}
#include <stddef.h>// 计算字符串长度
size_t my_strlen(const char *str);// 字符串复制
char *my_strcpy(char *dest, const char *src);

在制作静态库的过程中需要将.c文件先编译成.o文件 。

为什么需要先编译成.o文件呢?

若直接编译源文件到库,每次修改一个源文件都需要重新编译整个库。而使用 .o 文件,只需重新编译修改过的源文件,然后更新静态库。

目标文件(.o)是独立的编译单元:每个 .c 文件单独编译,不依赖其他源文件。例如,一个库可能包含 string.c、math.c 等多个源文件,编译后生成 string.o、math.o,可以独立更新其中一个而不影响其他。

静态库是 .o 文件的集合:通过 ar 工具将多个 .o 文件打包成库,方便复用。例如,多个项目可以共享同一个 libmystring.a。

这里介绍一下ar指令(归档指令)

ar [选项] [归档文件] [文件...]

归档文件(Archive File)是一种通过特定工具将多个文件或目录打包成一个独立文件的集合,主要用于存储、备份、分发或压缩数据。

静态库的本质就是将.o文件进行打包

这里使用ar指令进行归档

ar -rcs libmystring.a mytring.o

此时文件中就形成了libmystring.a。但是库的名字并非是libmystring.a,而是去掉前面的lib和后面的.a,所以库名字是mystring

假设我们有一个使用该库的程序main.c,可以这样进行编译:

gcc main.c -L. -lmystring -o main

 -L:gcc在链接外部的库时,默认不会在当前目录下寻找,让操作系统在-L后面的路径中搜索库,所以-L.就表示在当前目录下搜索库

-l:链接名为mystring的库文件,-l后面只要库的真实名字

最后就形成了依赖与libmystring.a的可执行文件main

但是在删除掉libmystring.a后,main依旧可以运行,这就是静态库的特点。

动态库

• 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

• ⼀个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的⼀个表,而不是外部函数所在目标文件的整个机器码

• 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

• 动态库可以在多个程序间共享,所以动态链接使得可执行文件件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的⼀份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间

动态库的制作

动态库的依旧采用最开始的两份代码来实验,两份代码分别是mytring.c和mytring.h

动态库必须编译为位置无关代码(Position Independent Code, PIC),使用 -fPIC 选项:

gcc -fPIC -c mytring.c -o dong.o

为什么需要编译成位置无关码?

动态库的核心特性是可以被加载到内存的任意地址,并被多个程序共享。如果库代码中包含固定地址的引用(如绝对跳转、直接变量地址),则当库被加载到不同地址时会出错。位置无关代码通过特殊机制解决了这个问题。

使用 -shared 选项将目标文件链接为动态库:

gcc -shared -o libdong.so dong.o

最后将main.c文件进行编译

gcc main.c -L. -ldong -o main

 此时就出现了main可执行文件,但是./运行却出现提示

为什么能够形成可执行文件却无法运行呢? 

这是由于我们只把目标文件所依赖的库告诉了gcc而已,编译的时候只有gcc在处理,但是在运行的时候就不需要gcc,转而需要操作系统进行处理,此时所依赖的库只有gcc直到,但是操作系统并不知道,所以就会报错

这是由于操作系统找不到文件所依赖的动态库,但是为什么静态库不会出现这个问题呢?

这是由于二者对链接方式的差别,静态库是直接将库方法记录到代码中的,动态库是将地址记录到代码中

这就是为什么静态链接的文件没有静态库也能够运行,而动态链接的文件缺失动态库就无法运行了,此时文件就无法通过地址来查找库方法了 

如何解决?

 1.修改临时环境变量

只需要将LD_LIBRARY_PATH修改,../dong表示父目录中的dong

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../dong

 2.修改配置文件

将库复制到系统默认搜索路径(如 /usr/lib 或 /usr/local/lib)

sudo cp libmath.so /usr/local/lib
sudo ldconfig  # 更新动态链接器缓存(Linux)

3.使用 rpath 硬编码路径

编译时使用 -Wl,-rpath 选项指定运行时搜索路径

gcc main.c -L. -lmath -Wl,-rpath=. -o main

-Wl:将后面的参数传递给链接器(ld)

-rpath:运行时搜索路径,在可执行文件中硬编码动态库的搜索路径,告诉动态链接器(如 Linux 的 ld-linux.so)在程序运行时优先搜索哪些目录。

总结动静态库

gcc/g++默认使用动态库

非得使用静态库只能使用-static,一旦使用-static就必须拥有对应的静态库

在Linux系统下,默认安装的大部分库,默认都优先安装动态库

目标文件

• 编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们⼀般都是⼀键构建非常方便,但⼀旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这⼀系列操作。接下来我们深入探讨⼀下编译和链接的整个过程,来更好的理解动静态库的使用原理。

• 先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。

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

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

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

ELF文件 

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

• 可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。

• 可执行文件(Executable File) :即可执行程序。

• 共享目标文件(Shared Object File) :即 xxx.so文件。

• 内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。

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

• ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。

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

• 节头表(Section header table) :包含对节(sections)的描述。

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


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

ELF从形成到加载轮廓

ELF形成可执行

 • step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件

• step-2:将多份 .o 文件section进行合并

实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并

readelf

readelf 是 Linux 系统中用于分析 ELF(Executable and Linkable Format)格式文件的强大工具。ELF 文件包括可执行文件、动态库(.so)、目标文件(.o)等。通过 readelf,可以查看文件的各种元信息、段表、符号表等,是调试和逆向工程的重要工具。

readelf [选项] <elf-file>

1. 查看文件头信息(-h)

2.查看段表信息(-S)

3. 查看程序头信息(-l)

4. 查看动态链接信息(-d)

5.查看符号表(-s)

ELF可执行文件加载

• ⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment

• 合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等

• 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起

• 很显然,这个合并⼯作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的程序头表(Program header table) 中 

可以通过readelf来查看可执行程序的section以及合并的segment

为什么要将section合并成为segment  

•在文件中与内存的交互基本上是以4kb为基本单位,如果想要存储数据,不足4kb的内容也是按照4kb来进行存储的,而section所占空间一般很小,如果单个section进行存储的话会浪费空间

• Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载、管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。  

• 此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。  

对于程序头表和节头表又有什么用呢?其实ELF文件提供2个不同的视图/视角来让我们理解这两个部分:  

• 链接视图(Linking view)- 对应节头表 Section header table  

文件结构的粒度更细,将文件按功能模块的差异进行划分。静态链接分析时一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。  

为了空间布局上的效率,在链接目标文件时,链接器会把多个节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并后空间利用率更高,否则很小的段会导致物理内存页浪费(物理内存页分配一般为整数倍,如4K),因此链接器在链接阶段就将小块合并。  

• 执行视图(Execution view)- 对应程序头表 Program header table  

告诉操作系统如何加载可执行文件,完成进程内存的初始化。一个可执行程序格式中必须有program header table。  

说白了就是:一个在链接时作用,一个在运行加载时作用。   

• 从链接视图来看:  

• 命令 readelf -S hello.o可以帮助查看ELF文件的节头表。  
• .text节:保存了程序代码指令的代码节。  
• .data节:保存了初始化的全局变量和局部静态变量等数据。  
• .rodata节:保存了只读的数据(如C语言代码中的字符串)。由于.rodata节是只读的,只能存在于可执行文件的只读段中(如text段,而非data段)。  
•.BSS节:为未初始化的全局变量和局部静态变量预留位置。  
• .symtab节(Symbol Table符号表):保存源码中函数名、变量名与代码的对应关系。  
• .got.plt节(全局偏移表-过程链接表):.got节保存全局偏移表,与.plt节共同提供对导入共享库函数的访问入口,由动态链接器在运行时修改(后文详述)。  
• 使用 readelf 命令查看.so文件可以看到该节。  

• 从执行视图来看:

 • 告诉操作系统哪些模块可被加载进内存。  
• 定义加载后各分段的权限(可读、可写、可执行)。  


在ELF头中可以查看文件基本信息,并了解程序头表和节头表的定位。例如查看 hello.o可重定位文件的主要信息:  

$ readelf -h hello.o 
ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 # ⽂件类型Data: 2s complement, little endian # 指定的编码⽅式Version: 1 (current)OS/ABI: UNIX - System VABI Version: 0Type: REL (Relocatable file) # 指出ELF⽂件的类型Machine: Advanced Micro Devices X86-64 # 该程序需要的体系结构Version: 0x1Entry point address: 0x0 # 系统第⼀个传输控制的虚拟地址,在那启动进程。假如⽂件没有如何关联的口点,该成员就保持为0。Start of program headers: 0 (bytes into file)Start of section headers: 728 (bytes into file)Flags: 0x0Size of this header: 64 (bytes) # 保存着ELF头⼤⼩(以字节计数)Size of program headers: 0 (bytes) # 保存着在⽂件的程序头表
(program header table)中⼀个⼊⼝的⼤⼩Number of program headers: 0 # 保存着在程序头表中⼊⼝的个数。因此,e_phentsize和e_phnum的乘机就是表的⼤⼩(以字节计数).假如没有程序头表,变量为0。
Size of section headers: 64 (bytes) # 保存着section头的⼤⼩(以字节计数)。⼀个section头是在section头表的⼀个⼊⼝ Number of section headers: 13 # 保存着在section header table中的⼊⼝数⽬。因此,e_shentsize和e_shnum的乘积就是section头表的⼤⼩(以字节计数)。
假如⽂件没有section头表,值为0。 Section header string table index: 12 # 保存着跟section名字字符表相关⼊⼝的section头表(section header table)

在操作系统中,想要对ELF文件进行解读,首先需要对文件判断是否为ELF文件,使用readelf -h查看文件会出现Magic,Magic中存储的是一段随机值,操作系统可以根据这段随机值来判断文件是否是ELF文件

理解连接与加载

静态链接

• 无论是自己的.o,还是静态库中的.o,本质都是把.o文件进行连接的过程

• 所以:研究静态链接,本质就是研究.o是如何链接的

那么静态库是如何形成可执行程序的

第一段代码是hello.c,第二段代码是code.c

#include<stdio.h>
void run();
int main() {printf("hello world!\n");run();return 0;
}#include<stdio.h>
void run() {printf("running...\n");
}

将他们编译成.o文件后使用objdump进行反汇编

我们能够发现在hello.o的反汇编中的printf和run对应的call找不到地址,e8后面是8个0

此时我们能够得出结论:hello.o不认识printf和run,run.o不认识printf。

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

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

 这个地址会在什么时候被修正?链接的时候!为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.text)中还存在⼀个重定位表,这张表会在链接阶段根据表里记录的地址进行修正。 

将code.o和hello.o文件一起编译成可执行程序,再次进行反汇编

此时发现了main函数和run函数中的call中的e8都找到了对应的地址,并非是全0了

1.两个.o的代码段合并到了⼀起,并进行了统⼀的编址

2.链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进⾏相关call地址,完成代码调用

静态链接就是把库中的.o进行合并,和上述过程⼀样

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

ELF加载与进程地址空间

虚拟地址/逻辑地址  

问题:  
• ⼀个ELF程序,在没有被加载到内存的时候,有没有地址呢?  
• 进程 mm_struct、vm_area_struct 在进程刚刚创建的时候,初始化数据从哪里来的?  

答案:  
•一个ELF程序还没加载到内存中的时候就已经有地址了,ELF程序的地址采用起始地址和偏移量的方式进行计算,目前的计算机体系中,起始地址都是从0开始的,所以起始地址不可能是真实的物理地址,它是虚拟地址。所以磁盘上的可执行程序,代码,数据编址,起始就是虚拟地址的统一编址

• ⼀个ELF程序,在没有被加载到内存的时候,本来就有地址。当代计算机⼯作时采用"平坦模式",因此ELF需要对自己的代码和数据进行统⼀编址。通过 objdump -S 反汇编后的代码

最左侧显示的是ELF的虚拟地址(严格来说是逻辑地址:起始地址+偏移量)。默认起始地址为0,这意味着程序未被加载时已预先完成虚拟地址编址。  

• 进程 mm_struct、vm_area_struct 的初始化数据来源:ELF文件的各个 segment 提供起始地址和长度,用于初始化内核结构中的 [start, end] 范围数据,并通过详细地址填充页表。  

结论:  
• 虚拟地址机制不仅需要操作系统⽀持,也需要编译器配合。  

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

ELF 被编译后,会将程序入口地址记录在ELF header的 Entry 字段中。

 通过查看ELF表头,能够发现里面记录了Entry point address,这就是程序的入口地址 

 在磁盘中,可执行程序的虚拟地址就已经记录在磁盘中了,当可执行程序加载到物理内存中,进程会创建task_struct,task_struct中的mm_struct会使用磁盘中可执行程序的起始地址到结尾地址进行初始化,代码加载到物理内存中,代码也是要占据物理内存的,所以每一行代码都有自己的物理地址,然后页表会将虚拟地址加载到页表的左侧,物理地址加载到页表的右侧

现在的问题是:我们想要运行程序,地址的映射关系都建立好了,但是cpu怎么直到起始地址?

cpu中有两种寄存器,分别是EIP和CR3,EIP负责读取程序的起始地址,而CR3负责加载起始地址的内容,cpu就是通过这种方法来执行程序的

动态链接与动态库加载

进程如何看到动态库

当运行进程A的时候,进程A所依赖的库也会加载到物理内存中,task_struct的页表就会建立与库的映射关系,因为库也是ELF文件,也有自己的虚拟地址空间

当库函数被调用的时候,程序会从代码区跳转到共享区中执行库函数,执行后便返回到代码区中

进程间如何共享库的 

原理与单个进程类似,都是把所依赖的库加载到物理内存中,不同的进程中的共享区都会在页表中创建一块空间指向加载在物理内存中的库,但是库只会在内存中出现一次,在内存中并不会出现相同的库

当运行库函数时,程序运行会从代码区跳转到各自的共享区中,执行完库函数就返回到各自的代码区

动态链接  

动态链接其实远比静态链接要常用得多。比如我们查看下 hello 这个可执行程序依赖的动态库,会发  现它就用到了一个c动态链接库:  这里的 libc.so 是C语言的运行时库,里面提供了常用的标准输入输出文件字符串处理等等这些功能。  

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

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

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

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

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

当动态库被加载到内存以后,⼀旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。

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

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

1. 设置堆栈:为程序创建一个初始的堆栈环境。  

2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位  置,并清零未初始化的数据段。  

3. 动态链接:这是关键的一步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的  
动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调  
用和变量访问能够正确地映射到动态库中的实际地址。  

动态链接器:  
◦ 动态链接器(如ld-linux.so)负责在程序运行时加载动态库。  
◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。  
环境变量和配置文件:  
◦ Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置  
文件)来指定动态库的搜索路径。  
◦ 这些路径会被动态链接器在加载动态库时搜索。  
缓存文件:  
◦ 为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。  
◦ 该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先  
搜索这个缓存文件。  

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 函数来终止程序。  

动态库中的相对地址  

动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址采用相对编址的方案进行编制的(其实可执行程序也一样,都要遵守平坦模式,只不过exe直接加载的)。  

# ubuntu下查看任意⼀个库的反汇编
objdump -S /lib/x86_64-linux-gnu/libc-2.31.so | less# Cetnos下查看任意⼀个库的反汇编
$ objdump -S /lib64/libc-2.17.so | less

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

在创建task_struct时,mm_struct 指向 struct file 指向 struct path  指向 struct dentory 最终指向inode ,通过inode信息就可以找到磁盘文件中的数据块,这样就建立了与动态库的映射关系

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

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

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

先从inode获取库的位置,再将库的代码和数据加载到物理内存中,然后和页表建立映射关系,最终就得到了库的起始虚拟地址,在代码汇编的时候,会将库函数替换成在库中的偏移量,最终在建立映射关系之后,库函数就变成了库的起始虚拟地址+偏移量

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

注意:  
• 也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该提前知道  
• 然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)  
• 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?  

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

1. 由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。  

2. 在单个.so下,由于GOT表与 .text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找到GOT表。  

3. 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。  

4. 这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT。  

库间依赖

注意:  
• 不仅仅有可执行程序调用库  
• 库也会调用其他库!!库之间是有依赖的,如何做到库和库之间互相调用也是与地址无关的呢??  
• 库中也有.GOT,和可执行一样!这也就是为什么大家都为什么都是ELF的格式!

由于GOT表中的映射地址会在运行时去修改,我们可以通过gdb调试去观察GOT表的地址变化。

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

思路是:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码会负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现。总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,它虽然牺牲了一定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。解析依赖关系的时候,就是加载并完善互相之间的GOT表的过程。  

相关文章:

Linux动静态库制作与原理

什么是库 库是写好的现有的&#xff0c;成熟的&#xff0c;可以复用的代码。现实中每个程序都要依赖很多基础的底层库&#xff0c;不可能每个人的代码都从零开始&#xff0c;因此库的存在意义非同寻常。 本质上来说库是一种可执行代码的二进制形式&#xff0c;可以被操作系统…...

ArkUI Tab组件开发深度解析与应用指南

ArkUI Tab组件开发深度解析与应用指南 一、组件架构与核心能力 ArkUI的Tabs组件采用分层设计结构&#xff0c;由TabBar&#xff08;导航栏&#xff09;和TabContent&#xff08;内容区&#xff09;构成&#xff0c;支持底部、顶部、侧边三种导航布局模式。组件具备以下核心特…...

winrar 工具测试 下载 与安装

https://zhuanlan.zhihu.com/p/680852417 https://www.angusj.com/resourcehacker/#download 点击String Table&#xff0c;在展开列表中找到80:2052展开&#xff0c;删除1277行。点击右上方编译按钮&#xff0c;并保存。...

代码随想录算法训练营第四十四天

卡码网题目: 99. 岛屿数量100. 岛屿的最大面积 其他: 今日总结 往期打卡 99. 岛屿数量 跳转: 99. 岛屿数量 学习: 代码随想录公开讲解 问题: 给定一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的矩阵&#xff0c;你需要计算岛屿的数量。岛屿由水…...

每日Prompt:自拍生成摇头娃娃

提示词 将这张照片变成一个摇头娃娃&#xff1a;头部稍微放大&#xff0c;保持面部准确&#xff0c;身体卡通化。[把它放在书架上]。...

制作我的计算器

1. 界面布局 新建项目 MyCalculator&#xff0c;开始布局。 2. 静态布局 代码如下&#xff1a; // etc/pages/Index.ets Entry Component struct Index {build() {Column() {/*** 运算区*/Column() {TextInput({ text: 12x13 }).height(100%).fontSize(32).enabled(false).f…...

如何查看 Ubuntu开机是否需要密码

要查看 Ubuntu 开机是否需要密码&#xff0c;可以通过以下方法进行判断&#xff1a; 1. 检查自动登录设置 图形界面操作&#xff1a; 进入系统设置&#xff08;Settings&#xff09;→ 用户账户&#xff08;User Accounts&#xff09;→ 解锁设置&#xff08;输入当前用户密码…...

今日行情明日机会——20250519

上证指数缩量收十字星&#xff0c;个股涨多跌少&#xff0c;这周反弹的概率比较大。 深证指数缩量调整&#xff0c;临近反弹&#xff0c;个股表现更好。 2025年5月19日涨停股主要行业方向分析 并购重组&#xff08;政策驱动资产整合&#xff09; • 涨停家数&#xff1a;16…...

【CodeBuddy 】从0到1,让网页导航栏变为摸鱼神器

【CodeBuddy 】从0到1&#xff0c;让网页导航栏变为摸鱼神器 我正在参加CodeBuddy「首席试玩官」内容创作大赛&#xff0c;本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 &#x1f31f;嗨&#xff0c;我是LucianaiB&#…...

PCL点云库点云数据处理入门系列教材目录(2025年5月更新....)

PCL点云库点云数据处理入门系列教材目录 基础阶段 第 1 讲&#xff1a;PCL库简介和安装&#xff08;Win10/11VS2019PCL 1.12.0&#xff09;第 2 讲&#xff1a;PCL库中点云基本知识和数据类型结构第 3 讲&#xff1a;PCL库中点云数据格式PCD和PLY及其输入输出&#xff08;IO&…...

同一颗太阳:Australia、Austria、Arab、Africa、Augustus、August、Aurora、Athena

我们来看一下下面这一堆单词&#xff1a; Australia n.澳大利亚&#xff1b;澳洲 Australian n.澳大利亚人 a.澳大利亚的 Austria n.奥地利 Austrian n.奥地利人 a.奥地利(人)的 Africa n.非洲 African n.非洲人* Arab a.阿拉伯的&#xff1b;阿拉伯人的 n.阿拉伯人(pl.Arabs)…...

用户账号及权限管理:企业安全的基石与艺术

在当今数字化时代,用户账号及权限管理已成为企业IT安全体系中不可或缺的核心组件。它不仅是保护敏感数据的第一道防线,更是确保业务运营效率和合规性的关键。本文将深入探讨用户账号及权限管理的重要性、最佳实践以及实施策略,助您构建一个安全、高效且灵活的访问控制体系。…...

存储系统03——数据缓冲evBuffer

存储系统03——数据缓冲evBuffer 数据缓冲evBuffer分段存储零拷贝线程安全 evbuffer 实例——存储系统事件触发 数据缓冲evBuffer evbuffer 是 Libevent 提供的一个高效内存缓冲区管理工具&#xff0c;用于存储和操作数据。它类似于一个动态增长的字节缓冲区&#xff0c;支持多…...

留给王小川的时间不多了

王小川&#xff0c;这位头顶“天才少年”光环的清华学霸、搜狗输入法创始人、中国互联网初代技术偶像&#xff0c;正迎来人生中最难啃的硬骨头。 他在2023年创立的百川智能&#xff0c;被称为“大模型六小虎”之一。今年4月&#xff0c;王小川在全员信中罕见地反思过去两年工作…...

Python海龟绘图-斗地主

#导入库 import random as r import turtle as t #数据 pk[红心A,红心2,红心3,红心4,红心5,红心6,红心7,红心8, 红心9,红心10,红心J,红心Q,红心K,黑桃A,黑桃2,黑桃3,黑桃4,黑桃5,黑桃6,黑桃7,黑桃8, 黑桃9,黑桃10,黑桃J, 黑桃Q,黑桃K,方块A,方块2,方块3,方块4,方块5,方块6,方块…...

一、内存调优

一、内存调优 什么是内存泄漏 监控Java内存的常用工具 内存泄露的常见场景 内存泄露的解决方案 内存泄露与内存溢出的区别 内存泄露&#xff1a;在Java中如果不再使用一个对象&#xff0c;但是该对象依然在GC ROOT的引用链上&#xff0c;这个对象就不会被垃圾回收器回收&…...

机器学习--特征工程具体案例

一、数据集介绍 sklearn库中的玩具数据集&#xff0c;葡萄酒数据集。在前两次发布的内容《机器学习基础中》有介绍。 1.1葡萄酒列标签名&#xff1a; wine.feature_names 结果&#xff1a; [alcohol, malic_acid, ash, alcalinity_of_ash, magnesium, total_phenols, flavanoi…...

Java-List集合类全面解析

Java-List集合类全面解析 前言一、List接口概述与核心特性1.1 List在集合框架中的位置1.2 List的核心特性1.3 常见实现类对比 二、ArrayList源码剖析与应用场景2.1 内部结构与初始化2.2 动态扩容机制2.3 性能特点与最佳实践 三、LinkedList 源码剖析与应用场景3.1 内部结构与节…...

在Cursor中启用WebStorm/IntelliJ风格快捷键

在Cursor中启用WebStorm/IntelliJ风格快捷键 方法一&#xff1a;使用预置快捷键方案 打开快捷键设置 Windows/Linux: Ctrl K → Ctrl SmacOS: ⌘ K → ⌘ S 搜索预设方案 在搜索框中输入keyboard shortcuts&#xff0c;选择Preferences: Open Keyboard Shortcuts (JSON) …...

32、跨平台咒语—— React Native初探

一、时空晶体架构&#xff08;核心原理&#xff09; 1. 量子组件桥接协议 // 原生组件映射 <View> → iOS UIView / Android ViewGroup <Text> → UILabel / TextView 魔法特性&#xff1a; • JavaScriptCore引擎&#xff1a;通过V8/Hermes引擎执行JS逻辑…...

无源探头衰减比与带宽特性的关联性研究

引言 在电子测量领域&#xff0c;无源探头作为示波器的重要附件&#xff0c;其性能参数直接影响测量结果的准确性。本文将从电路设计原理出发&#xff0c;深入分析衰减比与带宽这两个关键参数的相互关系&#xff0c;帮助工程师正确理解并选择适合的测量探头。 技术原理分析 …...

虚拟机的三个核心类加载器

虚拟机的三个核心类加载器 在Java虚拟机(JVM)中,类加载器(ClassLoader)负责将类的字节码加载到内存中,并生成对应的Class对象。以下是三个核心类加载器的详细说明: 1. 启动类加载器(Bootstrap ClassLoader) 职责: 加载Java核心类库(如java.lang、java.util等),位…...

国家互联网信息办公室关于发布第十一批深度合成服务算法备案信息的公告

wenz 根据《互联网信息服务深度合成管理规定》&#xff0c;现公开发布第十一批境内深度合成服务算法备案信息&#xff0c;具体信息可通过互联网信息服务算法备案系统&#xff08;https://beian.cac.gov.cn&#xff09;进行查询。任何单位或个人如有疑议&#xff0c;请发送邮件…...

深入理解动态规划:从斐波那契数列到最优子结构

引言 动态规划(Dynamic Programming, DP)是算法设计中一种非常重要的思想&#xff0c;广泛应用于解决各类优化问题。许多看似复杂的问题&#xff0c;通过动态规划的视角分析&#xff0c;往往能找到高效的解决方案。本文将系统介绍动态规划的核心概念&#xff0c;通过经典案例展…...

AT 指令详解:基于 MCU 的通信控制实战指南AT 指令详解

在 MCU&#xff08;单片机&#xff09;项目中&#xff0c;我们经常需要与各种通信模组&#xff08;GSM、Wi-Fi、蓝牙等&#xff09;交互。而这类模组通常都通过串口&#xff08;UART&#xff09;与 MCU 通信&#xff0c;控制它们的“语言”就是——AT 指令。 一、什么是 AT 指…...

初学c语言16(内存函数)

1.memcpy 形式&#xff1a; 功能&#xff1a;完成内存块拷贝&#xff08;所以可拷贝任何类型的数据&#xff09; 过程&#xff1a;从source开始拷贝num个字节的数据到destination指向的空间里 返回值&#xff1a;返回目标空间的起始地址 应用&#xff1a; 模拟实现&#xf…...

【git进阶】git rebase(变基)

文章目录 合并分支提交信息修改合并提交记录时间问题1时间问题2时间问题3git rebase有很多用武之地,我一一道来 合并分支 当多人协作同一个分支时,在提交我们自己版本之前,我们会先用git pull获取远端最新的版本。但是 git pull = git fetch + git mergegit merge是一个非…...

Pytorch---view()函数

在PyTorch中&#xff0c;view函数是一个极为重要的张量操作函数&#xff0c;其主要功能是对张量的形状进行重塑&#xff0c;与此同时会尽力维持张量中元素的总数不变。 1. 基本功能与语法 view函数的主要作用是改变张量的维度和大小&#xff0c;不过要保证重塑前后张量的元素…...

AI Agent开发第71课-一个完善的可落地企业AI Agent全架构

开篇 在之前的若干篇章里我们大量叙述了DIFY AI工作流、重排序、提示词重写、文档chunk、AI翻页、各种高级RAG应用以及AI Agent案例甚至全代码的输出。 目的,就是为了帮助大家认识到这么一件事,那就是: 当前AI主要还是在被叫好却不卖座,很多人(包括我身边的太多大厂)去…...

Prompt、Agent、MCP关系

AI基础概念概述 链接: https://www.bilibili.com/video/BV1aeLqzUE6L?t419.4 Agent&#xff08;智能体&#xff09;&#xff1a;智能体是能够执行特定任务的程序或实体&#xff0c;它可以根据环境变化调整自身行为。 MCP&#xff08;多通道协议&#xff09;&#xff1a;MCP是…...

人工智能100问☞第27问:神经网络与贝叶斯网络的关系?

神经网络与贝叶斯网络是两种互补的智能模型:神经网络通过多层非线性变换从数据中学习复杂模式,擅长大规模特征提取和预测,而贝叶斯网络基于概率推理建模变量间的条件依赖关系,擅长处理不确定性和因果推断。两者的融合(如贝叶斯神经网络)结合了深度学习的表征能力与概率建…...

Vue-样式绑定-style

样式绑定-style 对象写法数组写法 对象写法 :style"{fontSize: x}", x是动态值 &#xff08;{fontSize: x}是样式对象&#xff09; 代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>…...

Python60日基础学习打卡D30

回顾&#xff1a; 导入官方库的三种手段导入自定义库/模块的方式导入库/模块的核心逻辑&#xff1a;找到根目录&#xff08;python解释器的目录和终端的目录不一致&#xff09; # 直接导入 from random import randint print(randint(1, 10)) # 导入自定义库 import module m…...

el-upload图片设置了url不显示问题

引用&#xff1a;可以使用Image组件测试url是否可以用 此时不显示图片&#xff0c;因为打印后提示图片加载失败 此时图片显示 使用的base64的格式。但要注意在生成的base64码前要加上data:image/png;base64的内容才可以赋值给url...

学习黑客PowerShell的历史、架构与工作原理深度解析

PowerShell的历史、架构与工作原理深度解析 &#x1f50d; 作者: 海尔辛 | 发布时间: 2025-05-19 12:28:44 UTC 1. PowerShell的历史演变 &#x1f4dc; &#x1f539; 诞生背景与起源 PowerShell的诞生源于微软解决Windows管理工具碎片化问题的需求。在PowerShell出现之前…...

视觉-和-语言导航的综述:任务、方法和未来方向

22年6月来自UC Santa Cruz、澳大利亚的阿德莱德大学和 USC 的论文“Vision-and-Language Navigation: A Survey of Tasks, Methods, and Future Directions”。 人工智能研究的一个长期目标是构建能够用自然语言与人类交流、感知环境并执行现实世界任务的智体。视觉与语言导航…...

JUC入门(四)

ReadWriteLock 代码示例&#xff1a; package com.yw.rw;import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteDemo {public static void main(String[] args) {MyCache myCache new MyCache…...

day 21 常见降维算法

一、无监督降维 定义&#xff1a;这类算法在降维过程中不使用任何关于数据样本的标签信息&#xff08;比如类别标签、目标值等&#xff09;。它们仅仅根据数据点本身的分布、方差、相关性、局部结构等特性来寻找低维表示。 输入&#xff1a;只有特征矩阵 X。 目标&#xff1a…...

代理IP高可用性与稳定性方案:负载均衡、节点健康监测与智能切换策略

一、负载均衡策略&#xff1a;动态分配与场景适配 多算法协同调度 轮询与加权轮询&#xff1a;适用于定时数据采集等低频任务&#xff0c;通过静态IP池按顺序分配请求&#xff0c;避免单一节点过载。例如&#xff0c;静态IP池可支持电商商品价格监控&#xff0c;按固定周期切换…...

C语言:在操作系统中,链表有什么应用?

在操作系统中&#xff0c;链表是一种重要的数据结构&#xff0c;凭借其灵活的内存管理和高效的插入/删除特性&#xff0c;被广泛应用于多个核心模块。以下是其主要应用场景及详细说明&#xff1a; 1. 内存管理&#xff1a;空闲内存块管理 应用场景&#xff1a;操作系统需要管…...

RocketMQ

一、引言 Message Queue&#xff08;消息 队列&#xff09;&#xff0c;从字⾯上理解&#xff1a;⾸先它是⼀个队列。FIFO先进先出的数据结构——队列。消息队列就是所谓的存放消息的队列。 消息队列解决的不是存放消息的队列的⽬的&#xff0c;解决的是通信问题。 …...

学习BI---QuickBI介绍

BI是什么 BI 数据仓库&#xff08;存数据&#xff09; OLAP&#xff08;多维分析&#xff09; 数据挖掘&#xff08;找规律&#xff09; 可视化&#xff08;图表/看板&#xff09; 人话解释就是把企业里乱七八糟的数据变成老板能看懂的报告&#xff0c;帮他们做更聪明的决…...

【数据结构】AVL树的实现

文章目录 1. AVL 的概念2. AVL 树的实现2.1 AVL 树的结构2.2 AVL 树的插入2.2.1 AVL 树插入一个值的大致过程2.2.2 平衡因子更新 2.3 旋转2.3.1 旋转的原则2.3.2 右单旋2.3.3 左单旋2.3.4 左右双旋2.3.5 右左双选 2.4 AVL 树的查找2.5 AVL 树平衡检测 1. AVL 的概念 AVL树是最…...

基于Zynq SDK的LWIP UDP组播开发实战指南

一、为什么选择LWIP组播? 在工业控制、智能安防、物联网等领域,一对多的高效数据传输需求日益增长。Zynq-7000系列SoC凭借其ARM+FPGA的独特架构,结合LWIP轻量级网络协议栈,成为嵌入式网络开发的理想选择。本文将带您实现: LWIP组播配置全流程动态组播组切换技术零拷贝数据…...

【esp32 控制台】-命令

文章目录 1 esp32控制台简介2 控制台命令编程2.1 控制台配置交互设备初始化控制台初始化等待命令输入 2.2 命令实现2.2.1 命令注册 踩坑记录 1 esp32控制台简介 可以通过idf.py monitor调出idf的控制台&#xff0c;结束控制台用ctrl ]。 esp32的控制台就像linux中的shell一样…...

DApp开发全流程解析:模式设计、功能参考与合约管理实践

DApp开发全流程解析&#xff1a;模式设计、功能参考与合约管理实践 引言&#xff1a;Web3.0时代的DApp开发范式 随着区块链技术的成熟&#xff0c;DApp&#xff08;去中心化应用&#xff09;已成为Web3.0生态的核心载体。截至2025年&#xff0c;全球DApp数量突破10万&#xf…...

I/O多路复用:poll与epoll

一、select/poll与epoll对比 核心区别 特性select/pollepoll内核数据结构数组&#xff08;线性结构&#xff09;红黑树&#xff08;存储监听的fd&#xff09;内存拷贝每次调用需将fd列表从用户态拷贝到内核态仅在注册fd时拷贝一次&#xff08;epoll_ctl&#xff09;内核事件检…...

【调制识别】PGD攻击中参数的含义

在PGD&#xff08;Projected Gradient Descent&#xff09;对抗攻击中&#xff0c;代码如下&#xff1a; # 定义PGD对抗样本生成类 class AttackPGD(nn.Module): def __init__(self, model, config):super(AttackPGD, self).__init__()self.model model …...

设备数据看板助力自动化工厂实现生产智能精细化管理

工厂数字化转型需要实现自动化设备生产现场可视化、设备系统间的互联互通&#xff0c;以及数据的智能决策。然而&#xff0c;当前许多制造企业仍面临着传统单机设备同质化严重、数字化服务能力不足、售后成本高企、系统集成效率低下等挑战。企业如何通过自动化装备看板和实时数…...

单点登录是是什么?具体流程是什么?

SSO⼀般都需要⼀个独⽴的认证中⼼&#xff08;passport&#xff09;&#xff0c;⼦系统的登录均得通过passport&#xff0c;⼦系统本⾝将不参与登录操作&#xff0c;当⼀个系统成功登录以后&#xff0c;passport将会颁发⼀个令牌给各个⼦系统&#xff0c;⼦系统可以拿着令牌会获…...