segmentation和保护模式(一)

分段机制和保护模式之间的关系

前面的几篇文章讲到了页表,以及构成页表的页表描述符,分页机制(paging)是80386引入的,而在其之前的8086和80286,使用的是分段机制(segmentation)。一些新的处理器架构,比如ARM,从诞生之日,就只支持分页机制。虽然分段机制看起来似乎是一个上古时代的,已经被淘汰的东西,但毕竟人家是分页机制的前辈。

以兼容性著称的x86处理器,即便到了今天的64位时代,依然保留了对segmentation的向前兼容,而linux操作系统最开始也是基于i386写的,所以也保留了对segmentation的支持。了解下历史,往往可以让我们对后来出现的某些事物有更好的理解。

提到segment,做嵌入式软件的同学可能会想到elf(executable and linkable file)文件,一个elf文件包含了text段,data段,bss段。Linux操作系统在加载elf文件运行进程后,还会生成stack段,heap段,每个segment用一个vm_area_struct结构体管理。

x86中segment划分也是类似的,包括cs(code segment),ds(data segment),ss(stack segment)等。同elf和linux中软件实现的segment有所不同,x86中的segmentation机制含有更多硬件的参与,比如硬件实现的对权限位的检测(类似于MMU对页表描述符中权限位的检测)。此外,x86中还有一些特殊的system segment,比如存储task相关信息,以提供对context switch的硬件支持的task state segment。

让我们从1978年的8086开始,看看x86中的segmentation机制到底是怎样一回事。

8086实模式时代

8086的CPU是16位的,但intel的工程师希望在不改变CPU寄存器和指令集的基础上,让它可以可以寻址更大的内存范围。于是他们使用了一种叫segmentation的机制,即一个逻辑地址(logical address)由segment加上offset组成,一般表达为segement:offset的形式。

当segment的值放入segment register时,就是告诉CPU:现在要访问这个segment对应的内存区域,然后再根据offset的值,在这个segment内找到对应的字节。转换后形成的地址被称作线性地址(linear address),若offset为16位,linear address = segment<<4 + offset,则寻址范围就扩大成了20位,比如logical address为0xA000:0x5F00,转换成linear address就是0xA5F00。

这种内存访问被称为实模式(Real Mode),因为linear address是由segment基址移位加上一个偏移得到,因此实模式下的这种segmentation被称为shift-and-add segmentation。

在实模式下segment register直接指向segment:

80286保护模式时代

80286也使用segmentation,但和8086不同的是,80286中segment register存的不再是segment的起始地址,而是一个segment selector,通过这个selector查找GDT表获得segment descriptor,segment descriptor存的才是segment的起始地址,因此保护模式下的这种segmentation被称为table-based segmentation(区别于实模式的shift-and-add方式)。

名词轰炸有没有?别慌,我们先从段描述符(segment descriptor)入手,看看它的组成是怎样的。

一个segment descriptor占8个字节,居然和现在64位系统的page table descriptor一样。一个segment对应的内存区域由base和limit确定,base占32位,limit占20位,最大为0xFFFFFH,这52位是表达地址的(页表使用20到36位存储地址),然后还剩下12个bit表达属性(上图红框部分),居然又和页表描述符是一样的,那再具体看看这些属性哪些和页表描述符是相同的,哪些是不同的。

G - Granularity,粒度,byte或者page,因为limit占20位,如果粒度为byte,则该segment的寻址范围是1MB,如果粒度为page(按4KB算),则该segment的寻址范围是4GB。所以,一个segment的size是由limit和G位共同确定的。

D/B - Default Size/Bound,为1表示在32位模式下运行,为0在16位模式下运行。

L - 仅在64位系统中有效,为1表示在64位模式下运行,为0表示在32位兼容模式下运行。

AVL - Available for software,留给软件用的,但反正在linux里是被忽略的。

P - Present, 同页表描述符里的P位。

DPL - Descriptor Privledge Level,表示可以访问segment的最低级别。x86处理器的特权级别从ring 0到ring 3,数字越小,级别越高,通常用户空间运行于ring3,内核空间运行于ring0,所以这个基本等同于页表描述符里的U/S位。假设DPL为1,则只有当前特权级别为0或者1时,才可以访问该decriptor指向的segment。

S - 对于code segment和data segment都是1,system segment(比如task state segment)为0。

Type - 这个对于task segment是没有意义的,对于code segment和data segment主要是关于Writable,Executable的属性,等同于页表描述符中的R/W, XD。

stack段也是可读写的,区别仅在于stack通常是向下增长的,所以也可视为data段的一种。对于这种向下增长的,需要注意是其地址边界不再是base+limit,而是base-limit。

A - Accessed,同页表描述符里的A位一样。

当task运行时一个page的映射关系被建立,操作系统就需要在page table中添加一个entry记录映射的物理页面号,并填写权限控制位,形成一个descriptor,同样的,当一个segment建立,操作系统就需要在GDT中添加一个segment descriptor,那GDT又是什么呢?请看下文分解。


原创文章,转载请注明出处。

参考资料

https://zhuanlan.zhihu.com/p/67735248


segmentation和保护模式(一)
https://ysc2.github.io/ysc2.github.io/2024/01/02/segmentation和保护模式(一)/
作者
Ysc
发布于
2024年1月2日
许可协议