x86保护模式下的内存管理

188 阅读6分钟

x86保护模式下的内存管理

相关概念

在了解内存管理机制前先了解几个概念,方便后续阅读。

内存寻址

内存是指一组有序字节组成的数组,每个字节有唯一的内存地址。内存寻址则是指对储存在内存中的数据地址进行定位。对于x86 CPU指令,一条指令主要由操作码操作数组成。操作数可以位于寄存器中,也可以在内存中。如果我们要访问内存中的操作数,就需要进行内存寻址。而内存寻址也有很多不同的寻址方案。

段寻址

为了进行内存寻址,80x86(注:简单理解这就是8086的增强型,不是8086)使用了段(Segment)的寻址技术。这种技术将线性的内存区域划分成一个个段,对每个段内的数据进行寻址就需要使用段的起始地址(即段地址)段内偏移地址两部分构成。段地址由16为段选择符指定,段内偏移地址由32位值指定,因此段内地址可以是0 ~ 4GB,一个段最大长度可达4GB。在程序中,段地址和段内偏移地址构成一个唯一确定的逻辑地址

段选择符

80x86为段部分提供了6个存放段选择符的寄存器:

  • CS、DS、ES、SS、FS和GS

其中,CS寄存器总是用于寻址代码段,SS寄存器专门用于堆栈段。

在任意时刻内CS内的段称为当前代码段。EIP寄存器中包含了下一条要执行指令的段内偏移地址。

在任意时刻内SS内的段成为当前堆栈段。栈顶由ESP寄存器指定,ESP指向栈顶元素,栈底存在EBP寄存器内,由于压栈操作是由高地址压向低地址,因此push操作先减减再赋值。

另外四个段寄存器是通用段寄存器,当指令中没有指定所操作数据的段时,那么DS将是默认的数据段寄存器

地址变换

一个完整的内存管理系统通常包含两个关键部分:内存保护和地址变换。内存保护比较好理解,就是保护一个任务的内存不被另一个任务访问修改,操作系统的内存不被普通任务访问和修改。而地址变换是指逻辑地址如何映射到物理地址。因此在地址变换的过程中我们可以实现内存保护,即不让某些特定的物理地址不被任意逻辑地址映射。

内存管理机制

内存中的物理地址是字节为单位的线性数组,每个字节具有一个唯一的物理地址。而程序中的地址是由段地址和段内偏移地址构成的逻辑地址。这种逻辑地址并不能直接访问物理内存,而是需要使用地址变换机制将它变换或映射到物理内存上。内存管理机制即用于将逻辑地址转换为物理内存地址。

为了减少地址映射所需要的信息,变换通常都以内存块为单位进行操作,目前广泛存在运用的技术有分段机制和分页机制,通过这两种技术对内存进行管理。

image-20230720163036206.png

分段机制

分段提供了两种机制:

  • 隔离代码、数据和堆栈,多个程序可以运行在同一个处理器而不会相互干扰。
  • 把处理器可寻址的线性地址空间划分为较小的称为段的受保护地址空间区域。段可以用来存放代码、数据、堆栈,或者一些系统数据结构(TSS和LDT)。在多个程序运行的情况下,每个程序可以分配各自的一套段,处理器可以加强这些段的限制管理。

为了方便控制,每个段必须有一些相对应的配套信息,如段的大小、访问权限、特权级、类型、基址等信息,这些信息由一个数据结构包含,称为段描述符。每个段描述符被包含在段描述符表中,如全局描述符表GDT,这个描述符表会有一个特定的寄存器保存,GDT的寄存器是GDTR。段描述符在段描述表中的偏移量由上文中的段选择符提供,因此,程序只需要再提供段内偏移地址就能形成可处理的线性物理地址空间地址。

分页机制

分页机制主要用于虚拟储存技术,可以让编程人员产生内存空间比实际物理内存空间大的错觉。其实现原理主要是由小块的物理内存和大容量外部储存实现,其实就是我们现在内存+硬盘的结构,将硬盘中的空间虚拟化成内存。分页机制开启时,每个段被划分成通常是4KB大小的页,页面被储存在物理内存或硬盘中,操作系统通过维护页表和页目录来确定页面。当程序要访问线性地址时,处理器会通过页目录和页表将逻辑地址映射成物理地址再访问。当被访问的页面不在内存中时,会产生一个中断,然后处理器执行相应的中断处理程序,将硬盘中的页读入或置换入内存中再访问。

分页机制会使用大小固定的内存块,分段机制使用大小可变的内存块。因此,两种方式各有优劣。

保护

80x86支持两种保护:

  • 任务间的保护

    给每个任务各自的段表和页表,这样每个任务之间就会映射到不同的物理区域,因此,切换任务时,任务之间表的切换就是关键部分,在x86架构下,intel提供TSS方式实现任务切换,也可以自己编程代码实现。TSS是intel为了方便操作系统管理进程而加入的一种结构,用法也很简单。TSS是一个段,即一块内存,这里保存要切换的进程的cpu信息,包括各种寄存器的值、局部描述表ldt的段选择子等,切换时cpu会将这段内容存进各自对应的寄存器,然后就完成了切换。按照intel最初的设计,每个任务或者进程都应该设置一个TSS段,任务切换时直接将对应的TSS段的内存加载到CPU就行了。但是后来发现这种设计会带来过多的系统开销,每次切换都要将所有的寄存器更新,需要数百个指令周期,因此主流的操作系统均不使用这种方法。linux采取的方法是绕开TSS段进行任务切换,每个CPU仅设置一个TSS段,仅使用esp0和iomap,采用软件方法切换寄存器,节省了开销。

  • 特权级保护

    一个任务中定义了4个特权级,由0 ~ 3表示,0最高,3最低。我们知道,处理器从CS中取代码执行,当程序企图访问某个段时,会将当前特权级和段特权级比较,确定访问许可,高的可以访问低的。