Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

09|Intel 内存管理两板斧:分段与分页

讲述:闪客 大小:10.37MB 时长:00:10:49
00:00
1.0×

你好,我是闪客。

这一讲,我们看一个特别重要的东西,即 CPU 是如何管理内存的?当然,我们聊的是最最基础和底层的管理手段——分段和分页。

分页机制如何开启

上一讲咱们说到,head.s 代码在重新设置了 gdt 与 idt。之后,我们就会看到这样一段代码:

jmp after_page_tables
...
after_page_tables:
    push 0
    push 0
    push 0
    push L6
    push _main
    jmp setup_paging
L6:
    jmp L6

也就是开启分页机制,并且跳转到 main 函数。如何跳转到之后用 c 语言写的 main.c 里的 main 函数,是个有趣的事,也包含在这段代码里。

不过,我们先瞧瞧这分页机制是如何开启的,也就是 setup_paging 这个标签处的代码。

setup_paging:
    mov ecx,1024*5
    xor eax,eax
    xor edi,edi
    pushf
    cld
    rep stosd
    mov eax,_pg_dir
    mov [eax],pg0+7
    mov [eax+4],pg1+7
    mov [eax+8],pg2+7
    mov [eax+12],pg3+7
    mov edi,pg3+4092
    mov eax,00fff007h
    std
L3: stosd
    sub eax,00001000h
    jge L3
    popf
    xor eax,eax
    mov cr3,eax
    mov eax,cr0
    or  eax,80000000h
    mov cr0,eax
    ret

看到这么多行代码也别怕,都是纸老虎,我们一点点来分析。

首先要了解的就是,啥是分页机制?还记不记得之前我们在代码中给出一个内存地址,在保护模式下,要先经过分段机制的转换,才能最终变成物理地址,就是这样。

图片

这是在没有开启分页机制的时候,只需要经过这一步转换即可得到最终的物理地址了,但是在开启了分页机制后,又会多一步转换,如下图所示:

图片

也就是说,在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是线性地址,然后再通过一次分页机制转换,得到最终的物理地址。

分页机制如何变换

我们已经清楚分段机制如何对地址进行变换了,那分页机制又是如何变换的呢?

我们直接以一个例子来学习这个过程。比如我们的线性地址(已经经过了分段机制的转换)是

15M,二进制表示就是:

0000000011_0100000000_000000000000

我们看一下它的转换过程:

图片

也就是说,CPU 在看到我们给出的内存地址后,首先把线性地址被拆分成了这样:

高 10 位:中间 10 位:后 12 位

高 10 位负责在页目录表中找到一个页目录项,这个页目录项的值加上中间 10 位拼接后的地址去页表中去寻找一个页表项,这个页表项的值,再加上后 12 位偏移地址,就是最终的物理地址。

感兴趣的同学可以阅读 Intel 手册 Volume 3 Chapter 4.3 32-bit Paging的内容。

图片

而这一切的操作,都离不开计算机的一个硬件叫 MMU,中文名叫内存管理单元,有时也叫 PMMU,中文名是分页内存管理单元。这个部件负责的就是把虚拟地址转换为物理地址。

所以整个过程我们不用操心,作为操作系统这个软件层,只需要提供好页目录表和页表,这种页表方案叫做二级页表,第一级叫页目录表 PDE,第二级叫页表 PTE。他们的结构如下:

图片

同样如果想了解里面字段的细节,可以阅读 Intel 手册 Volume 3 Chapter 4.3 32-bit Paging部分的内容。

图片

之后再开启分页机制的开关。其实就是更改 cr0 寄存器中的一位(31 位),还记得我们开启保护模式么?也是改这个寄存器中的一位的值。

图片

然后,MMU 就可以帮我们进行分页的转换了。此后指令中的内存地址(就是程序员提供的逻辑地址),就统统要先经过分段机制的转换,再通过分页机制的转换,才能最终变成物理地址。

所以这段代码,就是帮我们把页表和页目录表在内存中写好,之后开启 cr0 寄存器的分页开关,仅此而已,我们再把代码贴上来。

setup_paging:
    mov ecx,1024*5
    xor eax,eax
    xor edi,edi
    pushf
    cld
    rep stosd
    mov eax,_pg_dir
    mov [eax],pg0+7
    mov [eax+4],pg1+7
    mov [eax+8],pg2+7
    mov [eax+12],pg3+7
    mov edi,pg3+4092
    mov eax,00fff007h
    std
L3: stosd
    sub eax,00001000h
    jge L3
    popf
    xor eax,eax
    mov cr3,eax
    mov eax,cr0
    or  eax,80000000h
    mov cr0,eax
    ret

我们先说这段代码最终产生的效果吧。

当时 Linux-0.11 认为,总共可以使用的内存不会超过 16M,也即最大地址空间为 0xFFFFFF

而按照当前的页目录表和页表这种机制,1 个页目录表最多包含 1024 个页目录项(也就是 1024 个页表),1 个页表最多包含 1024 个页表项(也就是 1024 个页),1 页为 4KB(因为有 12 位偏移地址)。

因此,16M 的地址空间可以用 1 个页目录表 + 4 个页表搞定。

4(页表数)* 1024(页表项数) * 4KB(一页大小)= 16MB

所以,上面这段代码就是,将页目录表放在内存地址的最开头,还记得上一讲开头让你留意的 _pg_dir 这个标签吧?

_pg_dir:
_startup_32:
    mov eax,0x10
    mov ds,ax
    ...

之后紧挨着这个页目录表,放置 4 个页表,代码里也有这四个页表的标签项。

.org 0x1000 pg0:
.org 0x2000 pg1:
.org 0x3000 pg2:
.org 0x4000 pg3:
.org 0x5000

最终将页目录表和页表填写好数值,并覆盖整个 16MB 的内存。随后,开启分页机制。此时内存中的页表相关的布局如下图所示:
图片

这些页目录表和页表放在了整个内存布局中最开头的位置,就是覆盖了开头的 system 代码,不过被覆盖的 system 代码已经执行过了,所以被覆盖了也无所谓。

同时,如 idt 和 gdt 一样,我们也需要通过一个寄存器,告诉 CPU 我们把这些页表放在了哪里,具体就是这段代码。

xor eax,eax
mov cr3,eax

通过上面的代码,我们相当于告诉 cr3 寄存器,0 地址处就是页目录表,再通过页目录表就可以找到所有的页表,也就相当于 让CPU 知道分页机制的全貌了。

至此后,整个内存布局如下:图片

那么具体页表设置好后,映射的内存是怎样的情况呢?那就要看页表的具体数据了,就是这一坨代码:

setup_paging:
    ...
    mov eax,_pg_dir
    mov [eax],pg0+7
    mov [eax+4],pg1+7
    mov [eax+8],pg2+7
    mov [eax+12],pg3+7
    mov edi,pg3+4092
    mov eax,00fff007h
    std
L3: stosd
    sub eax, 1000h
    jpe L3
    ...

很简单,对照刚刚的页目录表与页表结构看。
图片

代码前五行表示,页目录表的前 4 个页目录项,分别指向 4 个页表。比如页目录项中的第一项 [eax] 被赋值为 pg0+7,也就是 0x00001007。

根据页目录项的格式,表示页表地址为 0x1000,页属性为 0x07。

其中页属性的 0x07 用二进制表示为 111,表示该页存在(P=1)、用户可读写(RW=1)、特权为用户态(US=1)。

后面几行代码表示,填充 4 个页表的每一项,一共 4*1024=4096 项,依次映射到内存的前 16MB 空间。

画出图就是这个样子,眼熟吧,之前我们就和这张图“见过面”。

图片

看,最终的效果就是,经过这套分页机制,线性地址将恰好和最终转换的物理地址一样

现在只有四个页目录项,也就是将前 16M 的线性地址空间,与 16M 的物理地址空间一一对应起来了。

图片

地址的转换

关于地址,我们已经见过了好多词,包括逻辑地址线性地址物理地址,以及这一讲中没出现的,你可能在很多地方看到过的虚拟地址。而这些地址后面加上空间两个字,似乎又成为了一个新词,比如线性地址空间物理地址空间虚拟地址空间等。

好了,我猜这一串词放一起,你目前可能有点晕头转向了,没关系,是时候展开一波讨论,将这块的内容梳理一番了,且听我说。

Intel 体系结构的内存管理可以分成两大部分,也就是标题中的两板斧,分段分页

分段机制在之前几回已经讨论过多次了,其目的是为每个程序或任务提供单独的代码段(cs)、数据段(ds)、栈段(ss),使其不会相互干扰。

分页机制是这一讲的内容,开机后分页机制默认是关闭状态,需要我们手动开启,并且设置好页目录表(PDE)和页表(PTE)。其目的在于可以按需使用物理内存,同时也可以在多任务时起到隔离的作用。

在 Intel 的保护模式下,分段机制是没有开启和关闭一说的,它必须存在,而分页机制是可以选择开启或关闭的。所以如果有人和你说,他实现了一个没有分段机制的操作系统,那这个人一定是个外行。

再说说那些地址:

  • 逻辑地址:我们程序员写代码时给出的地址叫逻辑地址,其中包含段选择子和偏移地址两部分。
  • 线性地址:通过分段机制,将逻辑地址转换后的地址,叫做线性地址。而这个线性地址是有个范围的,这个范围就叫做线性地址空间,32 位模式下,线性地址空间就是 4G。
  • 物理地址:就是真正在内存中的地址,它也是有范围的,叫做物理地址空间。那这个范围的大小,就取决于你的内存有多大了。
  • 虚拟地址:如果没有开启分页机制,那么线性地址就和物理地址是一一对应的,可以理解为两者相等。如果开启了分页机制,那么线性地址将被视为虚拟地址,这个虚拟地址将会通过分页机制的转换,最终转换成物理地址。

有关逻辑地址到线性地址再到物理地址的转换,详细的过程可以阅读 Intel 手册 Volume 3 Chapter 3 Protected-Mode Memory Management。

图片

不过实际上,我本人是不喜欢虚拟地址这个叫法的,因为它在 Intel 标准手册上出现的次数很少,我觉得知道逻辑地址、线性地址、物理地址这三个概念就够了,逻辑地址是程序员给出的,经过分段机制转换后变成线性地址,然后再经过分页机制转换后变成物理地址,就这么简单。

总结时刻

这一讲我们看到了 CPU 通过分段和分页的手段对内存进行了最底层的管理,供操作系统使用。同时,我们讲述了由于分段和分页的存在,内存地址衍生出的四个不同名称,你可还记得?

死记硬背概念只会让你很痛苦,我们再看一下这张流程图,把这个过程看明白,也有助于你理解各种地址。

图片

好,我是闪客。欲知后事如何,且听下回分解。