segmentation和保护模式(二)

分段模式和保护模式(二)

segmentation和保护模式(一)

上文讲到了segment descriptor,把这些descriptors放在一起(在内存里连续分布),就构成了GDT(Global Descriptor Table),所以GDT也可以被称为段(描述符)表。

页表的首地址是存储在CR3寄存器中的,类似的,GDT的首地址存在GDTR寄存器中。页表的查找是通过使用虚拟地址作为index,而GDT的查找使用的是segment selector。

要找到某个page中的一个byte,得先通过虚拟地址的前面20位(以32位系统2级页表,4KB的page为例),查找页表得到物理页面号,再通过虚拟地址的后12位作为偏移(offset),在物理页面中找到对应的byte。

类似的,要找到某个segment的一个byte,得先通过segment selector,查找GDT得到segment的基址(base),再通过一个offset,在该segment中找到对应的byte。那segment selector又是从哪些获取的呢?

x86中有6个专门的寄存器CS, DS, ES, FS, GS, SS来存放这些selectors(拿专门的寄存器来放selector,主要是为了加快获取selector的速度),CS里存放的selector是指向GDT中code段的描述符,SS里存放的selector是指向GDT中stack段的描述符,DS里存放的selector是指向GDT中data段的描述符。

ES(Extra), FS, GS(FS和GS是80386引入的,是ES的克隆产品)既可以指向code,也可以指向data,但实际应用中大多是指向data,等同于DS(从属性上来说,heap, bss都算是data段,所以可能data需要的段比较多吧)。

一个segment seletor的构成如下(16个bit):

index就是在GDT中查找的索引,占13位,因此GDT最大可以有 213=81922^{13}=8192 个entries。

TI表示要查的表是GDT还是LDT(Local Descriptor Table)。GDT是所有task共享的,是必须要有的,而LDT是task单独拥有的,是可选的,有点类似于linux中进程的page table是单独拥有的,而kernel的page table是所有进程共享的,但在segmentation的实际应用中,LDT较少被使用。

说到RPL (Requested Privilege Level),还得提到另一个概念,叫CPL (Current Privilege Level) 。CPL是放在CS和SS中的,它只会在当前运行的特权级别更改的时候(比如从用户空间trap到内核空间)被更改,而RPL是软件设置的。

如果要访问的数据也在当前segment,就不需要切换segment,这时的jmp跳转被称作near jump(直接给出offst即可),与之相对的是要重新查找GDT,切换segment的far jmp(需要给出segment和offset)。

    jmp eax      ; Near jump
    jmp 0x10:eax ; Far jump

对于CS,只有RPL和CPL的值都小于上文介绍到的segment descriptor里的DPL,也就是特权级别高于要访问的segment要求的级别才可以。在切换segment的时候,需要将新的segment selector放入segment register,此时CPU会进行根据公式 MAX(CPL,RPL)<=DPLMAX(CPL, RPL)<= DPL 来进行这一检查,

出于一些特殊用途的考虑,RPL的值可以设置的比DPL低,但是在访问 segment的时候是无效的,还得听CPL的。只有RPL的值大于DPL,也就是主动降低级别时(也是出于其他用途,在此不展开),RPL才有效。下图中,实线部分的访问(A和B访问E)是被允许的,而虚线部分的访问(C和D访问E)是被禁止的。

对于SS, RPL, DPL, CPL的值必须完全一样。

介绍完这些bit field的含义,来看一段实际的设置代码:

mov ax, 0x10
mov ds, ax

x86里不能把立即数直接往DS里送,得通过AX中转一下(好在AX这个中间商没有赚差价)。代码执行完后DS寄存器中的值应该为0x10,表达为二进制的对应关系如下:

则DS中的selector指向的是GDT中的第三个entry(index从0开始算)。

同样的segement:offset的logical address格式,这时表达的linear address就不再是"segment<<4 + offset",而是"segment base (found from GDT[segment selector]) + offset"了。

80286中的segment register不再直接指向segment,而是要经过一个GDT查表的过程,而GDT表中的的segment descriptor有limit和各种权限检测位,起到了对地址访问的保护作用,所以这种内存访问被称作保护模式(protected mode)。

所有task共用GDT,岂不是task A也可以访问到task B的segment了?理论上是可以的,但是每个task用的segment descriptor,都是操作系统来建立的,建立好之后某个task只能访问为它分配的那几个segments。所以,保护模式下的segmentation机制既提供了不同属性的segment之间的隔离,也提供了多任务环境下不同task之间的隔离。

番外 - segmentation中的TLB

Paging机制里有为了加快查找速度,缓存页表项的TLB,同样,在segmentation里也有类似的硬件单元。每当一个segment seletor被载入segment register中, 表明该segment当前正在被访问,则将该segment seletor指向的segment descriptor存入专门的nonprogrammable register中(可理解位描述符缓存)。

一个segment register对应一个nonprogrammable register,所以只要segment register中的内容没有发生变化,CPU就不需要去查找GDT,直接从nonprogrammable register获取segment的地址。

segmentation的机制介绍完了,那它和paging机制是怎么共存和交互的呢?请看下文分解。


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

参考资料

文章转载于此


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