Linux 0.11 内存管理-寻址 Linux 0.11 MMU Addressing Machanism

Linux 0.11 内存管理-寻址 Linux 0.11 MMU Addressing Machanism

什么是内存管理? 内存管理,是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源。

在此之下还有一个关键基础内容需要了解,也就是寻址

寻址, 是指计算机处理器通过某种特定的规则访问内存或存储器中的数据.

在操作系统中,”寻址”通常指的是处理器或CPU(中央处理器)计算有效地址的过程。以便内存管理或其他程序从内存中读取或写入数据。

在Linux0.11中, 内存做了分段和分页处理, 由此形成了两种不同的寻址方式.

下面我会花大量篇幅先把这两种的寻址原理介绍清楚。

下一期再介绍MMU(Memory Management Unit)。

寻址

当程序需要访问内存时,操作系统负责将虚拟地址转换为物理地址,这个过程称为地址寻址。通过地址寻址,操作系统能够确保不同程序或进程的内存空间相互隔离,并为每个程序提供独立的虚拟地址空间。这有助于提高系统的安全性和稳定性,同时允许程序使用比实际物理内存更大的虚拟内存空间。

寻址相当于是程序的访问内存的遵循的规则,程序遵循这个规律,才能正确访问内存。

用函数表示逻辑地址到物理地址的关系, 见下方

                                            内存的物理地址=f(逻辑地址)

f(x)在这里是一个寻址规则, 这个规则也称作变换.

操作系统中一次完整的寻址, 经过段变换(Segment Translation)和页变换(Page Translation). 两种变换的示意图如下

段变换(Segment Translation): 将逻辑地址(虚拟地址)变换为线性地址,如果没有开启分页机制,则变换为物理地址。

页变换(Page Translation) : 将线性地址变换为物理地址

一个逻辑地址(LOGICAL ADDRESS)通过一次完整的寻址经过两次变换段变换和页变换. 如果页变换没有开启, 则线性地址(LINEAR ADDRESS)经过段变换的地址直接映射到物理地址(PHYSICAL ADDRESS).

所以可以总结, 寻址情况有两种

  1. 经过一次段变换后实现寻址

  2. 经过段变换和页变换后实现寻址

段变换寻址

段变换寻址也称在保护模式下寻址。

内存是分段的,通过一种机制如何寻址到某一个内存段。

理论模型呈现

在段变换中, 逻辑地址中选择子(Selector)会指向描述符表(Descriptor Table)的段描述符(Segment Descriptor), 再根据段描述符中记录的基地址与逻辑地址的偏移值(OFFSET)来得到新的地址.

如果分页机制没有开启, 那么新的地址就是得到物理地址. 否则, 得到的新地址是一个线性地址, 需经过页变换才能得到物理地址. 所述如下图

这里有几个概念需要澄清一下

描述符表(Descriptor Table), 是一个数据结构里面设置了多个描述符组成的表单,通常是GDT或者LDT.

选择子(Selector), 是用于指向描述符表中某一个描述符,其实也是一个偏移值, 所以称作选择子. GDTR 确定了基地址, Selector 确定了偏移值。确定GDT中的某一个描述符。

段描述符(Segment Descriptor), 用于指向一段已经划分好的内存基地址, 并且还描述了这个内存段的限长等其他信息.

总之,这里的Selector是GDT的一个偏移值,OFFSET线性地址段的偏移值。所以逻辑地址(Logical address)的构成是两个偏移地址的组合(selector, offset)。

最终还是符合一个寻址原则:基地址+偏移地址,并且由前一个地址推导出后面一个地址。

代码呈现

根据上面的一段论述, 现在已经确认好了模型

再来看看代码中是如何实现的?

代码在Setup.s中体现

1
2
3
4
5
end_move:
mov $SETUPSEG, %ax # right, forgot this at first. didn't work :-)
mov %ax, %ds
lidt idt_48 # load idt with 0,0
lgdt gdt_48 # load gdt with whatever appropriate
1
2
3
4
gdt_48:
.word 0x800 # gdt limit=2048, 256 GDT entries
.word 512+gdt, 0x9 # gdt base = 0X9xxxx,
# 512+gdt is the real gdt after setup is moved to 0x9020 * 0x10
1
2
3
4
5
6
7
8
9
10
11
12
gdt:
.word 0,0,0,0 # dummy

.word 0x07FF # 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 # base address=0
.word 0x9A00 # code read/exec
.word 0x00C0 # granularity=4096, 386

.word 0x07FF # 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 # base address=0
.word 0x9200 # data read/write
.word 0x00C0 # granularity=4096, 386

上方的几段代码执行后, 完成了工作如下图所示

lgdt命令将gdt_48 的数据结构加载到GDTR中, GDTR是一个位于CPU内的寄存器。 48bit 和 数据结构是取决于GDTR的大小和结构. ( 数据结构为 32bit 基地址 | 16bit 段限长。) 限长设置0x800 .

GDT中包含 256 GDT entries, 但是只是用了3个entries .  第一项 为 空 ; 第二项为 code ; 第三项为data。

gdt 变量是一个地址。512+gdt 是移动后的gdt基地址.

在完成上述的初始化配置后, CPU的寻址过程就是 通过读取GDTR的高32bit找到GDT, 根据GDT中的描述符GD entry 的基地址找到对应的内存段, 在结合offset偏移值, 最后获取到数据. 如下图所示.

Segment Descriptor 段描述符是一个64bit数据结构的描述符,数据结构定义如下

为什么需要段机制(保护模式)?

实模式寻址VS保护模式寻址

从上方描述,“索引值”包含 内存段的基地址 ,段的最大长度值和段的访问级别。因为有这几个参数,内存访问都遵循着这个规律,所以保护了其他的内存段,程序访问时不会超过其他内存边界,不会影响到其他的内存。另外还有访问时的权限,说明只有特定的程序访问特定的内存段,所以也起到了保护作用。

保护模式寻址 需要通过一个段描述符(设置了寻址的属性),作为映射到物理地址的一个中间手续。

举一个不恰当的例子。就像用户作为CPU,要想存钱和取钱需要经过银行柜台的认证和许可才可以取钱。这里的银行柜台就是中间手续.

银行柜台能够对银行中的钱起到保护作用;相当于描述符能够对内存起到保护作用。

银行柜台会告诉你不同身份的用户可以存取多少钱,也就是描述符会告诉你内存段的限长。银行柜台再你取钱时会确定你的身份信息, 如果另外一个人来取钱,肯定是不允许的。对于不同身份不同级别的程序,对内存的操作权限也是不同的。

相对于实模式,连银行柜台都没有,可以直接获取金库的钱。所以这很不安全。

页变换寻址

为什么需要分页机制?

我们看到,分段管理机制已经提供了很好的保护机制,那为什么还要加上分页管理机制呢?其实它的主要目的在于实现虚拟存储器(虚拟内存)。线性地址中任意一个页都能映射到物理地址中的任何一个页,这无疑使得内存管理变得相当灵活。

虚拟内存允许程序使用比实际物理内存更大的内存空间,同时具有以下特点:

  1. 虚拟内存空间的大小可以超出物理内存空间大小。
  2. 通过操作系统的管理和调用,虚拟内存和物理内存之间的映射关系可以变化。

因此,虚拟内存提供了一种灵活的内存管理方式,使得不同的进程之间彼此独立,使得某一个进程在执行时看起来拥有独立的地址空间,避免了不同进程的地址冲突问题。而线性地址空间则是实现虚拟内存机制的方式,它为程序提供了一种连续的地址空间,使得程序看起来具有独立的地址空间,与其他的程序和操作系统本身隔离开来。

理论模型呈现

页变换是将线性地址转化为物理地址,将线性地址空间转化为物理地址空间

线性地址来源是由逻辑地址经过段变换后得到的

一个线性地址组成 如下

DIR | PAGE | OFFSET

DIR 包含页目录表项(PDT entry)的地址,也就是页目录表的偏移地址

PAGE 包含页表项(PT entry)的地址,也就是页表的偏移地址

OFFSET 包含内存页(4K)的偏移地址

这张图表示如何通过线性地址寻址到物理地址的. 首先将线性地址拆分成三块分别是DIR, PAGE, OFFSET.

CR3是属于CPU中的寄存器包含着某个页目录表(PDT)的基地址, 而DIR则可以进一步地确认PDT中具体使用到哪个页目录项(PD entry)

页目录项(PD entry)包含着某个页表的基地址, 而线性地址中的PAGE则可以进一步地确定具体是哪个页表项

页表项中包含着某个内存分页(PAGE FRAME或者称为页帧)的基地址, 而OFFSET偏移值则可以进一步地确定内存分页中具体的内存地址.

总的来看, 不管是段变换还是页变换,寻址依旧是遵守一个原则就是, 基地址+ 偏移值,并且由前面一个推导出后面一个。

代码呈现

说完了分页机制寻址模型, 接下来看看代码是如何实现的

关于分页的工作主要在代码文件head.s中, 整个代码文件主要完成的工作有:

head.s做了什么工作

  1. 从这里开始,内核完全都是在保护模式下运行了

  2. 加载各个数据段寄存器,重新设置中断描述符表 idt

  3. 然后重新设置全局描述符表 gdt

  4. 设置管理内存的分页处理机制

  5. 紧随后面放置共可寻址 16MB 内存的 4 个页表,并分别设置它们的表项

从以上来看, 与分页机制相关的工作是3,4,5几个步骤. 工作内容3其实已经在分段机制阶段说明了, 不再赘述. 4和5工作内容详细展开来讲讲. 下面关于页变换实现的步骤陈述如下

第一步: 页目录表和页表初始化

为1个页目录表和 4 个页表(为了能够索引 16MB 内存空间)在内核中申请空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
....
pg_dir:
....
/*
* I put the kernel page tables right after the page directory,
* using 4 of them to span 16 Mb of physical memory. People with
* more than 16MB will have to expand this.
*/
.org 0x1000
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000

第二步: 清空页目录表和 4 个页表的内容。如代码 201~204 行所示

第三步: 设置页目录表项映射4个页表

内核中我们设置了4个页表, 所以我们需要在页目录表中设置4个页目录项来索引它们. 如代码205 ~ 208 行所示. 第一个页表所在的线性地址为0x1000, 赋予第一个页表的属性为0x7, 表示该页存在, 可读可写. 由于每个页目录项大小为4B, 故pg_dir+4可以跳转到下一个页目录项.

第四步: 对页表中的每项映射内存分页地址

设置每个页表中的页表项。每个页表大小为 4*1024B(标识物理页号范围:0-0xfff),如代码 209~214 行所示。pg3+4092 表示从最后一页的最后一个页表项开始填起,填写的内容为该页表项所映射的物理内存页号以及该页的属性0x7,将循环判断变量 eax 减去 4K,继续设置下一页表项,直至零,表示已将 4096 个页表项填写完毕,即 16M 内存分页完毕。

第五步: 设置CR3映射到页目录表和设置CR0开启分页机制

设置页目录表的起始地址. 将页目录表起始地址赋予CR3寄存器, 如代码216-217行所示. 开启分页机制. 如代码218 - 221 行所示, 将CR0寄存器的最高位设置为1来开启分页机制.

总结:经过以上几个步骤之后,配置好了以下几个方面:
1.CR3指向页目录表
2.页目录表中的页目录项指向4个页表
3.页表中的页表项指向内存寻址空间(以4K分隔)

运行和调试

线性地址到物理地址

在分页模式下,线性地址到物理地址的过程会涉及:

CR3 – Page DIR – Page DIR Entry – Page Table – Page Table Entry – Frame Page (physical address)

主要是想查看相关变量和寄存器,看下是否符合理论并且能够加深理解

通过Bochs 虚拟机,并输入相关指令可以查看寄存器和变量

  1. 查看CR3寄存器

    CR3寄存器中设置了页目录的基地址

    1. 查看Page DIR

      按照理论,CR3的寄存器地址就是Page DIR

      通过上图的指令,可以查看几个页目录表的地址内容。从代码里面可以看到主要设置了4个页目录表。基本上与上方的显示结果对应。

      第一个Page DIR : 0x1027 = 0x1000 + 0x27

      0x1000是Page Table的地址, 0x27 是关于设置页的属性。

    2. 查看Page Table

      通过相同的方法查看内存地址0x1000得到的值是0x800000.这正好是一个FRAME PAGE的地址。内存寻址空间的最大在0xFFFFFF

总结

  1. 通过调试查看线性地址到物理地址寻址过程相关的地址。对代码有了 更深的理解。回顾一下线性地址组成

                                                DIR | PAGE | OFFSET

    LINEAR ADDRESS: 0x0(+DIR) – 0x1000(+PAGE) – 0x800000(+offset)
    假设线性地址的偏移地址都是零,用(0,0,0)来表示。所以这个线性地址得到的最终物理地址是0x800000。

    如果线性地址LINEAR ADDRESS: (1,14,25),那么最终计算结果也就前一个地址加上该位置的偏移地址后,最终推导出物理地址。而线性地址中的每一位其实是与之相对应容器的偏移值。DIR是PAGE DIR的偏移值;PAGE是PAGE TABLE的偏移值; OFFSET是FRAME PAGE的偏移值。

逻辑地址(虚拟地址)到线性地址

在分段模式下,逻辑地址到线性地址的过程会涉及:

GDTR – Descriptor Table – Segment Descriptor – 线性地址

  1. 查看GDTR

    在bochs中输入sreg可以查看到gdtr保存的地址, 这个0x5cb8就是指向Descriptor Table的地址

  1. 查看Descriptor Table与Segment Descriptor

    通过x查看某内存地址的值. 0x5cb8是gdt的基地址, +8 可以查看下一个Descriptor , 一次查看几个Descriptors, 验证一下是否与书上, 代码上的是否一致.

    移动后的GDT,如下图所示:

查看相同地址,在bochs下的显示结果:

查看相同地址,在qemu下的显示结果:

查看理论上移动后的GDT基地址:

分段寻址参考:

总结

  1. 因为Segment descriptor的数据结构是64bit(8B), 所以base+8可以找到下一个descriptor

  2. 通过qemu和bochs打印的descriptor会不一样,但是和理论的大差不差。

  3. descriptor最后寻址到的是线性地址。如果没有开启分页模式那实际上就是物理地址。

参考资料

Linux 内核完全注释

Linux 内核设计的艺术

segmentation和保护模式(二)

深入理解Linux内核虚拟内存原理与实现 - 掘金

【Linux 0.11】第十三章 内存管理 - 掘金

I386 Datasheet | Intel - Datasheetspdf.com


Linux 0.11 内存管理-寻址 Linux 0.11 MMU Addressing Machanism
https://pans0ul.github.io/2023/11/23/Linux-0-11-MMU-Addressing-Machanism/
Author
pans0ul
Posted on
November 23, 2023
Licensed under