在3.CPU的工作模式介绍三种模式的时候,我们用段描述符描述了内存的划分。但这其实是不够的。主要有一下问题:
- 程序之间的地址冲突问题。
- 程序之间的隔离性问题,避免一个程序访问另一个程序的地址空间。
- 物理内存有限的问题
为了解决这些问题,引入了虚拟地址。让每个程序都拥有从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
步骤如下:
- 使CPU进入保护模式或者长模式
- 准备好页表数据
- 把顶级页表的物理内存地址赋值给CR3寄存器
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax- 把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根据页表将虚拟地址转为物理地址,在这个过程中如果页表项的数据为空,权限不够等都会导致转换失败。 失败的处理步骤如下:
- MMU停止转换地址。
- MMU将转换失败的虚拟地址写入CR2寄存器。
- MMU触发CPU的14号中断,暂停当前指令执行。
- CPU执行14号中断代码,代码会检查原因,然后处理好页表数据返回。
- CPU中断返回继续执行MMU地址转换失败的指令。
目的就是记录一下转换失败的虚拟地址,然后执行中断程序去检查原因并处理错误,中断返回后继续执行转换失败的指令。