6.1、 x64 下的物理资源及系统数据结构
6.1.1、 segment registers
x64 体系在硬件级上最大限度地削弱了 segmentation 段式管理。采用平坦内存管理模式,因此体现出来的思想是 base 为 0、limit 忽略。
但是,x64 还是对 segmentation 提供了某种程度上的支持。体现在 FS 与 GS 的与众不同。
segment registers 的 selector 与原来的 x86 下意义不变。
在 64 bit 模式下:
(1)code register(CS)
● CS.base = 0(强制为 0,实际上等于无效)
● CS.limit = invalid
● attribute:仅 CS.L 、CS.D、CS.P、CS.C 以及 CS.DPL 属性是有效的。
情景提示: 64 bit 模式下的 code segment descriptors 中的 L 位、D 位、P 位、C 位以及 DPL 域是有效的。code segment descriptor 加载到 CS 后仅 CS.L 、CS.D、CS.P、CS.C 以及 CS.DPL 属性是有效的。
|
在 compatibility 模式下 code segment descriptor 和 CS 寄存器与原来 x86 意义相同。
(2)data registers (DS、ES 以及 SS)
● DS.base = 0(强制为 0,实际上等于无效)
● DS.limit = invalid
● DS.attribute = invalid:所有的属性域都是无效的。
data registers 的所有域都是无效的。data segment 的 attribute 是无效的,那么也包括 DPL、D/B 属性。
在 64 bit 模式下,所有的 data segment 都具有 readable/writable 属性,processor 对 data segment 的访问不进行权限 check 以及 limit 检查。
(3)FS 与 GS
● FS.base 是完整是的 64 位。
● FS.limit = invalid
● FS.attribute = invalid
与其它 data registers 不同的是,FS 与 GS 的 base 是有效的。支持完整的 64 位地址。但是 limit 和 attribute 依旧无效的。
1、为 FS 和 GS 加载非 0 的 64 位 base 值,使用以下指令:
mov fs, ax
或
pop fs
--------------------------------------------
这条指令只能为 fs 提供 32 位的 base 值,这根本的原因是:data segment descriptor 提供的 base 是 32 位值。在 x64 里的 segment descriptor 是 8 个字节。也就是 base 是 4 个字节。通过 selector 加载 base 值,只能获取 32 位地址值。
2、为 fs 和 gs 提供 64 位地址值,可以使用以下指令:
mov ecx, C0000100 /* FS.base msr 地址 */
mov edx, FFFFF800
mov eax, 0F801000
wrmsr /* 写 FS.base */
--------------------------------------------------
上面代码为 FS.base 提供 0xFFFFF8000F801000 地址。
mov ecx, C0000101 /* GS.base msr 地址 */
mov edx, FFFFF800
mov eax, 0F801000
wrmsr /* 写 GS.base */
--------------------------------------------------
上面代码为 GS.base 提供 0xFFFFF8000F801000 地址。
另一种方法是使用 swapgs 指令,这条指令将 kernelGS 地址与 GS.base 交换。
6.1.2、 descriptors 结构
x64 体系已经不提供对 segmentation 的支持(或者说最大程度削弱了),对于 segment descriptor 来说,还是停留在 x86 的阶段,绝大部分的功能已经去掉。但是对于 system descriptor 来说,它是被扩展为 16 个字节,是 128 位的数据结构。
因此,descriptors 结构要分两部分来看。
1、segment descriptors
包括 code segment descriptor 和 data segment descriptor,code segment descriptor 除这几个属性: P 位、C 位、D 位、L 位以及 DPL 外,其它都是无效的(当然 code segment descriptor 的 S 位为 0 表示 segment descriptor,code / data 位为 1 表示 code segment)。
data segment descriptor 除了 P 属性外,其它都是无效的(当然 data segment descriptor 的 S 属性为 0,code/data 属性为 0)。
segment descriptor 还是 8 个字节 64 位的数据结构,没有被扩展为 16 个字节,根本的原因是 base 域是无效的。
2、 system descriptors
包括 LDT descriptor、TSS descriptor 。这些 descriptor 被扩展为 16 个字节共 128 位。descriptor 的 base 域被扩展为 64 位值。用来在 64 位的线性地址空间中定位。
在 64 bit 模式下,LDT / TSS descriptor 被扩展为 64 位的 descriptor,base 是 64 位值。在 compatibility 模式下,LDT / TSS 依旧是 32 位的 descriptor。
3、 gate descriptor
long mode 下不存在 task gate。所有的 gate(call、interrupt / trap) 都 64 位的。gate 所索引的 code segment 是 64 位的 code segment(L = 1 && D = 0)
情景提示: 1、long mode 下的 segment descriptor 与 x86 原有的 segment descriptor 格式完全一致,只是在 64 bit 模式中 descriptor 的大部分域是无效的。 2、64 bit 模式下的 system descriptor 被扩展为 16 个字节。由于 system descriptor 中的 base 是有效的,base 被扩展为 64 位,故 system descriptor 被扩展为 128 位。 |
6.1.3、 descriptor table
1、long mode 下的 GDT / LDT 表
试想一下,以下情景:
当 processor 处于 long mode 下,而要执行的目标代码是 32 位的,也就是说 processor 将要进入 compatibility mode(兼容模式)去执行 x86 的 32 位代码,此时变得有玄妙 ... ...
1、首先,processor 处于 long mode,这表明此时:processor 既可以执行 64 位代码也可以进入 compatibility mode 去执行 32 位代码。什么情形下进入 compatibility mode 呢?这是根据目标代码的 descriptor 的 L 属性,目标代码的 descriptor 加载进入 CS 后,即 CS.L 决定是 64 bit mode 还是 compatibility mode。
当 CS.L = 1 时,processor 处于 64 bit mode, CS.L = 0 时,processor 处于 compatibility mode。
2、其次,一个既可执行 64 位代码,又可 32 位代码的 64 位 OS ,它的核心 kernel 及系统服务例程、相关的系统库是运行在 64 bit 模式下的。原有的 32 位代码运行 compatibility 模式下。
所以,在 GDT 表中可能同时存在 32 位的 descriptor 和 64 位的 LDT/TSS descriptor、64 位的 call gate descriptor。
3、最后,processor 是怎样识别哪个是 32 位的 descriptor?哪个是 64 位的 call gate descriptor ?在 32 与 64 位 descriptor 相互存在的 descriptor table 里又是如何去正确读取所要的 descriptor ?
-----------------------------------------------------------------------------------------------
在 long mode 下所有的 gate 都是 64 位的,所有的 segment descriptors 都是 32 位的。processor 根据 descriptor 的 types 来判断哪些是 64 位,哪些是 32 位。
这样需要解决一个 32 位 descriptor 与 64 位 descriptor 重叠的问题,思考以下的指令:
call [call_gate] /* call gate */
若 [call_gate] 里放着的 selector 是 0x20,若它是 64 位的 call gate descriptor selector。那么 processor 将进入 64 位模式执行 64 位代码。这个 selector 中的 SI 值是 4,所以 GDT 表中的第 4 项是个 64 位的 call gate descirptor。
这个 64 位的 call gate descriptor 结构有 128 位宽。64 位的 base 中,低 32 位 base 在前 8 个字节里,高 32 位 base 在后 8 个字节里的前2 个字节。
在这种情形下,当程序中,使用了以下指令,意图去访问 32 位的 code segment descriptor 时,就会产生问题:
call 0x28:00000000 /* 意图访问 32 位的 code segment */
这个 selector 的 SI 是 5,所以 processor 刚好访问到 64 位的 call gate descriptor 的高半部分。这个高半部分是 base 的高 32 位值。当程序中以 32 位 code segment descriptor 形式访问 64 位的 call gate descriptor 高半部分,这时 32 位的 code segment descriptor 与 64 位 call gate 的高半部分重叠了,这根本不是想要访问的 32 位 code segment descriptor,这就产生了不可预测的结果。
也就是说:以 64 位 gate 的高半部分作为 32 位的 descriptor 访问将现出不可预制的结果。
情景提示: 这个原因产生的根本原因是:GDT 的索引是固定为以 8 字节宽度进行索引。在 64 位模式下,不会以 16 字节为宽度进行索引,还是以 8 字节宽度进行索引,这导致 32 位的 descriptor 可能会与 64 位 descriptor 部分重叠了。 |
解决这个问题的设计思想是,将 64 位的 gate descriptor 或 LDT descriptor、TSS descriptor 的高半部分的对应 descriptor 的 type 的域置为 0000。
当 type = 0000 时,表示这个 descriptor 是非法的,这样会引发 #GP 异常。
所以,当上述的情形下,64 位的 call gate desciptor 的高半部分的 type 域定义为 0000 时,以 32 位 descriptors 方式访问 64 位 call gate 的高半部分时,将产生 #GP 异常。这样将避免产生不可预测的结果改变产生 #GP 异常,从而提醒软件设计人员进行更改。
这种实施策略相当于:
char *p = 0;
*p = 'a'; /* 产生异常 */
--------------------------------------------
OS 将反馈给程序员这将产生异常,目的是将不可预测的结果改为可预测的错误结果。从而避免错误。
2、 long mode 下的 IDT 表
前面提到,在 long mode 下:GDT/LDT 中是以 8 个字节进行索引的。那么:在 IDT 中则是以 16 个字节索引的。
情景提示: 1、GDT / LDT 是以 8 个字节进行索引 descriptors。 2、IDT 是以 16 个字节进行索引 descriptors。 |
造成这种在不同 descriptor table 中按不同 size 进行索引的根本原因是:
1、在 GDT / LDT 中,可以同时存放 32 位与 64 位的 descriptors,这些 descriptors 包括:32 位的 segment descriptors、64 位的 system descriptors(LDT descriptors、TSS descriptors),还有就是 64 位的 call gate descriptors。
所以,在 GDT / LDT 中只能以原来的 8 个字节进行索引,那么在 64 位的 system descriptors 中,必须在其高 8 字节的 type 属性里设为: type = 0000 以防止 32 位 descriptors 与 64 位 descriptors 重叠而产生问题。
2、在 IDT 中,只能存放 64 位的 descriptors,所以不像 GDT / LDT 那样需要兼容 32 位的 descriptors。因此,在 IDT 中固定以 16 个字节进行索引 descriptor entrys。
IDT 是不能存放 call gate descriptors 的,只能存放 interrupt /trap descriptors 。task gate 在 long mode 下不存在。
阅读(1456) | 评论(0) | 转发(0) |