计算机原理学习(7)– x86-32 CPU和内存管理之分段管理

 

前言

 

前一篇我们介绍了内存管理中的分页试内存管理,分页的主要作用就是使得每个进程有一个独立的,完整的内存空间,通过虚拟内存技术,使得程序可以在较小的内存上运行,而进程之间内存空间相互独立,提高了安全性。这一篇将主要介绍内存管理中分段管理,以及两种的结合,也是目前计算机普遍采用的段页式内存管理。这也直接决定了的后面程序的编译,加载以及允许时的内存布局。

 

 

 

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. 动态链接: 动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当运行过程中又需要调用某段时,才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
  6. 保持兼容性

所以在现在的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位偏移地址,段寄存器不在保存段基址,而是保存段描述符的索引。

 

  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 段页式地址转换

 

 

上面的图简单的描述了在段页式内存管理的系统中,地址转换的过程。实际上就是我们前面介绍的分段和分页地址转换的结合。

  1. CPU给出要访问的逻辑地址;
  2. 通过分段内存管理的地址转换机制,将逻辑地址转换为线性地址,也就是分页系统中的虚拟地址;
  3. 通过分页内存管理的地址转换机制,将虚拟地址转换为物理地址;

 

 

 

 4. 总结

 

 

这一篇文章主要介绍了IA-32系统中分段式内存管理是如何工作的。而目前主流的系统中都采用了段页相结合的内存管理方式,当然不同的系统具体实现起来是不同的。比如在Linux中,所有段首地址都是从0x0000000开始,所以逻辑地址和转换得到的线性地址完全是一样的。到这一篇,有关x86的内存管理方式以及介绍晚了。内存的分页和分段也决定了程序的编译,可执行文件的结构,程序的内存布局等等。这个将在后面介绍到。

 

 

 

参考

 

《深入理解Linux内核第三版》

 

《现代操作系统》

 


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

4 评论

Rian_from_sky进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注