3.CPU的工作模式介绍三种模式的时候,我们用段描述符描述了内存的划分。但这其实是不够的。主要有一下问题:

  1. 程序之间的地址冲突问题。
  2. 程序之间的隔离性问题,避免一个程序访问另一个程序的地址空间。
  3. 物理内存有限的问题

为了解决这些问题,引入了虚拟地址。让每个程序都拥有从0到最大地址的空间,这个空间是每个进程私有的,相互独立的。

虚拟地址

引入虚拟地址之后,我们程序代码内出现的地址全部都变成了虚拟地址。 反汇编一个二进制程序,可以看到所有应用程序的开始部分都是一样的,这也说明了每个应用程序的虚拟地址是相同且独立的。

例如下列反汇编代码

00000000000004e8 <_init>:
 4e8:  48 83 ec 08            sub    $0x8,%rsp
 4ec:  48 8b 05 f5 0a 20 00   mov    0x200af5(%rip),%rax        # 200fe8 <__gmon_start__>
 4f3:  48 85 c0               test   %rax,%rax
 4f6:  74 02                  je     4fa <_init+0x12>
 4f8:  ff d0                  callq  *%rax
 4fa:  48 83 c4 08            add    $0x8,%rsp
 4fe:  c3                     retq 

虚拟地址是由链接器产生的,链接器负责把多个代码模块组装在一起,解决模块之间的引用。

引入虚拟地址使得程序之间相互独立运行,并且通过工具链解决了虚拟地址的产生的问题,对于开发人员则只需要关注代码本身,而不用关注内存地址的安排,大大减轻了开发人员的负担。

物理地址

虚拟地址虽然解决了许多问题,但是它指示逻辑上的地址。但是最终我们要从内存中取得指令和数据,还是需要物理地址。

物理地址就是用来选择内存上的一个个存储单元的地址,内存也只认得物理地址。每个存储单元对应一个字节的数据。

地址的转换

我们前面介绍了每个程序的虚拟地址空间和内存寻址的物理地址,那么在程序执行的过程中,肯定需要将虚拟地址转换成物理地址。

而完成这个工作的,就是内存管理单元(MMU)。采用软硬件结合的方式,根据软件给出的地址对应关系数据进行地址转换。

看上图,可以看到绿色部分的地址转换关系表,MMU就会根据这个表然后将进程的虚拟地址转为物理地址空间。

要注意,地址转换关系表是存放在物理地址空间的。可以看到地址转换关系表只占用了一部分,所以虚拟地址空间和物理地址空间的转换的单位肯定不是单位空间,而是以4KB、2MB、4MB等单位进行转换的,这个单位就叫做页,一般是4KB,也是现代内存管理模式——分页模型

一个虚拟页对应一个物理页,在地址转换关系表中只需要存放虚拟地址对应的物理页地址就行了。

MMU

MMU即内存管理单元,硬件电路逻辑实现的一个地址转换器件。接受地址转换关系表和虚拟地址,输出对应的物理地址。

x86 CPU的MMU只能在保护模式或者长模式下才能开启。

保护模式的内存模型是分段模型,需要使用保护模式的平坦模型,这样就绕过了分段模型。对于保护模式来说,分段后的线性地址才与平坦模式和长模式下的虚拟地址相等(也可以说,平坦模型和长模式下的虚拟地址和线性地址是相等的)。 保护模型下可以关闭MMU,这样线性地址就是物理地址。而长模式下需要开启MMU才能访问内存地址空间。

MMU页表

前面提到过的地址关系转换表有一个专业的名字——页表

页表是存放在物理内存中的,其实存放的只有物理页面的地址,MMU以虚拟地址作为索引去查表返回物理页面地址。并且页表通常是分级的(因为一个页存放不下全部物理页的地址)。结构如下: 根据上图,可以看出虚拟地址分为了多个段,将每个段用作偏移地址去索引页表,然后最终找到对应的物理地址。

保护模式下的分页

32位虚拟地址经过分段得到线性地址,通常使用平坦模型的话,虚拟地址和线性地址是相同的。

保护模式下的分页一般有两种,一种是4KB的页,一种是4MB的页。 页大小的不同,会造成虚拟地址段位划分和页目录层级的不同,但是虚拟页和物理页的大小是始终相同的

保护模式下的分页 —— 4KB

页大小为4KB,那么只有一级页目录,每个目录对应一个页表,每个页表有1024项,一项对应一个物理页,每个物理页4KB,正好对应4GB。

虚拟地址分为三个位段:页目录索引、页表索引、页内偏移。如下图:

最顶级的页目录的地址存放在CR3寄存器中,MMU就是通过CR3来找到页目录的。

由于页的大小是4KB,所以对应的物理页地址也是4KB对齐的,可以将CR3,页目录项,页表项的低12位都为0,但是实际上,我们可以利用这低12位,存放一些额外信息,如是否可写、可读等等,以便节省空间。

保护模式下的分页——4MB

4MB的页大小,32位的物理内存只需要1K个页,也就是说只需要一个页表即可。 虚拟地址分为两个位段:页表索引、页内偏移。 CR3仍然4KB对齐即可,页表项只需要用高10位来保存物理页面的基地址即可。

长模式下的分页

保护模式可以选择是否开启MMU,而长模式则必须开启MMU分页模式,因为长模式下的段描述符忽略了段基地址和段长度,长模式段描述符。弱化了分段模型。

长模式下的分页有4KB和2MB两种。

首先需要注意的是,长模式下的地址总线是64位的,但是实际上长模式的地址空间是48位的。 只是用了047作为地址空间。 4863要么全部为0,要么全部为1,这是根据第47位决定的。如果第47位为1,那么全部为1,如果为0,那么全部为0,可以方便以后扩展。

这样将内存地址天然的划分成了两部分,实际上,在linux操作系统中,正式将高地址部分划分给了内核空间,低地址部分划分给了用户空间。

长模式下的分页——4KB

与保护模式下差不多,只是页目录又多了几级,因为有了更大的地址空间。

每级页表中都可以根据对齐的一个空余位来携带一些额外信息。

长模式下的分页——2MB

2MB的页大小放弃了页表项,直接使用页目录指向2MB大小的物理页面。然后把虚拟地址的低21位作为页内偏移。 页目录项的低21位可以用来存放页面属性。

其余部分与4KB页大小分页相同

开启MMU

步骤如下:

  1. 使CPU进入保护模式或者长模式
  2. 准备好页表数据
  3. 顶级页表的物理内存地址赋值给CR3寄存器
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
  1. 把CPU的CR0的PE位置为1,这样就开启了MMU。
;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE =1
bts eax, 31   ;CR0.P = 1
mov cr0, eax 

MMU地址转换失败

MMU根据页表将虚拟地址转为物理地址,在这个过程中如果页表项的数据为空,权限不够等都会导致转换失败。 失败的处理步骤如下:

  1. MMU停止转换地址。
  2. MMU将转换失败的虚拟地址写入CR2寄存器。
  3. MMU触发CPU的14号中断,暂停当前指令执行。
  4. CPU执行14号中断代码,代码会检查原因,然后处理好页表数据返回。
  5. CPU中断返回继续执行MMU地址转换失败的指令。

目的就是记录一下转换失败的虚拟地址,然后执行中断程序去检查原因并处理错误,中断返回后继续执行转换失败的指令。