内存寻址初探

前言

前一篇博客,我简单了解了一下了Linux内核中虚拟文件系统的内容。在这里,我将学习内存寻址的相关内容。

实模式和保护模式

在引进分段和分页的概念前,我这里简单说明一下实模式和保护模式。

实模式

实模式出现于早期8088CPU时期。当时由于CPU的性能有限,一共只有20位地址线(所以地址空间只有1MB),以及8个16位的通用寄存器,以及4个16位的段寄存器。所以为了能够通过这些16位的寄存器去构成20位的主存地址,必须采取一种特殊的方式。当某个指令想要访问某个内存地址时,它通常需要用下面的这种格式来表示:

(段基址:段偏移量)

其中第一个字段是段基址,它的值是由段寄存器提供的(一般来说,段寄存器有6种,分别为cs,ds,ss,es,fs,gs,这几种段寄存器都有自己的特殊意义,这里不做介绍)。

第二字段是段内偏移量,代表你要访问的这个内存地址距离这个段基址的偏移。它的值就是由通用寄存器来提供的,所以也是16位。那么两个16位的值如何组合成一个20位的地址呢?CPU采用的方式是把段寄存器所提供的段基址先向左移4位。这样就变成了一个20位的值,然后再与段偏移量相加。

即:

物理地址 = 段基址<<4 + 段内偏移
可以参照下图:

由上面的介绍可见,实模式的"实"更多地体现在其地址是真实的物理地址。

保护模式

那么实模式有什么缺陷呢?首先就是不安全,程序可以随意访问任何物理地址,就像逛菜市场一样,无拘无束。为了不让某些非法分子到处瞎转悠,保护模式孕育而生!

在 8086 的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示。

IA32的三种地址

  • 逻辑地址:

机器语言指令仍用这种地址指定一个操作数的地址或一条指令的地址。 这种寻址方式在Intel的分段结构中表现得尤为具体,它使得MS-DOS或Windows程序员把程序分为若干段。每个逻辑地址都由一个段和偏移量组成。

  • 线性地址:

线性地址是一个32位的无符号整数,可以表达高达4GB的地址。通常用16进制表示线性地址,其取值范围为0x00000000~0xffffffff。

  • 物理地址:

也就是内存单元的实际地址,用于芯片级内存单元寻址。 物理地址也由32位无符号整数表示。

MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。

所以,总结来说,保护模式就是通过MMU硬件电路,实现将我们的逻辑地址映射到物理地址的一种模式。

如何开启保护模式呢?当然也没那么神秘,就是将 CR0 控制寄存器中的标志位打开就好了。除了打开开关,还需要准备好保护模式所需要的一些数据,如上面所说的全局描述符表,然后直接跳往某个构建好的段选择子,就完成了实模式向保护模式的跳跃。

实际上,在计算机上面,实模式存在的时间非常之短,所以一般我们是感觉不到它的存在的。CPU复位(reset)或加电(power on)的时候就是以实模式启动。

保护模式的具体实现

上面说到,保护模式主要是通过硬件电路MMU实现的,这里我们详细探究一下实现思路。

分段

在实模式和保护模式中都有分段这个概念,但其实大有不同。在具体介绍分段之前,我们来看看几个概念。

IA32的段寄存器

IA32中有六个16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)。

6个寄存器3个有专门的用途:

  1. cs:代码端寄存器,指向包含程序指令的段
  2. ss:栈段寄存器,指向包含当前程序栈的段
  3. ds:数据段寄存器,指向包含静态数据或者全局数据段

其实,段寄存器的唯一目的是存放段选择符(段标识符),具体结构见下图。

段标识符

逻辑地址由16位段标识符和一个32位段内相对地址偏移组成。段标识符是一个16位长的字段,称为段选择符。具体数据结构如下:

  • TI:Table Indicator,当TI=0表示查找GDT表,TI=1则查找LDT表

  • INDEX:在GDT表或LDT表中的索引号,查找哪个段描述符

  • 请求特权级字段RPL提供了段保护信息。

这里特别说明cs寄存器还有一个很重要的功能:他含有一个两位的字段,用以指明cpu当前特权级。值0代表最高优先级,而值3代表最低优先级。Linux指用0和3级,分别称之为内核态和用户态。

段描述符

所谓描述符(Descriptor),就是描述段的属性的一个8字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度等等,而在保护模式下则复杂一些。IA32将它们结合在一起用一个8字节的数表示,称为描述符 。

段描述符表

各种各样的用户描述符和系统描述符,都放在对应的全局描述符表、局部描述符表和中断描述符表中。描述符表(即段表)定义了IA32系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。

1. 全局描述符表(GDT)

全局描述符表GDT(Global Descriptor Table),除了任务门,中断门和陷阱门描述符外,包含着系统中所有任务都共用的那些段的描述符。 它的第一个8字节位置没有使用。

2. 中断描述符表IDT(Interrupt Descriptor Table)

中断描述符表IDT(Interrupt Descriptor Table),包含256个门描述符。IDT中只能包含任务门、中断门和陷阱门描述符,虽然IDT表最长也可以为64K字节,但只能存取2K字节以内的描述符,即256个描述符,这个数字是为了和8086保持兼容。

3. 局部描述符表(LDT)

局部描述符表LDT(local Descriptor Table),包含了与一个给定任务有关的描述符,每个任务各自有一个的LDT。 有了LDT,就可以使给定任务的代码、 数据与别的任务相隔离。每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。

段描述符放在全局描述符表GDT或局部描述符表LDT中。GDT通常只有一个,而进程如果需要创建附加的段,可以创建自己的LDT,GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正在被使用的ldt地址和大小放在ldtr控制寄存器中。

所以,分段机制总结起来就是,我们的逻辑地址是由段标识符和一个指定段内相对地址的偏移量构成。而这个段标识符中的index是指向我们的GDT或者LDT中对应的文件描述符,文件描述符中有对应段的基地址,通过这个段基地址加上我们的段内偏移,便构成了我们的逻辑地址。

在Linux中,所有段都从0x00000000开始,所以Linux下的逻辑地址和虚拟地址的值其实就是一样的。

分页

分页机制在分段机制之后进行,完成线性地址到物理地址转换的过程。如果不允许分页,段机制转换的线性地址就是物理地址;如果允许分页机制,那么线性地址就需要通过分页机制找到物理地址。

线性地址空间被划分为若干块大小相等的片,称为页,并把各页编号,同样的,物理地址也被划分为若干块大小相等的片。

内核可以指定一个页的存取权限和物理地址,如果请求的访问类型和页的存取权限不匹配,会产生一个缺页异常。

cr0寄存器中的PG位用来指明是否启用分页机制,当PG=0时,线性地址就被解释为物理地址了。

把线性地址映射到物理地址的数据结构称为页表(page table),页表存放在主存中。

一个32位的线性地址被分为三个部分:

线性地址到物理地址的转换分两步进行,第一步是根据地址中的[目录]部分 到页目录中查询获得页表的地址,第二部则是根据地址中的[页表]部分到页表中查询页的地址,查询到页的地址后,加上地址中的[offset]即可得到线性地址对应的物理地址了。如下图:

每个活动进程都必须会分配得到一个页目录,但是页表则没有必要马上装进主存,当进程实际需要一个页表时才给该页表分配RAM,即,当真正访问一个物理地址的时候,才建立该页(线性地址)和物理页框(物理地址)的映射,这样会更有效率。

正在使用的页目录存放在cr3寄存器中,线性地址中的Directory字段指出目录表中的目录项,而目录项中存放有页表的地址;线性地址中的Table字段又指出页表中的表项,而表项中则含有页框的物理地址;最后,由Offset字段指定页内的偏移。由于offset字段为12位,因此一个页的大小就是2^12=4096字节(4KB)

页目录项和页表项是同样的结构:

如图所示,其中位31~12含有物理地址的高20位,用于定位物理地址空间中一个页面(也称为页帧)的物理基地址。表项的低12位含有页属性信息。

P–位0是存在(Present)标志,用于指明表项对地址转换是否有效。P=1表示有效;P=0表示无效。在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。如果P=0,那么除表示表项无效外,其余位可供程序自由使用,如图4-18b所示。例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。

R/W–位1是读/写(Read/Write)标志。如果等于1,表示页面可以被读、写或执行。如果为0,表示页面只读或可执行。当处理器运行在超级用户特权级(级别0、1或2)时,则R/W位不起作用。页目录项中的R/W位对其所映射的所有页面起作用。

U/S–位2是用户/超级用户(User/Supervisor)标志。如果为1,那么运行在任何特权级上的程序都可以访问该页面。如果为0,那么页面只能被运行在超级用户特权级(0、1或2)上的程序访问。页目录项中的U/S位对其所映射的所有页面起作用。

A–位5是已访问(Accessed)标志。当处理器访问页表项映射的页面时,页表表项的这个标志就会被置为1。当处理器访问页目录表项映射的任何页面时,页目录表项的这个标志就会被置为1。处理器只负责设置该标志,操作系统可通过定期地复位该标志来统计页面的使用情况。

D–位6是页面已被修改(Dirty)标志。当处理器对一个页面执行写操作时,就会设置对应页表表项的D标志。处理器并不会修改页目录项中的D标志。

AVL–该字段保留专供程序使用。处理器不会修改这几位,以后的升级处理器也不会。

参考

https://www.cnblogs.com/linhaostudy/p/9178436.html#_label1_1
https://www.jianshu.com/p/97871c14aaf2
https://www.sunxiaokong.xyz/2020-06-29/lzx-baby-linux-addressing/#Linux中的分页
https://blog.csdn.net/gatieme/article/details/52402967
https://www.jianshu.com/p/51c2286a6268

原文地址:https://www.cnblogs.com/T1e9u/p/13922854.html