You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
20 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | 虚幻与真实:程序中的地址如何转换?
你好我是LMOS。
从前面的课程我们得知CPU执行程序、处理数据都要和内存打交道这个打交道的方式就是内存地址。
读取指令、读写数据都需要首先告诉内存芯片hi内存老哥请你把0x10000地址处的数据交给我……hi内存老哥我已经计算完成请让我把结果写回0x200000地址的空间。这些地址存在于代码指令字段后的常数或者存在于某个寄存器中。
![](https://static001.geekbang.org/resource/image/b0/fc/b0e93b744dfdc62c4a3ce8816b25b1fc.jpg)
今天,我们就来专门研究一下程序中的地址。说起程序中的地址,不知道你是否好奇过,为啥系统设计者要引入虚拟地址呢?
我会先带你从一个多程序并发的场景热身,一起思考这会导致哪些问题,为什么能用虚拟地址解决这些问题。
搞懂原理之后,我还会带你一起探索**虚拟地址和物理地址的关系和转换机制**。在后面的课里,你会发现,我们最宝贵的内存资源正是通过这些机制来管理的。
## 从一个多程序并发的场景说起
设想一下如果一台计算机的内存中只运行一个程序A这种方式正好用前面CPU的[实模式](https://time.geekbang.org/column/article/375278)来运行因为程序A的地址在链接时就可以确定例如从内存地址0x8000开始每次运行程序A都装入内存0x8000地址处开始运行没有其它程序干扰。
现在改变一下内存中又放一道程序B程序A和程序B各自运行一秒钟如此循环直到其中之一结束。这个新场景下就会产生一些问题当然这里我们只关心内存相关的这几个核心问题。
1.谁来保证程序A跟程序B **没有内存地址的冲突**换句话说就是程序A、B各自放在什么内存地址这个问题是由A、B程序协商还是由操作系统决定。
2.怎样保证程序A跟程序B **不会互相读写各自的内存空间**?这个问题相对简单,用保护模式就能解决。
3.如何解决**内存容量**问题程序A和程序B在不断开发迭代中程序代码占用的空间会越来越大导致内存装不下。
4.还要考虑一个**扩展后的复杂情况**如果不只程序A、B还可能有程序C、D、E、F、G……它们分别由不同的公司开发而每台计算机的内存容量不同。这时候又对我们的内存方案有怎样的影响呢
要想完美地解决以上最核心的4个问题一个较好的方案是让所有的程序都各自享有一个从0开始到最大地址的空间这个地址空间是独立的是该程序私有的其它程序既看不到也不能访问该地址空间这个地址空间和其它程序无关和具体的计算机也无关。
事实上,计算机科学家们早就这么做了,这个方案就是**虚拟地址**,下面我们就来看看它。
## 虚拟地址
正如其名,这个地址是虚拟的,自然而然地和具体环境进行了解耦,这个环境包括系统软件环境和硬件环境。
虚拟地址是逻辑上存在的一个数据值比如0~100就有101个整数值这个0~100的区间就可以说是一个虚拟地址空间该虚拟地址空间有101个地址。
我们再来看看最开始Hello World的例子我们用objdump工具反汇编一下Hello World二进制文件就会得到如下的代码片段
```
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
```
上述代码中左边第一列数据就是虚拟地址第三列中是程序指令“mov 0x200af5(%rip),%raxje 4facallq \*%rax”指令中的数据都是虚拟地址。
事实上,所有的应用程序开始的部分都是这样的。这正是因为每个应用程序的虚拟地址空间都是相同且独立的。
那么这个地址是由谁产生的呢?
答案是链接器,其实我们开发软件经过编译步骤后,就需要链接成可执行文件才可以运行,而链接器的主要工作就是把多个代码模块组装在一起,并解决模块之间的引用,即处理程序代码间的地址引用,形成程序运行的静态内存空间视图。
只不过这个地址是虚拟而统一的,而根据操作系统的不同,这个虚拟地址空间的定义也许不同,应用软件开发人员无需关心,由开发工具链给自动处理了。由于这虚拟地址是独立且统一的,所以各个公司开发的各个应用完全不用担心自己的内存空间被占用和改写。
## 物理地址
虽然虚拟地址解决了很多问题,但是虚拟地址只是逻辑上存在的地址,无法作用于硬件电路的,程序装进内存中想要执行,就需要和内存打交道,从内存中取得指令和数据。而内存只认一种地址,那就是**物理地址**。
什么是物理地址呢?物理地址在逻辑上也是一个数据,只不过这个数据会被地址译码器等电子器件变成电子信号,放在地址总线上,地址总线电子信号的各种组合就可以选择到内存的储存单元了。
但是地址总线上的信号即物理地址也可以选择到别的设备中的储存单元如显卡中的显存、I/O设备中的寄存器、网卡上的网络帧缓存器。不过如果不做特别说明我们说的物理地址就是指**选择内存单元的地址**。
## 虚拟地址到物理地址的转换
明白了虚拟地址和物理地址之后我们发现虚拟地址必须转换成物理地址这样程序才能正常执行。要转换就必须要转换机构它相当于一个函数p=f(v)输入虚拟地址v输出物理地址p。
那么要怎么实现这个函数呢?
用软件方式实现太低效用硬件实现没有灵活性最终就用了软硬件结合的方式实现它就是MMU内存管理单元。MMU可以接受软件给出的地址对应关系数据进行地址转换。
我们先来看看逻辑上的MMU工作原理框架图。如下图所示
![](https://static001.geekbang.org/resource/image/d5/99/d582ff647549b8yy986d90e697d33499.jpg "MMU工作原理图")
上图中展示了MMU通过地址关系转换表将0x80000~0x84000的虚拟地址空间转换成 0x10000~0x14000的物理地址空间而地址关系转换表本身则是放物理内存中的。
下面我们不妨想一想地址关系转换表的实现.如果在地址关系转换表中,这样来存放:一个虚拟地址对应一个物理地址。
那么问题来了32位地址空间下4GB虚拟地址的地址关系转换表就会把整个32位物理地址空间用完这显然不行。
要是结合前面的保护模式下分段方式呢,地址关系转换表中存放:一个虚拟段基址对应一个物理段基址,这样看似可以,但是因为段长度各不相同,所以依然不可取。
综合刚才的分析,系统设计者最后采用一个折中的方案,即**把虚拟地址空间和物理地址空间都分成同等大小的块,也称为页,按照虚拟页和物理页进行转换。**根据软件配置不同这个页的大小可以设置为4KB、2MB、4MB、1GB这样就进入了现代内存管理模式——**分页模型**。
下面来看看分页模型框架,如下图所示:
![](https://static001.geekbang.org/resource/image/9b/d0/9b19677448ee973c4f3yya6b3af7b4d0.jpg "分页模型框架图")
结合图片可以看出,一个虚拟页可以对应到一个物理页,由于页大小一经配置就是固定的,所以在地址关系转换表中,只要存放**虚拟页地址对应的物理页地址**就行了。
我知道说到这里也许你仍然没搞清楚MMU和地址关系转换表的细节别急我们现在已经具备了研究它们的基础下面我们就去探索它们。
## MMU
MMU即内存管理单元是用硬件电路逻辑实现的一个地址转换器件它负责接受虚拟地址和地址关系转换表以及输出物理地址。
根据实现方式的不同MMU可以是独立的芯片也可以是集成在其它芯片内部的比如集成在CPU内部x86、ARM系列的CPU就是将MMU集成在CPU核心中的。
SUN公司的CPU是将独立的MMU芯片卡在总线上的有一夫当关的架势。下面我们只研究x86 CPU中的MMU。x86 CPU要想开启MMU就必须先开启保护模式或者长模式实模式下是不能开启MMU的。
由于保护模式的内存模型是分段模型它并不适合于MMU的分页模型所以我们要使用保护模式的平坦模式这样就绕过了分段模型。这个平坦模型和长模式下忽略段基址和段长度是异曲同工的。地址产生的过程如下所示。
![](https://static001.geekbang.org/resource/image/b4/88/b41a2bb00e19e662b34a1b7b7c0ae288.jpg "CPU地址转换图")
上图中程序代码中的虚拟地址经过CPU的分段机制产生了线性地址平坦模式和长模式下线性地址和虚拟地址是相等的。
如果不开启MMU在保护模式下可以关闭MMU这个线性地址就是物理地址。因为长模式下的分段**弱化了地址空间的隔离**所以开启MMU是必须要做的开启MMU才能访问内存地址空间。
### MMU页表
现在我们开始研究地址关系转换表,其实它有个更加专业的名字——**页表**。它描述了虚拟地址到物理地址的转换关系,也可以说是虚拟页到物理页的映射关系,所以称为页表。
为了增加灵活性和节约物理内存空间因为页表是放在物理内存中的所以页表中并不存放虚拟地址和物理地址的对应关系只存放物理页面的地址MMU以虚拟地址为索引去查表返回物理页面地址而且页表是分级的总体分为三个部分一个顶级页目录多个中级页目录最后才是页表逻辑结构图如下.
![](https://static001.geekbang.org/resource/image/2d/yf/2df904c8ba75065e1491138d63820yyf.jpg "MMU页表原理图")
从上面可以看出,一个虚拟地址被分成从左至右四个位段。
第一个位段索引顶级页目录中一个项,该项指向一个中级页目录,然后用第二个位段去索引中级页目录中的一个项,该项指向一个页目录,再用第三个位段去索引页目录中的项,该项指向一个物理页地址,最后用第四个位段作该物理页内的偏移去访问物理内存。**这就是MMU的工作流程。**
## 保护模式下的分页
前面的内容都是理论上帮助我们了解分页模式原理的,分页模式的**灵活性、通用性、安全性**,是现代操作系统内存管理的基石,更是事实上的标准内存管理模型,现代商用操作系统都必须以此为基础实现虚拟内存功能模块。
因为我们的主要任务是开发操作系统而开发操作系统就落实到真实的硬件平台上去的下面我们就来研究x86 CPU上的分页模式。
首先来看看保护模式下的分页保护模式下只有32位地址空间最多4GB-1大小的空间。
根据前面得知32位虚拟地址经过分段机制之后得到线性地址又因为通常使用平坦模式所以线性地址和虚拟地址是相同的。
保护模式下的分页大小通常有两种一种是4KB大小的页一种是4MB大小的页。分页大小的不同会导致虚拟地址位段的分隔和页目录的层级不同但虚拟页和物理页的大小始终是等同的。
### 保护模式下的分页——4KB页
该分页方式下32位虚拟地址被分为三个位段**页目录索引、页表索引、页内偏移**只有一级页目录其中包含1024个条目 每个条目指向一个页表每个页表中有1024个条目。其中一个条目就指向一个物理页每个物理页4KB。这正好是4GB地址空间。如下图所示。
![](https://static001.geekbang.org/resource/image/00/f8/00b7f1ef4a1c4f6fc9e6b69109ae0bf8.jpg "保护模式下的4KB分页")
上图中CR3就是CPU的一个32位的寄存器MMU就是根据这个寄存器找到页目录的。下面我们看看当前分页模式下的CR3、页目录项、页表项的格式。
![](https://static001.geekbang.org/resource/image/36/c9/361c48e1876a412f9ff9f29bf2dbecc9.jpg)
可以看到页目录项、页表项都是4字节32位1024个项正好是4KB一个页因此它们的地址始终是4KB对齐的所以低12位才可以另作它用形成了页面的相关属性如是否存在、是否可读可写、是用户页还是内核页、是否已写入、是否已访问等。
### 保护模式下的分页——4MB页
该分页方式下32位虚拟地址被分为两个位段**页表索引、页内偏移**只有一级页目录其中包含1024个条目。其中一个条目指向一个物理页每个物理页4MB正好为4GB地址空间如下图所示。
![](https://static001.geekbang.org/resource/image/76/52/76932c52a7b6109854f2de72d71bba52.jpg "保护模式下的4MB分页")
CR3还是32位的寄存器只不过不再指向顶级页目录了而是指向一个4KB大小的页表这个页表依然要4KB地址对齐其中包含1024个页表项格式如下图。
![](https://static001.geekbang.org/resource/image/9a/08/9a4afdc60b790c3e2b7e94b0c7fd4208.jpg)
可以发现4MB大小的页面下页表项还是4字节32位但只需要用高10位来保存物理页面的基地址就可以。因为每个物理页面都是4MB所以低22位始终为0为了兼容4MB页表项低8位和4KB页表项一样只不过第7位变成了PS位且必须为1而PAT位移到了12位。
## 长模式下的分页
如果开启了长模式,则必须同时开启分页模式,因为长模式弱化了分段模型,而分段模型也确实有很多不足,不适应现在操作系统和应用软件的发展。
同时长模式也扩展了CPU的位宽使得CPU能使用64位的超大内存地址空间。所以长模式下的虚拟地址必须等于线性地址且为64位。
长模式下的分页大小通常也有两种4KB大小的页和2MB大小的页。
### 长模式下的分页——4KB页
该分页方式下64位虚拟地址被分为6个位段分别是保留位段顶级页目录索引、页目录指针索引、页目录索引、页表索引、页内偏移顶级页目录、页目录指针、页目录、页表各占有4KB大小其中各有512个条目每个条目8字节64位大小如下图所示。
![](https://static001.geekbang.org/resource/image/ec/c9/ecdea93c2544cf9c1d84461b602b03c9.jpg "长模式下的4KB分页")
上面图中CR3已经变成64位的CPU的寄存器它指向一个顶级页目录里面的顶级页目项指向页目录指针依次类推。
需要注意的是虚拟地址48到63这16位是根据**第47位**来决定的47位为1它们就为1反之为0这是因为x86 CPU并没有实现全64位的地址总线而是只实现了48位但是CPU的寄存器却是64位的。
这种最高有效位填充的方式即使后面扩展CPU的地址总线也不会有任何影响下面我们去看看当前分页模式下的CR3、顶级页目录项、页目录指针项、页目录项、页表项的格式我画了一张图帮你理解。
![](https://static001.geekbang.org/resource/image/e3/55/e342246f5cfa21c5b5173b9e494bdc55.jpg)
由上图可知长模式下的4KB分页下由一个顶层目录、二级中间层目录和一层页表组成了64位地址翻译过程。
顶级页目录项指向页目录指针页页目录指针项指向页目录页页目录项指向页表页页表项指向一个4KB大小的物理页各级页目录项中和页表项中依然存在各种属性位这在图中已经说明。其中的XD位可以控制代码页面是否能够运行。
### 长模式下的分页——2MB页
在这种分页方式下64位虚拟地址被分为5个位段 保留位段、顶级页目录索引、页目录指针索引、页目录索引页内偏移顶级页目录、页目录指针、页目录各占有4KB大小其中各有512个条目每个条目8字节64位大小。
![](https://static001.geekbang.org/resource/image/68/ea/68bf70d8bcae7802e5291140ac1ec6ea.jpg "长模式下的2MB分页")
可以发现长模式下2MB和4KB分页的区别是2MB分页下是页目录项直接指向了2MB大小的物理页面放弃了**页表项**然后把虚拟地址的低21位作为页内偏移21位正好索引2MB大小的地址空间。
下面我们还是要去看看2MB分页模式下的CR3、顶级页目录项、页目录指针项、页目录项的格式格式如下图。
![](https://static001.geekbang.org/resource/image/45/0b/457f6965d0f25bf64bfb9ec698ab7e0b.jpg)
上图中没有了页表项取而代之的是页目录项中直接存放了2MB物理页基地址。由于物理页始终2MB对齐所以其地址的低21位为0用于存放页面属性位。
## 开启MMU
要使用分页模式就必先开启MMU但是开启MMU的前提是CPU进入保护模式或者长模式开启CPU这两种模式的方法我们在前面[第五节课](https://time.geekbang.org/column/article/375278)已经讲过了下面我们就来开启MMU步骤如下
1.使CPU进入保护模式或者长模式。
2.准备好页表数据,这包含顶级页目录,中间层页目录,页表,假定我们已经编写了代码,在物理内存中生成了这些数据。
3.把顶级页目录的物理内存地址赋值给CR3寄存器。
```
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
```
4. 设置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地址转换失败了怎么办呢失败了既不能放行也不是resetMMU执行的操作如下。
1.MMU停止转换地址。
2.MMU把转换失败的虚拟地址写入CPU的CR2寄存器。
3.MMU触发CPU的14号中断使CPU停止执行当前指令。
4.CPU开始执行14号中断的处理代码代码会检查原因处理好页表数据返回。
5.CPU中断返回继续执行MMU地址转换失败时的指令。
这里你只要先明白这个流程就好了,后面课程讲到内存管理的时候我们继续探讨。
## 重点回顾
又到了课程的尾声,把心情放松下来,我们一起来回顾这节课的重点。
首先,我们从一个场景开始热身,发现多道程序同时运行有很多问题,都是内存相关的问题,内存需要**隔离和保护**。从而提出了虚拟地址与物理地址分离,让应用程序从实际的物理内存中解耦出来。
虽然虚拟地址是个非常不错的方案但是虚拟地址必须转换成物理地址才能在硬件上执行。为了执行这个转换过程才开发出了MMU内存管理单元MMU**增加了转换的灵活性**,它的实现方式是**硬件执行转换过程,但又依赖于软件提供的地址转换表。**
最后我们下落到具体的硬件平台研究了x86 CPU上的MMU。
x86 CPU上的MMU在其保护模式和长模式下提供4KB、2MB、4MB等页面转换方案我们详细分析了它们的**页表格式**。同时,也搞清楚了**如何开启MMU以及MMU地址转换失败后执行的操作。**
## 思考题
在分页模式下,操作系统是如何对应用程序的地址空间进行隔离的?
欢迎你在留言区和我交流互动。如果这节课对你有启发的话,也欢迎你转发给朋友、同事,说不定就能帮他解决疑问。
我是LMOS我们下节课见