C/C++动静态库的制作与原理 -- 静态库,动态库,目标文件,ELF文件,动态链接,静态链接
目录
1. 什么是库
2. 静态库
2.1 静态库的制作
2.2 静态库的使用
3. 动态库
3.1 动态库的制作
3.2 动态库的使用
4. 目标文件
5. ELF文件
6. ELF从形成到加载轮廓
6.1 ELF形成可执行
7.2 ELF可执行文件加载
7. 理解链接和加载
7.1 静态链接
7.2 ELF加载与进程地址空间
7.2.1 虚拟地址与逻辑地址
7.2.2 重新理解进程虚拟地址空间
7.3 动态链接与动态库加载
7.3.1 进程如何使用动态库
7.3.2 进程间如何共享库
7.3.3 动态链接
7.3.3.1 概要
7.3.3.2 可执行程序被编译器动了手脚
7.3.3.3 动态库中的相对地址
7.3.3.4 程序怎么和库具体映射起来
7.3.3.5 程序怎么进行库函数调用
7.3.3.6 全局偏移量表GOT(global offset table)
7.3.3.7 库间依赖
7.3.4 总结
1. 什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
静态库:.a文件 [Linux],.lib文件 [windows]
动态库:.so文件 [Linux],.dll文件 [windows]
2. 静态库
静态库(.a):本质就是对源文件对应的 .o 文件进行一个打包,打包成一个 .a 文件。程序在编译链接的时候把 .a 文件链接到可执行文件中,程序运行的时候将不再需要静态库。
静态库的命名:规定上以 lib 开头,以 .a 结尾,中间部分就是库的名字。
一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们编译的时候默认为动态链接库,只有在该库找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库。
2.1 静态库的制作
下面做一个实验,演示静态库的制作过程,先给出预备的使用的代码:
//mystdio.h#pragma once//定义C标准库缓冲区大小
#define MAX 1024// 三种缓冲类型标志位
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2//使用mFILE结构体模拟C标准库中的FILE结构体
typedef struct IO_FILE
{int flag;int fileno;char outbuffer[MAX];int size;
}mFILE;// mfopen == fopen, mfwrite == fwrite, mfflush == fflush, mfclose == fclose
mFILE *mfopen(const char * filename, const char * mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
//mystring.h#pragma onceint my_strlen(const char *s);
//mystdio.c#include "mystdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.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_WRONLY | O_CREAT | O_TRUNC, 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 == NULL){close(fd);}mf->fileno = fd;mf->flag = FLUSH_LINE;mf->size = 0;return mf;
}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->outbuffer[stream->size - 1] == '\n'){mfflush(stream);}return num;
}
void mfflush(mFILE *stream)
{//写到文件内核级缓冲区中if (stream->size > 0){write(stream->fileno, stream->outbuffer, stream->size);}fsync(stream->fileno);stream->size = 0;
}
void mfclose(mFILE *stream)
{if (stream->size > 0){mfflush(stream);}close(stream->fileno);free(stream);
}
//mystring.c#include "mystring.h"
#include <stdio.h>int my_strlen(const char *s)
{const char *start = s;while(*s){s++;}return s - start;
}
步骤1:形成库需要的 .o 文件
步骤2:使用命令 ar -rc [静态库.a] [*.o] 进行静态库的生成。 ar是gnu归档工具。
2.2 静态库的使用
任意目录下新建 main.c ,内容如下
// main.c#include "mystdio.h"
#include "mystring.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>int main()
{mFILE *fp = mfopen("log.txt", "w");if (fp == NULL){return 1;}int cnt = 10;while(cnt--){printf("write %d\n", cnt);char buffer[64];snprintf(buffer, sizeof(buffer), "hello xiaoc, cnt is : %d\n", cnt);mfwrite(buffer, strlen(buffer), fp);// mfflush(fp);sleep(1);}mfclose(fp);return 0;
}
使用命令将 main.c 文件编译为 main.o 文件,并将 mystdio.h,mystring.h,libmyc.a 文件复制到该目录下。
使用命令 gcc -o main main.o -L. -lmyc 将main.o 和 静态库 libmyc.a链接起来形成可执行文件 main。
执行文件 ./main,下列内容表示程序可正常执行,说明自己写的静态库是可以使用的。
-L:指定库路径,上述 . 表示指定当前路径。
-l:指定库名称,库名称是静态库文件去掉开头的 lib 和结尾的 .a 的中间部分。 这个l是小写的“L”。
-I:指定头文件搜索路径,这里头文件在当前目录中,所以不用指定。这个I是大写的“i”。
测试目标文件生成后,静态库删除,程序照样可以运行。如果要链接任何非C/C++标准库,都需要指明 -L -l。
C/C++标准库链接的时候不用指定头文件路径和库路径,是因为在安装gcc/g++的时候,就已经把标准库的头文件和库文件复制到了系统中指定的路径下了,所以在使用的时候不需要指定头文件路径和库文件路径。C/C++标准库编译的时候也不需要指定库名称,因为gcc/g++是C语言和C++的编译器,它们认识C/C++标准库。
所以在链接第三方库的时候不想指定头文件和库文件的路径时,也可以把第三方库的头文件和库文件拷贝到Linux系统中的指定文件中,但是gcc/g++不认识第三方库,所以就算这样链接的时候也需要指定库的名称。
这里给出一个makefile脚本,创建一个lib目录,lib目录中用include目录和mylib目录,分别存放静态库的头文件和库文件,并打包成压缩包。
libmyc.a:mystdio.o mystring.oar -rc $@ $^
mystdio.o:mystdio.cgcc -c $<
mystring.o:mystring.cgcc -c $<.PHONY:output
output:mkdir -p lib/includemkdir -p lib/mylibcp -f *.h lib/includecp -f *.a lib/mylibtar czf lib.tgz lib.PHONY:clean
clean:rm -rf *.o libmyc.a lib lib.tgz
3. 动态库
动态库(.so):本质也是对源文件对应的 .o 文件进行一个打包,打包成一个可执行的 .a 文件。程序运行的时候才去链接动态库,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它使用到的函数的入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程为动态链接(dynamic linking)。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
3.1 动态库的制作
步骤1:将 .c 源文件编译成 .o 文件,编译的使用需要带 -fPIC选项。
步骤2:使用 gcc -shared -o libmyc.so *.o 命令,形成动态库 .so 文件。
shared:表示生成共享库格式
fPIC:产生位置无关码(position independent code)
库名规则:libxxx.so
可以看到该文件是一个动态链接文件
3.2 动态库的使用
动态库的使用和静态库的使用相同,需要指定头文件路径,库文件路径以及链接的库名称。但是直接运行可执行程序的时候却,报错找不到动态库。
解决方案1:将头文件和库文件安装到系统路径下。
解决方案2:在系统存储库文件的路径下,建立与第三方库的软链接。
解决方案3:给环境变量 LD_LIBRARY_PATH 添加上第三方库文件的路径,程序运行时会在系统路径中查找动态库,也会在该环境变量中的路径中查找动态库。
将上述环境变量写入相关配置文件中,就可以使其永久可以找到第三方库了。
解决方案4:在 /etc/ld.so.conf.d/ 目录中任意创建一个以.conf结尾的配置文件。然后将第三方库的路径复制到该文件中,使用 ldconfig 刷新一下,这样就能找到动态库了。
知识点1:
gcc/g++默认使用动态库链接。当动态库和静态库都存在的时候,非要静态链接,就在链接的时候加选项 -static。只存在动态库的时候,不能进行静态链接。只存在静态库的时候,链接的时候都只能静态链接。
知识点2:
在Linux系统下,默认情况安装的大部分库,都优先安装的是动态库。
4. 目标文件
编译和链接这两个步骤在Windows下已经被IDE封装的很完美了,一般都是一键构建非常方便,但一旦遇到错误的时候,尤其是链接相关的错误,很多人就束手无措了。在Linux下,之前也学习过如何通过gcc编译器来完成这一系列的操作。
下面深入探讨一下编译和链接的整个过程,来更好的理解动静态库的使用原理。
编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。在编译之后会生成拓展名为 .o 的文件,它们被称作目标文件。 要注意的是如果修改了一个源文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件是一个二进制文件,文件格式是ELF,是对二进制代码的一种封装。
这里给出两份代码hello.c 和code.c,通过编译,编译出hello.o 和 code.o。
//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");
}
使用 file [文件名] 命令查看文件,可以看到目标文件是ELF格式的文件
5. 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):保存已初始化的全局变量和局部静态变量。
6. ELF从形成到加载轮廓
6.1 ELF形成可执行
步骤1:将多份C/C++源代码,翻译成目标.o文件。
步骤2:将多份 .o 文件section进行合并。
实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库的合并。
7.2 ELF可执行文件加载
一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成Segment。
合并原则:根据相同属性,比如:可读,可行,可执行,需要加载时申请空间等将section合并为segment。
即使是不同的Section,在加载到内存中,可能会以segment形式,加载到一起。
很显然,这个合并工作也已经在形成ELF的时候,合并方式就已经确定了,具体合并原则被记录在了ELF程序头表中。
使用命名 readelf -S [ELF文件名] 查看文件的section信息:
可以看到ls命令可执行程序,有31个section,并且每一个section的信息以类似于数组的方式存储起来。
使用命令 readelf -l [ELF文件名] 查看文件的segment信息:
可以看到 ls 程序里面有13个segement,在05下标对应的segment中,将 .data 和 .bss节合并到一个segment中,.data存储的是数据,如全局变量和局部静态变量,.bss中存储的是全局未初始化变量,都是属于数据,所以合并到了一个segment中。上述每一个segment后面都有相关RWX的标志位,表示该segment的权限属性。
对于 程序头表 和 节头表,其实ELF文件提供2个不同的视图/视角来让我们理解这两个部分:
链接视图(Linking View)-- 对应节头表(Section Header Table)
文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
为了空间布局上的效率,在链接目标文件时,链接器会把很多节合并,规整成可执行的段、可读写的段、只读段等。合并后,空间利用率就高了,否则,对于物理内存页浪费太大(物理内存页分配一般都是整数倍一块一块分配的,比如4K)。
执行视图(Execution View)-- 对应程序头表(Program Header Table)
告诉操作系统如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table。
section header table 在链接时作用,program header table 在加载时作用。
上图右边的第一个图,对应的是分段的图,通过RWX属性将每个section分为几个段。第二个图就是每个section的分布图。可以看到上图中.init和.text分到了一个段中,.data自己分为一个段,.rodata和.got分到一个段。
从链接视图来看:
.text节:是保存了程序代码指令的代码节。
.data节 :保存了初始化的全局变量和局部静态变量等数据。
.rodata节 :保存了只读的数据,如⼀⾏C语⾔代码中的字符串。由于.rodata节是只读的,所以只能存在于⼀个可执⾏⽂件的只读段中。因此,只能是在text段(不是data段)中找到.rodata 节。
.BSS节 :为未初始化的全局变量和局部静态变量预留位置。
.symtab节 : Symbol Table 符号表,就是源码⾥⾯那些函数名、变量名和代码的对应关系。
.got.plt节 (全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导⼊的共享库函数的访问⼊⼝,由动态链接器在运⾏时进⾏修改。
从执行视图来看:
告诉操作系统哪些模块可以被加载进内存。加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执⾏的。
可以在ELF头中找到文件的基本信息,以及看到ELF头如何定位程序头表和节头表的。使用命令 readelf -h [ELF文件名] 查看ELF头的信息:
对于ELF header部分,只用知道其作用即可,它的主要目的是定位文件的其他部分。
知识点1:
为什么要将section合并成segment?
section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果 .text 部分为4097字节,.init 部分为512字节,那么将占用3个页面。而合并后,加载是将一个segment同时加载,这两个部分只需要2个页面。
此外,操作系统在加载程序时,会将具有相同属性的 section 合并成一个大的 segment ,这样就可以实现不同的访问权限,从此优化优化内存管理和权限访问控制。
7. 理解链接和加载
7.1 静态链接
无论是自己的.o文件,还是静态库中的.o文件,本质都是把.o文件进行链接的过程,所以静态链接本质就是研究.o文件是如何链接的。
这里将上述的编译过的hello.o和code.o进行反汇编查看,使用 objdump -d [目标文件名] 对目标文件进行反汇编:
上图是code.s的内容,这里的e8表示函数调用的机器码,后面的0表示函数地址。 这个函数调用对应着代码中printf的调用。
上图是hello.s的内容,这里的函数地址也是0。 这里的函数调用对应着代码中printf和run的调用。
可以从上看出,光是编译的情况下,hello.o中是不认识code.o中的函数的,也不认识C标准库中的函数。因此,编译器只能将函数的跳转地址先暂时设为0。
这个地址会在链接的时候进行修正。为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码节(.data)中还存在一个重定位表,在链接的时候就会根据表里记录的地址将其修正。
使用readelf -s code.o,读取code.o的符号合集,这里可以看到printf函数是未被定义的(UND表示未被定义,puts就是printf的实现)。
使用readelf -s hello.o,可以看到run和printf都是未被定义的。
将这两个.o文件通过链接,链接成可执行文件exe,这是再使用 readelf -s exe 进行查看,就可以看到run函数有了对应的地址。printf是动态链接的,这里不做说明。而这里run前面的16表示在exe程序的第16节中,使用 readelf -S exe 可以看到这个节是 .text节。这表示了它属于代码节。
使用 objdump -d exe > exe.s,在exe.s中的main中可以看到printf和run的地址被进行了修改。
静态链接:1.将所有.o和静态库中的.o文件的section合并成segment。2. 将函数的地址进行修正。
7.2 ELF加载与进程地址空间
7.2.1 虚拟地址与逻辑地址
一个ELF可执行程序,在没有被加载到内存的时候,本身就有地址,当代计算机工作的时候,都采用“平坦模式”进行工作。所以也要求ELF对自己的代码和数据进行统一编制。下面是对exe.s中的部分内容。
平坦模式:就是可执行程序中的所以segment都是从0开始依次递增的统一编制。
最左侧的就是ELF的虚拟地址,其实严格意义上应该叫做逻辑地址(起始地址+偏移量),但是平坦模式下,起始地址为0,所以起始虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编制了。
进程的mm_struct,vm_area_struct在进程刚刚创建的时候,初始化的数据就是从ELF各个segment来的,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中vm_area_struct的[start, end] 等范围数据,另外在将虚拟地址和物理地址的映射填入页表当中。
所以虚拟地址机制不仅仅操作系统要支持,编译器也要支持。
7.2.2 重新理解进程虚拟地址空间
ELF被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry point address字段当中。
所以一个可执行程序在Linux系统中的加载过程如下:
步骤1: 在 bash 进程中,通过命令行的形式输入可执行程序的文件名,识别到是一个可执行程序之后,在 bash 进程中打开了可执行程序的文件,并且创建可执行程序的进程。
步骤2:在这个进程中通过 dentry 结构体找到文件的 inode,找到 inode 之后,加载文件的属性,并且通过 inode 和 data blocks 的映射关系,找到数据和代码,将其放入物理内存中。
步骤3:然后初始化 mm_struc t和 vm_area_struct 结构体的[start, end],并且填充页表。
步骤4:将程序的入口地址加载到 cpu当中,此时 cpu 进行调度然后运行该进程。cpu中有个EIP字段,里面存放的是当前指令的下一条指令的地址,这样就可以通过入口地址依次运行整个程序了。
知识点1:
在计算机中,磁盘上的逻辑地址就是进程中的虚拟地址,cpu使用的也是虚拟地址。
7.3 动态链接与动态库加载
7.3.1 进程如何使用动态库
下面对上图进行说明:
如果一个可执行程序的进程要调用动态库,首先需要先将动态库的代码和数据加载到物理内存当中,其次通过页表将动态库的物理地址映射到进程的虚拟地址空间的共享区上,然后在通过代码区的代码调用动态库中函数的地址,进行地址跳转调用动态库中的库函数。
7.3.2 进程间如何共享库
如果两个进程同时使用一个动态库,就是将一份动态库的物理地址通过两个页表分别映射到两个进程的虚拟地址空间的共享区中。
所以在多个进程使用同一个动态库的时候,动态库只需要加载一份在内存中,减少了内存的浪费。
7.3.3 动态链接
7.3.3.1 概要
动态链接远比静态链接要常用得多。使用 ldd [可执行程序文件名] 就可以查看该可执行程序链接的动态库。
编译器默认不使用静态链接,是因为静态链接最大的问题在于生成的文件体积大,并且相当耗费内存资源。
动态链接的优势就是,我们可以将需要共享的代码单独提取出来,保存成一个独立的动态库,等到程序加载的时候再将它们加载到内存,这样不但可以节省磁盘和内存的空间,而且可以被不同的进程所共享。
首先,动态链接实际上是将链接的整个过程推迟到了程序加载的时候。比如运行一个程序,操作系统会首先将程序的数据和代码连同使用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存之后,一旦它的内存地址被确定,就可以去修正动态库中库函数在进程虚拟地址空间中的函数跳转地址了。
7.3.3.2 可执行程序被编译器动了手脚
在C/C++程序中,当程序开始执行时,首先并不会直接跳转到main函数。实际上,程序的入口点是_start,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。上图中就是Linux中的一个链接器。
在_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 函数来终⽌程序。
7.3.3.3 动态库中的相对地址
动态库为了随时进行加载,为了支持并映射到任意进程的任意为止,对动态库中的方法的统一编址,在进程虚拟地址空间中采用相对编址的方案进行编址的(动态库的编址和可执行程序一样,都要遵守平坦模式)。
在动态库中的编址还是起始地址(0)+ 偏移量,由于起始地址为0,所以偏移量就是动态库的编址。映射到进程的虚拟地址空间时,在进程的虚拟地址空间中,动态库的库函数地址就是动态库在虚拟地址空间中的地址,加上动态库的库函数的偏移量,这被称为相对编址。
7.3.3.4 程序怎么和库具体映射起来
动态库也是一个文件,要被访问也是要先被加载,要被加载就是要被打开的,让进程找到动态库的本质:就是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。
首先进程打开动态库,通过动态库struct_file结构体找到dentry然后找到动态库的inode,将动态库的属性,数据和代码加载到内存中,然后为动态库开辟一个vm_area_struct并初始化,最后再将动态库的物理地址通过页表映射到进程的虚拟地址空间中。
7.3.3.5 程序怎么进行库函数调用
库已经被映射到了当前进程的虚拟地址空间中,库的虚拟其实地址也知道,库中每一个方法的偏移量也知道,所以访问库中的任意方法,只需要知道 库的起始虚拟地址 + 库方法偏移量 就可定位库中的方法。
而且,整个调用过程是从代码区跳转到共享区,调用完毕再返回到代码区,整个过程完全在进程虚拟地址空间中进行的。
7.3.3.6 全局偏移量表GOT(global offset table)
程序运行之前先把所有动态库加载并映射,所有库的起始虚拟地址都提前知道,然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(加载地址重定位)。但是修改地址不是修改代码区吗?代码区不是只读的吗?怎么进行修改?
给出的解决方法是,动态链接采用的做法是在 .data 中专门预留一片区域用来存放函数的跳转地址,它也被叫做全局偏移量表GOT,表中每一项都是本运动模块要引用的一个全局变量或函数的地址(偏移量)。因为 .data 区域是可读写的,所以可以支持动态进行修改。
1. 有了GOT表,代码便可以被所以进程共享。但在不同进程的虚拟地址空间中,各个库的起始虚拟地址不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
2. 在单个.so下,由于GOT表与.text的相对位置是固定的,所以完全可以利用相对寻址来找到GOT表。
3. 在调用函数的时候会首先查GOT表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
4.这种方式实现的动态链接被叫做PIC地址无关代码。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这就是为什么之前给编译器指定 -fPIC 参数的原因,PIC = 相对编址+GOT。
7.3.3.7 库间依赖
不仅仅可执行程序调用库,库也会调用其他库。库之间是有依赖的,库中也有.got。
由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一步降低开销,操作系统还做了一些其他的优化,比如延迟绑定,或者也叫做PLT(过程链接表)。与其在程序一开始就对所有函数进行地址重定位,不如将这个过程推迟到函数第一次被调用的时候,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。
动态链接实际上将链接的整个过程,比如符号查询,地址的重定位从编译时推迟到程序运行时,虽然牺牲了一定的性能和程序加载时间,但动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。
知识点1:
解析依赖关系的时候,就是加载并完善相互之间的GOT表的过程。
7.3.4 总结
1. 静态链接提⾼了程序的模块化⽔平。对于⼀个⼤的项⽬,不同的⼈可以独⽴地测试和开发⾃⼰的模块。通过静态链接,将自己的demo代码和静态库链接起来⽣成可执⾏⽂件,进行自己模块的开发测试。
2. 静态链接会将编译产⽣的所有⽬标⽂件,和⽤到的各种库合并成⼀个独⽴的可执⾏⽂件,
其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。
3.动态链接是将链接的整个过程推迟到了程序加载的时候。运⾏⼀个程序时,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,但是⽆论加载到什么地⽅,都要映射到进程对应的地址空间,然后通过.GOT⽅式进⾏调⽤(运⾏重定位,也叫做动态地址重定位)。
相关文章:
C/C++动静态库的制作与原理 -- 静态库,动态库,目标文件,ELF文件,动态链接,静态链接
目录 1. 什么是库 2. 静态库 2.1 静态库的制作 2.2 静态库的使用 3. 动态库 3.1 动态库的制作 3.2 动态库的使用 4. 目标文件 5. ELF文件 6. ELF从形成到加载轮廓 6.1 ELF形成可执行 7.2 ELF可执行文件加载 7. 理解链接和加载 7.1 静态链接 7.2 ELF加载与进程地…...
Java 并发编程之synchronized
一、前言 在并发编程中,多个线程访问同一个共享资源时,我们必须考虑如何维护数据的原子性。在JDK1.5之前,Java是依靠Synchronized关键字实现锁功能来做到这点的。Synchronized是JVM实现的一种内置锁,锁的获取和释放是由JVM隐式实…...
Windows 11【1001问】查看Windows 11 版本的18种方法
随着技术的飞速发展,操作系统作为连接硬件与软件的核心桥梁,其版本管理和更新变得尤为重要。对于用户而言,了解自己设备上运行的具体Windows 11版本不仅有助于优化系统性能,还能确保安全性和兼容性。然而,不同场景和需…...
python 元组tuple
元组:有序不可变列表 (相当于只读的list) 注意:元组里的普通元素不可以修改,但是元组里的list可以修改 index(元素) 查找某个元素,有的话返回下标,没有的话报错 count(元素) 统计某元素在元组中出现的次数 len(元组) 统计元组内的元素个数 #定义元组,元组支持嵌套 t1("…...
485 多路信号采集,校验干扰问题
在RS-485总线中同时采集多路信号时,若某一路出现CRC校验失败,通常由总线冲突、信号干扰或硬件设计缺陷引起。以下是具体影响分析和解决方案: 一、多路信号同时采集的影响 1. 总线冲突风险 现象:多路信号同时发送时,485总线(半双工)无法区分信号,导致数据叠加损坏。 后…...
【Eureka 缓存机制】
今天简单介绍一下Eureka server 的缓存机制吧✌️✌️✌️ 一、先来个小剧场:服务发现的"拖延症" 想象你是个外卖小哥(客户端),每次接单都要打电话问调度中心(Eureka Server):“现在…...
MySQL并发知识(面试高频)
mysql并发事务解决 不同隔离级别下,mysql解决并发事务的方式不同。主要由锁机制和MVCC(多版本并发控制)机制来解决并发事务问题。 1. mysql中的锁有哪些? 表级锁: 场景:表级锁适用于需要对整个表进行操作的情况,例如…...
Git GitHub基础
git是什么? Git是一个分布式版本控制系统,用于管理源代码的变更。它允许多个开发者在同一个项目上协作,同时跟踪每个修改的历史记录。 关键词: 分布式版本控制软件 软件 安装到我们电脑上的一个工具 版本控制 例如论文&…...
Rabbit MQ 高频面试题【刷题系列】
文章目录 一、公司生产环境用的什么消息中间件?二、Kafka、ActiveMQ、RabbitMQ、RocketMQ有什么优缺点?三、解耦、异步、削峰是什么?四、消息队列有什么缺点?五、RabbitMQ一般用在什么场景?六、简单说RabbitMQ有哪些角…...
Ubantu22.04系统docker部署Open WebUI+Ollama【教程】
Open WebUI 是一个可扩展、功能丰富且用户友好的自托管 AI 平台,旨在完全离线运行。它支持各种 LLM 运行器,如 Ollama 和 OpenAI 兼容的 API,并内置了 RAG 推理引擎,使其成为强大的 AI 部署解决方案。 1.docker拉取镜像 &#x…...
知识图谱科研文献推荐系统vue+django+Neo4j的知识图谱
文章结尾部分有CSDN官方提供的学长 联系方式名片 文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站,有好处! 📑 编号:D030 📑 vuedjangoneo4jmysql 前后端分离架构、图数据库 📑 文献知识图谱&#…...
我的世界开发模组的心得体会
最头疼的问题 本人也是小白,也就跟着ai学学怎么开发模组,不会的上网搜搜,但是目前最令我头疼的就是运行rundata和runcilent时的模块冲突,解决办法就是使用以下的build.gradle代码,不要接受人工智能的建议,…...
HTML:自闭合标签简单介绍
1. 什么是自结束标签? 定义:自结束标签(Self-closing Tag)是指 不需要单独结束标签 的 HTML 标签,它们通过自身的语法结构闭合。语法形式: 在 HTML5 中:直接写作 <tag>,例如 …...
Oracle性能调优(一):时间模型统计
Oracle性能调优(一):时间模型统计 时间模型统计视图时间模型统计指标时间模型统计视图 📖 DB Time的含义: DB Time表示前台会话在数据库调用中所花费的总时间,它是衡量数据库实例总负载的一个重要指标。DB Time是从实例启动时开始累计测量的,其计算方法是将所有前台会话…...
MacBook Pro使用FFmpeg捕获摄像头与麦克风推流音视频
FFmpeg查看macos系统音视频设备列表 ffmpeg -f avfoundation -list_devices true -i "" 使用摄像头及麦克风同时推送音频及视频流: ffmpeg -f avfoundation -pixel_format yuyv422 -framerate 30 -i "0:1" -c:v libx264 -preset ultrafast -b:v 1000k -…...
【构建工具】Gradle Kotlin DSL中的大小写陷阱:BuildConfigField
在Android开发当中,BuildConfig是一个非常有用的功能,它允许我们在构建过程中定义常量,并在运行时使用它们。But!!当我们从传统的Groovy DSL迁移到Kotlin DSL时或者被Android Studio坑的时候,有一些细微的差…...
Linux网络 TCP全连接队列与tcpdump抓包
TCP全连接队列 在 Linux 网络中,TCP 全连接队列(也称为 Accept 队列)是一个重要的概念,用于管理已经完成三次握手,即已经处于 established 状态但尚未被应用程序通过 accept( ) 函数处理的 TCP 连接,避免因…...
ChatGPT与DeepSeek:开源与闭源的AI模型之争
目录 一、模型架构与技术原理 二、性能能力与应用场景 三、用户体验与部署灵活性 四、成本与商业模式 五、未来展望与市场影响 六、总结 随着人工智能技术的飞速发展,ChatGPT和DeepSeek作为两大领先的AI语言模型,成为了行业内外关注的焦点。它们在…...
泛微Ecode新增Button调用服务器中的JSP页面里的方法
前言 前端Ecode调用 后端接口编写 JSP文件方法 总结 前言 因为我们是从之前E8版本升级到E9的,所以会有一些接口是通过jsp文件来实现前后端调用的,这里介绍的就是如果你有接口是写在jsp文件里面调用的,但是你又想在Ecode中调用的对应的接…...
知识图谱+智能问诊预诊系统vue+django+neo4j架构、带问诊历史
文章结尾部分有CSDN官方提供的学长 联系方式名片 文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站,有好处! 🤍编号:D032 🤍智能问答:智能问答自诊、预诊功能,同时可以保存问答历史 &…...
redis repl_backlog_first_byte_offset 这个字段的作用
repl_backlog_first_byte_offset 是 Redis 复制积压缓冲区(Replication Backlog)中的一个关键字段,其作用是 标识积压缓冲区中第一个字节对应的全局复制偏移量。 通俗解释 当主从节点断开重连时,Redis 需要通过复制积压缓冲区&am…...
第49天:Web开发-JavaEE应用SpringBoot栈模版注入ThymeleafFreemarkerVelocity
#知识点 1、安全开发-JavaEE-开发框架-SpringBoot&路由&传参 2、安全开发-JavaEE-模版引擎-Thymeleaf&Freemarker&Velocity 一、开发框架-SpringBoot 参考:https://springdoc.cn/spring-boot/ 访问SpringBoot创建的网站 1、路由映射 RequestMapping…...
python数据容器切片
从一个序列中取出一个子序列 序列[起始位置:结束位置:步长] 起始位置和结束位置 省略,表示从头取到尾 步长省略表示1 步长负数,表示从后往前取 步长-1 等同于将序列反转了...
GCM模式在IPSec中的应用
本文详细介绍使用GCM模式加密的IPSec数据包的组成部分及验证方法。 关联RFC-4106 The Use of Galois/Counter Mode (GCM) in IPsec Encapsulating Security Payload (ESP) GCM数据包格式:此处采用ESP封装(未加密)数据包。用于介绍数据包的详…...
Zynq移植canopen协议站canfestival+控制电机运动
一、 内容介绍 从零开始,在ZYNQ开发板上移植cnafestival,并最终控制电机运动。主要分别五部分 1. Vivado导出硬件XSA文件 2. 创建vitis工程,并移植Canfestival 3. 对象字典工具的安装及使用 4. 开发板通过SDO报文配置电机PDO参数 5. 开发板通…...
弱监督语义分割学习计划(2)-使用CoT进行Open Vocabulary Label简单实现类激活图
零: 项目说明 是这样的一个事情,经过与deepseek的一番讨论和交流,DeepSeek为我设计了一个30天高强度学习计划,重点聚焦弱监督/无监督语义分割在野外场景的应用,结合理论与实践,并最终导向可落地的开源项目。目前开始了…...
TCP/IP 5层协议簇:网络层(IP数据包的格式、路由器原理)
目录 1. TCP/IP 5层协议簇 2. IP 三层包头协议 3. 路由器原理 4. 交换机和路由的对比 1. TCP/IP 5层协议簇 如下: 2. IP 三层包头协议 数据包如下:IP包头不是固定的,每一个数字是一个bit 其中数据部分是上层的内容,IP包头最…...
ISP 常见流程
1.sensor输出:一般为raw-OBpedestal。加pedestal避免减OB出现负值,同时保证信号超过ADC最小电压阈值,使信号落在ADC正常工作范围。 2. pedestal correction:移除sensor加的基底,确保后续处理信号起点正确。 3. Linea…...
Mybatis调用存储过程
在mysql数据库中创建一个存储过程: DELIMITER $$ CREATEPROCEDURE mybatisdemo1.pgetallusers(IN sid INT,IN eid INT)BEGINSELECT * FROM sb_users WHERE id>sid AND id<eid;END$$ DELIMITER ;在Mapper接口里创建方法,和普通的查询数据方法没区别…...
uniapp 系统学习,从入门到实战(六)—— 样式与布局
全篇大概 4700 字(含代码),建议阅读时间 30min 📚 目录 Flex 布局在 UniApp 中的应用响应式设计与适配多端使用 SCSS 提升样式开发效率实战案例演示总结 1. Flex 布局在 UniApp 中的应用 1.1 基础布局实现 通过 display: flex 快速构建弹性容器&#…...
微服务学习(1):RabbitMQ的安装与简单应用
目录 RabbitMQ是什么 为什么要使用RabbitMQ RabbitMQ的安装 RabbitMQ架构及其对应概念 队列的主要作用 交换机的主要作用 RabbitMQ的应用 通过控制面板操作(实现收发消息) RabbitMQ是什么 RabbitMQ是一个开源的消息队列软件(消息代理…...
【Spring】Spring AOP原理
目录 前言 代理模式 静态代理 优缺点 动态代理 JDK动态代理 工作原理 JDK动态原理实现关键步骤 CGLib动态代理 CGLIB动态代理实现关键步骤 总结 前言 在上一篇中,我们讲解了什么是AOP,以及Spring AOP是如何使用的,那么本篇我们就…...
CentOS vs Ubuntu - 常用命令深度对比及最佳实践指南20250302
CentOS vs Ubuntu - 常用命令深度对比及最佳实践指南 引言 在 Linux 服务器操作系统领域,CentOS 和 Ubuntu 是广泛采用的发行版。它们在命令集、默认工具链及生态系统方面各有特点。本文深入剖析 CentOS 与 Ubuntu 在常用命令层面的异同,并结合实践案例…...
【JavaScript】《JavaScript高级程序设计 (第4版) 》笔记-Chapter27-工作者线程
二十七、工作者线程 工作者线程 前端开发者常说:“JavaScript 是单线程的。”这种说法虽然有些简单,但描述了 JavaScript 在浏览器中的一般行为。因此,作为帮助 Web 开发人员理解 JavaScript 的教学工具,它非常有用。单线程就意味…...
微信小程序 - 页面跳转(wx.navigateTo、wx.redirectTo、wx.switchTab、wx.reLaunch)
API 跳转 1、wx.navigateTo (1)基本介绍 功能:保留当前页面,跳转到应用内的某个页面,使用该方法跳转后可以通过返回按钮返回到原页面 使用场景:适用于需要保留当前页面状态,后续还需返回的情…...
vscode使用豆包MARSCode----集成doubao1.5 DeepSeekR1 DeepseekV3模型的ai编程插件
引入扩展 打开VSCode扩展窗口,在搜索窗口搜索MarsCode,找到MarsCode 插件单击「install」,完成安装,登录即可使用MarsCode 编程助手。 主要功能 主要快捷键 / explain 解释项目代码,AI 返回的内容有结构分类&#…...
C 语言共用体:深入理解与实践】
目录 一、引言 二、共用体的定义和基本语法 三、共用体的使用 3.1 声明共用体变量 3.2 给共用体成员赋值 3.3 共用体的内存布局 四、共用体的应用场景 4.1 节省内存空间 4.2 处理不同类型的数据 五、共用体使用的注意事项 六、总结 一、引言 在 C 语言中,共…...
Cherry Studio + 火山引擎 构建个人AI智能知识库
🍉在信息化时代,个人知识库的构建对于提高工作效率、知识管理和信息提取尤为重要。尤其是当这些知识库能结合人工智能来智能化地整理、分类和管理数据时,效果更为显著。我最近尝试通过 Cherry Studio 和 火山引擎 来搭建个人智能知识库&#…...
曹操智行构建国内首个全域自研闭环智驾生态
2月28日,曹操出行举办曹操智行自动驾驶平台上线仪式,宣布已成功构建国内首个“F立方”全域自研闭环智驾生态,同时在苏杭两地开启Robotaxi运营试点,并投放搭载吉利最新智驾系统的车辆。 此次试点运营,标志着曹操出行在…...
在 ASP.NET Core 中压缩并减少图像的文件大小
示例代码:https://download.csdn.net/download/hefeng_aspnet/90294127 在当今的数字时代,图像是 Web 应用程序和用户体验不可或缺的一部分。但是,处理大型图像文件可能会导致网页加载缓慢和更高的存储费用。为了解决这个问题,在…...
58、深度学习-自学之路-自己搭建深度学习框架-19、RNN神经网络梯度消失和爆炸的原因(从公式推导方向来说明),通过RNN的前向传播和反向传播公式来理解。
一、RNN神经网络的前向传播图如下: 时间步 t1: x₁ → (W_x) → [RNN Cell] → h₁ → (W_y) → y₁ ↑ (W_h) h₀ (初始隐藏状态) 时间步 t2: x₂ → (W_x) → [RNN Cell] → h₂ → (W_y) → y₂ ↑ (W_h) h₁ 时间…...
【Qt-信号与槽】connect函数的用法
🏠个人主页:Yui_ 🍑操作环境:Qt Creator 🚀所属专栏:Qt 文章目录 1.信号和槽的概念1.1 信号的本质1.2 槽的本质1.3 补充说明2. 信号和槽的使用2.1 connect函数介绍2.2 connect函数的简单使用2.2.1 图形化方…...
sql深入学习
文章目录 前言知识学习注释的两种形式字符型注入万能密码 布尔盲注报错注入堆叠注入时间盲注二次注入 小技巧 前言 这次学习建立在对数据库有基本的认识,了解基础的增删改查语句,数字型注入和字符型注入的基础上,进一步深入学习知识…...
Canvas修仙传·第三重天金丹境(上集) ——九转游戏开发心法之《灵蛇奇谭》
各位道友,历经前两重天的修炼,恭喜诸位突破"动画"与"交互"桎梏!今日我们将解锁Canvas修仙路上第一个质变境界——将代码炼化为游戏元神!(ノ◕ヮ◕)ノ*:・゚✧ 章前黑话词典 🔍 金丹境术语表: 游戏循环(Game Loop):驱动游戏逻辑的灵气泵状态机(Sta…...
Matplotlib基础知识总结
1、简介 安装使用pip install matplotlib命令即可; 2、基本绘图流程 3、pyplot基础语法 (1)创建画布与创建子图 figure语法说明:figure(numNone, figsizeNone, dpiNone, facecolorNone, edgecolorNone, frameonTrue)࿱…...
linux vim 撤销 回退操作
在Linux的vim编辑器中,撤销和回退操作是非常基本的,但它们可以通过不同的方式实现,具体取决于你想要的精确效果。下面是一些常用的方法: 1. 撤销(Undo) 单个撤销: 你可以通过按下u键来撤销上一…...
kubernetes Device Plugin原理与源码分析
一、背景与核心概念 1.1 Kubernetes设备管理演进之路 1.1.1 Extended Resource的局限性 在Kubernetes早期版本中,管理非标准硬件资源(如GPU、FPGA)主要依赖 Extended Resource(扩展资源) 机制,Extended …...
天佐.乾坤袋 基于抽屉式文件存储的NoSql数据库
天佐.乾坤袋 天佐.乾坤袋 简介 天佐.乾坤袋 基于抽屉式文件存储的NoSql数据库,可用于文件打包,数据整合,加密存放等多种用途。可以方便快捷的搭建和部署存储应用的系统。 传说: 弥勒所有,专做储物之用。拥有不可思议之力&#x…...
Spring Boot 消息队列(以RabbitMQ为例)
文章目录 RabbitMQ 简介与安装1. RabbitMQ 简介2. RabbitMQ 安装 Spring Boot 集成 RabbitMQ1. 创建 Spring Boot 项目2. 配置 RabbitMQ3. 定义消息队列和交换机4. 发送消息5. 接收消息6. 测试消息发送和接收 RabbitMQ 简介与安装 1. RabbitMQ 简介 RabbitMQ 是一个开源的消息…...
深度学习原理与Pytorch实战
深度学习原理与Pytorch实战 第2版 强化学习人工智能神经网络书籍 python动手学深度学习框架书 TransformerBERT图神经网络: 技术讲解 编辑推荐 1.基于PyTorch新版本,涵盖深度学习基础知识和前沿技术,由浅入深,通俗易懂…...