* TTB ==> translation table base
* TLB ==> translation look-aside buffer
* PTE ==> page table entry
* VA ==> virtual address
* PA ==> physical address
* PGD ==> page global directory
* PUD ==> page upper directory
* PMD ==> page middle directory
* PT ==> page table
*
* PGD struct:
* Section Base Address (31-20)
* SBZ (19)
* 0 (18)
* NG (17)
* S (16)
* APX (15)
* TEX (14-12)
* AP (11-10) 11
* P (9)
* DOMAIN (8-5) 00000
* XN (4)
* C (3)
* B (2)
* 10 (1-0) means direct map
MMU
MMU(Memory Management Unit)主要用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权、多任务多进程操作系统。
为什么要有MMU
- 内存空间保护
- 内存地址映射
对于一段代码
main:
mov r0,#1
mov r1,#2
ldr r2,add_func_l ;这是条伪指令,把add_func_l标号地址的内容装入r2寄存器
bl r2
die:
b die
add_func:
add r0,r0,r1
bx lr
.align 4
add_func_l: .word add_func ;定义一个内存字空间存放标号add_func的地址
如果指定链接器从0地址开始链接和生成纯指令的二进制文件,装入内存后情况:
这里是将编译出的程序直接放到了绝对物理地址上,内存芯片根据绝对地址来读取或者存放指令和数据。编译器和链接器在将代码转换成二进制可执行文件时,自然也是要将程序指令数据中写入相应的地址,程序才能工作。
- 1)ldr r2,[pc,#20],这条指令执行后R2中为0x14,当前PC加20的地址正好是0x1C,这里的20和0x14是由编译器和链接器写入的。
- 2)bl r2,保存该条指令的下一条指令的地址到R14,然后跳转到0x14地址上执行。
在这个例子中,可以发现:
1)程序只能装载到0地址开始内存中运行,因为“bl r2”指令执行时它会跳转到0x14地址,一旦移动这个程序的位置,0x14地址的内存单元中不再是这个程序的指令。
2)如果另一个程序,也是从0地址开始链接并且也要装载到0地址开始的内存中运行,这样就无法同时运行多个程序了,因为第二个程序会覆盖第一个程序的内存空间。当然让每个程序从不同的地址开始链接也是可以的。如果你的操作系统有数以万计的应用,并且这些应用来自不同的厂商,你会发现这种情况有多么糟糕。
采用了MMU之后情况就变了:
- 每个应用程序或者进程,都认为自己拥有一个完整且连续的地址空间(ARM920T处理器有4GB空间)
- 对于每个应用程序来说,4GB的地址空间是私有的,其他程序不能占有。
- 编译器和链接器总是可以从这个连续的地址空间的某个地址开始编译链接我们的程序,而且每种应用软件都一样
变量名就是编译期存在于编译器中的一个符号地址,编译或链接的时候,编译器或者链接器会给每个变量确定内存地址或偏移,并且通过符号表的方式将变量名和地址的映射关系保存起来,当代码中进行变量访问时,实际上是编译器通过变量名找到对应的内存地址,将变量名操作替换为内存地址操作,运行时程序访问该内存内存,从内存中读取数据
每个obj文件在编译时都会生成自己的符号表,我们要把这些符号都合并起来进行符号解析,在进行合并段,调整段偏移时,输入文件的各个段在连接后的虚拟地址就已经确定了,这一步完成后,连接器开始计算各个符号的虚拟地址,因为各个符号在段内的相对位置是固定的,所以段内各个符号的地址也已经是确定的了,只不过连接器需要给每个符号加上一个偏移量,使他们能够调整到正确的虚拟地址,这就是符号的重定位过程
在 elf文件中,有一个叫重定位表的结构专门用来保存这些与从定位有关的信息,重定位表在elf文件中往往是一个或多个段
加入MMU后内存中指令的情况
图中有多个应用程序,每个应用程序都有各自的MMU地址转换规则,这些规则由操作系统内核控制,在进程调用发生时,只需要切换到另一个进程的地址转换规则,就可以让应用程序安全地在物理地址上执行。
应用软件开发商可以始终认为他们拥有一个非常大而且连续的内存地址空间,编译链接时也不必考虑要把自己程序精确地放到哪个安全地位置上去。
在计算机发展初期,裸机开发年代,内存大小只有几KB,这时如果有一个很大的应用程序要运行,就无法直接放下。通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。
后来有了虚拟存储器,其基本思想是:程序,数据,堆栈的总大小可以超过物理存储器的大小,操作系统把当前使用的部分留在内存中,未使用的部分驻留在磁盘上。16MB的应用程序 和 4MB的机器内存,通过操作系统的选择,可以决定同一时刻哪4MB能够放在内存中。在内存和磁盘间交换程序片段,可以把16MB的程序一截借着一截地在4MB机器上运行,这个分割过程也不必程序员手工进行。
虚拟存储器使得计算机可以使用比实际的物理内存更多的存储空间。同时,内存管理单元还对实际的物理内存进行分割和保护,使得每个软件任务只能访问其分配到的内存空间。如果某个任务试图访问其他任务的内存空间,内存管理单元将自动产生异常,保护其他任务的程序和数据不受破坏。
MMU 硬件
MMU是位于处理器内核 高速缓存 和 物理存储器的总线 之间的一个设备。
处理器内核取指令和存取数据的时候,会提供一个逻辑地址,也是虚拟地址。这个地址是可执行代码在编译时由链接器生成的。
应用开发程序员对硬件的物理配置信息知之甚少,不需要了解存储器的物理配置细节,当应用代码需要使用存储空间时,操作系统通过对MMU为其分配合适的物理存储空间。
虚拟地址不需要和系统的实际物理地址相匹配,通过MMU时由它来通过地址映射规则去映射成对应的物理地址,以访问指令和数据。
页/页帧/页表/页表项(PTE)
因为MMU需要做地址映射规则,需要解决两个问题
- 映射的最小单位(粒度)
- 映射的规则
MMU从VA到PA映射有几种粒度可以选择:
- 1MB,称为段
- 64KB,称为大页
- 4KB,称为小页
- 1KB,称为极小页
粒度决定了映射地址空间块的大小。
对于映射规则,有个问题是如何存储。这就用到了页表。
页表存在于内存中的一块数据,这块数据中有很多条目,在ARM920T的MMU下,每个条目是一个存储字大小,即32位。
这些条目的位段有一定的格式,根据这些条目中数据的不同,MMU转换后的物理地址和地址空间块即页面的大小也有所不同。
页表放在物理地址上,其首地址存放在协处理器CP15的C2寄存器中:
试试选择段映射,页表条目中数据应该如何配置:
段映射下,MMU将虚拟地址空间和物理地址空间分成1MB大小的地址空间块,每个页面的大小为1MB。
因为1MB的大小的页面,粒度很粗,所以以该粒度划分地址空间,所需要的页表条目就很少,只有4096个条目(总物理地址空间大小是4GB)。因此只需要一级页表(如果粒度很细,需要的条目就多,那就需要二级页表了)
这里各个位段的意义可以查询这个表格:
要记得前面说的,根据这些条目中数据的不同,MMU转换后的物理地址和地址空间块即页面的大小也有所不同。如果是按大页或者小页粒度取划分,那么这里页表条目的位段配置是不同的。
需要关注的是:31:20这12位存储的是映射到的物理地址的起始位置,在段映射下,每个页面大小是1MB,所以这也暗示了该页面的偏移量,有了起始地址和地址偏移量,这个页面的首尾也就确定了。1MB的粒度大小的页面,所有页面的起始地址一定是1MB对齐的,地址的低20位一定是0。所以这个条目的最后20位不需要保存地址信息了,这才可以拿来存放访问属性控制的这些信息。
有了映射规则之后,再看看MMU是如何利用这些规则找到物理地址的:
MMU从处理器或者Cache那里拿到虚拟地址后:
将CP15 C2寄存器的高31:14位 和 虚拟地址高31:20位 尾部追加两个二进制0,一起组成32位的地址,这地址就是指向页表条目的地址
再用这个地址取页表中读取一个页表条目
用定位到的页表条目的高31:20位 与 虚拟地址的低19:0位 一起组成32位的物理地址
这个物理地址就可以用来访问物理内存了
这里只详解了段映射下的情况,其他情况也是类似的。