目录
1、文件系统
1.1、磁盘
1.2、文件系统
1.3、文件的增删查改
2、软硬链接
2.1、软链接
2.2、硬链接
3、物理内存与文件
4、动静态库
4.1、静态库
4.1.1、静态库的制作
4.1.2、静态库的使用
4.2、动态库
4.2.1、动态库的制作
4.2.2、动态库的使用
4.3、动静态库一起使用
4.4、ncurses库
5、动态库的加载
6、进程地址空间
6.1、未被加载的程序的地址
6.2、程序加载后的地址
6.3、动态库的地址
1、文件系统
如果一个文件没有被打开,是放在磁盘中进行存储的。磁盘上存储文件=存储文件的内容+文件的属性。Linux中的文件在磁盘中存储是将属性和内容分开存储的。
注意:磁盘又叫机械硬盘,和固态硬盘是不同的。像这种笔记本基本上目前都是用的固态硬盘。但是今天谈论的不是固态硬盘,而是磁盘,因为目前大多服务器还是使用的磁盘,而不是固态硬盘。
1.1、磁盘
为什么要了解磁盘呢?上一篇文章中,主要讲了被打开的文件,而这一讲中主要讲未被打开的文件,未被打开的文件放在磁盘上进行存储,因此有必要了解一下磁盘。磁盘是计算机中唯一的机械设备。
磁盘的样貌:
内部结构为:
一个盘片是有两面,每一面都有一个磁头。磁头和磁盘面是不接触的。主轴里面有马达,可以使磁盘高速旋转,在磁盘进行高速旋转时,磁头臂也会来回摆动。信息就被存储在盘片中。磁盘不工作时,磁头会停靠在磁头停泊区。
这些磁头是连在一起的,也就是说这些磁头会同时摆动。
我们可以对上面的磁盘建立如下的模型,结构如下图:
磁头来回摆动本质上就是在定位磁道,磁盘进行旋转本质上就是在定位扇区。
磁盘读写的最基本的单位是扇区,一般一个扇区的大小为512字节或者4kb。数据在磁盘上并不是随便进行摆放的,比如相关的数据会尽可能的放在一起。
我们可以把磁盘看成由很多扇区构成的存储介质,要把数据存储到磁盘,第一个要解决的问题就是定位一个磁头(这将决定访问哪一个盘面的哪一面),然后再确定在哪一个磁道,最后再确定在哪一个扇区。
我们可以把磁盘展开,例如:
其中每一面是磁盘盘片的一面,每一面又有很多磁道组成,每个基本磁道的基本单元为一个扇区。因此我们可以把磁盘抽象成一个线性结构,每个基本单元的大小为一个扇区。如下图:
不仅仅只有CPU有寄存器的概念,像是磁盘也是有类似于寄存器的概念的,比如:控制寄存器(控制读和写),数据寄存器(数据),状态寄存器(IO结果的状态),地址寄存器(存放地址信息)。
1.2、文件系统
为了管理磁盘,就需要有文件系统,我们在此了解的是Linux中的ext2文件系统。
上面我们已经把磁盘抽象为一个线性结构了,对于这样一个线性结构,为了管理起来,对磁盘进行分区(之所以要进行分区是因为文件系统能管理的容量是有限的,不是无限的),只要把每个分区管理好,就可以管理好整个磁盘。
Linux下每个分区上的文件系统是相互独立的, 也就是说每个分区都有自己的文件系统,每个分区的文件系统可以不同,当然也可以相同。所以一个分区就代表了一个文件系统。
逻辑块(block):block是在分区中进行文件系统的格式化时所指定的"最小存储单位",这个最小存储单位以扇区的大小为基础,大小为扇区的 2ⁿ 倍。一般逻辑块的大小都是4KB,即由连续的 8 个扇区构成的一个块,这样就大大提高了文件的读取效率。逻辑块也并不是越大越好。因为一个逻辑块最多仅能容纳一个文件(这里指 Linux 的 ext2 文件系统)。所以逻辑块很大可能会导致空间的浪费。所以最好的方式是根据实际的使用场景来设置逻辑块的大小。简单来说就是扇区是物理上的一个单位概念,文件系统的单位是block,这是一个逻辑上的概念。简单来讲就是文件系统对磁盘的访问和操作的基本单位是block(一般也就是4kb),多访问的数据可以理解为这就是预加载,预加载的目的是为了提高效率。
inode:Linux 操作系统中的文件内容和文件属性是分开存储的,权限与属性放置到 inode中,一个文件有一个inode,inode有唯一的编号。
Boot block(引导块):是一个重要的组成部分,用于存储与启动操作系统相关的信息。这个区域出问题,整个文件系统就挂掉了。
Block Group n:ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。
Super Block(超级块):超级块会记录整个文件系统的整体信息,记录的信息主要有:bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息等等。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。其实上除了第一个 block group 内会含有 super block 之外,后续的 block group 一般都包含了 super block,即做为第一个 block group 内 super block 的备份。所以,如果第一个超级块损坏了,则可以从后面的超级块复制过来,super block 的大小为 1024 Bytes。
Group Descriptor Table(组描述符):描述块组属性信息,描述每个 group 的开始与结束位置的 block 号码,以及说明每个块(super block、bitmap、inode bitmap、data block) 分别介于哪一个 block 号码之间。组描述符信息和超级块信息一样,后面的block group一般也都包含Group Descriptor Table也是用来作为备份。
Block Bitmap(数据块位图):其中每个bit表示一个data blocks中的单元是否空闲可用。
inode Bitmap(索引节点位图):其中每个bit表示一个inode Table中的单元是否空闲可用。
inode Table(索引节点表):中存放着一个个 inode,inode 的内容记录文件的属性以及该文件实际数据放置在哪些 block 内。每个 inode 大小均固定为 128 Bytes,每一个文件都有一个唯一的inode,Linux中文件的属性不包括文件的名称,在Linux中标识文件使用inode编号,可以使用下面的命令查看文件的inode编号,例如:其中开头的第一个代表的就是inode编号。
注:每个inode中的属性包括:inode编号、文件类型、权限、引用计数、拥有者、所属组等等。 另外inode的是以分区进行设置的,不能跨分区,也就是说每一个分区内用一套inode,分区与分区的inode是有重复的。
Date Blocks(数据区):存放文件内容的地方。
注意:每一个分区被使用之前,都必须提前将部分文件系统的属性信息设置进对应的分区中,这个动作被称为格式化,这个格式化是对前四个块来讲的,即超级块、组描述符、数据块位图、索引节点位图。比如:格式化后,一个分区的可以使用的inode的总数是确定的。
注意:目录也是一个文件,也有自己的inode,目录也是由属性+内容组成的,目录的内容存放的是该目录下,文件的文件名和对应文件的inode编号的映射关系。
1.3、文件的增删查改
1、新建一个文件
分配 inode:为新文件分配一个 inode,记录文件的属性数据。
分配数据块:为文件分配数据块,存储文件内容。
更新目录内容:在包含该文件的目录中创建相应的目录项,记录文件名与 inode之间的映射关系。
2、删除一个文件
查找文件:通过目录中的映射关系找到对应的 inode。
释放数据块:释放与 inode 关联的数据块,标记这些块为可用。
释放 inode:将 inode 标记为可用。
更新目录内容:从目录中删除对应的映射关系。
3、查询一个文件
查找 inode:在目录中查找文件名与inode的映射关系,找到对应的 inode。
读取数据块:根据 inode 获取指向数据块的指针,读取文件的内容。
4、修改一个文件
查找 inode:通过文件名和inode的映射关系,找到相应的 inode。
写入数据块:修改文件的内容,更新相应的数据块。
更新属性信息:更新 inode 内部的时间戳信息等。
2、软硬链接
2.1、软链接
软链接和Windows中的快捷方式很像,我们可以使用如下命令建立软链接:
ln -s file.txt soft-link
其中,ln表示建立链接,-s选项表示建立软链接,上面所表达的意思是给file.txt文件建立soft-link软链接。
软链接是一个独立的文件,有独立的inode编号,也有独立的数据块,该文件中的数据块中保存的是指向文件的路径。例如:我们可以使用ls -li命令查看
很显然,两个文件的inode编号是不同的,因此是两个不同的文件。
如果我们把软链接指向的文件给删除了,就会出现:
删除软链接可以使用rm 来进行删除,但是给目录上的软链接使用rm是没法删除的,例如:
可以使用下面的命令进行删除软链接,无论是给文件上软链接还是给目录上软链接都可以被删除,例如:
unlink soft-link
软链接的应用:我们可以给目录或者是文件添加软链接。软链接的应用就是类似于Windows的快捷方式。
2.2、硬链接
所谓的建立硬链接,本质就是在特定的目录的数据块中新增文件名和指向文件的inode编号的映射关系,相当于给文件起了一个别名。我们可以使用如下命令建立硬链接:
ln test.txt hard-link
上面的所表达的意思是给test.txt文件建立hard-link硬链接。
硬链接不是一个独立的文件,因为不具有独立的inode编号,例如:
通过观察便可以发现这两个文件的inode编号是一样的,因此是同一个文件, 只是名字不同而已。另外,我们发现链接数从1变成了2,说明这里的数字本质表示的是一个文件的硬链接数的个数。
注:任意一个文件,无论是目录还是文件,都有inode编号。每一个inode内部都有一个叫做引用计数的计数器,用来表明有多少个文件指向同一个inode。只有计数器为零时,才会正真的删除文件,否则仅仅只是计数器减一。
硬链接的应用:硬链接最典型的应用就是目录,比如我们创建一个目录,我们就可以看到他的链接数为2。
之所以链接数为2是因为在dir目录下有一个名字为.的隐藏文件 ,该文件就是dir目录的硬链接。
从图中可以看出dir与dir目录下的.隐藏文件的inode是一样的。其中dir目录下的..隐藏文件也是一样的,只不过它是指向上级目录的。又因为ying04目录下又有一个.隐藏文件,因此链接数是3。
注意:可以给目录添加软链接,但是不可以给目录建立硬链接,因为给目录建立硬链接会破坏目录树的结构,会导致出现环路问题,比如查找一个文件,因为有环,所以会导致死循环。系统对目录中的.和..两个隐藏文件做了特殊的处理,使得在进行一些操作时,会忽略这两个文件,比如查找文件时,忽略这两个隐藏文件进行查找。
总的来说就是用户是不被允许给目录建立硬链接的。
3、物理内存与文件
操作系统对内存的管理是相当复杂的,这里仅仅只是补充一些对内存管理的认识。
1、内存的本质是对数据的临时存取,我们可以把内存看作为一个很大的缓冲区。物理内存也是划分为一块一块的,与磁盘进行交互时的单位是4kb。
2、操作系统要管理物理内存也是要先描述、再组织的。描述使用struct page结构体,该结构体中描述了每个4kb物理内存的属性。组织采用数组的方式,以4GB物理内存为例,struct page mem_array[1048576],因此对物理内存的管理就变成了对数组的管理,又因为数组是有下标的,因此就有了页号的概念,每个4kb大小的单位都对应一个页号。要访问一个物理内存,我们只需要先找到这个4kb所对应的page,就能在系统中找到对应的物理内存的页。所有申请物理内存的动作都是在访问内存数组。
3、Linux中,我们每一个进程,打开的每一个文件,都要有自己的inode属性和自己的文件页缓冲区(内核级缓冲区)。 系统启动时,会把磁盘的一些常用的内容给加载到内存,这个就是预加载。
其中struct inode和struct page就是内核级缓冲区。从图中就可以看出,文件内容要写到磁盘至少要经过三次拷贝,向缓冲区进行一次拷贝,向内核级缓冲区再拷贝一次,最后向磁盘再拷贝一次。
4、动静态库
动静态库之前我们简单了解过,下面我们将尝试着自己去制作动静态库。
4.1、静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。静态库的命名规则:libxxx.a
4.1.1、静态库的制作
把我们自己提供的方法给别人用,有两种方式,一种是把源代码直接给他;另一种方法就是把我们的源代码打包成库,然后把库和对应的头文件给他(头文件是一定要给的,因为头文件就像一份说明书,如果没有这个说明书,使用者是不知道如何使用库的)。
例如:我们有这样一个程序
mymath.h:
#pragma once #include<stdio.h>extern int myerrno;int add(int x,int y);
int sub(int x,int y);
int mul(int x,int y);
int div(int x,int y);
mymath.c:
int myerrno=0;int add(int x,int y)
{return x+y;
}
int aub(int x,int y)
{return x-y;
}
int mul(int x,int y)
{return x*y;
}
int div(int x,int y)
{if(y==0){myerrno=1;return -1;}return x/y;
}
制作静态库库其实就是将源文件编译成目标文件,然后再打包成库。
例如:我们先将mymath.c文件编译成目标文件mymath.o
gcc -c mymath.c -o mymath.o
然后再进行打包,形成libmymath.a:
ar -rc libmymath.a mymath.o
现在就生成了静态库libmymath.a了。
我们可以使用如下命令查看静态库中的内容,例如:
ar -tv libmymath.a
结果为:
然后我们可以将生成的静态库与头文件放在一起,给别人使用,例如:
mkdir -p mylib/include
mkdir -p mylib/lib
cp mymath.h mylib/include
cp libmymath.a mylib/lib
现在我们就有了lib目录,里面存放着头文件和对应的库。下面我们使用一下自己制作的库。
4.1.2、静态库的使用
例如:我们现在写了一个代码,要调用我们上面制作的库里面的函数
#include"mymath.h"int main()
{printf("div:%d,errno:%d\n",div(10,0),myerrno);return 0;
}
然后我们尝试使用gcc直接编译该文件,就会出现:
原因是gcc在找头文件时会去默认路径/usr/include/或者在当前目录下去找(前提是使用""包含头文件)头文件,而我们自己编写的头文件既不在当前目录也不在/usr/include/中,因此出错。
我们可以在使用gcc时加上-I选项,该选项后面跟头文件的搜索路径。除了使用这种加-I(大写的i)选项的方式外,还可以直接在源程序main.c中使用include直接包含头文件路径(相对或者绝对路径都可以)。我们使用加选项的方式,例如:
很显然,这是一个链接的错误,出现这个错误是因为找不到我们写的静态库,因为gcc默认去系统路径/lib64/下或者当前路径下去找库,所以报错了。
我们可以在使用gcc的时候加上-L选项,指明库文件所在的路径。例如:
运行后我们发现又有问题了,原因是我们没有指定库名,gcc是认识C语言中标准库的名称的,但不认识我们自己写的库的名称,所以出现了报错。
我们在使用gcc时,可以加上-l选项,指明库的名称。例如:
注意:库的真实名称是去掉前面的lib和后缀所剩下的部分,例如:libmystdio.a,这个库的真实名称是mystdio。有人可能有疑惑的是,为什么库需要指定库的名称,而头文件不需要指定头文件的名称呢?原因是在,main.c程序中我们已经包含了头文件的名称。
到这里,我们已经明白了如何使用我们自己制作的静态库。如果想要不带这么多的选项,可以将我们写的库和头文件分别放在/lib64/路径下以及/usr/include/路径下,这样我们仅需带-l选项指定库名即可。
除了上面说的方法,我们也可以给头文件所在的路径和库文件分别在/lib64/和/usr/include中建立软链接,例如:给头文件所在的路径在/usr/include中建立软链接:
给库文件建立软链接:
此时我们要想直接gcc main.c -l mymath是不行的,我们需要更改一下头文件,例如:
#include"myinc/mymath.h"int main()
{printf("1+1:%d,errno:%d\n",add(1,1),myerrno);return 0;
}
此时我们可以直接使用gcc main.c -l mymath来直接编译了。例如:
但是我们发现程序的结果有问题,想象中的errno是1才对,之所以会有这个结果是因为C语言中函数参数的处理顺序是从右向左的,所以会导致这样一个结果,我们可以进行如下修改,例如:
#include"myinc/mymath.h"int main()
{int n=div(10,0);printf("div:%d,errno:%d\n",n,myerrno);return 0;
}
然后再进行编译后运行结果为:
这样就完成了。
注:第三方库的使用,必定会用到-l选项来指定库名。如果仅提供了静态库,则gcc只能对该库进行静态链接,简单来说就是,在不加-static选项时,有动态库就连动态库,没动态库就连静态库;如果加上-static,则只连静态库。
4.2、动态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。动态库的命名规则:libxxx.so
4.2.1、动态库的制作
动态库和静态库的制作是有相似之处的,也是要编译成目标文件的。在静态库中,我们只将一个文件制作成库,这次我们试着将多个文件制作成库,例如:我们有如下文件
myprint.c
#include"myprint.h"void Print()
{printf("hello linux\n");printf("hello linux\n");printf("hello linux\n");printf("hello linux\n");
}
myprint.h
#pragma once #include<stdio.h>void Print();
mylog.c
#include"mylog.h"void Log(const char* info)
{printf("warning:%s\n",info);
}
mylog.h
#pragma once #include<stdio.h>void Log(const char*);
首先,要把mylog.c和myprint.c文件编译成目标文件,和静态库的区别是要多加一个-fPIC选项,例如:
gcc -fPIC -c mylog.c
gcc -fPIC -c myprint.c
生成mylog.o和myprint.o文件,然后再使用gcc加上-shared选项进行下面的操作:
gcc -shared -o libmymethod.so mylog.o myprint.o
最后就生成了libmymethod.so动态库。
我们同样将动态库以及对应的头文件分别放入mylib/lib/和mylib/include目录下,结果为:
4.2.2、动态库的使用
main.c
#include"mylog.h"
#include"myprint.h"
int main()
{Print();Log("hello log function\n");return 0;
}
动态库的使用和静态库类似,例如:
然后我们去运行该程序,便会看到:
使用ldd命令,便会看到:
上面显示libmymethod.so动态库是找不到的,要解决这个问题,我们可以把动态库拷贝到/lib64/的目录下,或者在/lib64/目录中建立动态库的软链接,例如:
sudo ln -s /home/wang/dy-static/test/mylib/lib/libmymethod.so /lib64/libmymethod.so
然后使用ldd可以看到:
我们发现此时已经找到了动态库,然后我们便可以运行程序了。例如:
或者我们使用一个环境变量来解决这个问题,这个环境变量就是LD_LIBRARY_PATH,一般来说系统中是没有这个环境变量的,因为我之前配置过vim所以才有了这个环境变量,这个环境变量是专门给用户提供搜索用户自定义的库的路径的,例如:
我们只要把库路径添加到该环境变量中即可,例如:
这样就导入成功了。此时使用ldd就可以看到:
显然,已经找到了动态库。当然,这种方式不是长久有效的,一旦关掉Xshell然后重新登陆后我们配置的环境变量信息也就没了,如果想要配置长久有效,就要把我们配置的信息填写到系统启动时的一些文件中。
或者我们也可以在/etc/ld.so.conf.d/下建立一个以.conf为后缀的文件,库路径填写到这个文件中,然后使用ldconfig命令把配置文件重新加载一下,也可以解决这个问题,这种方式是长久有效的,例如:
然后我们使用ldd查看我们的可执行文件便会发现:
此时动态库就找到了。 如果想要删除这个配置,仅需把test.conf文件删除掉或者清空,然后重新执行一下ldconfig即可。
有人可能会感到疑惑的是,我们明明已经指定了路径和库名,为啥还会要再指明呢?原因是之前的指明是告诉的编译器,而这里指明是告诉加载器。之所以我们之前写的C语言程序的运行不用指明的原因是系统在加载时,会默认去某些路径下去进行搜索对应的动态库。
注意:实际情况中,我们使用的库都是别人成熟的库,都采用直接安装到系统的方式。通俗的讲就是把库和对应的头文件分别放到系统的默认路径下。下面我们通过ncurses库来实践一下。
4.3、动静态库一起使用
例如:我们的main.c文件
#include"mymath.h"#include"mylog.h"#include"myprint.h"int main()
{int n=div(10,0);printf("div:%d,errno:%d\n",n,myerrno);Print();Log("hello log function\n");return 0;
}
使用下面的方式编译此文件并运行:
注:a.out这个可执行程序的运行并不依赖与静态库,只要该可执行程序形成,哪怕我们把静态库删掉也不会影响该可执行程序的运行。但是如果我们把动态库删掉,则a.out这个可执行程序就不能运行了。
4.4、ncurses库
ncurses库是一个基于终端的图形界面的库,我们可以试着用一下这个库。
其实我们对ncurses本身并不陌生,因为vim的制作就使用了ncurses库。下载:
sudo yum install ncurses-devel.x86_64
安装完成之后,我们可以查看一下系统中是否成功安装库,例如:
现在我们就可以使用该库进行开发了。
例如:test.c
#include <string.h>
#include <ncurses.h>int main()
{initscr(); // 初始化屏幕raw(); // 禁用行缓冲noecho(); // 禁用回显curs_set(0); // 隐藏光标char *c = "Hello, World!";mvprintw(LINES/2, (COLS-strlen(c))/2, c); // 在屏幕中央打印字符串refresh(); // 刷新屏幕getch(); // 等待用户输入endwin(); // 结束ncurses模式return 0;
}
编译该程序,例如:
gcc test.c -o test -l ncurses
注意:在编译时要使用-l选项来指定库名 。
程序运行结果为:在屏幕中间显示“Hello, World!”(居中),隐藏光标,不回显键盘输入,然后等待用户按键后退出。
关于ncurses库的其他使用,自行了解。
5、动态库的加载
动态库在程序执行时是需要被加载的,而静态库不会被加载,动态库在被加载之后,会被所有用到它的进程所共享。
当第一个进程运行时需要某个动态库的时候,动态库被加载到物理内存,然后通过页表,被映射进虚拟地址空间的共享区里面,然后从程序从当前的正在执行的位置(也就是代码区中)跳转到共享区里面执行库的程序,执行完成之后,再返回代码区继续执行接下来的代码。
当第二个进程也需要使用该库时,先判断这个库是否被加载了,如果被加载到了物理内存,则直接通过页表映射进该进程的虚拟地址空间的共享区中,然后程序跳转到共享区里面执行库的代码,然后再返回代码区继续向下执行。如下图所示:
有人可能会疑惑的是,如果多个进程在使用动态库的时候对其中的一些全局变量进行修改的话,岂不是会影响其他进程吗?比如我们的libc.so动态库中有一个errno全局变量,如果某个进程修改了它,那其他正在使用该动态库的进程也会被影响吗?事实上是不会被影响的,因为会发生写时拷贝。因此一个动态库,是可以被多个进程使用且进程与进程之间使用同一个动态库是互不影响的。所以动态库也叫做共享库。系统在运行中,一定会存在多个动态库,为了管理这些动态库,采用先描述再组织的方式进行管理。
6、进程地址空间
之前我们简单的了解过进程地址空间,这里再进行一些补充。
6.1、未被加载的程序的地址
未被加载的程序的地址,简单来说就是,程序还没被加载到内存,还在磁盘上。
程序编译好后,形成的可执行程序内部已经采用虚拟地址编址了,也就是说内部是有地址的,这里面的地址是虚拟地址。例如:我们把程序反汇编,这里面的地址就是虚拟地址。我们使用下面的命令对可执行程序进行反汇编
objdump -S a.out
部分结果为:
其中左边这一列数表示后面右边每一行指令所对应的地址。 在可执行程序中,左边的这一列地址的是可以去掉的,而仅有右边的指令操作。
程序编译好后形成的可执行程序是被分成很多段的,简单来说就是在磁盘上的可执行程序的内部的分段和虚拟地址空间的一些分区基本是一致的,例如:以4GB为例
因此程序在被编译好后就已经为运行做好准备了。
6.2、程序加载后的地址
当可执行程序被加载到内存的时候,可执行程序中依然是虚拟地址,但是它要被加载到物理内存上,要占据物理内存的空间,自然也就有了对应的物理地址。因此每条指令也就有了自己在物理内存上的物理地址,以及在可执行程序中的虚拟地址。
可执行程序的头部中是有该可执行程序的入口地址的,该地址不是物理地址,是虚拟地址。
CPU中的寄存器读到可执行程序的入口的内容(也就是读到了入口地址),然后通过页表的转换找物理地址,如果此时对应的映射关系还没有建立,则会引发缺页中断,然后加载程序,然后映射关系建立,然后开始执行,执行的时候CPU中的寄存器的地址会发生自增,增加的大小与指令的长度有关,然后就可以一条一条的执行。
6.3、动态库的地址
绝对地址:指的是内存中的具体位置,从内存的起始位置算起的唯一地址。相对地址(偏移地址):指的是相对于某个基准地址或起点的偏移量。
例如,当我们的可执行程序要执行标准库中的printf函数时,可执行程序内部已经没有函数名这种东西了,仅有地址,但是这个地址是固定的,那就意味着共享库被加载后,共享库中的printf函数在共享区的地址也应该是固定的,否则的话,可执行程序就找不到printf函数。可是如果地址空间中该地址被其他东西占用了怎么办?
共享库是可以在共享区的任意位置加载的。这是怎么做到的呢?只要让动态库内部的函数不要采用绝对编址,而是采用每个函数在库中的偏移量即可。因此只要知道共享库在虚拟地址空间中的起始位置,然后再加上偏移量,就可以找到要找的内容在虚拟地址空间的绝对地址。
之前在生成动态库时,用到了一个选项,叫-fPIC,这个选项表示与地址无关。默认gcc是采用绝对编址的,使用这个选项后,就使用偏移量对库中的函数进行编址。