目录
理解软硬链接
动态库与静态库
手动制作静态库并使用
制作静态库
使用静态库方法一
使用静态库方法二
使用静态库方法三
手动制作动态库并使用
制作动态库
使用动态库方法一
使用动态库方法二
使用动态库方法三
动静态库同时使用的细节说明
动态库的理解、动态库的加载
可执行程序格式
重谈进程地址空间
动态库加载的理解
理解软硬链接
在前面,对于ll列出来的文件属性,这个1的含义并没有说
我们可以使用ln -s 源文件 链接名对任何一个文件或目录建立软链接
链接名和后缀都是可以随意取的
可以看到,这个软链接是有自己的inode编号的。所以,软链接本质上是一个独立的文件。
在用户层,使用软链接等同于使用目标文件
我们可以使用ln 源文件 链接名对任何一个文件建立硬链接
可以看到,硬链接file-hard.link一旦建立好,test.txt前面的数字就从1变成了2
file-hard.link的inode编号与file.txt的inode编号是相同的,并且将file-hard.link删除后,file.txt前面的2又变成了1。所以,硬链接本质不是一个独立的文件。那是什么呢?
如何理解软硬链接?
软链接有独立的inode,也就需要有独立的属性和内容,属性是和普通文件一样的,内容是目标文件或目录的路径,就是windows下的快捷方式
在windows中,查看一个图标的快捷方式就可以看到里面有一个目标,目标里面存放的就是一个路径。所以,我们平时用的快捷方式本质上就是一种软连接。
硬链接没有独立的inode,所以不是独立的文件,硬链接本质就是一组文件名和已经存在的文件的映射关系。
所以,当我们建立软链接就是在当前目录下建立一个快捷方式。访问软链接时,会从软链接的内容中读取目标文件字符串,然后进行路径解析,去访问目标文件字符串中的目标文件。建立硬链接就是在当前目录中新增一段文件名和inode的映射关系。在建立硬链接之前,目标文件的inode只和目标文件的文件名存在映射关系,但是建立硬链接后,目标文件的inode还和硬链接存在映射关系,也就是类似于一个文件有两个文件名。
当一个文件真正被删除,应该是没有文件名与这个文件存在映射关系时。怎么知道一个inode和几个文件名存在映射关系呢?所以,在inode中存在引用计数。所以,Il查看的文件的数字就是这个文件的引用计数,称为硬链接数。表示的是有几个文件名与这个inode建立了映射关系
可以看到,讲test.txt删除后,完全不影响file-hard.link。若我们再删除file-hard.link,就会真的将这个文件真正删除。在这里软链接会闪,是因为在这个路径下已经找不到test.txt了,我们可以将file-hard.txt进行重命名
此时就能够找到了,所以,软链接是通过路径找的,并不是通过inode编号
为什么要有软硬链接?
软链接
我们将code.c编译形成可执行程序code.exe,可以直接./code.exe执行可执行程序,但是若我们想code直接执行呢?可以将当前所处的路径添加到系统的环境变量中;也可以将code.exe拷贝到系统的默认路径下;也可以在系统的默认路径下创建一个code,让code这个文件与我们当前路径下的可执行程序建立软链接
所以,软链接就是一种快捷方式
删除软链接有两种方式
因为软链接是一个独立的文件,所以,可以直接使用rm删除;第二种是使用unlink删除
当可执行程序在一个比较深的路径下时,也可以建立软链接
软链接既可以给普通文件创建,也可以给目录创建,但是硬链接只能给普通文件创建
include这个目录里面存放的时头文件,每次查看里面内容都需要/...,我们可以直接给这个目录建立一个软链接
虽然进入了这个软链接的目录后,仍然处于这个目录当中,但是在这里面Is是可以看到include目录里面的全部内容的
所以,软链接存在的目的是让我们能够快速地定位某一个文件,以最简单的方式访问。
硬链接
可以看到,一个新建立的目录的硬链接数是2
并且到这个目录下创建一个目录后,硬链接数变成了3
我们进入目录empty。会发现包括隐藏文件在内会有3个文件之前说过.就是当前文件,为什么?因为inode编号与当前文件的inode编号是相同的。当我们创建任何一个空目录,里面都会默认有.,另外还有自己本身,会有两个映射关系。所以,硬链接数就是2。当我们在empty中创建一个空目录后,这个空目录的..就是empty,所以empty的硬链接数就会变成3。
Linux为什么要在空目录下有.和..?因为有了.和..之后,可以快速地进行路径切换。
.和..为什么是本身和上级目录?通过inode编号就可以知道。
有了硬链接数,我们就可以在不进入一个目录的情况下,知道这个目录里面有几个目录。
子目录数量 = 硬链接数 - 2(目录本身和.)
/的硬链接数是19,除去它本身和里面的.之外,还有17个..,所以里面有17个目录。
无法进入/的上级目录的原因就是/的..就是它自己,但是这里并不会增加硬链接数。
所以,通过硬链接实现的.和..,维护了Linux的文件路径。但是硬链接的用处不仅仅于此。
我们在系统的/tmp路径下构建了一个log.txt.backup文件,是log.txt的硬链接
假设我们误删了文件,可以通过硬链接拿到文件的内容,所以,硬链接可用于备份。
在Linux下,是不允许给目录建立硬链接的,因为这样可能会形成环形路径。
若我们给hard这个目录建立一个硬链接,而这个硬链接指向的是3,就会导致出现环状路径。破坏了Linux的路径结构
动态库与静态库
手动制作静态库并使用
制作静态库
将我们之前实现的文件接口拷贝到当前目录下,并再创建一些其他方法的实现
若我们想将我们写的这些方法交给别人使用,就必须将这些方法制作成库。若想制作一个静态库,必须将.c隐藏,.h必须暴露。先将所有.c编译成.o
现在就可以将.o打包形成静态库了
静态库的开头必须是lib,后缀必须是.a。打包完成后,我们只需要提供头文件(.h)和库文件(.a)即可。若其他人想要使用,就需要将库安装到他的系统中(包括库文件和头文件)。Linux下,所有的头文件是放在/usr/include里面的。创建静态库时,也可以.a.0.0.1带上版本。
使用静态库方法一
我们将刚刚打包的库文件和头文件、源文件都放到一个目录下面。现在,我们模拟将库下载到操作系统的过程。
首先,我们先将库的头文件拷贝到/usr/include里面
Linux操作系统中,库文件是放在/lib64下的
现在我们就完成了静态库的安装,我们到另外一个目录下,看看能否使用这些方法
写完代码之后会发现无法使用gcc编译。是编译时报错、链接时报错,还是运行时报错?是链接时报错
lib64中会有非常多的库,我们要链接的是哪一个库呢?
这是libc的库和C++的库。为什么之前在链接时,用到其他的库不需要指定呢?因为gcc是专门用来连接libc的,g++同理。我们自己写的库属于是第三方库。在使用gcc/g++编译第三方库时,需要在gcc/g++后面加上-l,来指定库的名称,并且这个库的名称是去掉前缀和后缀的。
-l是引入指定名称的第三方库,当我们要链接多个库时,可以带上多个-l。也可以带上-o编译成指定的名称,gcc main.c -o main -lmystido。上面这种方法是使用静态库的方法一:安装到系统里
使用静态库方法二
我们先将安装到系统中的头文件和库文件删掉
方法二是直接将库文件与头文件给使用者
这时候要如何使用这个库呢?
直接编译肯定是不行的,因为这样并不认识这个头文件
头文件和源文件在同一个路径下时,是可以使用""的
会发现头文件使用""还是不行,此时不再是编译时报错,而是链接时报错
即使指定库名称也还是不行,这就说明gcc在查静态库时,不会在当前路径下查,而是在系统默认的路径下查
此时可以-L.,然后再加-静态库名称。-L.就是告诉编译器,在查找静态库时,除了要在系统默认路径下查找,还要在我指定的路径下查找。-L+指定的路径。若有两个库的名称相同,且放在不同路径下,那个库先被找到就使用那个库
方法二:库文件和头文件与源文件在一起的情况
使用静态库方法三
库里面是不会有main函数的。并且库是需要被发布的。库发布时,建议将头文件放在include目录下,库文件放在lib目录下
这里带上@就不会在执行命令时打印出信息了
此时,make就可以形成.o和静态库
make out就可以直接将头文件和库文件打包
删除时也不仅仅只将库文件和头文件删除,还要将打包形成的目录和压缩包删除
我们打包后,就可以将形成的压缩包发送给别人,别人就可以使用我们的方法了
拿到安装包中的内容后,可以将这个安装包删除
直接编译还是会编译时报错。实际上,gcc编译时的选项不仅仅只有-l,再来认识两个-L和-I。
- -l(小写L):指定库的名称
- -L:指定库文件的路径
- -I(大写i):指定头文件的路径
注意:在默认情况下,找库文件只会到系统默认的路径下查找,但是找头文件不仅仅只会到系统默认的路径下查找,还会到当前路径下查找
库在不同平台下有不同版本,因为库是.o文件的集合,而源文件不同平台下编译生成的.o可能会有些不同,但是源代码是相同的。
有3个选项,说明无法使用一个库时,错误的类型要种:找不到头文件(-I)、不知道库路径(-L)、不知道库名称(-l)
手动制作动态库并使用
制作动态库
动态库也称为共享库
Linux中,动态库同样是以lib开头,后缀是.so,名字随便起。依赖的同样是.o
生成动态库使用的是gcc,gcc若没用-shared生成的是可执行程序,加了-shared后生成的是动态库。另外,生成动态库的.o与生成静态库的.o不一样,所以在使用.c生成.o时,需要加上-fPIC
此时就生成了动态库,想将动态库给其他人使用,同样有3种方法
使用动态库方法一
将头文件放到include目录下,库文件放到lib64目录下
可以使用ldd查看一个可执行程序依赖那些库
可以看到,将头文件删除后,可执行程序仍然是可以运行的,但是将动态库删除后,可执行程序就无法运行了。因为可执行程序还在,但是库找不到了。删除静态库对可执行程序是没用影响的
使用动态库方法二
将头文件和库文件直接给使用者
此时头文件能够找到,因为就在当前路径下,但是库文件找不到,即使库文件就在当前路径下。
所以,动静态库的查找规则是一样的
除了要告诉库的路径,还要告诉库的名称,即使这个路径下只有一个库
现在介绍的动态库的使用与静态库的使用是完全一样的
使用动态库方法三
此时会发现直接运行这个可执行程序运行不了,虽然成功编译成了可执行程序
原因是找不到动态库。所以,是有可能出现库缺失但是代码依然能够编出来的情况,只要不是特别强依赖的一个库,今天写的这个库只是单纯的.so动态方法。可是为什么会找不到库呢?不是在编译的时候已经告诉了库的路径和库的名称了吗?
我们在gcc时确实是告诉了库在哪里和库的名称,但是这是告诉编译器,生成可执行程序后,编译器的工作就完成了,等到运行可执行程序时操作系统会加载我们的程序,将其变成进程,但是此时操作系统并不知道这个动态库在哪里,所以这个报错是操作系统说的。静态库操作系统是不需要找的。静态库一旦静态链接生成可执行程序后,与静态库就完全没有关系了,因为已经将方法拷贝进来了。所以,动态库是需要加载的。系统找动态库默认是到lib64路径下找的。
也就是说,程序使用静态库时,静态链接时是将方法的时间放到可执行程序调用的地方;而程序使用动态库时,是当运行到了库方法时,才去动态库中查找对应的方法。
如何能让操作系统看到我们的动态库呢?
1. 将自己的动态库直接拷贝到系统默认查找的路径下,如lib64
2. 在系统路径下,建立软链接
此时是不需要重新生成可执行程序的
3. 修改环境变量的值
前面说过,操作系统查找动态库时,默认会到/lib64下查找,操作系统为什么会到/lib64下查找呢?
实际上,Linux中,查找动态库是看环境变量LD_LIBRARY_PATH的值,所以我们可以将我们的路径添加到环境变量当中
注意:可执行程序知道依赖的是那个库,也就是知道这个库的名称,只是不知道这个库的路径而已
这种方法是内存级的,重启bash后就没有了。若想要持久化,可以将这个路径写到配置文件当中
动静态库同时使用的细节说明
若同时有动态库和静态库,优先使用哪一个呢?
我们在other的lib目录下,创建两个相同名称的静态库和动态库
所以,同时使用静态库和动态库时,gcc/g++优先使用动态库。此时这个可执行程序是可以跑起来的,因为我们已经将这个路径放入到了环境变量中
在既有动态库,又有静态库的情况下,若想使用静态库,可以在gcc/g++后面加上-static
现在将静态库移走,继续使用-static
会发现此时是无法编译通过的。也就是说,如果要强制静态链接,必须要提供对应的静态库
现在将动态库移走,只留下静态库
此时指定静态链接肯定是可以编译通过的
此时会发现,即使我们只提供了静态库,但是当没有指定要静态链接时,使用的仍然是动态链接。并且通过ldd的结果可以发现,这个可执行程序竟然不依赖mystdio这个库
是可以正常运行的
如果你只提供静态库,但是连接方式是动态链接的,gcc、g++没得选,只能针对你的.a局部性采用静态链接。
此时即使将stdc删除,这个可执行程序依然可以执行
动态库的理解、动态库的加载
可执行程序、动态库、静态库都是文件,都是保存在磁盘上的。我们现在只讨论动态库,因为只有动态库需要加载。./main之后,main就变成了一个进程。进程会将自己的代码和数据加载到内存当中,因为main中调用了动态库中的方法,所以是需要将动态库加载到内存当中的。
进程如何能够看到加载到内存当中的库呢?
我们的进程看到自己的代码和数据是通过页表映射。所以,可以将加载到内存中的库通过页表映射到进程地址空间堆栈之间的共享区当中。这样,进进程就能够看到加载到内存当中的库了。当进程执行到库函数时,只需要从正文代码跳转到共享区当中,执行虚拟地址对应的各种方法,执行完再返回正文代码继续执行即可。所以,进程使用库,全都是在自己的进程地址空间内完成的。
可是,在我们的操作系统内部,可能不只有一个进程依赖这个动态库。像指令大部分都是C语言写的,都依赖与glibc这个库。假设我们现在又有了一个可执行程序other,并且other也依赖于这个库。other运行起来后,也会变成一个进程,就会有自己的进程地址空间、页表等,并且也是通过页表映射拿到自己的代码和数据。因为我们需要使用库,所以需要将库加载到内存当中,,可是现在库已经被加载到内存当中了,此时就不需要重复加载了,只需要将库与other的页表进行映射即可。所以,动态库在内存中只需要加载一份,未来多个进程使用同一个库时,这个库就可以被这些进程共享。所以动态库也称为共享库。所以,相较于静态库,动态库更加节省空间,因为库中的方法在所有进程的代码中都不会重复出现
可执行程序格式
我们写的C/C++的代码,里面就是代码和数据,编译生成可执行程序,这个可执行程序里面不只有原先的代码和数据,还需要有可执行程序相关的属性、格式等
text是代码,data是已初始化数据区,bss是未初始化数据区,data和bss统称为数据区,filename是文件名。像堆、栈、命令行参数、环境变量等,都是程序加载到内存时,被OS动态创建出来的,不需要在可执行程序中进行特定的构成。所以,在编译生成可执行程序的时候,是有规则的。在Linux中,生成可执行程序是有特定格式的,这个格式称为ELF。
这里的节也可以称为段
所以,静态库就是将我们自己程序的ELF和静态库文件的ELF进行合并。若code1.c里面调用了一个函数,这个函数是在code2.c当中实现的,链接后,形成的这一个大的代码段,每一个函数都是有实现的。所以,在C/C++中写多文件时是不能出现命名冲突的也是这个原因
对于每一个可执行程序,有几个section呢?每个section的大小是多少呢?每个section的权限又是什么呢?也就是说每个section最后都需要被管理起来,所以ELF中还有3个比较重要的东西
ELF Header
是ELF格式的表头
readelf用于显示ELF文件的信息,-h就是只读header
Program Header Table
表示的是未来程序加载到内存中,在内存中的布局
加载到内存时,每一个表结构(除了section之外的3个表结构)、数据节(各个section)的起始和结束,是如何知道的呢?
对任何一个文件,文件的内容就是一个巨大的"一维数组”,标识文件任何一个区域,偏移量+大小的方式。所以,标识任何一个数据节,只需要知道偏移量即可,大小实际上都可以不需要。这样,对于每一个节,只需要知道它在ELF中的大小,以及偏移量,就可以将其标记出来。所以,未来将可执行程序的代码和数据加载到内存时,OS如何知道在哪一个文件里加载那一部分信息呢?只需找到可执行程序,当前进程是有PWD的,此时就找到这个文件了,然后将我们要加载的代码的这一节的偏移量和大小记录下来。当要加载时,直接到指定路径下找到指定文件,在特定偏移量处找到指定长度的数据,就可以将其加载到内存了
上面两个LOAD的含义就是要将偏移量位0x000...000,大小位0x000...d44和偏移量为0x000...e10,大小为0x000...25c的数据加载到内存中,第一个是代码区,第二个是数据区
Section Header Table
前面两个是给OS识别的,是OS加载要用的。这也是给OS加载用的,但是它更多地描述了数据节的信息,如每个节的大小,每个节的偏移量等信息
重谈进程地址空间
mm_struct里面各个区域是由谁初始化的?可执行程序在没有加载到内存时,有没有地址?
是有地址的。我们可以将可执行程序反汇编看一下
反汇编之后可以看到一个一个的section
Linux下,C程序最先执行的函数是_start,再往下才是main等。通过反汇编可以看到,在可执行程序还没有加载到内存时,已经有地址了,称为逻辑地址。所以,在可执行程序中,特别是代码区和数据区,每一行代码,都有天然的编址
可以看到,在不同函数之间是连续编址的。并且在不同section之间也是连续编址的
当代对可执行程序编址时,是按照平坦模式编址的。正常来说,逻辑地址=起始地址+偏移量,采用平坦模式后,所有的起始地址都是0,也就是说逻辑地址=偏移量。也就是说,逻辑地址的取值范围[0000...0000,FFFFF...FFFF].这里没有看到0是因为这是反汇编出来的,并且系统会在main之前添加一些东西。所以,是对所有的代码从全0到全F线性地进行了编址
像这些push、mov是汇编,最后编译完就是指令。指令是二进制,也是有长度的。汇编上是将这些指令一行一行显示的,但是可执行程序未来就是一串二进制,所以指令最后也会变成二进制,这些指令变成二进制之后是可以标记自己的长度的。
所以,下一行的地址-当前行的地址 = 当前行指令的长度。
因为这些指令都知道自己的长度,所以,其实只需要找到第一个地址,这个程序就可以运行了
自己的地址 + 指令的长度 =下一条指令的地址
结论:ELF在可执行程序没有加载到内存时,就将整个程序以线性地址的方式,从全0到全F对代码全部编址了。当然,这里不一定会到全F,可能只使用了一部分
注意:这里说的全0到全F的地址是虚拟地址。所以,虚拟地址不是在内存才形成的,虚拟地址是在编译器编译时就已经形成了。
因为是使用平坦模式,所以:逻辑地址 == 虚拟地址。但是二者在不同地方的叫法不同。在ELF/磁盘中称为逻辑地址,加载到内存后,称为虚拟地址。
所以,一个进程的进程地址空间的初始化是读取可执行程序进行初始化的。这里指的是代码区、已初始化数据区、未初始化数据区。堆、栈等先不管
通过这里的函数调用可以看到,函数内部使用的地址就是虚拟地址
可执行程序加载到内存后,CPU如何知道要从哪里开始执行这个进程的代码呢?
在可执行程序编译好之后,在ELF头部已经记录下了。这是整个程序的入口地址,所以未来加载到内存时,直接将这个地址放到CPU的pc指针里,就可以开始执行了。从这里可以看出:CPU执行程序的时候,使用的是虚拟地址。这里的0x400660就是函数_start的起始地址。
将可执行程序加载到内存后,每一条指令也会占据物理内存,所以会有一个物理内存的地址。此时这个程序既有虚拟地址,又有物理地址,就可以完善页表了。CPU内部还有一个寄存器CR3,这是操作系统自己使用的寄存器,不会暴露给用户。CR3寄存器会保存页表的起始地址,是保存物理地址。CPU中一定要通过虚拟地址找到物理地址,所以在CPU中存在一个硬件MMU,这个硬件可以完成查表的操作(就是将虚拟地址转换成物理地址),是通过硬件电路完成的。MMU能够找到页表是因为有CR3。可执行程序刚加载到内存时,pc指针保存的是可执行程序的起始地址,若现在要开始执行可执行程序的代码,CPU就会将pc指针中的地址交给MMU,MMU再拿着这个地址到页表中查找,然后找到对应的物理地址,将物理地址上的指令放到EIP这个寄存器中,这样CPU就可以开始执行指令了,在执行过程中,CPU会计算这条指令的长度,再结合当前pc指针中的地址,计算下一条指令的虚拟地址,并将这个地址填入到pc指针当中,等现在正在执行的指令执行完毕,再将pc指针中的地址交给MMU,完成由虚拟到物理的闭环。所以,进去CPU的都是指令,携带的是虚拟地址,从CPU出去的都是物理地址。
通过上面的学习我们可以知道,进程地址空间是操作系统、CPU、编译器共同协作下的产物
为什么要有虚拟地址和虚拟地址空间?之前说的也算,现在再补充一些
编译器在编译代码时,再也不需要考虑物理内存的情况了,将编译器和操作系统作了解耦d
重谈区域划分
可执行程序的section中,只有正文代码区、已初始化数据区、未初始化数据区,并没有堆、栈、共享区、命令行参数环境变量等区域,mm_struct是如何对这些区域进行设置的呢?
在mm_struct当中,有一个结构体struct vm_area_struct。vm_area_struct就是一个链表的结点。进程地址空间中的每一个区域,都是一个vm_area_struct,里面有这个空间的起始虚拟地址、结束虚拟地址等信息。所以,前面说过,将动态库加载到内存后,要为其分配地址空间,分配地址空间的本质就是创建一个vm_area_struct对象,设置start和end,并链入到链表当中,这样虚拟地址和物理地址都有了,再构建页表。构建堆区、栈区、共享区等也是同理,每一个区域就是一个vm_area_struct,创建好vm_area_struct后再构建页表即可。mm_struct中对于各个区域的信息是宏观上的,如果要查看每个区域的详细信息,需要通过遍历链表的形式,找到这个区域的vm_area_struct才能够获得详细的信息
可执行程序中会有非常多的section,但是未来可执行程序加载到内存后,并不是所有的section都会加载到内存当中。加载可执行程序时,是按节进行加载。并且对于一个节,一次也不是将整个节都加载到内存中的,可以先加载一部分,对加载的这一部分同样构建一个vm_area_struct,等加载的这一部分代码跑完之后,释放掉vm_area_struct,再加载另一部分,重新构建vm_area_struct,这就是分批加载。因为可能会出现内存只有4GB,而可执行程序有7GB等情况。所以,一开始都可以不需要将可执行程序加载到内存,只需要将可执行程序中,虚拟地址的起始地址放到pc指针上即可,当CPU开始调度,使用这个虚拟地址到页表中进行映射时,映射失败会触发缺页中断,此时再懒加载即可。触发缺页中断后,如何知道要加载那一部分代码呢?在ELF内标记一段代码,使用的是偏移量。我们此时是没有将代码加载到内存中的,只加载了起始的虚拟地址,所以是只有虚拟地址,而没有物理地址的,所以页表的左边可以填入虚拟地址,右边填入偏移量即可。因为操作系统肯定知道当前进程所对应的可执行程序是在磁盘中的哪一个区域,就可以通过这个偏移量找到要加载的内容了。当触发缺页中断时,就可以以块的方式将代码加载进来了
动态库加载的理解
库也是ELF格式的,并且里面方法的编址方式与可执行程序的编址方式是完全相同的。
我们之前说过,动态库是要加载到内存的,OS会将库映射到不同的地址空间里,那操作系统要不要管理这个库呢?要,因为在物理内存中可能存在非常多的库。管理就是先描述,再组织。可以使用一个结构体来描述一个库。
struct libso
{int ref_count; // 引用计数XXXXXXX; // 库的属性struct libso* next;
};
库映射到虚拟地址空间就是创建一个vm_area_struct,这个vm_area_struct中就会保存有库的虚拟地址,再通过页表映射就可以找到物理地址,这样,一个进程就能够找到库了。假设现在动态库libso中有一个方法printf,它在库中的偏移量是0x100,然后一个可执行程序调用了这个库,在链接之前,可执行程序中的代码是call libso:printf,链接之后就会变成call libso:0x100。库映射到虚拟地址空间后,还会再进行替换,变成call 库的起始虚拟地址:0x100。因为库已经映射到我们的进程地址空间了,所以库的起始虚拟地址是知道的。这样,我们只需要将库的虚拟起始地址+偏移量,就能得到方法的虚拟地址,就可以执行这个方法了。我们将程序运行、加载时,把地址进行替换的过程叫做地址重定位。
这个共享库被映射到了堆栈之间的共享区,并且映射到这个共享区的任意位置都是可以的。这种特点叫做与位置无关。
可是代码区不是只读的吗?
实际上,在可执行程序的数据区的section中,有一个GOT --- 全局偏移量表
我们在上面介绍时,是为了方便理解,没用引入GOT。实际上,一个可执行程序调用库中的方法时,在链接时不是将方法名称替换成方法在库中的偏移量,链接时会构建处GOT表,里面包含了使用了动态库中的方法的名称以及偏移量,然后将call libc.so:printf替换成了.got:index的格式,这个index是printf在GOT表中的下标,.got是GOT这张表的虚拟起始地址。因为ELF是统一编址的,所以在加载之前就已经有了GOT的虚拟地址了。假设GOT的虚拟起始地址是0x1234,printf是下标为0的,所以就算0x1234:0。所以,代码区的变化在加载到内存之前就已经作了修改,加载到内存之后是不变的。
当库映射到虚拟地址空间后,会使用库的虚拟地址区替换GOT表中的libc.so,因为GOT表是在数据区内的,是可以修改的。未来在执行库中的方法时,会先去查表,然后根据结果去访问库的方法。所以,在进行地址重定位时,不需要再修改正文代码,而是修改GOT表即可。
GOT这个数据段是每个进程都有的,未来库映射到虚拟地址的任意地方,在不同进程映射到不同虚拟地址,都无所谓,只需要使用在这个进程中的虚拟地址修改这个表即可,此时库与位置无关。与位置无关=GOT+库方法偏移量
可以看到,确实是有GOT的,无论是动态库,还是可执行程序
静态库并没有上面说的这些东西,因为静态库是直接将各个.o的ELF结合到一起的