IA-32 x86保护模式(Protected mode)

IA-32 x86 Protected mode

Real-address Mode

实模式简称Real mode,这就要从我们x86的ancestor 8086处理器说起,这个时候我们使用的地址都是物理地址

>>>为什么会有段寄存器cs ds es ss?

  • 8086处理器面临的第一个问题就是20位地址总线和16bit寄存器的问题:解决这个问题是把段寄存器左移4个bit位,形成20bit段地址,再加上16位的偏移地址进行20位寻址,这也决定了段地址必须是按16字节对齐的,因为16位寄存器,所以每个段最大限长为64KB,同时同一个物理地址对应着多个逻辑地址

  • 第二个问题就是程序重定位的问题,我们的指令是由操作码和操作数构成(小部分只有操作码),如果我们指令里的操作数是一个绝对地址,我们程序加载的位置是不确定的,所以我们代码和数据的地址也不是确定的,所以绝对地址是不可行的,处理器采用了内存分段机制,这种分段是逻辑上的分段,并不是物理上的分段,段地址:偏移地址,我们指令里的操作数写相对于段地址的相对偏移地址,就可以正确重定位

物理地址(physicaladdress)=段值(segment) * 16 + 偏移(offset)

32位处理器之后我们给这个取了个名字叫实模式

实模式的问题

  1. 缺乏保护机制:实模式下没有保护机制,也没有内存管理单元(MMU)来保护内存免受非授权访问和操作系统进程之间的干扰。这使得操作系统和应用程序容易受到恶意代码的攻击。
  2. 缺乏多任务支持:实模式下,只能运行一个程序,并且程序必须在处理器的控制下运行,因为没有任何机制来切换到其他程序。这个限制意味着操作系统无法支持多任务处理,这限制了计算机的性能和效率。

Protected mode

保护模式是在80286(16位)引入,80386(32位)开始盛行

80386 32位地址总线最多支持4GB的物理内存,视为IA-32(Intel Architecture)架构的开始

开始了平坦内存模型(flat memory model),整个内存都在一个段里,即每个应用程序可以使用地址0~2^32-1来索引

CRO寄存器的最低位PE(Protection Enable)标志为1处理器进入保护模式

>>>怎么保护

对内存进行权限控制,那就应该会有权限信息的描述,段寄存器只有16位(其实是可见部分只有16位,还有80位是不可见,描述属性(16),基址(32),界限(32))而且只有几个,每一个程序都需要多个单独的描述信息,那就需要存放在内存里

分段机制

段描述符

就出现了段描述符(segment descriptor),8字节长的数据结构,用来描述一个段的位置、大小、访问控制和状态等信息,

image-20230420200038198

  • 32位段基地址,段起始位置
  • 20位的段边界,段边界的扩展最值,即最大扩展到多少或最小扩展到多少。扩展方向只有上下两种,栈和数据段代码段区别(DNA里的恐惧:segmentation fault)
  • G(Granularity)是粒度来描述段边界,G为0,段边界以字节为单位,为1(1B~1M),则以4k为单位(4K~4G)
  • S(System),S=0代表该描述符描述的是一个系统段,S=1代表该描述符描述的是代码段、数据段或堆栈段
  • DPL(Descriptor Privilege Level)描述符特权级0-3,0最高,高的可以访问低的,反之不行,比如操作系统的代码和数据会被放在比用户程序具有更高特权的段里,DPL表示访问这个段对当前特权级(CPL)的最低等级要求
  • TYPE和S段配合使用,段的访问权限或系统控制描述类型

CPU 硬件负责检测,操作系统提供异常处理程序

image-20230420195852680

描述信息这么乱是为了兼容80286

image-20230420200152426

段描述符表

系统中会有很多段描述符,代码段要占用一个段描述符、数据段和栈段等,多个内存段也要各自占用一个段描述符,cpu定的全局描述符表(Global Descriptor Table,GDT)局部描述符表LDT(Local descriptor table)是一个段描述符数组,里面存的就是这些描述符

全局描述符表(系统只有一个)的地址存放在GDTR寄存器里,局部描述符表在LDTR寄存器,表由操作系统维护,cpu硬件来使用

GDTR寄存器

48位

image-20230408155337790

32位线性地址描述全局描述符表在内存里的位置。16位表长度描述表的大小(为0时大小是1),byte为单位,所以最大2^16/8=8192个描述符

LDTR寄存器

局部描述符表寄存器LDTR表示当前任务的LDT在GDT中的索引,也是一个段选择子

段选择子

有了段描述符,段寄存器里面就不需要存段基地址,而是存的段选择子

image-20230408172110614

  • TI位代表要索引的段描述符表(table indicator),TI=0表示全局描述符表,TI=1表示局部描述符表。
  • 高13位是描述符索引,要选择的段描述符在TI所表示的段描述符表中的索引号
  • RPL(Requestor Privilege Level)请求特权级,表示我将以什么样的身份去访问,比如我现在cpl为0,我可以以rpl为3或0的身份去访问,之所以会有3的身份和我们有时候怕失误修改一个可修改的文件从而以只读权限打开该文件一样
  • (cs(ss)[cs和ss特权级一样]寄存器中的RPL字段表示当前特权级(Current Privilege Level,CPL)),和段描述符里的DPL配合cpu做检查

这样通过段选择子和GDTR,就可以定位到段描述符,找到段基地址,加上偏移找到线性地址,如果没有分页机制这样取出来的线性地址就是我们的物理地址

取出一次后会放入程序员不可见部分—段描述符缓冲寄存器( Descriptor Cache Registers ),下次会直接从这里面取,,只要往段寄存器中赋值, CPU 就会更新段描述符缓冲寄存器

image-20230420200014884

linux中的分段机制

RISC对分段支持有限,linux为了更好的移植性,简化了分段机制,在初始化时就把段描述符里的基地址设为0,段边界设置为0xfffff,每个段都能访问4G

分页机制

分段会造成一些问题

image-20230408194444921

  • 这样一个分段内存,如果此时BD程序停止,此时如果有一个程序F占3个内存块,虽然内存里由三个空闲内存,但是却不是连续的无法加载我们的程序F,时间长了会造成大量内存碎片,

  • 另一个问题是程序跑起来后有些代码和数据可能很久都不会用到,但是在分段下我们还必须加载到内存,造成内存浪费

  • 分段下我们没办法动态分配我们程序的内存,因为在分段下加载到内存的块大小是必须要提前计算好的

  • 物理内存太小时,程序就不能加载

在分段的前提上在加一层东西

分页把线性地址和物理地址空间都划分为页面,linux一般使用4k大小的页面,分页机制的主要目的是高效地利用内存,按页来组织和管理内存空间,把暂时不用的数据交换到空间较大的外部存储器(通常是硬盘)上(称为page out,换出),需要时再交换回来(称为page in,换进)同时,可以将逻辑上连续的线性地址映射的物理地址可以不连续,把内存碎片利用起来。现用现映射,不用不映射,所以从理论上,我们只需要8k的内存就可以跑一个程序,4k数据,4k代码,不断进行内存和硬盘的转换

操作系统的虚拟内存也是利用分页来实现的

CRO寄存器的PG为为1时开启分页

创建进程时,就会为这个进程创建页表,进程空间隔离主要因为每个进程都有一套相对独立的页表,映射过的物理内存会在页表里登记这个映射关系,进程的切换会伴随着页表的切换

image-20230420195713371

CR3寄存器便是用来记录当前任务的页表物理地址。

一级页表

只有一级页表的话需要有2^20次方个表项,表项里用4个字节存放页面的开始物理基地址,由于页时4k大小对齐,所以低12位始终为0,不用记录,低十二位用来存放一些权限信息

我们用线性地址的高20位去索引页表数组,再加上线性地址的低12位偏移就可以得到物理地址

一级页表的缺点是,2^20次方个4字节的表项需要4M的连续物理内存去存储,每个进程都有一个单独的页表,n个进程,4*nM的内存消耗太大

32位经典分页

二级页表

CR0的PG标志为1、CR4的PAE为0时,使用这个模式,在这个模式下,页表结构为两级,第一级称为页目录表(Page Directory Table),第二级称为页表(Page Table),最终属性的是由页目录项和页表项相与(PDE&PTE)来决定的
页目录存放在一个4K大小的页面上,存放1024个指向一个二级页表地址,二级页表大小也是一个页面,最多含有1K个4字节页表项(Page-Table Entry,PTE)

每个二级页表存放1024个物理页的地址,1024*4KB=4MB,1024个二级页表1024*4MB=4GB,满足4G寻址

目录项

image-20230420195930886

页表项

image-20230420195952065

查找过程

image-20230420191045242

  1. 通过CR3寄存器定位到页目录的起始地址,取线性地址的高10位x4作为索引选取页目录的目录项(由于一个目录项是4字节,所以x4)
  2. 那么根据页目录项中的页表基地址(高20位)和低12位为0,定位到二级页表
  3. 取线性地址的12位到21位(共10位)x4,作为索引选取二级页表的一个页表项
  4. 取出页表项的页表基地址
  5. 取线性地址的低12位作为页中偏移和上一步基地址相加得到物理地址

这个时候一想,4k大小的二级页表有1024个,还外加一个4k目录表这不比之前的4M还大4k?

这个时候我们分析下目录项的作用

  1. 页目录项的P位表示是否在物理内存,这就说明二级页表本身也可能被交换到虚拟内存中,只有一部分需要的二级页表被加载到物理内存,其余可以放到磁盘,目录项的如果P位为0会触发缺页异常,缺页异常调用内核中的缺页异常处理程序把需要的页面映射到物理内存
  2. 有了这个机制我们可以把页表分散到物理内存不同位置,而不是在一块连续的4M物理内存空间
  3. 4k的页目录是一直在物理内存上的

多级页表的坏处在于tlb不命中处罚加剧,二级页表就需要两次访存,一次用于页目录,另一次用于 PTE 本身

这种地址转换都是硬件来自动完成的,操作系统来处理缺页

64位linux内存分页

目前64位linux基本上都采用四级分页模型

  • PGD:page Global directory(47-39), 页全局目录
  • PUD:Page Upper Directory(38-30),页上级目录
  • PMD:page middle directory(29-21),页中间目录
  • PTE:page table entry(20-12),页表项

image-20230826213151091