用户程序 A 君,是某酒店公寓的新住户,该公寓有若干层,每层有若干寓所,还包括公用的日用百货仓库间,会所等公共设施,是一座自助式的电子化的公寓大厦。
这一天是用户程序 A 君,新入住公寓的日子,管理员 OS 交给了 A 君自己寓所的电子门匙,A 君要凭这条电子门匙在公寓的管理系统里找到自己的寓所。
5.1、寻找 room 的 key:segment registers 和 selectors
----------------------------------------------------------------------------------------
OS 载入用户程序 A,A 从 OS 里接过控制权,A 找到自己的执行代码然后跳转到自己的代码执行,OS 通过 call 跳转到用户程序 A 执行。
----------------------------------------------------------------------------------------
(1)用户程序 A 君从管理员 OS 接过属于自己的 key,A 君用这把电子门匙在公寓管理系统的检索模块进行验证,管理系统正在验证匹配中... ...
电子门匙内部是一块芯片,芯片存储着这条 key 对应的寓所的相关信息。管理系统从这条电子门匙中读取信息... ...
5.1.1、开启 room 的 key: segment registers(CS、DS、SS、ES、FS & GS)
segment registers 事实上就是这把电子门匙,不过 segment registers 仅仅是个载体,即:segment registers 是存储相关的 descriptors 信息的载体,光有载体而失去里面的 descriptors 信息,这把门匙等于是没用。 |
segment registers 是个 16 位宽的 registers,segment registers 里装载的是 selector 以及相应的 descriptors 信息。x86 体系使用分段式的内存管理机制,将线性的内存区域分成若干个区域,这个区域的起始地址,区域的大小及相关的属性信息将用一个 descriptors(描述符)的结构存储起来。
5.1.2、 key 里的索引信息: selectors
前面已经提过,光有 segment registers 这个载体是不够的,这个载体首先要存储索引信息,这就叫做 selector。通过 selectors 索引子到出相关的描述段结构的信息 descriptors (描述符) |
看一看 selector 的结构:
16 位宽的 selector 分成三个域:
0000000000000 X XX
--------------- - ---
| | |
| | +------------> RPL (Requestor Privilege Level)
| |
| +------------------> TI ( Table Index)
|
+-----------------------------> SI (Selector Index)
RPL (Requestor Privilege Level):实际上 RPL 的作用是构建一个集线器部件。
TI ( Table Index):指引在 GDT 表还是在 LDT 中寻找 descriptors,0 --- GDT 1 --- LDT
SI (Selector Index):这是真正的索引子,这个域形成一个数值,在相应的 descriptor table 中接这个数值寻找。
情景提示: x86 的权限级别分为 0 ~ 3 级,共 4 个权限级别。对于 segment 级保护措施来说,0 级为最高权限级别,依次是1、2 级,最低是 3 级。 对于 page 级的保护措施来说,只有 2 个级别:0 ~ 2 属于 supervisor 级别,3 级则属于 user 级。 |
用户程序通过使用不同的 RPL 来访问不同权限的代码段:
+---------> selector A(0x11) -----+
| |
| |
user A ------------> +---------> selector B (0x10) -----| ----+
| | |
| | |
+----------> selector C (0x13) -----+ |
|
|
|
+------------------------+
|
|
|
V
--------------------------------------------------
code 3
--------------------------------------------------
code 2
--------------------------------------------------
code 1
--------------------------------------------------
code 0
selector A 是 0x11,表示: RPL = 1,TI = 0,SI = 2
selector B 是 0x10,表示: RPL = 0,TI = 0,SI = 2
selector C 是 0x13,表示: RPL = 3,TI = 0,SI = 2
code 3 ~ code 0 分别是运行在权限级别为 3 ~ 0 的代码
这 3 个 selector 都是在 GDT 表上索引第 2 个 descriptor,所不同的是各自使用了不同的请求权限。
若当前 CPL = 2:
(1) 用户代码 A 可以使用 selector A 和 selector B 通过 call/jmp 来访问 code 2 ,效果是完全一样的。
(2) 用户代码 A 可使用 selector A、selector B 以及 selector C 通过 3 级的 gate 来访问 code 0 与 code 1 代码。
(3) 用户代码不能使用这 3 个 selector 来达到访问 code 3 代码。
使用同样的 descriptor index 但使用不同 RPL 的 selector 达到访问不同权限级别的代码。
特别的 selector: NULL selector
当 TI = 0,意味着将 GDT 寻找到 descriptor,当 RPL = 0 以及 SI = 0 时,即 selector 等于 0 时,这是一个特别的 selector,被称为 NULL selector。
NULL Selector: 在 GDT 表的第一个 entry,即索引为 0 的第 1 个 descriptor,这个 descriptor 是个无效的不可用的 descriptor,设计这样一个 selector 的目的,是防止 0 值被加载到 CS 和 SS。 当一个 0 值被加载到 CS 和 SS 时,将会产生 #GP 异常。 |
实际上,当 SI = 0、TI = 0 时,但 RPL 不等于 0,即 0x00 ~ 0x03 的 descriptors 也是 NULL selectors ,这样的 Null Selector 被加载到 CS 和 SS 同样会产生 #GP 异常。
5.1.3、 descriptor 的载体: segment register
前面已经提到:segment registers 相当于 key,而这把 key 内信息才是最重要的,才是最终的目的。
segment register 的结构:
segment register 在物理上分为 2 部分:用户可见的 selector 部分和用户不可见的隐藏部分,用户可见的 selector 为 16 位宽,这个 selector 就是被加载进 segment register 的 selector。 隐藏部分仅对 processor 可见,这部分存储着被加载的 descriptor 信息,包括:base、limit 以及 attribute 部分。 |
base 是 64 位宽,但在 x86 下 base 高 32 位不可用,意即为 32 位宽。limit 固定为 32 位宽,在 x64 下 limit 是无效的, limit 仅在是可用的,所以 limit 固定为 32 位。
接下来,attribute 推测为 16 位宽,Intel 和 AMD 都没明显说明 attribute 是多少。
descriptor 的载体:
当 selector 加载进 segment registers,processor 根据 Selector.TI 在 GDT 或者 LDT 搜索对应的 Descriptor,将 Descriptor 的相应部分加载到 segment register 的隐藏部分。 |
以 CS 为例:
当 selector 0x13 被 load 到 CS,Selector.TI = 0、Selector.SI = 2,那么 processor 在 GDT 中寻找第 2 个 descriptor,当通过了权限检查后,这个 descriptor 的 base、limit 及 attribute 被相应加载到 CS.base、CS.limit 及 CS.attribute 域。CS.selector 保持不变。
情景提示: 在实模式下,相关的保护模式的数据结构不可用,descriptors、GDT 等数据结构是不存在的。当 selector 0x13 被 load 到 CS 时,processor 作如下处理: CS.base = selector << 4,selector 左移 4 位加载到 base 域,即 base 变成了 0x00000130,Selector.RPL 及 Selector.TI 是不可用的。 |
这就是实模式分段管理模式的典型内存寻址:段寄存器左移 4 位 + offset。
有一点值得注意的是:
descriptor 里描述的 limit 只有 20 位宽,在加载到 segment register 里时,limit 被扩展到 32 位宽后再加载到 segment register 里。
具体的扩展措施是:descriptor 的 G 标志位代表粒度,G = 1 表示 limit 域的粒度为 4K (4096 bytes),G = 0 表示 limit 域的粒度为 1 byte。因此 G = 1 时:32 位 limit = 4096 * limit + 0xFFF,G = 0 时:32 位 limit = 1 * limit。
5.2、 descriptors 是怎样描述 segment 信息的?
descriptors 描述符,顾名思义就是描述 segment 相关信息的数据结构,定义了 segment 的基址,长度、类型及相关属性。不同类型的 descriptors 有不同的意义。
总体上有 2 种类型的 descriptors:user 描述符和 system 描述符。由 descriptors 结构的 S 标志位指出。user 描述符就是指基本的 segment descriptors,system 描述符包括有 LDT descripts、TSS descriptors 和各种 Gate descriptors。
5.2.1、理解 descriptors 结构是很容易的,基本上 descriptors 结构描述的无非就是以下几个信息:
(1) segment 的基址,即 base
(2) segment 的长度,即 limit
(3) segment 的类型,即 S(System 标志位)。
(4) segment 的具体类型,在确定 descriptors 是 System 还是 user 后,还要定义具体的类型,如:code segment / date segment、LDT / TSS 还是 Gate(什么类型的 gate)。
(5) segment 的属性,接下来就是 descriptors 相关的属性,如:G(粒度标志位)、D/B(缺省 operand)、L(long mode)等
(6) 最后就是访问该 segment 的权限,即 DPL(Descriptor Privlilege-Level)
下面是 x86 下的 segment descriptor 结构:
例 5.2.1.1
struct x86_descriptor_struct {
/**** descriptor 的前 4 字节 ****/ /**** descriptor 的后 4 字节 ****/ |
5.2.2、descriptor 结构的 size
这个也很好理解:
(1) base 是 32 位
(2) limit 是 20 位(前面已经说过 descriptors 结构里的 limit 域是 20 位)
(3) S 标志位占 1 位 加上相关的属性及类型
相信已经道结果了:x86 下的 descriptors 结构是 8 个字节,即 64 位,因为 base 已经是 4 个字节了,加上 limit 及其它域就是 8 个字节。这样 descriptors 结构是 8 bytes 边界对齐的。
那么,在 x64 的 long mode 下情况稍有些复杂:
(1)由于 long mode 下采用了单一内存管理模式,有效地屏蔽了 segmentation 分段机制的内存管理模式,base、limit 及相关的属性都是无效的,因此,在 long mode 下,user 描述符,即 segment descriptors 结构依旧是 8 bytes 长。
(2)对于 System 描述符,由于 base 需要指出具体值,因此,long mode 下,System 描述符是 16 bytes 长的。典型的如:Call Gate descriptors 结构,需要指出具体 64 位的 offset 值 及 16 位的 selector 值,因此它是 16 bytes 边界对齐的。
5.2.3、 segment 的 base
x86 下的 descriptor 描述 segments base 是 4 bytes 32 位地址。前面提到过 x86 下 descriptors 结构是 8 bytes 64 位结构。
由例 5.2.1 可知,4 bytes 的 segment base 分布在 descriptor 的 8 bytes 空间里的第 3 字节 ~ 第 5 字节以及第 8 个字节,两部分组成 4 个字节。
在 x64 long 模式下的 segment descriptor 它的 base 是无效的,因此,它不描述的 segment base。
5.2.4、 segment 的 limit
同样,segment limit 仅在 x86 & x64 的 32 位环境中才有效,在 long mode 是无效的。前面已经提到:limit 在 descriptor 结构是 20 bit,它如何描述 segment 的 32 位长度呢?
(1)G =1: limit = 4096 * limit + 0xFFF
(2)G = 0: limit = 1 * limit
最终,形成的 32 位的 limit 会被加载到 segment register 的 limit 域里。
5.2.4.1、processor 是怎样检查 segment 的 limit
对于 CS 来说,它描述的 segment 是 Expand-Up 类型的。那么,对于 Expand-Up 类型的 segment 来说,检查 limit 是否超限相对简单。
情景提示: 当 G = 0 时:segment 的范围是:base ~ base + limit 当 G = 1 时:segment 的范围是:base ~ base + limit * 4096 + 0xFFF |
这里有 2 个比较特殊的情况:
(1)、当 G = 0 时,若 limit = 0,那么 segment 的宽度是 1 byte,segment 范围是:base ~ base + 1
(2)、当 G = 1 时,若 limit = 0,那么 segment 的宽度是 0xFFF, segment 范围是:base ~ base + 0xFFF
对于描述 Expand-Up 类型的 segment descriptor,它描述的 segment 的宽度是在粒度内,因此:当 G = 0 时,segment 的宽度 1 byte,而 G =1,segment 宽度是 4095,也就是 0xFFF。
5.2.4.2、 下面解释一下为什么 G = 1 时要加上 0xFFF 这个值
以 descriptor 20 位的 limit 饱和时:limit = FFFFF 为例,若 G = 1,那么此时,它是描述饱和的 32 位空间,即 4G 空间。
看一看:
limit = 4096 * limit = 1000h * limit,也就是 limit << 12 结果是:FFFFF000,此时的还不足描述 4G 空间,因此必须加上 0xFFF 这个值,类似补码的特征。
此时:FFFFF000 + FFF = FFFFFFFF 这就能表示完整的 4G 空间。
5.3、 descriptors 存放的地方:descriptor table
没错!若干个 descriptors 存放在一个连续的空间,形成数组形式的结构,这个数组就被称为 descriptor table。以前面例 5.2.1.1 定义的 descriptor 结构来说,那么,descritptor table 结构就是这样的:
descriptor table 结构:
struct x86_descriptor_struct dtable[n]; /* descriptor table 结构*/ |
上面定义了 descriptor table 结构。
5.3.1、 descriptor table 的类型
有三种类型的的 descriptor table:GDT(Global Descriptor Table)、LDT(Local Descriptor Table)以及 IDT(Interrupt Descriptor Table)。这三种 descriptor table 能容纳的 descriptors 类型是不同的。
GDT 容纳所有类型的 descriptors,包括:segment descriptor(code segment & data segment)、system descriptors(LDT descriptor、TSS descritpor、所有的 gate descriptor)。
LDT 容纳 segment descriptor 以及 gate descriptor。
IDT 仅容纳 interrupt gate descriptor、trap gate descriptor 和 task gate descriptor,但不包含 call gate descriptor。
5.3.2、 descriptor table 的容量
descriptor table 的 base 存储在相应的 descriptor table register 中:
(1) GDT base 存放在 GDTR 中
(2) LDT base 存放在 LDTR 中
(3) IDT base 存放在 IDTR 中
同时,descriptor table register 还存储着 descriptor table 的 limit 值。descriptor table 的 limit 值是 16 位,即值为:FFFFh。
情景提示: (1)selector 中有 13 位是表示 index 值,1111111111111b = 1FFFh = 8191,也就是说:index 的范围是 0 ~ 8191,共可以寻址 8192 个 descriptors。 (2)另一个角度来看:descriptor table register 中的 limit 是 16 位,值为 FFFFh,这是一个字节值。在 x86 下 descriptor 的 size 是 8 bytes,那么它容纳的 descriptors 数是: FFFFh / 8 = 1FFFh = 8191,也就是 FFFFh >> 3 = 1FFFh 刚刚好是:8191,这和 selector 中的最大可容纳的 index 值是一样的。 (3)在 x64 的 long 模式下,system descriptor 和 gate descriptor 是 16 bytes。那么,它能容纳的 descriptors 数是:FFFFh / 16 = FFFF >> 4 = FFFh = 4095,selector 仅能索引到 4095,若是 selector 的 index 为 4096 时将产生越界,引发 #GP 异常。 |
以 GDT 为例,加载 base 到 GDTR 的指令是 LGDT,指令形式是 LGDT dword ptr [dtr]。这个 dtr 地址处存放着 descriptor table 的 base 和 limit 值。低 16 位是 limit,高 32 位是 base 值。
(2)... ... 管理系统从 key 中读取出信息,用户程序 A 君使用的 key 是 0x1b 号,管理系统需要在数据库里检索房号为 0x1b 的 room 的具体位置。 管理系统正在检索中... ...
-------------------------------------------------------------------------------
GDTR 与 GDT 构建成公寓的数据库系统,descriptors 是数据库里的数据信息。管理系统就是从 GDTR 与 GDT 里检索 seleoctor 为 0x1b 的 descriptor 数据信息。
在用户程序中使用了指令 call 0x1b:0x00401000 进行接收控制权,执行自己的任务... ...
5.4、 查找 descriptor
selector 0x1b 的结构是:
0000000000011 0 11
--------------- - --
| | |
| | |
| | +-------> DPL = 3
| |
| +----------> TI = 0
|
+------------------> SI = 3
使用这个 selector 的结果是:以 DPL = 3 的权限,在 GDT 中寻找编号为 3 的 descriptor,
gdtr.base = 0x800f3000 gdtr.limit = 0x3ff
这是一段截取 xp 启动时瞬间情况。
5.4.1、 查找 descriptor 的规则。
下面是 C 代码表示:
1、获取 descriptor table base
/* * 假如 selector 的 TI 为 0 时,在 gdt 中索引。 * 假始 selector 的 TI 为 1 时,则在 LDT 中索引。 */ if (selector.TI == 0) |
2、索引 descriptor 地址
/* * 根据 selector 的 SI (索引值)在 base 索引。 * 索引因子是 sizeof(descriptor),x86/x64 的 segment descriptor 是 8 个字节的 * 所以,等于乘上 8 */ descriptor = base + selector.SI * sizeof(descriptor); /* base + SI * 8 */ |
从更高级的 C 语言层面来描述是:
struct x86_descriptor_struct descriptor_table[gdtr.limit]; /* 定义 descriptor table 结构 */
descriptor = descriptor_table[selector.SI]; /* 获取 descriptor 结构 */
5.4.2、 加载 descriptor
当获得相应的 descriptor 后,processor 会将 descirptor 信息加载到相应的 segment registers 中。
以下是默认的 descriptor 加载到相应的 segment registers 列表。
code segment descriptor -----> cs
data segment descriptor -----> ds
data segment descriptor -----> ss
data segment descriptor -----> ss
data segment descriptor -----> fs
data segment descriptor -----> gs
前面已经提过:segment registers 分部两部分,一个是可见的 selector 部分,另一个是仅对 processor 可见的隐藏部分。因此 descriptor 被找到之后,通过仅限及相关的检查之后,会被 processor 加载到 segment registers 的隐藏部分。
正如所料:
1、descriptor 的 selector 是 segment register 的 selector 域。
2、descriptor 的 base 被加载到 segment register 的 base 域。
3、descriptor 的 limit 被扩屋到 32 位后,加载到 segment register 的 limit 域。
4、最后 descriptor 的相关 attribute 加载到 segment register 的 attribute 域。
segment register 的 base、limit 以及 attribute 域构成 segment register 的隐藏部分。这部分信息在下一次加载之前是保持恒值的。使用同样的 selector 来获取数据,无须做多次的 segmentation 转换过程。
下面是 descriptor 的寻扯过程示意图:
base+SI*8
selector ----------> GDT -------------> descritptor -------> segment register
| TI = 0 ^
| |
| |
+--------> LDT --+
TI = 1
5.5、 Linear Address 的产生
selector : offset 这种形式的地址被 Logic Address(逻辑地址)。Logic Address(逻辑地址)和 Linear Adress(线性地址)都应该被称为 Virtual Address(虚拟地址)。Logic Address 经过 segmentation(分段机制)转换为 Linear Address。
逻辑地址中的 offset 值实际上也被称为 Effective Address(有效地址)。
Linear Address 的形式是:LA = base + offset 或表达为: LA = base + EA。 在 32 位的地址空间中,linear address 的表达范围是:0x00000000 ~ 0xFFFFFFFF 共 4G 的地址空间。在 64 位地址空间范围是:0x00000000_00000000 ~ 0xFFFFFFFF_FFFFFFFF。
如前所述:linear address 的 base 地址是由 selector 寻址 descriptor 获取。
因此:
Logic Address 形式中的 selector 索引到相应的 descriptor,从而获得 base 地址值,再加上 Logic Address 形式中的 offset 值,就产生了 Linear Address 。
下面是 Linear Address 产生的示例图:
selector : offset
------- ------
| |
| +------------------------------+--------------> linear address
| |
| |
+--------> descriptor ------> base ------+
当第 1 次加载 descriptor 后,在 selector 不变的情况下,linear address 的产生已经成为 cs.base + offset 或 ds.base + offset ...