动静态库的原理与简单制作
目录
1.库的概念
2.静态库
2.1制作
2.2使用
3.动态库
3.1制作
3.2使用
3.3库运行搜索路径
4.目标文件
5.ELF文件
6.ELF形成到加载(仅了解)
6.1ELF形成可执行
6.2ELF可执行文件加载
7.理解链接和加载
7.1静态链接
7.2ELF加载和进程地址空间
7.2.1虚拟地址/逻辑地址
7.2.2进程地址空间
7.3动态链接和动态库加载
7.3.1动态链接如何工作(仅了解)
7.3.2编译器对可执行程序的操作(仅了解)
7.3.3动态库的相对地址
7.3.4库映射
1.库的概念
之前的文章其实有说过,但这里简单重新正式描述下。
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:
静态库 .a[Linux]、.lib[windows]
动态库 .so[Linux]、.dll[windows]一个库的真正名字,是去掉lib之类的前缀,去掉的.后面后缀。
// ubuntu 动静态库// C$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so -rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so$ ls -l /lib/x86_64-linux-gnu/libc.a -rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a//C++$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l lrwxrwxrwx 1 root root 45 May 1 02:20 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -> ../../../../x86_64-linux-gnu/libstdc++.so.6$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a -l -rw-r--r-- 1 root root 8402120 May 1 02:20 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a// Centos 动静态库// C$ ls /lib64/libc-2.17.so -l -rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so$ ls /lib64/libc.a -l -rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.a// C++$ ls /lib64/libstdc++.so.6 -l lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 -> libstdc++.so.6.0.19$ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l -rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a我们平时多个文件的编译,编译器是把每个文件都先变成.o文件,然后再与库文件一起生成可执行程序。如果我们不想让别人看到我们的源代码,但又想让别人用我们的程序(比如一些我们自己写的函数,让别人使用),这时候,我们可以把.h和生成的.o文件给别人。
但是这样还是太麻烦了,所以我们会将这些.o文件打包成一个文件,这个文件就是库文件。这样别人用的时候只需要拷贝所有.h文件和一个库文件即可。
制作库的时候不能包含main函数。
2.静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。
一个可执行程序可能用到许多的库,这些库运行有的是静态库,有的是动态库,而我们的编译默认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用gcc的-static强转设置链接静态库。
2.1制作
ar是gnu 归档工具,rc表示 replace and create,不存在就创建目标文件,存在就替换
ar的选项:
t:列出静态库中的文件。
v:verbose 详细信息
具体的内容分析,我就不手打了,下面是AI给的结果。
2.2使用
// 场景1:头文件和库文件安装到系统路径下(比如lib64目录下找,就是path里的)gcc main.c -lmystdio// 场景2:头文件和库文件和我们自己的源文件在同一个路径下gcc main.c -L. -lmymath// 场景3:头文件和库文件有自己的独立路径gcc main.c -I头文件路径 -L库文件路径 -lmymath-L:指定库路径
-I:指定头文件搜索路径 这个是大写i
-l:指定库名
测试目标文件生成后,静态库删掉,程序照样可以运行
库文件名称和引入库的名称:去掉前缀lib,去掉后缀.so,.a,如:libc.so->c关于-static 选项,下面再说
3.动态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接 (dynamiclinking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
3.1制作
shared:表示生成共享库/动态库 格式
fPIC:产生位置无关码(positionindependentcode)
库名规则:libxxx.so
<是一个个依赖文件依次执行gcc,^是所有依赖文件一起执行gcc。
注意,我们可以把静态库和动态库的制作放在同一个makefile里,只是其中一个的生成需要以make 库文件名的形式才能生成(不能一次生成,得make生成一个,make 名字生成一个),而且先生成的那个需要加句rm *.o,因为动态库和静态库的gcc选项不一样,生成的.o自然是不一样的。
3.2使用
// 场景1:头文件和库文件安装到系统路径下 gcc main.c -lmystdio// 场景2:头文件和库文件和我们自己的源文件在同一个路径下 gcc main.c -L. -lmymath // 从左到右搜索-L指定的目录// 场景3:头文件和库文件有自己的独立路径 gcc main.c -I头文件路径 -L库文件路径 -lmymath// 查看库或者可执行程序的依赖 ldd libmystdio.so linux-vdso.so.1 => (0x00007fffacbbf000) libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000) /lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)使用上跟静态库差不多。只是有个很大的坑,如下:
动态库的链接会相当麻烦
编译时:-L./stdc/lib告诉编译器在链接时去哪个目录找库文件
运行时:动态链接器不知道 ./stdc/lib这个路径,它只在系统默认的库路径中查找所以就很尴尬,编译过了但没用。
注意,静态库不会出现这样的问题,因为静态库是在链接阶段就把代码拷贝到可执行程序里了,运行的时候不需要再次搜索库文件。而动态库是运行时再次搜索库文件的。库文件默认搜索路径在/lib64下,注意vim的一些安装包可能会改系统路径,导致当前目录下的库文件也是可以的。
头文件类似静态库,都是拷贝放入可执行的,所以不管是静态还是动态,只要编译时给予指定路径即可,因此可执行程序生成后,头文件(编译阶段后可删)和静态库(链接阶段后可删)都可以删除。
头文件会自动在当前目录和/usr/include下搜索
所谓的安装库或其他软件,本质上就是把文件拷贝到系统指定的目录中
同名的动态库和静态库,gcc优先使用动态库,需要加-static选项才能优先使用静态库。如果我们只提供静态库,那么gcc不加static,也可以对该库静态链接,但是程序整体不一定是静态链接(也就是说程序其他的库依旧是动态链接,该库是静态)。如果只提供动态库,然后强行static,那么就会发生链接报错!。
3.3库运行搜索路径
从上面我们可以发现,动态库的链接实际上是有2套路径的,一个是编译时,一个是运行时。
我们编译时给的路径,只是让编译器知道了,但路径并不会写入可执行程序,同样当系统调用可执行的时候,自然也不知道相应路径,只会去系统设置的默认路径下找(比如/lib64下),这也是为什么会出现上面的问题。
那么如何解决呢?
1:拷贝.so 文件到系统共享库路径下,一般指/usr/lib、/usr/ocal/ib、/ib64 等等,而且不需要重新生成可执行,我们通过ldd可以发现,so文件在上述目录中,相应库就不会是not found,不在就会是not found。这种系统目录,不管是编译还是运行都会搜索,所以放进去就一劳永逸。
2:向系统共享库路径下建立同名软连接跟第一个方法很像,只是这里只放了软连接,不真正把库文件放入系统路径里罢了。
3:更改环境变量:LD_LIBRARY_PATH
纯净的系统下默认不会有这个环境变量,如果有配置过vim等,才会有。当然没有的也可以手动添加这个环境变量。注意,系统默认是在lib64等路径下,但也不止是lib64等路径,这个环境变量里的就是系统运行程序时,动态库查找的辅助路径
添加方法就是export LD_LIBRARY_PATH:新的绝对路径
注意,在shell里输入只会对当前的shell下的程序起作用,如果想一直留有,就必须去/etc/profile(所有用户,全局的),当前用户的家目录中的.bash_profile或者.bashrc(只对该用户起效果)等配置文件里添加这条语句。
4:ldconfig方案:配置/etc/ld.so.conf.d/,ldconfig更新
在/etc/ld.so.conf.d 目录下创建一个.conf文件,前缀随便。文件里写入你想添加的库搜索路径即可。写完之后root权限下执行ldconfig进行更新即可。
5:rpath
# 假设库在 /home/user/mylib/libmylib.so gcc -L/home/user/mylib -Wl,-rpath,/home/user/mylib -lmylib main.c -o myprogram上述方法,如果是比较官方的库,建议直接第一种方法,如果是自己写的,第2种和第4种都推荐
运行时搜索路径优先级(从高到低):
编译时指定的rpath(-Wl,-rpath)
环境变量LD_LIBRARY_PATH
系统缓存(/etc/ld.so.cache)
默认系统路径(/lib, /usr/lib等)
4.目标文件
编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。
编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码。gcc -c就是对.c的源文件进行编译,生成同名.o的目标文件。注意如果有多个目标文件,不需要全部重新编译,把有改动的源文件重新编译即可。
目标文件是一个二进制的文件,文件格式是ELF,是对二进制代码的一种封装。
5.ELF文件
以下四种都是ELF文件:
可重定位文件(RelocatableFile):即xxx.o文件。包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
可执行文件(ExecutableFile):即可执行程序。
共享目标文件(SharedObject File):即xxx.so文件。
内核转储(coredumps):存放当前进程的执行上下文,用于dump信号触发。一个ELF文件由以下四部分组成:
ELF头(ELFheader):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
程序头表(Programheadertable):列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分割开。
节头表(Section header table):包含对节(sections)的描述。
节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。常见的节:
代码节(.text):用于保存机器指令,是程序的主要执行部分。
数据节(.data):保存已初始化的全局变量和局部静态变量。bss是未初始化数据
6.ELF形成到加载(仅了解)
这里开始的内容,都仅做了解,我们只需要知道ELF是可执行程序的文件类型,有自己的格式信息。可以直接跳到7.2的部分
6.1ELF形成可执行
第一步:将多份C/C++源代码,翻译成为目标.O文件+动静态库(ELF)
第二步:将多份.o文件section进行合并
实际合并的时候,是在链接时进行的,且不是图中这么简单的合并,还涉及库的合并,具体有兴趣可以自己了解。
6.2ELF可执行文件加载
一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等,这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起
这个合并工作在形成ELF的时候,合并方式就已经确定了,具体合并原则被记录在
了 ELF 的 程序头表(Program header table)中下面是查看可执行程序的section
下面是查看section合并的segment
可以看到,形成了多个segment。而且.data,.text也被合并了。
为什么要合并section?
Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
此外,操作系统在加载程序时,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
程序头表和节头表,有什么用呢?
从两个视角来看待
链接视图(Linkingview)-对应节头表 Section header table
文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。
为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。合并了后,空间利用率就高了,否则,很小的很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一块给你,比如4k),所以,链接器趁着链接就把小块们都合并了。命令 readelf -S test.o 可以帮助查看ELF文件的 节头表。
text节:是保存了程序代码指令的代码节。
data节:保存了初始化的全局变量和局部静态变量等数据。
rodata节:保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。
.BSS节:为未初始化的全局变量和局部静态变量预留位置
.Symtab节:SymbolTable符号表,就是源码里面那些函数名、变量名和代码的对应关系。
.got.plt节(全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。使用readelf 命令查看.so 文件可以看到该节执行视图(execution view)-对应程序头表 Program header table
告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有 program header table 。
告诉操作系统哪些模块可以被加载进内存。
加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。简单的说,一个是在链接时起作用,一个是在运行加载时起作用。
下面的图看看就行,仅做了解,是网图。
ELF头中有文件的很多基本信息,还有elf头如何定位程序头表和节头表的。
//查看目标文件 $ readelf -h hello.o ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF文件标识符(魔数)Class: ELF64 # 文件类别:64位架构Data: 2's complement, little endian # 数据编码:小端序二进制补码Version: 1 (current) # ELF版本:当前版本(1)OS/ABI: UNIX - System V # 操作系统ABI:System V UNIXABI Version: 0 # ABI扩展版本:未扩展Type: REL (Relocatable file) # 文件类型:可重定位文件(目标文件)Machine: Advanced Micro Devices X86-64 # 目标平台:x86-64架构Version: 0x1 # 对象文件版本:1Entry point address: 0x0 # 入口点地址:无(目标文件为0)Start of program headers: 0 (bytes into file) # 程序头表起始偏移:无(目标文件为0)Start of section headers: 728 (bytes into file) # 节头表起始偏移:728字节处Flags: 0x0 # 处理器特定标志:无标志(0)Size of this header: 64 (bytes) # ELF头大小:64字节Size of program headers: 0 (bytes) # 程序头表条目大小:无(目标文件为0)Number of program headers: 0 # 程序头表条目数:无(目标文件为0)Size of section headers: 64 (bytes) # 节头表条目大小:64字节Number of section headers: 13 # 节头表条目数:13个Section header string table index: 12 # 节头字符串表索引:第12节(.shstrtab)//查看可执行程序 $ gcc *.o $ readelf -h a.out ELF Header:Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF文件标识符(魔数)Class: ELF64 # 文件类别:64位架构Data: 2's complement, little endian # 数据编码:小端序二进制补码Version: 1 (current) # ELF版本:当前版本(1)OS/ABI: UNIX - System V # 操作系统ABI:System V UNIXABI Version: 0 # ABI版本:0Type: DYN (Shared object file) # 文件类型:动态共享对象文件(so)Machine: Advanced Micro Devices X86-64 # 目标平台:x86-64架构Version: 0x1 # 对象文件版本:1Entry point address: 0x1060 # 入口点地址:0x1060(动态链接后解析)Start of program headers: 64 (bytes into file) # 程序头表起始偏移:64字节处Start of section headers: 14712 (bytes into file) # 节头表起始偏移:14712字节处Flags: 0x0 # 处理器特定标志:无Size of this header: 64 (bytes) # ELF头大小:64字节Size of program headers: 56 (bytes) # 程序头表条目大小:56字节Number of program headers: 13 # 程序头表条目数:13个Size of section headers: 64 (bytes) # 节头表条目大小:64字节Number of section headers: 31 # 节头表条目数:31个Section header string table index: 30 # 节头字符串表索引:第30节(.shstrtab)上面仅供了解,只需要知道其作用,也就是主要目的是定位文件的其他部分即可。
7.理解链接和加载
7.1静态链接
不是重点,再加上内容很复杂,这里只输出结论
不是很感兴趣的话,其实粗粒度的了解下,就是将静态库的代码数据以绝对编址的方式写入可执行程序罢了。下面是稍微详细的内容,更详细的可以自行百度。
静态链接本质上和多个.o文件进行合并链接是一样的。
链接就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。其中就包括地址修正,当所有模块组合在一起之后,链接器会根据我
们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这其实就是静态链接的过程。链接过程中会涉及到对.o中外部符号进行地址重定位。
7.2ELF加载和进程地址空间
7.2.1虚拟地址/逻辑地址
一个ELF程序,在没有被加载到内存的时候,本来就有地址,当代计算机工作的时候,都采用"平坦模式"也就是绝对地址进行编址,从而进行工作。所以也要求ELF对自己的代码和数据进行统一编址,下面是objdump -S反汇编之后的代码
最左侧的就是ELF的虚拟地址(进程地址空间中所说的虚拟地址),其实,严格意义上应该叫做逻辑地址(在磁盘里是逻辑地址)或者说相对地址(起始地址+偏移量),但是我们认为起始地址是0,这样就可以把平坦模式和历史上的这种相对编址方式兼容理解。注意,在linux中,虚拟和逻辑是差不多的。
也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编址了。而且为了方便初始化进程地址空间的虚拟地址,可执行程序内的地址范围大小也是[0,4GB],跟进程地址空间的大小一致,这样就可以直接拿可执行程序的地址套到进程地址空间的虚拟地址了。
进程mm_struct、vm_area_struct(初始化地址空间的各个区,页表等)在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start,end]等范围数据,另外在用详细地址,填充页表。虚拟地址机制,不光光OS要支持,编译器也要支持
7.2.2进程地址空间
ELF在被编译好之后,会把自己未来程序的入口地址(也就是main的虚拟或者说逻辑地址)记录在ELFheader的Entry字段中
我们这里粗粒度的重新再了解整个elf程序。
elf程序中每个代码,不管是main函数还是普通的函数(函数名也会被翻译成函数的地址),每个代码在汇编的视角下,都已经被编址,如上面所说,已经是依照平坦模式编址,再加上elf的表头,有入口程序地址以及整个可执行程序的区域划分和各个区域的起始地址
编址之后,我们也能够通过反汇编,在未加载到内存的时候就可以人工理解里面的内容。
每个elf可执行程序的每行代码在加载入内存后有2个地址,一个是物理地址,一个是虚拟地址(在linux中就是逻辑地址),就可以将这2个地址在页表中进行映射。
前面说了Elf表头有入口程序地址,cpu怎么执行一个程序?先将入口地址放入pc寄存器(程序计数器,存在当前正在执行指令的下一条指令的地址),然后通过MMU+页表将虚拟地址转换为物理地址,根据物理地址从物理内存中取出指令放入指令寄存器,之后程序就是不停的接受虚拟地址,转换物理地址,取指令,分析指令,解释指令,循环往复。
综上所述,所谓的进程地址空间,本质是由操作系统+编译器+计算机体系结构(CPU)三者共同配合完成。
而我们的汇编语言对应的是一个个二进制序列,是可以被CPU识别的指令集
7.3动态链接和动态库加载
单个进程怎么看到动态库的
有需要的时候把库加载到物理内存,通过页表映射到进程地址空间的共享区即可。
多个进程如何共享动态库
所有系统进程中公共的代码和数据,都只需要存一份即可。
os会自己决定,哪些库加载了,哪些库没加载。
系统中可能会同时存在非常多的已经加载的库。
os需要管理库,依旧是先描述再组织,将对库的管理转化为对某个数据结构的管理(比如链表)
7.3.1动态链接如何工作(仅了解)
动态链接实际上将链接的整个过程推迟到了程序加载的时候。
比如我们去运行一个程序,操作系统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使用情况为它们动态分配一段内存。当动态库被加载到内存以后,一旦它的内存地址被确定,我们就可以去修正动态库中的那些函数跳转地址了。
7.3.2编译器对可执行程序的操作(仅了解)
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到main函数。实际上,程序的入口点是_start,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。
在_start函数中,会执行一系列初始化操作,这些操作包括:设置堆栈:为程序创建一个初始的堆栈环境。
初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。
动态链接:这是关键的一步,_start函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(sharedlibraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。动态链接器:
动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。
环境变量和配置文件:
Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。
这些路径会被动态链接器在加载动态库时搜索。
缓存文件:
为了提高动态库的加载效率,Linux系统会维护一个名为/etc/ld.so.cache的缓存文件。
该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件。调用__libc_start_main:一旦动态链接完成,_start 函数会调用__libc_start_main(这是glibc提供的一个函数)。__libc_start_main 函数负责执行些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。
调用 main函数:最后,,__Libc_start_main 函数会调用程序的 main 函数,此时程序的执行控制权才正式交给用户编写的代码。
处理 main函数的返回值:当main函数返回时,__libc_start_main会负责处理这个返回
值,并最终调用_exit函数来终止程序。
7.3.3动态库的相对地址
动态库为了随时进行加载,为了支持并映射到任意进程的任意位置,对动态库中的方法,统一编址,采用相对编址的方案进行编址的,也就是说每个库内的所有方法都是以相对编址的方式编址的。
7.3.4库映射
动态库也是一个文件,要访问也是要被先加载,要加载也是要被打开的。
让我们的进程找到动态库的本质:也是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中。
问题来了,具体的库函数怎么调用?
库已经被我们映射到了当前进程的地址空间中
库的虚拟起始地址我们也已经知道了
库中每一个方法的偏移量地址我们也知道
访问库中任意方法和数据,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法
,整个调用过程,是从代码区跳转到共享区,调用完毕在返回到代码区,整个过程完
全在进程地址空间中进行的。
































