【Linux 0.12】80x86保护模式——分段和分页
什么是逻辑地址?
虚拟地址==逻辑地址。是程序由段选择符+段内偏移组成的地址。逻辑地址选哟分段地址的变化处理之后才能得到相应的物理内存地址。
书中提到的 分段提供了代码、数据的隔离,是否可以理解为 在IDA看到的,整个线性的结构,其实代码和数据被加载到了不同的段中?
说来话长。
80x86保护模式及其编程
保护模式内存寻址
什么是保护模式 和 实模式?
个人的简单理解:实模式使用的是真实的物理地址,20位地址线。
地址保护模式,使得处理器能够对内存以及其他外设做 硬件级别的保护设置。在保护模式下,程序员指定逻辑地址,用16位段寄存器和32位寄存器来表示。但此时段寄存器保存的值不再是段基址,而是一个Selector(段选择符)。操作系统根据Selector的值来算出段基地址,再和偏移地址相加得到实际的物理地址。
8086中有4个16bit的段寄存器 CS DS SS ES,保存的是段基地址
80386中,有6个16bit的段寄存,增加了FS,GS,不再存放段基地址,而是存放段选择符,因为16bit寄存器无法存放32位的段基地址。段基地址保存在描述符表中(Description Table),比如GDT和LDT。GDTR和LDTR就是保存GDT和LDT的地址和大小。
内存寻址
80x86 CPU 是小端序处理器。(Of Course.)
为了实现内存寻址,80x86 使用了一种称为段(Segment)的寻址技术。该技术把内存空间分割成一个或多个称为段的线性区域,从而对内存中一个数据对象的寻址就需要使用一个段的起始地址 + 段内偏移。
段地址使用16bit段选择符指定,其中14bit可以选择2^14=16384个段。
段内偏移为32bit,因此段内地址可以是0~4GB。
16bit的段+32bit的偏移构成了48bit的地址,或称长指针,称为逻辑地址/虚拟地址
80x86为段部分提供了6个存放段选择符的段寄存器:CS,DS,ES,SS,FS,GS。其中CS总用于寻址代码段,SS总用于堆栈段。EIP寄存器包含当前代码段下一条要执行指令的段内偏移地址,所以下一条指令的地址可以表示为 CS:[EIP]。
同理,SS:[ESP] 指向栈顶。
当指令中没有指令操作数据的段时,默认使用DS段。
地址变换
保护措施(保护模式)可以防止一个任务访问另一个任务或者操作系统的内存区域。而地址变换使得操作系统在给任务分配内存时更具有灵活性。同时由于我们可以让某些物理地址不被任何逻辑地址映射,所以在地址变换的过程中同时也提供了内存保护功能。
计算机物理内存是字节的线性数组,每个字节具有唯一的物理地址。
程序中的地址是由两部分构成的逻辑地址,这种逻辑地址并不能直接用于访问物理内存,而需要使用地址变换机制将其映射到物理内存上。内存管理机制用于此。
所以什么是线性地址?
总的来说,虚拟地址指的是 [段描述符]:段内偏移,即进行段式转换之前的地址。那么线性地址就是段式转换之后,页式转换之前的地址。
分段机制提供了隔绝各个代码、数据、堆栈区域的机制,因此多个程序可以在同一个处理器上而不被干扰。
分页机制为传统需求页、虚拟内存系统提供了实现机制。其中虚拟内存系统用于实现程序代码按要求被映射到物理内存中。
分段机制
分段可以将处理器能寻址的线性地址空间 划分为一些较小的,称为段的受保护地址空间区域。段可以用于存放程序的代码、数据、和堆栈,或者用于存放系统数据结构(TSS,LDT)。如果处理器中有多个程序或者任务正在运行,那么每个程序可以分配各自的一套段。此时处理器就可以加强这些段之间的界限,并确保一个程序不会通过访问另一个程序的段来干扰它的运行。分段机制还允许对段进行分类,特定类型的段操作会受到限制。
一个系统所有的段都包含在处理器线性地址中。为了定位指定段中的一个字节,程序必须提供一个逻辑地址(逻辑地址包括一个段选择符和一个偏移量)。通过段选择符,在段描述符表(比如GDT,LDT)中找到一个数据结构(即段描述符)。段描述符指明段的大小、访问权限、特权级、段类型、段的基地址等。逻辑地址的偏移部分加到段的基地址上就形成了线性地址。
分页机制
多任务系统通常定义的线性地址空间都比其物理内存空间大很多,所以需要使用某种虚拟化线性地址空间的方法(这不是骗子吗)。该技术可以让程序员制作大型程序时无需考虑物理内存的容量。
分页机制支持虚拟存储技术。大容量的线性地址空间需要使用小块的物理内存(RAM,ROM)以及某些外部存储空间(如硬盘)来模拟。当使用分页时,每个段被划分为页面(一般4K),操作系统维护一个页目录,还有页表。当程序访问线性地址空间时,处理器用页目录和页表将线性地址转换成一个物理地址。
(如果访问的页不在内存中,则引发缺页中断,将该页读入内存。该机制用到了页面交换。)
保护
80x86 提供两种保护。
- 给每个任务不同的虚拟地址空间,完全隔离各个任务。
- 对任务进行操作,以保护操作系统内存段和处理器特殊系统寄存器不被应用程序访问。
任务之间的保护
每个任务都有自己的段表和页表,从而完全隔离虚拟地址。
通过在所有任务中安排具有相同的虚拟到物理地址映射部分,并把操作系统存储在这个公共的虚拟地址空间部分,操作系统可以被所有的任务共享。这个所有的任务都具有的相同的虚拟地址空间部分被称为全局地址空间(Global Address Space)。
每个任务唯一的地址空间部分被称为局部地址空间(Local Address Space)。局部地址空间含有需要与系统中其他任务区别开的私有的代码和数据。由于每个任务中具有不同的局部地址空间,因此两个不同任务中对相同虚拟地址处的引用将转换到不同的物理地址处。这使得操作系统可以给予每个任务内存相同的虚拟地址,但仍然能隔绝每个任务。
特权级保护
一个任务中定义了4个特权级(Privilege Level 0~3),用于依据段中含有数据的敏感度以及任务中不同程序部分的受信程度。
每个内存段都和一个特权级相关。特权级限制具有足够特权级的程序来访问这个段。CPL表示当前程序的特权级。
每个特权级都有自己的程序栈,避免共享栈带来的保护问题。当程序从一个特权级切换到另一个特权级执行时,堆栈段也随之改变。
分段机制
段的定义
每个段由以下参数定义:
- 段基地址:段在线性地址空间的起始地址
- 段限长:段内最大可用偏移位置
- 段属性:该段是否可读可写可执行,特权级等。
这些属性保存在一个称为段描述符(Segment Descriptor)的结构体中。段描述符保存在段描述符表(Descriptor Table),是一个简单数组。
逻辑地址由16bit段选择符和32bit偏移组成。逻辑地址向线性地址的转换过程如下。
如果没有开启分页,则处理器直接将线性地址映射到物理地址。
段选择符
在保护模式下,段寄存器存放的就是段选择符
- RPL:请求特权级(Requested Privilege Level)
- TI:表指示标志(Table Index)。TI=0表示位于GDT,TI=1表示位于LDT。
有六个段寄存器(CS, DS, SS, ES, FS, GS)用于保存段描述符。
对于访问某个段的程序,必须已经把段选择符加载到一个段寄存器中。因此尽管一个系统可以定义很多的段,但同时只有6个段可供立即访问。
此外,为了避免访问内存时每次都去引用描述符表来解码一个段描述符,每个段寄存器都有一个“可见部分” 和一个 “隐藏部分”。隐藏部分也被称为描述符缓冲。当一个段选择符被加载到一个段寄存器的可见部分中时,处理器也将段描述符中的段地址、段限长、以及访问控制信息加载到段寄存器的隐藏部分中。
缓冲在段寄存器中的信息使得处理器可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和限长。
由于描述符缓冲保存了描述符信息的一个副本,所以操作系统必须确保对描述符表的改动反应到描述符缓冲上。最便捷的方法是在对描述符表的操作符经过任何改动后,立刻重新加载6个段寄存器。
段描述符表
段描述符表是段描述符组成的的数组,最多可以包含 2^(16-3)=8192个项,每个段描述符8Byte。
段描述符表有两种:
- GDT:全局描述符表
- LDT:局部描述符表
描述符表保存于操作系统维护的特殊数据结构中,且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅由操作系统软件访问的受保护内存区域中,以防止应用层修改其地址转换信息。
虚拟空间被分割为大小相等的两半,一半由GDT来映射,另一半由LDT映射。整个虚拟地址空间包含2^14个段(2 * 2^13)。GDT负责映射全局虚拟地址空间,LDT负责映射局部虚拟地址空间。即:GDT映射的区域是所有任务共享的。
当发生任务切换时,LDT会更换成新任务的LDT,但是GDT不会改变。
每个系统必须定义一个GDT,可选定义多个LDT,比如可以为每个任务定义一个LDT,也可以让一些任务共享一个LDT。
GDT本身是线性地址空间的一个数据结构。GDT的线性基地址和长度必须保存到GDTR中。GDT的基地址应该进行内存8Byte对齐,以得到最佳的处理器性能,因为段描述符总是8Byte。
LDT存放在LDT类型的系统段中。此时GDT必须含有LDT的段描述符。如果系统支持多LDT的话,每个LDT都必须在GDT中有一个段描述符和段选择符。
访问LDT时为了减少地址转换的次数,LDT的段选择符、基地址等信息需要存放在LDTR寄存器中。
段描述符
每个段描述符都是GDT和LDT的一项。
每个段描述符8Byte,包含三个主要字段:段基地址,段限长,段属性
段描述符一般由编译器,连接器,加载器,或者操作系统创建,但绝不是应用程序。段描述符的通用格式如下:
解释各字段:
段限长 Segment Limit: 指定段的长度。处理器会把两个段限长字段组合成一个20bit的值,并根据颗粒度G来指定值的实际含义。如果G=0,则Limit为1B
1MB,单位是1B。如果G=1,则Limit为4KB4GB,单位是4K。E为段扩展方向标志(??是不是TYPE??)。处理器以两种不同方式使用段限长Limit。对于上扩展的段(简称上扩段),逻辑地址中的便宜范围可以从0~段长limit。大于段限长limit的偏移将产生一般保护性异常。对于向下扩展的段,段限长的含义则相反。根据默认栈指针指针大小标志B的设置,偏移范围可以从段限长到0xFFFFFFFF或者0xFFFF,而小于段限长的偏移值则会引起一般保护性异常。对于下扩段,减小段限长字段的值会为在该段地址空间的底部分配新的内存,而不是在顶部分配。80x86的栈总是向下扩展的,因此该方式适合扩展堆栈。
上文中,默认高地址在上,低地址在下。
基地址字段 Base: 把三个分离的基地址字段组合形成一个32位的值。
段类型字段 Type:用行指定段或门(Gate)的类型,说明段的访问种类以及扩展方向。该字段的解释依赖于描述符类型标志S,指明时一个应用(代码或数据)描述符,还是系统描述符。不同的S对应的TYPE字段的解释都不同。
描述符类型标志 S:指明一个段描述符时系统段描述符(S=0)还是数据代码段描述符(S=1)
描述符特权级字段DPL:用于指明描述符的特权级。0~3
段存在标志P:表示段在内存中(P=1)还是不在内存中(P=0)。当段描述符的P标志位0时,把指向这个段描述符的选择符加载到段寄存器将导致产生一个段不存在异常。内存管理软件可以使用这个标志来控制在某一给定时间实际需要把哪个段加载到内存。该功能为虚拟存储提供了除了分页机机制以外的控制。
D/B 默认操作大小/默认指针大小和/或上界限 标志。根据段描述符描述的实一个可执行代码段、下扩数据段还是一个堆栈段,该标志有不同的功能。(对32位代码和数据段,该标志总为1;对于16位代码和数据段,标志置为0.)
颗粒度标志G:确定Limit值的单位。
代码和数据段描述符类型
若段描述符的S字段为1,则该描述符用于代码或者数据段。此时类型字符Type的最高位用于确定是数据段描述符(置为0)还是代码段描述符(置为1)
对于数据段描述符,Type的低3bit用于表示已访问(Accessed),可写(Write-enable)和扩展方向E(Expansion-direction)。
其中,堆栈段必须是可读写的数据段。如果使用不可写数据段的选择符加载到SS寄存器中,将导致一个一般保护异常。如果堆栈段的长度需要动态地改变,那么堆栈段可以是一个向下扩展的数据段。标志位A一般用于调试。
对于代码段,TYPE字段解释为 A,R(可读),C(conforming 一致性)。
一致性:高权限不允许访问低权限,低权限可以访问高权限,但特权级不改变。
非一致性:只允许同级别之间访问
所有的数据段都是非一致的,但数据段可以被高特权级访问(高——>低)。
系统段描述符类型
系统描述符如下:第12bit=1
处理器能够识别以下类型的系统段描述符,分两大类:系统段描述符,门描述符。
- 局部描述符表(LDT)的段描述符
- 任务状态段(TSS)描述符
- 调用门描述符
- 中断门描述符
- 陷阱门描述符
- 任务门描述符
分页机制
在分段机制的基础上完成虚拟地址到物理地址的转换过程。
处理器分页机制会把线性地址空间划分为页面,然后这些线性地址空间页面被映射到物理地址空间页面上。
通过设置控制寄存器CR0的PG位可以选择启用分页机制。
分页机制对固定大小的内存块进行操作(称为页面)。分页机制把线性和物理空间都划分成页面,线性地址空间中的任何页面都可以被映射到物理地址空间的任何页面上,并在两个空间之间提供任意映射。
80x86以4K作为固定页面大小。每个页面对齐于4K地址边界处。这表示分页机制把2^32B(4G)的线性地址空间划分为了2^20个页面。分页机制通过把线性地址空间中的页面重新定位到物理地址空间中进行操作。由于4K大小的页面,因此线性地址的低12bit可以作为页内偏移,直接作为物理地址的低12bit。分页机制的重定位功能可以看作替换高20bit。
线性地址到物理地址的转换功能被扩展为允许一个线性地址被标注为无效的,而非让其产生一个物理地址。在两种情况下一个页面可以被注册为无效:
- 操作系统不支持的线性地址:此时产生无效地址的程序必须终止。
- 对应在虚拟地址:该无效地址实际上时请求操作系统虚拟内存管理器将对应的页面从磁盘上加载到物理内存,供程序访问。因为无效页面通常和虚拟存储系统相关,因此他们被称为不存在对于额面,并由页表中称为存在(present)的属性确定。
在保护模式中,80x86允许线性地址空间直接映射大容量的物理内存,或者使用分页间接映射到较小容量的物理内存和磁盘存储空间中。这后一种映射线性地址的方法称为虚拟存储或者需求页虚拟存储。
如果包含线性地址的页面目前不再物理内存中,处理器就会产生一个页错误异常,页错误异常处理程序会让操作系统从磁盘中把相应页面加载到物理内存中(操作过程中可能还会把物理内存中不同的页面写到磁盘上)。当页面加载到物理内存之后,从异常处理过程的返回操作会使得导致异常的指令重新执行。处理器用于将线性地址转换为物理地址使用的信息保存在页目录和页表中。
为了减少转换的开销,最近访问的页目录和页表会存放在缓冲器件中。该缓冲器件称为转换查找缓冲区(Translation Lookaside Buffer)。TLB可以满足大多数读页目录和页表的请求而无需使用总线周期。只有当TLB不包含要求的页表项时才会使用额外的总线周期从内存读取页表项。
页表结构
页表保存于内存,位于物理地址空间。页表可以看作简单的2^20个物理地址数组。线性到物理地址的映射功能可以简单地看作进行数组查找,线性地址的高20bit构成数组的索引值用于选择对应页面的物理(基)地址。
页表中每个表项大小为32bit。其中20bit用于保存物理基地址(4K对齐),剩余的12bit用于保存属性信息等。
二级页表结构
页表含有 2^20(1M)个表项,每项4B,。如果用一个表来保存,最多占用4MB的内存。
为了减少内存的占用量,80x86使用了二级表。高20bit线性地址到物理地址的变换通过两部进行,每步转换10bit。
第一级表称为页目录,存放在一个页面中(4K),具有2^10(1K)个4B长度的表项。这些表项指向对应的二级表。线性地址的最高10bit(位31~22)用作一级表/页目录的索引。
第二级表称为页表,长度也是一个页面(4K),最多含有1K个4B的表项。
下图给出查找过程。其中CR3寄存器指定页目录表的基地址。
CR3 存放页目录表页面的物理地址,因此也被称为PDBR。由于页目录表页面时页对齐的,所以该寄存器只有高20位是有效的。而低12位保留供高级处理器使用。
然而。问题似乎并没有解决:页表还是需要4MB的空间,并没有减小啊。这反而又引入了页目录,不是更占空间吗?
然而,引入页目录虽然没有解决空间问题,但是允许页表被分散在内存的各个页面,而不必保存在一段连续的空间了(这样有意义吗?你问我我问谁)。此外,并不需要为不存在的,或者线性地址空间未使用的部分分配二级页表。虽然目录表页面总是必须存在于物理内存,但二级页表可以需要时再分配。
页表项结构
其中位31~12表示物理地址的高20bit,低12bit 含有页属性信息
- 存在标志P(Present):指明表项对地址转换是否有效。如果P=0,则其余位可以供程序自由使用。如图b所示。
- 读写标志R/W:如果=1,表明可读写执行;如果为0,表示只读或可执行。处理器运行于特权级(0,1,2)时,R/W位不起作用。页目录项的R/W对所有页起作用
- 用户/超级用户标志U/S:=1,则任何特权级程序都能访问;=0,则只有超级用户特权级才能访问。
- 已访问标志A:处理器访问页表项映射的页面时,页目录表项的该标志置为1。处理器只负责设置,操作系统可以通过定期复位,来统计页面的使用情况。
- 修改标志D(dirty):处理器对一个页面执行写操作时会设置。页目录项的D标志不会被修改。
- AVL:保留字段,专供程序使用。
虚拟存储
页目录和页表表项中的存在标志P为使用分页技术的虚拟存储提供了必要支持。若线性地址空间中的页面存在于物理内存中,则P=1,且该表项中含有相应物理地址。如果页面不在物理内存,则P=0,程序访问物理内存不存在的页面就会产生缺页异常,操作系统可以利用该异常处理过程,将缺少的页面从磁盘调入物理内存。
标志A和D可以用于实现虚拟存储技术。通过周期性地检查和复位A标志,操作系统能确定哪些页面最近没有访问过,这些页面可以成为移出到磁盘的候选者。