原文:https://blog.csdn.net/cc_net/article/details/24726287
清华大学2020操作系统课程学习:https://www.bilibili.com/video/BV1x7411T7mh?p=27
1. 内存分段
1.1 为什么分段?
在x86-16体系中,为了解决16位寄存器对20位地址线的寻址问题,引入了分段式内存管理。而CPU则使用CS,DS,ES,SS等寄存器来保存程序的段首地址。当CPU执行指令需要访问内存时,只会送出段内的偏移地址,而通过指令的类型类确定访问那一个段寄存器。具体可以参考:计算机原理学习(5)-- x86-16 CPU和内存管理
到了IA-32,Intel引入了保护模式,所以在IA-32中为了保持兼容性,所以同样支持内存分段管理。另外我们讨论过了内存分页,页面中包含了程序的代码,数据等信息,它们都有各自的地址。这些地址是在编译的时候就确定的,因为每个进程都有独立完整的内存空间,只需要把页和物理页映射就能运行,所以这个地址是可以在编译时就决定的。在编译时,编译器会等程序进行语法词法等分析,在编译过程中会建立许多的表,来确定代码和变量的虚拟地址:
被保存起来供打印清单的源程序正文;
符号表,包含变量的名字和属性;
包含所有用到的整形和浮点型数据的表;
语法分析树,包括程序语法分析的结果;
编译器内部过程调用的堆栈。
前面4张表会随着编译的进行不断增大,而堆栈的数据也会变化,现在的问题就是,每一张表的大小都不确定,那么如何指定每一张表在虚拟内存空间的地址呢?
如上图,没一张表都有自己的起始地址,但是当变量很多的时候,符号表需要的空间可能会超过程序正文的起始地址,这个时候就会把源程序的表的地址覆盖掉。当然编译器没有这么傻,它可以提示无法继续编译,当然这样并不合适,另一个办法就是拿出一部分没有使用的空间给符号表。造成这个问题的原因就是分页系统中的虚拟地址是一维的,所以在编译过程中必须给变量,代码分配虚拟地址。这个有点类似没有采用分页之前,进程之间使用物理地址导致相互覆盖的问题。
所以我们可以为不同的表分配自己的空间地址,也就是分段,这样他们地址都是相对地址,全部编译完成后确定了每张表的大小,就可以计算出实际的虚拟地址了。
1.2 分段的作用
分页实际是一个纯粹逻辑上的概念,因为实际的程序和内存并没有被真正的分为了不同的页面。而分段则不同,他是一个逻辑实体。一个段中可以是变量,源代码或者堆栈。一般来说每个段中不会包含不同类型的内容。而分段主要有以下几个作用:
1.解决编译问题: 前面提到过在编译时地址覆盖的问题,可以通过分段来解决,从而简化编译程序。
2.重新编译: 因为不同类型的数据在不同的段中,但其中一个段进行修改后,就不需要所有的段都重新进行编译。
3.内存共享: 对内存分段,可以很容易把其中的代码段或数据段共享给其他程序,分页中因为数据代码混合在一个页面中,所以不便于共享。
4.安全性: 将内存分为不同的段之后,因为不同段的内容类型不同,所以他们能进行的操作也不同,比如代码段的内容被加载后就不应该允许写的操作,因为这样会改变程序的行为。而在分页系统中,因为一个页不是一个逻辑实体,代码和数据可能混合在一起,无法进行安全上的控制。
5.动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
保持兼容性
所以在现在的x86的体系结构中分段内存管理是必选的,而分页管理则是可选的。
1.3 与x86-16分段管理的区别
Intel对分段内存管理逻辑地址的定义是【段号+段内地址】。在x86-16体系的分段管理中,CPU给出的内存地址是16位的段内偏移地址,段的基址从段寄存器中获得,最后计算出24位的物理地址。 而在IA-32体系中引入了保护模式,每个进程有4G的独立地址空间,CPU直接给出的是32位的段内偏移地址,段的基址从内存中获得,最后计算出32为的物理地址。他们最大的不同就在于获取基址的方式以及计算方法。
IA-32为了保持向前兼容,保留了CS/DS/ES/SS这4个寄存器,但因为不在从段寄存器中获得段价值,这4个段寄存器实际上已经失去了原本的作用(但不代表没有使用)。IA-32在内存中使用一张段表来记录各个段映射的物理内存地址(如下图)。
在译地的过程中,x86-16是通过16位的段基址和16位的段内偏移不是简单的相加,而是通过 段值*0x10 + 偏移地址 对基址重定向的方式计算得到物理地址,而IA-32中则相对简单,不需要对基址重定向,这一点和前面分页内存管理是相似的。而CPU只需要为这个段表提供一个记录其首地址的寄存器就可以了。 同样也可以使用TLB来加速。
与x86-16中分段管理另一个不同是,在IA-32中,因为有了独立的地址空间,对多程序也支持的非常好。而分段可以很好的支持进程间数据的共享。
2. 分段内存管理
2.1 段选择器
在IA-32中保留的CS/DS/ES/SS这4个16位段寄存器不再被解释为段的基地址,Intel为了保持兼容性将这些寄存器的16个位分成3个用于不同功能的域,称为段选择器。
其中3-15是选择子,存放的是段描述符的索引(可以理解为段号),该描述符为64bit用于描述存储器段的位置、长度和访问权限。而段描述符可以分为两种,全局描述符(GDT)和局部描述符(LDT),对应着第2位,而0,1两位是表示CPU的权限级别(0-4级)。在IA-32中一共有6个段选择器
- CS保存了代码段描述符的索引;
- DS保存了数据段描述符的索引;
- SS保存堆栈段描述符索引;
- ES、FS、GS则作为一般用途,可以指向任意的数据段,实现自定义寻址。
2.2 段描述符
段描述符就是前面说到的段表中的每一个项目的,一个段描述符由8个字节组成。它描述了段的特征。前面提到段描述符可以分为GDT和LDT两类。通常来说系统只定义一个GDT,而每个进程如果需要放置一些自定义的段,就可以放在自己的LDT中。IA-32中引入了GDTR和LDTR两个寄存器,就是用来存放当前正在使用的GDT和LDT的首地址。
上面的图是Linux中不同段的描述符,结构基本是一致的,只有少数字段有差别。其中最重要的就是BASE字段,一共32位,保存的是当前段的首地址。 在Linux系统中,每个CPU对应一个GDT。一个GDT中有18个段描述符和14个未使用或保留项。其中用户和内核各有一个代码段和数据段,然后还包含一个TSS任务段来保存寄存器的状态。其他的段则包括局部线程存储,电源管理,即插即用等多个段。而Linux系统中,大多数用户态的程序都不使用LDT。
2.3 段地址转换
在IA-32中,逻辑地址是16位的段选择符+32位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。
当前使用段描述符地址保存在GDTR或LDTR寄存器中
则 想要获得的代码段|数据段|堆栈段 的Base字段 = 对应段寄存器高13位index * 8 + [2:3]决定的段描述符GDTR或LDTR 中的首地址
上述段描述符中BASE获得段的首地址
线性地址 = 段描述符的Base字段 + offset(EIP/PC)
所以要通过段选择子找到段描述符
假设index = 3, gdtr在0x00020000则段描述符的地址为3 * 8 + 0x00020000 = 0x00021100
得到段描述符地址后把段描述符的Base字段和offset相加就得到了线性地址
1.IA-32首选确定要访问的段(方式x86-16相同),然后决定使用的段寄存器。
2.根据段选择符号的TI字段决定是访问GDT还是LDT,他们的首地址则通过GTDR和LDTR来获得。
3.将段选择符的Index字段的值*8,然后加上GDT或LDT的首地址,就能得到当前段描述符的地址。(乘以8是因为段描述符为8字节)
4.得到段描述符的地址后,可以通过段描述符中BASE获得段的首地址。
5.将逻辑地址中32位的偏移地址和段首地址相加就可以得到实际要访问的物理地址。
2.4 缓存段描述符
为了加速地址转换的过程,根据程序的局部性原理,我们可以讲当前段寄存器指向的段描述符缓存在特定的寄存器中。这里为6个段寄存器准备了6个用来缓存段描述符的非编程寄存器。这样就能加快地址转换的过程。而仅当段寄存器内容变化时,才有必要去访问内存中的GDT或LDT。
3. 段页式内存管理
分段内存管理的优势在于内存共享和安全控制,而分页内存管理的优势在于提高内利用率。他们之间并不是相互对立的竞争关系,而是可以相互补充的。也就是可以把2种方式结合起来,也就是目前计算机中最普遍采用的段页式内存管理。段页式管理的核心就是对内存进行分段,对每个段进行分页。这样在拥有了分段的优势的同时,可以更加合理的使用内存的物理页。
2.1 段页式内存管理结构
对于段页式管理来说,我们需要通过段表来保存每一个段的信息,通过页表保存每个段中虚拟页的信息。在段页管理的系统中,CPU给出的不再是分页系统中的虚拟地址,而是给出的逻辑地址。(前面2篇有介绍逻辑地址和虚拟地址,简单的说逻辑地址是二维的,而虚拟地址是一维的,平坦的)。
2.2 段页式地址转换
上面的图简单的描述了在段页式内存管理的系统中,地址转换的过程。实际上就是我们前面介绍的分段和分页地址转换的结合。
CPU给出要访问的逻辑地址;
通过分段内存管理的地址转换机制,将逻辑地址转换为线性地址,也就是分页系统中的虚拟地址;
通过分页内存管理的地址转换机制,将虚拟地址转换为物理地址;
3. 段描述符表
段描述符表简称描述符表,用来存储保护方式下段描述符的一个阵列。80386/80486 CPU 共有3 种描述符表:全局描述符表GDT、局部描述符表LDT 和中断描述符表IDT。描述符表由描述符顺序排列组成,占一定的内存,由系统地址寄存器(GDTR 、LDTR、IDTR) 指示其在物理存储器中的位置和大小。
全局描述符表GDT 是供所有任务使用的描述符表,在物理存储器地址空间中定义全局描述符表GDT。通常操作系统使用的有代码段描述符、数据段描述符、调用门描述符、各个任务的LDT 描述符、任务状态段TSS 描述符、任务门描述符等。
局部描述符表LDT 是每一项任务运行时都要使用的描述符表。在多任务操作系统管理下,每个任务通常包含两部分:与其他任务共用的部分及本任务独有的部分。与其他任务共用部分的段描述符存储在全局描述符表GDT内;本任务独有部分的段描述符存储在本任务的局部描述符表LDT 内。这样,每个任务都有一个局部描述符表LDT,而每个LDT 表又是一个段,它也就必须有一个对应的LDT 描述符。该LDT 描述符存储在全局描述符表中。局部描述符表LDT 中所存储的属于本任务的段描述符通常有代码段描述符、数据段描述符、调用门描述符及任务门描述符等。
GDT 和LDT 段描述符表实际上是段描述符的一个长度不定的数据阵列,如图8.5 所示。描述符表在长度上是可变的,最多容纳213 个描述符,最少包含一个描述符。每个项有8 个字节长,称为一个段描述符。中断描述符表IDT暂不介绍。