查看系列文章点这里: 操作系统真象还原
前言
在前一篇文章我们主要讲了一下保护模式和实模式之间的主要变化,在这篇文章中,我们主要讲一下保护模式下的寻址过程,也就是如何得到真实的物理地址。
一、段描述符
顾名思义,段描述符就是用来描述一个段的相关信息的结构,它的大小是8字节,并且位于内存之中。直接来看一下它长啥样吧,见下表:
位置 | 63~56 | 55 | 54 | 53 | 52 | 51~48 | 47 | 46~45 | 44 | 43~40 | 39~16 | 15~0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
说明 | 段基址31~24 | G | D/B | L | AVL | 段界限19~16 | P | DPL | S | TYPE | 段基址23~0 | 段界限15~0 |
- 段基址(32位):被分割成2个部分,在寻址时才会被拼凑到一起;
- 段界限(20位):段边界的扩展最值,从0开始计算,用来计算段的长度范围。有两个单位,分别是1字节和4KB,因此一个段的长度=段界限*单位;
- S、TYPE:两者一起共同决定段的类型;
- DPL:表示该段描述符表示的段的特权级,共有0、1、2、3四个等级,数字越小特权越高。
- P:段是否在内存中,P=1则在,P=0则不在;
- AVL:对用户来说是否可用,操作系统和硬件不受此为控制;
- L:是否是64位代码段;
- D/B:用来指定段内偏移地址和操作数大小,对代码段来说,此位是D,D=0表示16位,使用IP寄存器,D=1表示32位,使用EIP寄存器;对栈段来说,此位是B,B=0表示16位,使用SP寄存器,B=1表示32位,使用ESP寄存器;
- G:表示粒度,就是段界限的单位,G=0表示单位是1字节,G=1表示单位是4KB;
上面把段描述符的每一位的作用都简单的说了一下,有些是现在依然很常用的,有些则是历史遗留产物,在实际中用处不大。不过还有S、TYPE这两个字段没有解释清楚,因为这两个字段比较复杂,单独拿出来讲一下。
S位用来指示该段是系统段(S=0)还是非系统段(S=1),只有先确定了S,TYPE字段才有具体的意义。
在讲TYPE字段之前,补充一些关于段的分类,避免搞混了。
对于CPU来说,它将段分成两大类,凡是硬件运行需要的段就是系统段,凡是软件运行需要的段就是非系统段(包括操作系统)。
咱们现在所讲的这个段描述符的结构,其实是用来描述非系统段的。系统段有它们自己的描述符,只不过名字不一样,通常也是“XXX描述符”。
但是它们都有一个共同点,都是8字节,在44、43~40位置都是S和TYPE字段,这样便于CPU区分它们。
接下来我们来详细说说TYPE字段如何将系统段或者非系统段再次划分,具体见下表:
系统段 | 系统段类型 | 第3~0位 | 说明 | |||
---|---|---|---|---|---|---|
3 | 2 | 0 | 1 | |||
未定义 | 0 | 0 | 0 | 0 | 保留 | |
可用的 80286 TSS | 0 | 0 | 0 | 1 | 仅限286的任务状态段 | |
LDT | 0 | 0 | 1 | 0 | 局部描述符表 | |
忙碌的 80286 TSS | 0 | 0 | 1 | 1 | 仅限286 | |
80286 调用门 | 0 | 1 | 0 | 0 | 仅限286 | |
任务门 | 0 | 1 | 0 | 1 | 现代操作系统很少用到 | |
80286 中断门 | 0 | 1 | 1 | 0 | 仅限286 | |
80286 陷阱门 | 0 | 1 | 1 | 1 | 仅限286 | |
未定义 | 1 | 0 | 0 | 0 | 保留 | |
可用的 80386 TSS | 1 | 0 | 0 | 1 | 386以上CPU | |
未定义 | 1 | 0 | 1 | 0 | 保留 | |
忙碌的 80386 TSS | 1 | 0 | 1 | 1 | 386以上CPU | |
80386 调用门 | 1 | 1 | 0 | 0 | 386以上CPU | |
未定义 | 1 | 1 | 0 | 1 | 保留 | |
中断门 | 1 | 1 | 1 | 0 | 386以上CPU | |
陷阱门 | 1 | 1 | 1 | 1 | 386以上CPU | |
对于非系统段,按代码段和数据段划分,分别有不同的意义 | ||||||
非系统段 | 内存段类型 | X | C | R | A | 说明 |
代码段 | 1 | 0 | 0 | * | 只执行代码段 | |
1 | 0 | 1 | * | 可执行、可读代码段 | ||
1 | 1 | 0 | * | 可执行、一致性代码段 | ||
1 | 1 | 1 | * | 可执行、可读、一致性代码段 | ||
内存段类型 | X | C | R | A | 说明 | |
数据段 | 0 | 0 | 0 | * | 只读数据段 | |
0 | 0 | 1 | * | 可读写数据段 | ||
0 | 1 | 0 | * | 只读、向下扩展数据段 | ||
0 | 1 | 1 | * | 可读写、向下扩展数据段 |
下面在解释一下非系统段各字段的含义:
- A:Accessed位,由CPU自动设置,我们只需要查看,每当CPU访问过后,就将此位置1,在创建新的段描述符时置0;
- C:是否是一致性代码段,C=1表示是;
- R:是否可读,R=1表示可读;
- X:是否可执行,X=1表示可执行;
- E:标识段的扩展方向,E=0表示向上扩展,E=1表示向下扩展;
- W:是否可写,W=1表示可写;
二、全局描述符表
在了解了段描述符,那么 全局描述符表(GDT) 就很好理解了,就是用来存储段描述符的一个数据结构,可以理解为数组。它的位置也在内存中,通过 GDTR 寄存器就可以在内存中找到它,通过 lgdt 指令可以为该寄存器赋值,该寄存器是一个48位的寄存器,如下表:
47~16 | 15~0 |
---|---|
GDT内存起始地址 | GDT界限 |
起始地址就是一个段基址,因为 GDT 本身也相当于一个特殊的数据段,想要访问,也必须要有段基址。GDT 界限是段边界扩展最值,就是用来定义这个表的大小的一个值,16位最大值为65536,表示这个表最大为65536字节,一个段描述符8字节,所以一个 GDT 最多可容纳 65536 / 8 = 8192 个段描述符。
三、选择子
GDTR 寄存器给出了 GDT 的段基址,就还差一个偏移地址就可以找到一个段描述符了,这个偏移地址就存储在段寄存器当中,因为存放的以及不是段基址了,所以就新取了一个名字——选择子,结构如下表所示:
15 ~ 4 | 2 | 1 ~ 0 |
---|---|---|
描述符索引值 | TI | RPL |
- 描述符索引值:就是偏移地址或者说数组下标,一共13位,最多可索引 8192 个段描述符;
- TI:Table Indicator,用来指示段描述符在 GDT 中还是在 LDT 中;
- RPL:请求特权级,分为0、1、2、3四种特权等级;
值得注意的是,描述符索引值必须大于等于1,第0个是不能使用的,这是为了避免因忘记初始化选择子而造成不可预知的错误。
LDT(局部描述符表):
有全局就肯定有局部,不过现代操作系统很少使用LDT,所谓局部就是指专门为某一个任务创建的描述符表,该表容纳的段只有该任务会使用。
LDT同样也在内存之中,LDTR 负责指向它,lldt 指令用于为该寄存器赋值。而且 LDT 是一个系统段,也就是说它也需要先在 GDT 中注册,然后用选择子去访问它。并且 CPU 每切换一个任务,就要重新加载 LDT。
总结
本文在好几个地方都提到了特权级或者系统段,这一部分知识和 GDT 关系不大,而且一两句话说不清楚,等到后面专门在讲解啦!
持续更新中~~