免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: mik

【x86 & x64 沉思录】(6.7 更新) [复制链接]

论坛徽章:
0
发表于 2009-04-06 22:46 |显示全部楼层
话题 7、    x86 & x64 提供的保护措施


  x64(x86)提供了 segmentation 与 paging 级的保护措施,x86 提供完整的 segmentation 保护措施,而 x64 提供了有限的 segmentation 保护措施。

  对于 segmentation 级的保护来说,processor 定义了 4 个权限级别,为:0 ~ 3 级,0 为最高级别,3 为最低级别。0 级能访问所有的 processor 资源,如:所有指令、I/O的访问权限,一些系统的管理资源等。3 级只能有限度的访问 processor 资源。
  对于 paging 级的保护来说,processor 实际上只使用了 2 个权限级别,为:supervisor 与 user 级别。3 级就是 user 权限,而 0 ~ 2 都划分为 supervisor 权限。在相应的 page table entry 结构中只有 1 位表示 supervisor /user 权限。




7.1、  segmentation 级保护措施

  在 segmentation 阶段控制访问权限时,定义了 3 个权限级别类型:RPL、CPL 以及 DPL。

RPL(Requestor Privilege Level):在使用 selector 时定义的权限级别类型,表示:你将使用什么样的权限去访问数据。用哪个级别的权限取决于你。在主动发起访问时,使用 0 级还是 3 级去访问完全取决于你。

CPL(Current Privilege Level):表示当前 processor 运行于哪个权限级别。在 Intel 的文档里表明:CPL 存储在 CS 寄存器 selector 里的 RPL(实质上是 DPL) 域里,当发生权限的改变时会更新 CS selector 里的 RPL(实质上是 DPL) 域。没错,的确可以这样理解!
  在 AMD 的文档没有明确表明 CPL 就是 CS selector 里的 DPL,只是描述了 CPL 会存储在 processor 内部的寄存器里,但是理解为:CPL 就是 CS selector 里的 DPL 是不会错误。这里有一个很重要线索就是:在 REST# 后,CS selector 的 DPL 是 0,表明实模式的 CPL 是 0 级。

DPL(Descriptor Privilege Level):在 descriptor 里定义了 DPL,表明访问的目标将需要什么级别的权限。这代表一个访问门坎,你需要的权限至少要不能小于这个设定的门坎。
  DPL 设为 2 级,那么你最少需要的 RPL 为 2 以及 CPL 为 2。


  RPL、CPL 以及 DPL 构造了 x86 & x64 segmentation 阶段的控制访问的权限检查机制。在绝大部分情况下,需要 RPL <= DPL && CPL <= DPL 的访问权限去访问 segment descriptor 或 gate descriptor。


  processor 除了对访问权限的检查外,还要围绕 selector、descriptor table、TSS segment 以及 descriptor 等系统数据结构进行相关的检查,如:Null-selector、limit、type 等,以确保当前的执行环境是正确有效的。





:wink:

[ 本帖最后由 mik 于 2009-4-7 11:42 编辑 ]

论坛徽章:
0
发表于 2009-04-07 12:20 |显示全部楼层
7.1.1、 date segment 的访问控制

  这里的 data segment 访问控制针使用对 DS、ES、FS 以及 GS selector register 进行访问,不包括 SS selector register,stack segment 访问与一般的 data segment 有些差别。

  以 DS 为例代表所有的 data segment 访问(ES、FS 以及 GS),需要的权限条件是:RPL(DS.RPL) <= DPL 且 CPL(CS.DPL) <= DPL。
  这里的 RPL 就是 DS.RPL,即:使用 ds selector 进行 data segment 访问时,使用什么级别的 RPL。CPL 就是当前 CS selector 里的 DPL,代表 processor 处于什么权限级别。
  DPL 就是目标 data segment descriptor 所定义的访问 data segment 所需要的权限。


用简单的式子表达为:

if (RPL <= DPL && CPL <= DPL) {
  /* 通过权限检查,允许访问,加载 descriptor 进入 DS */

} else {
  /* 拒绝访问,产生 #GP 异常 */
  goto do_#GP
}


  式子中的比较表示在数字上大小关系。数字越大权限越低,所以 RPL <= DPL 是判断 RPL 权限级别是否大于 DPL 的权限级别
  RPL 和 CPL 的权限极别必须大于或等于 DPL 的权限级别。


  在得到允许访问后的后续处理:目标 data segment descriptor 将加到了 DS 寄存器中。在不改变访问的目标 data segment 的情况下,processor 不会对 DS 进行更新,也就是:不会得新加载 data segment descriptor。
  DS 寄存器保持着内部的 descriptor 记录不变,直至到更新 DS 寄存器。因此,对同一个 data segment 的访问仅需要加载 1 次 descriptor 。这种情况对于所有的 selector registers 都是一样的。


情景提示:
  当 processor 发现 descriptor 的 P 标志位为 0 时,将引发 #NP 异常。所以系统必须要设定 descriptor 的 P 标志位为 1,processor 不会对 P 标志位进行更改。
  系统将 P 标志置为 0,有时会特定的用途,大多数OS 会在软件中会引发类似 segment falut 错误提示,指示程序员犯了某些错误。





7.1.1.1、 x64 的 long mode 下 data segment 的访问

  在 long mode 下的兼容模式与 x86 下的 data segment 的控制访问检查并无两样。但是在 64 bit 模式下就完全改变了。


在 64 bit 模式下:
  processor 不对 data segment 的访问进行任何的权限检查!data segment descriptor 中仅有 P 标志位是有效的,其它都无效。也就是 DS、ES、FS、GS 以及 SS 中除 P 属性有效外,其它都是无效的。
  所有基于 DS、ES 以及 SS 的 data segment 的 base 是无效的,被强制为 0。而基于 FS 与 GS 的 data segment 的 base 是有效的,故 FS 与 GS 的 data segment 可以定位在 64 位地址空间的任何位置。



所以:
  1、所有的 data segment 的访问都不进行权限检查,包括 DS、ES、FS、GS 以及 SS。这是因为,在 64bit 下所有的 data segment descriptor 的 DPL 属性是无效的。
  2、所有的 data segment 的访问也不进行 type、limit 检查,这些属性都是无效的。64 bit 下,所有的 data segment 都是 Readable/writable 属性。所有的 data segment 都具有 64 位地址空间。
  3、所有的地址 limit 检查都变为 canonical-address 的地址形式检查。




:wink:

[ 本帖最后由 mik 于 2009-4-7 14:52 编辑 ]

论坛徽章:
0
发表于 2009-04-07 14:14 |显示全部楼层
7.1.2、  stack segment 的访问控制


  基于 stack 的访问都将引发 stack 访问控制检查,对于 stack 的访问控制比一般的 data segment 访问检查要严格。


用简单的式子表达为:

if (RPL == DPL && CPL = DPL) {
  /* 通过检查,允许访问,加载 descriptor 进入 SS */

} else {
  /* 拒绝访问,引发 #GP 异常 */
  goto do_#GP
}


  访问 stack 仅限于同级访问,RPL、CPL 以及 DPL 三者必须相等。即使 0 级代码也不能访问 3 级的 stack。

So:
  TSS segment 中为每个权限级为了相应的 stack pointer,就是基于这个原因,TSS 中设了 0 ~ 2 级的 stack pointer,但不包括 3 级的 stack pointer,3 级的 stack pointer 要么存放在 SS & RSP 中,要么存放在 0 ~ 2 级的 stack 中。
  若发生代码从 3 级切换到 0 级时,切换 stack 时,3 级的 stack pointer 被切换出来,存放在 0 级的 stack 中,而 0 级的 stack pointer 被加到 SS & RSP 中。0 级代码返回 3 级代码时,3 级 stack pointer 被重新加载到 SS & RSP 中。


通过检查的后续处理:
  和 DS 一样,stack segment descriptor 会被加载到 SS 寄存器中,SS.RPL 就代表当前运行 stack 的权限级别。当发生 stack 切换时,SS.RPL 会被更新为新的权限级别。
  



7.1.2.1、 x64 下的 long mode 的 stack segment 访问
  
  long mode 的兼容模式与 x86 原有的模式一致。 64 bit 模式下的 stack segment 访问与前述的 data segment 访问一样,processor 不会对 stack segment 的访问进行权限检查。




:wink:

[ 本帖最后由 mik 于 2009-4-7 18:05 编辑 ]

论坛徽章:
0
发表于 2009-04-07 18:04 |显示全部楼层
题外话:关于数据的访问倒底是使用哪个 selector registers 进行引用?


  对于 code segment 的访问,都知道是使用 CS selector register 。对于 data segment 的访问,则是根据指令或内存的寻址方式来决定使用哪个 selector registers。




1、对于串操作指令来说,在缺省的情况下:源串引用 DS 作为参考对象,目标串引用 ES 作为参考对象。

  loadsd       /* 从源串 ds:[esi] 中 load 到 eax 中 */
  stosd        /*  从 eax 中 store 到目标串 es:[edi] 中 */
  movsd        /* 从源串 ds:[esi] 复制到目标串 es:[edi] 中 */
---------------------------------------------------------------- 
  stosd 缺省使用 ES selector register 来引用数据,而 movsd 更是同时使用了 DS 和 ES selector register 来引用数据。




2、强制性改变缺省的参考对象。

  loadsd dword ptr fs:[esi]             /* 这里强制性地将缺省引用对象 ES 改变为 FS,这将使用 FS 作为参考对象 */
      mov eax, dword ptr es:[eax]       /* 强制使用 ES 作为 参考对象 */
-------------------------------------------------------------
  以上是使用修改指令前缀方法,强制使用指令的 selector registers 作为引用对象进行数据访问。对于 mov eax, dword ptr es:[eax] 这条指令来说:它的内存操作数 [eax] 缺省是使用 DS 来访问 data segment 的。es:[eax] 则改为使用 ES 来访问 data segment。
  将 DS 强制改为 ES 后,当然是使用 ES selector 来读取 data segment descriptor。若 ES = DS 则与缺省情况并无两样,若 ES <> DS 则 ES selector 要先通过权限检查,加载到 ES register 再作访问。




3、push / pop 指令使用 SS 作为参考对象。  
  
  push / pop 之类的指令是固定使用 SS selector register 作为引用对象的。push 指令使用 SS 作为目标操作数的引用对象。而 pop 指令使用 SS 作为源操作数的引用对象,这两个是无法改变的。
  push 指令的源操作数和 pop 指令的目标操作数,它们的引用对象则是可改的。




4、 基于 ebp 和 esp 的寻址方式使用 SS 作为参考对象
  
  mov eax, dword ptr [ebp+0x0c]     
-------------------------------------------
  ebp 作为 stack frame base pointer 它缺省是使用 SS 来访问数据的。
  实际上等于显式的: mov eax, dword ptr ss:[ebp+0x0c],当然这个情况下,可以强制改为 DS,如:mov eax, dword ptr ds:[ebp+0x0c],这样改为使用 DS 访问数据。


又如:
  mov eax, dword ptr [esp]
------------------------------------------------
  esp 作为 stack pointer 它缺省是使用 SS 来访问数据的。
  实际上等于显式的: mov eax, dword ptr ss:[esp],同样也可以强制改为:mov eax dword ptr ds:[esp] 这样就使用 DS 来访问数据。



5、除了上述几种情况下,其它都是使用 DS 作为缺省的参考对象
  
  大多数情况下,指令的内存操作数都是使用 DS 作为缺省对象来访问数据。

典型的如:
  mov eax, [eax]
  mov eax, [ebx]
  mov byte ptr [esi], 0x0c
等等... ...




6、 在 64 bit 模式下的 selector registers 使用

  在 64 bit 模式下 DS、ES、SS 是无效的,因此,已经没有该使用哪个 selector register 来访问数据这种考虑了。所有的 data segment 的 base 都是 0。

无论是:
  mov rax, qword ptr [rsp]
  mov rax, qword ptr [rbp]
还是:
  movsq
  mov rax, qword ptr [rax]
--------------------------------------------------
  它们的结果都是在基于 0 的 data segment 上进行数据访问。都是在一个平垣的内存块上进行操作。所以,无所谓 selector 是什么?segment register 的值是什么?data segment descriptor 是什么?


  但是,FS 和 GS 还是有效的。程序中依旧可以使用 FS 和 GS 访问附加的 data segment。这些附加的 data segment 的 base 值可以不为 0,也就是 FS 和 GS 依旧可以构造有限的 segmentation 功能。

论坛徽章:
0
发表于 2009-04-07 23:45 |显示全部楼层
题外话:x64 的 canonical-address 地址形式


  canonical-address 地址形式就是:64 位的 linear address(virtual address)中的从 MSB(Most Significant Bit)最高有效位到第 63 位全是 0 或 1 的这种形式。

看下面的地址:
  FFFF8010_bc001000:这是符合 canonical-address 的地址,MSB 是 bit47,值为 1。 而 bit63 ~ bit48 全是 1

  00007c80_b8102040:这是符合 canonical-address 的地址,MSB 是 bit47,值为 0。 而 bit63 ~ bit48 全是 0


  实质上,canonical-address 地址中 bit63 ~ bit48 是符号扩展位。bit47 是 64 位地址中最高能表示的位(MSB)。



1、canonical-address 地址形式产生的前提

  当前的 x64 体系中,64 位的 virtual address(linear address)仅实现了 48 位 virtual address,剩下的高 16 位仅仅是作为符号扩展,组成最终的 64 位 virtual address。
  高 16 位是符号位加上 48 位真正的 virtual address 组成 64 位 virtual address,这种地址形式就是 x64 体系中的 canonical-address 地址形式。
  之所以这样做,是考虑到以后实现 52、56、60 以及真正的 64 位 virtual address 时,这种地址形式(48 位+16 符号位)无需做出任何的改变就可以平滑的兼容(52、56、60 或 64 位 virtual address)。
  即:48 位 + 16 符号位这个实现,既可以看成是 52 位 + 12 位符号,也可看成是 56 位 + 8 位符号位,或看成是 60 位 + 4 位符号位,或看成全 64 位 virtual address。这对于软件层面来看完全没有任何影响。
  实现的不同只是 processor 才能感受到,processor 在做 virtual address 转到为 physical address 才有影响。



2、非 canonical-address 地址形式检查

  processor 会对软件上的非 canonical-address 地址形式进行检查,程序中使用了非 canonical-address 会产生 #GP 异常。

象下面这种情形:
   mov rax, qword ptr [0x11223344557788]
--------------------------------------------------------------
  这里 [0x1122334455667788] 明显不是 canonical-address 地址,processor 检查到将会产生 #GP 异常。

  又如:64 bit 下 processor 不会对 descriptor 的 limit 进行任何检查,但会以 canonical-address 地址检查为代替。若 gate descriptor 中的 offset 值是非 canonical-address 地址,processor 检测到就会产生 #GP 异常。等等诸如之类的检查。

论坛徽章:
0
发表于 2009-04-07 23:58 |显示全部楼层
7.1.3、 目标 code segment 的访问控制

  当程序中使用指令 call / jmp,以及通过 int 引发中断例程的执行,这将都是对目标的 code segment 进行访问,当通过权限的检查后程序将会跳转到目标的 code segment 进行执行。
  在 code segment 的访问过程中涉及到权限级别的改变,stack 的改变等问题。


访问目标 code segment 的几种情形:

1、call / jmp offset 
  在段内直接 call / jmp,不改变目标 code segment

2、call / jmp code_selector:offset 
  直接 call / jmp 目标 code segment

3、call / jmp callgate_selector:offset 
  使用 call gate 的 call / jmp 形式

4、call / jmp tss_selector:offset  
  使用 TSS gate 的直接 call / jmp 形式,使用了 TSS 任务切换机制

5、call / jmp taskgate_selector:offset
  通过 task gate 使用 TSS 任务切换机制

6、int n
  使用 int 指令调用 interrupt / trp 例程,或者 task gate 提供的任务切换机制

7、使用 syscall / sysret、 sysenter / sysexit 指令快速调用系统服务例程

8、ret 或 iret
  利用 ret 以及 iret 指令构造另一种访问 code segment 的途径。


  以上是程序中主动发起访问目标 code segment 大多数方法,在继续执行之前,processor 会进行一系统的检查,包括相关的权限检查、type 检查、limit 检查等,通过了检查后,加载到 cs:rip 后继续执行。



.

[ 本帖最后由 mik 于 2009-4-8 12:16 编辑 ]

论坛徽章:
0
发表于 2009-04-08 15:33 |显示全部楼层
7.1.3.1、 call/jmp offset  段内调用

段内的调用/跳转的特性:

  不改变 CS,也就是不用重新加载 code segment descriptor,当然也就不需要进行权限的检查,但是这里需要做 segment 的 limit 检查,检测到越 segment limit 会产生 #GP 异常。
  由于在段时调用,所以使用 call offset 时,processor 仅会做 push eip 的处理,不会做 push cs。在执行 ret 返回时,processor 同样会做 limit 检查。


对于 call 来说,offset 值可以是:

1、16 位的 displacement 值、32 位的 displacement 值,这个 displacement 是个符号数。
  在 64 bit 模式下,rip = rip + disp32,这个 disp32 值会符号扩展为 64 位再加上 rip。

2、16 位、32 位的内存操作数,这个内存操作数支持 x86 的所有内存寻址模式。在 64 bit 模式下支持 64 位的内存操作数。
  16 位、32 位的寄存器操作数。在 64 bit 模式下支持 64 位的寄存器操作数。

3、64 bit 模式下还支持新增的 rip-relative (RIP 相对寻址)寻址模式。

论坛徽章:
0
发表于 2009-04-08 17:20 |显示全部楼层
7.1.3.2、 使用 call/jmp 直接调用/跳转目标 code segment

直接调用/跳转的形式是:
  call / jmp selector:offset

  这里的 selector 是 code segment selector 直接使用 selector 来索引 code segment,这将引发 CS 的改变,code segment descriptor 最终会被加载到 CS 寄存器里。
  在 code segment descriptor 加载到 CS 之前,processor 会进行一系列的检查,包括权限检查、type 检查、limit 检查等,在通过检查后,processor 才加载 descriptor 到 CS,紧接着 eip = CS.base + offset,最后跳转到 cs:eip 执行。



以下面的指令为例:

(1)  call  0x20:0x00040000
(2)  jmp 0x20:0x00040000

0x20 是目标 code segment selector ,看看 processor 如何处理。




1、索引 code segment descriptor

  selector:0x20 的 RPL = 00,TI = 0,SI = 4
  processor 在 GDT 以 SI = 4 索引查找 descriptor,当查找到 descriptor,processor 将判断这个 descriptor 的 types 是什么,再做进一步的处理。


这个查找 descriptor 的过程表述如下:

RPL = 00;
TI = 0;
SI = 4;


if (TI == 0)
  DT = GDT;                 /* 在 GDT 表 */
else
  DT = LDT;                 /* 在 LDT 表 */


temp_descriptor = DT.base + SI * 8;               /* 获取 descriptor */


switch (temp_descriptor.type)
{
case CODE_DESC:                    /* 是个 code segment descriptor */
  goto do_code_desc;

case CALL_GATE:                    /* 是个 call gate descriptor */
  goto do_call_gate;     

case TSS_DESC:                     /* 是个 TSS descriptor */
  goto do_tss_desc;            

case TASK_GATE:                   /* 是个 task gate descriptor */
  goto do_task_gate;

default:                           /* 若不是上述几种类型,则产生 #GP 异常 */
  goto do_#GP_exception;               
};


  processor 在判断 descriptor 后作进一步处理,这里假设 descriptor 是 code segment descriptor,下一步是 processor 将作权限的检查,检查程序是否有权限访问目标 code segment。

  在上述获取 descriptor 之前,processor 还会对 GDT 的 limit 作检测,若发现 GDT.base + SI * 8 > GDT.limit 同样会引发 #GP 异常。这种情况也就是说:索引值越界了。




2、权限 check

  processor 用当前的权限与目标 code segment descriptor 作的权限 check。当前的权限就是 RPL & CPL。在这直接调用/跳转目标 code segment 的 check 中 conforming 与 nonconforming 类型的 descriptor 有着很大的区别。



这个权限的 check 表述如下:

DPL = temp_descriptor.DPL;

if (temp_descriptor.C == 0) {    /* code segment 是 non-conforming 类型 */

  if (CPL == DPL) {

    if (RPL <= DPL) {
      goto do_next;            /* 通过检查,允许访问 */  

    } else
      goto do_#GP_exception;

  } else
    goto do_#GP_exception;     /* 产生 #GP 异常 */


     
} else {                        /* code segment 是 conforming 类型 */

  if (CPL >= DPL) {
    goto do_next;            /* 通过检查,允许访问 */  
  } else
    goto do_#GP_exception;           /* 产生 #GP 异常 */
}


当 code segment 是 non-conforming 类型时,需要 CPL == DPL && RPL <= DPL 才能通过。
当 code segment 是 conforming 类型时,仅需要 CPL >= DPL 就能通过了。

当 code segment 是 conforming 类型时,CPL >= DPL,表示当前的代码可以向高权限级别跳转。这里无需判断 RPL 权限。
  假设当前运行在 3 级代码上,通过 call / jmp 到 conforming 类型的 0 级别代码时,当前的 CPL 依然是 3 级。因为在直接 call/jmp 目标 code segment 这种调用方式上,是不会改变当前的运行级别。


情景提示:
  在直接 call/jmp 目标 code segment 方式上,CPL 是不会改变的。既使由低权限代码调用高权限的 conforming 类型的代码,CPL 也不会改变。
  在由低权限直接 call/jmp 高权限的代码仅限于 conforming 类型的 code segment。


  conforming 类型的 code segment 允许低权限的代码向高权限的这类代码调用/跳转,而 non-conforming 则不允许直接调用/跳转。直接 call/jmp 目标 code segment 不改变 CPL,基于这个原因 non-conforming 类型的 code segment 必须要 CPL == DPL。
  若要向高权限的 non-conforming 类型 code segment 调用/跳转时,必须通过 call gate 进行 call / jmp。




3、加载 descriptor

  通过上述权限检查后,processor 会将目标的 selector 加载到 CS 寄存器中,而 descriptor 也会加载到 CS 寄存中。



加载 descriptor 过程表述为:

selector = selector | (SI << 3) | (TI << 2) | CS.selector.DPL;

CS.selector = selector;                          /* 加载 selector */

CS.base = temp_descriptor.base;                     /* 加载 base 进入 CS*/
CS.limit = temp_descriptor.limit;                  /* 加载 limit 进入 CS */
CS.attribute = temp_descriptor.attribute;         /* 加载 attribute 进入 CS */


selector = selector | (SI << 3) | (TI << 2) | CS.selector.DPL; 
-----------------------------------------------------------
  在这一步里,使用目标 code segment selector 的 SI 更新 CS.selector.SI,使用 TI 更新 CS.selector.TI。
  但是这里不更新 CS.selector.DPL,因为 CPL 不会改变。

  CS 内的信息(selector & descriptor)会保持下去,直至下一次重新加载 descriptor 到 CS 为止。所以,在同一 code segment 内的 call/jmp 是不会做权限检查等等。




4、执行目标 code segment

  processor 会加载 CS.base + offset 进入 eip ,然后执行 CS: eip 处的代码。这个 offset 就是 call/jmp 指令的 eip 值,也就是上述的 0x00040000 值。



push old_cs;
push old_eip;

eip = CS.base + offset;         /* 加载 eip */

(void (*)()) &eip;                      /* 执行 cs: eip */



  由于这里不会改变 CPL,所以也无需做检测是否需要 stack 切换的工作。






7.1.3.2.1、 long mode 的 64 bit 模式下的直接 call / jmp

  在 64 bit 模式下不支持 call/jmp selector:offset 这种指令形式,在 64 bit 模式下,这种形式将引发 #UD 异常。

  
在 64 bit 模式下仅支持:

  call far ptr [target_code]

  jmp far ptr [target_code]
---------------------------------------
  仅支持目标是内存操作数的指令形式。当然这个内存操作数可以是任一种内存寻址模式。
如:
  call far ptr [rax+rcx*8+0xc]
  call far ptr [rip+0x80140]

  指令从 [target_code] 中取出 32 位的 offset 和 16 位 selector。 32 位的 offset 被零扩展至 64 位再加上 rip。


情景提示:
  Intel 明确说明: call far ptr [target_code],在 [target_code] 中可以直接读取 64 位的 offset 值和 16 位 selector 值。当编译机器码为:48 ff /3 时可以支持 64 位 offset 值 + 16 位 selector。
  AMD 则明确说明:当 operands 为 64 位时,读取的仅是 32 位的 offset 值 + 16 位的 selector,32 位的 offset 将零扩展至 64 位的 offset。



情景提示:
  Intel 说的是在指令编码中使用 REX.W 将 operands 扩展为 64 位,则读取的是 64 位 offset。AMD 的文档中没有说明当使用 REX.W 将 operands 扩展为 64 位时 call far 指令将会读取多少?
  但是,在调试器 x64 版的 windbg 里实验表明:使用 REX.W 确实可以将 call far 指令扩展为读取 64 位 offset + 16 位的 selector 。




processor 的处理过程:
  
1、索引 code segment descriptor 的方法和 x86 的一致。但和 x86 下不同的是:
  (1)、64 bit 下不存在 task gate
  (2)、若使用 selector 查找到的 descriptor 是 TSS descriptor 将产生 #GP 异常。
  (3)、64 bit 下不进行 limit 的 check。
  (4)、64 bit 下 processor 将检测 code segment descriptor 的 L = 1 &&  D = 0,表明目标代码是 64 位代码,若 L = 0 或者 D = 1 则产生 #GP  异常





2、权限的 check  

64 bit 的权限 check 和 x86 的一致,即:

if (non-conforming == 1) {   /* 是 non-conforming 类型 */

  if ( CPL == DPL && RPL <= DPL)
    /* 通过,允许访问 */
  else
    /* 失败,拒绝访问,产生 #GP 异常 */


}  else {        /* 是 conforming 类型 */

  if (CPL >= DPL)
    /* 通过,允许访问 */
  else
    /* 失败,拒绝访问,产生 #GP 异常 */
}




3、加载 descriptor 进入 CS

  由于 64 bit 模式下,code segment descriptor 中仅 L、D、DPL、C 及 P 属性有效,其它都无效的,这一步意义不大。CS.base 和 CS.limit 都是无效的。base 被强制为 0,limit 是固定的 64 位空间。
  代替的是进行 canonical-address 地址检查。

  此时,CPL 也不会改变,即:CS.selector.DPL 不会被更新。所以也不会引发 stack 切换。



4、执行 code segment

  接下来 64 位的 offset 值被加到了 rip 寄存器中,然后执行 rip 处的指令。







:)

[ 本帖最后由 mik 于 2009-4-8 21:09 编辑 ]

论坛徽章:
0
发表于 2009-04-09 01:48 |显示全部楼层
7.1.3.3、 通过 call gate 进访问目标 code segment

  直接 call / jmp 目标 code segment 不能改变当前的 CPL,若要 call / jmp 高权限的 code segment 必须使用 call gate,在 x86 下还要可以 call / jmp TSS descriptor 或者 call / jmp task gate,但在 64 bit 模式下 TSS 任务切换机制不被支持。



同样以下面的指令为例:

(1)  call  0x20:0x00040000
(2)  jmp 0x20:0x00040000

--------------------------------
  这里的 0x20 是 call gate selector,0x0004000 是 offset ,看看 processor 怎样处理。



1、索引 call gate descriptor 及 目标 code segment descriptor

  (1)第一步先找到 call gate descriptor,索引查找 call gate descriptor 的方法与 7.1.3.2 节中的 “索引 code segment descriptor “ 是一样的。

  (2)第二步再根据找到的 call gate descriptor,使用同样的方法用 descriptor 里的 selector 再找到目标 code segment descriptor。


两个过程表述如下:

call_gate_descriptor = get_descriptor(0x20);     /* 用 selector 0x20 先找到 call gate */

selector = call_gate_descriptor.selector;         /* 使用 call gate 中的 selector */

temp_descriptor = get_descriptor(selector);       /* 再找到 code segment descriptor */


  查找 call gate descriptor 与 code segment descriptor 的方法是一样的。根据得到的 selector 找到相应的 descriptor 结构。




2、权限的 check

  processor 检查权限,既要检查是否有权限访问 call gate,还要检查是否有权限访问 code segment。


check 过程表述如下:

DPLg = call_gate_descriptor.DPL;              /* call gate 的 DPL */
DPLs = temp_descriptor.DPL;                   /* code segment descriptor 的 DPL */


if (RPL <= DPLg && CPL <= DPLg) {                /* 检查是否有权限访问 call gate */
  /* pass */

  if (temp_descriptor.C == 0) {         /* 目标 code segment 是 non-conforming 类型 */
    if (Opcode == JMP)          /* 假如使用 jmp 指令 */
      if (CPL == DPLs) {
                        /* 通过,允许访问 */
      } else {
        goto do_#GP_exception;         /* 失败,拒绝访问,#GP 异常 */
      }
  }

  if (CPL >= DPLs) {           /* 检查是否有权限访问 code segment */
                                  /* 通过,允许访问 */
  } else {
    goto do_#GP_exception;        /* 失败,拒绝访问,#GP 异常 */
  }


} else {

  goto do_#GP_exception;                 /* 失败,拒绝访问 #GP 异常产生 */
  
}



  代码中,DPLg 代表 call gate descriptor 的 DPL,DPLs 代表目标 code segment descriptor 的 DPL。


检查通过的条件是:

  (1)(RPL <= DPLg)  &&  (CPL <= DPLg) 表示有权访问 call gate。
并且:
  (2)CPL >= DPLs  表示有权访问 code segment,表示:只允许低权限向高权限转移,或者平级转移。不允许高权限向低权限转移。

-------------------------------------------------------
在第(2)步的条件里:
  假如使用 call 指令:则无论是 conforming 类型还是 non-conforming 类型的 code segment,都可以成功通过。
  假如使用 jmp 指令:目标是 conforming 类型 code segment 可以通过。但是目标是 non-conforming 类型的 code segment 的情况下,必须:CPL == DPLs(CPL 必须等于 code segment descriptor 的 DPL)才能通过。

  这是基于 jmp 指令访问 non-conforming 类型的代码不改变 CPL 的原因。       


所以,这两个条件是:
  (1)RPL <= DPLg && CPL <= DPLg
并且:
  (2)CPL >= DPLs (call/jmp conforming 类型或者 call non-conforming 类型)
    或:
     CPL == DPLs (jmp non-conforming 类型)



  call gate 用来是建立一个保护的系统例程机制,目的是由低权限的代码调用高权限的系统例程。所以:CPL >= DPLs,当前的 CPL 权限要低于 DPL 权限。
  conforming 类型 code segment 的目的是可以由低权限向高权限代码转移。non-conforming 类型则要求严格按照规定的权限进行。




3、加载 descriptor 进入 CS

  同样,通过权限 check 后,processor 会加载 selector 和 descriptor 进入 CS 寄存器。但是,在一步里 processor 的额外工作是判断是否进行 CPL 改变。


  假设当前代码是 3 级,目标代码是 0 级,则发生权限的改变,CPL 改变也导致 3 级的 stack 切换到 0 级的 stack。

加载 descriptor 的表述如下:

CS.selector = temp_descriptor.selector;       /* 加载目标 code segment 的 selector */

CS.selector.DPL =  temp_descriptor.DPL;       /* 更新 CPL */

CS.base = temp_descriptor.base;
CS.limit = temp_descriptor.limit;
CS.attribute = temp_descriptor.attribute;


  CS.selector.DPL = temp_descriptor.DPL;
  由于权限的改变,CPL 需要更新,因此目标 code segment 的 DPL 将被更新至 CS.selector 的 DPL(或者说 RPL)中。




4、 stack 的切换

  由于 CPL 的改变,导致 stack pointer 也要进行切换。新的 stack pointer 在 TSS 中相应权限级别的 stack pointer 中获取。

  接上所述,stack 将由 3 级切换至 0 级。


stack 的切换表述如下:

DPL = temp_descriptor.DPL;

old_ss = SS;
old_esp = esp;

SS = TSS.stack_pointer[DPL].SS;                    /* 加载 SS */
esp = TSS.stack_pointer[DPL].esp;                /* 加载 esp */


push(old_cs);
push(old_esp);


if (call_gate_descriptor.count) {
  copy_parameter_from_old(call_gate_descriptor.count, old_ss, old_esp);
}


push(old_cs);
push(old_eip);




stack 切换主要做以下 5 个工作:

(1)用 code segment descriptor 的 DPL 来索引相应的级别 stack pointer(0 级)
(2)将索引找到的 stack pointer(0 级) 加载到 SS 和 ESP 寄存器,当前变为 0 级的 stack pointer。
(3)将原来的 stack pointer(0 级) 保存到新的 stack 中。
(4)如果 call gate 中的 count 不为 0 时,表示需要传递参数。
(5)保存原来的 CS 和 EIP

----------------------------------------------
  上面代码中的红色部分是判断 call gate 中是否使用了 count 域来传递参数。复制多少个字节?复制 count * sizeof(esp) 个字节。参数会被复制到新的 stack 中,也就是 0 级的 stack 中,以供例程使用。

  在将 SS selector 加载到 SS 寄存器时,processor 同样要做权限的检查。CPL 已经更新为 0,SS selector.RPL == 0 && stack segment descriptor.DPL == 0,所以条件:CPL == DPL && RPL == DPL 是成立的,新的 SS selector 加载到 SS 寄存器是成功的。

  SS selector 加载到 SS ,processor 会自动加载 stack segment descriptor 到 SS 寄存器,SS.selector.RPL 就是当前的 stack 的运行级别,也就是 0 级。
  旧的 SS selector(3 级) 被保存在 0 级的 stack 中,在例程返回时,会重新加载 old_SS 到 SS 寄存器,实现切换回原来的 stack pointer。



5、执行系统例程 code segment

  成功加载 CS 和 SS 后,EIP 将由 call gate 中的 offset 加载。


执行例程表述为:

eip = call_gate_descriptor.offset;              /* 加载 eip */

(void (*)()) &eip;                                /* 执行例程 */



  由于例程的入口地址在 call gate 中指定,所以指令中的 offset 是被忽略的。

指令:
  call 0x20:0x00040000
------------------------------------
  指令中的 offset 值 0x0004000 将被 processor 忽略。真正的 offset 在 call gate 中指出。但是从指令格式上必须给出 offset 值,即:cs:eip 这个形式对于 call far 指令来说是必须的。










7.1.3.3.1、 long mode 下的 call gate


指令:
  call 0x20:0x00040000

---------------------------------------
  当前 processor 运行在 long mode 的 compatibility 模式下,这条指仅是有效的。若在 long mode 的 64 bit 模式下,有条指令是无效的,产生 #UD 异常。

在 64 bit 模式下:

指令:  call far ptr [mem32/mem64]
--------------------------------------
  这种形式的 far call 才被支持。memory 操作数可以是 32 位 offset + 16 位 selector 或者 64 位 offset + 16 位 selector



所以最终的指令形式是:

(1) call far ptr 0x20:0x00040000    或  call far ptr [call_gate64]        /* compatibility 模式 */

(2) call far ptr [call_gate64]                                                       /* 64 bit 模式 */




情景提示:
  long mode 下仅允许 64 位的 gate 存在。无论是 compatibility 模式还是 64 bit 模式,都不允许 32 位的 gate 存在。


  因此 long mode 下 call gate 是 64 位的 call gate(共 16 个字节),offset 被扩展为 64 位。processor 会对 gate 中的 selector 指向的 code segment 进行检查。64 位的 call gate 指向的 code segment 必须是 64 位 code segment,即:L = 1 并且 D = 0。
  processor 若发现 L = 0 或者 D = 1 将会产生 #GP 异常。

情景提示:
  由于 long mode 的 gate 是 64 位的,当在 compatibility 模式下的 32 位代码执行调用 call-gate 执行系统服务例程,或由中断指令 INT n 陷入中断服务例程时,执行的是 64 bit 的系统服务例程(64 位的 OS 组件)。





  因此,0x20 是一个 64 位 call gate 的 selector。


1、获取 call gate 和 code segment。

  processor 对 call gate 的索引查找以及 code segment 的索引查找和 x86 下是一样的。见:上述第 1 步。



2、processor 对 call gate 和 code segment 的检查

  在索引到 call gate 后,processor 会对 call gate 首先进行检查,包括:

(1)检查 call gate 的高半部分的 types 是否为 0000,不为 0 则产生 #GP 异常。
(2)检查 call gate 中的 selector 指向的 code segment 是否为 L = 0 并且 D = 0,表明目标 code segment 是 64 bit 的。否则产生 #GP 异常。



3、权限的 check

  与 x86 下的 call gate 检查机制一样。

即:
(1)RPL <= DPLg && CPL <= DPLg  (访问 gate 的权限)
(2)CPL >= DPLs     (call/jmp conforming 类型或者 call non-conforming 类型)
  或:CPL == DPLs    (jmp non-conforming 类型)



同样:CPL >= DPLs  表明由低权限调用高权限代码
   CPL == DPLs  表明不能改变 CPL,这个情况是由 jmp non-conforming 时产生。



4、加载 code segment descriptor

  同样,目标 code segment 的 selector 和 descriptor 将被加载到 CS 寄存器中。

情景提示:  
  在 64 bit 模式下仅 CS.L、CS.D、CS.DPL、CS.C 以及 CS.P 是有效的,其它属性和域都是无效的。

  可是,即使 processor 当前处于 compatibility 模式下,在使用 gate 的情况下,加载到 CS 的结果和 64 bit 模式下是完全一样的。因为:在 long mode 下 gate 是 64 位的,所使用的目标 code segment 也是 64 位的。


  因此,当 CS 加载完后:CS.L = 1、CS.D = 0。
即:
  此时 processor 由 compatibility 模式切换到 64 bit 模式
  当系统服务例程执行完毕返回时,processor 会由 64 bit 切换回到 compatibility 模式,直至最软件退出返回 OS,最终 processor 再次切换回到 64 bit 模式。


code segment descriptor 在 long mode 下的意义是:

(1)建立一个 segmentation 保护机制。
(2)控制目标 code segment 是 compatibility 模式还是 64 bit 模式。



同样,若发生权限的改变,CPL 需要更新,stack 也需要切换。假设当前的代码为 3 级调用 0 级的代码:

(1)CS.selector.DPL = temp_descriptor.DPL  (使用目标 code segment 的 DPL 更新 CPL)
(2)接下着进行 stack 的切换




5、stack 切换

  经由 call-gate 转到服务例程,此时 processor 必定处于 64 bit 模式下。发生权限的改变时,processor 从 TSS 里取出相应级别的 stack pointe,即:RSP。此时的 TSS 是 64 位的 TSS


这个过程表述如下:

DPL = temp_descriptor.DPL;         

old_ss = ss;
old_rsp = rsp;

ss = NULL;                                  /* NULL selector 被加载到 ss */
ss.selector.RPL = DPL;                     /* 更新当前的 stack 的级别 */
rsp = TSS.stack_pointer[DPL];              /* 索引到相应的 rsp,加载到 rsp 中 */

push64(old_ss);
push64(old_rsp);

push64(old_cs);
push64(old_rip);



在这里的 stack 切换中注意:

(1)SS 被加载为 NULL selector(0x00)。
(2)SS.selector.RPL 需要被更新为 CPL,指示当前的 stack 级别。

------------------------------------------------------------

在由 compatibility 模式切换到 64 bit 模式的情况下:

(1)原来的 SS 和 32 位的 ESP 被扩展为 64 位压入 stack 中。
(2)同样,原来的 CS 和 32 的 EIP 被扩展为 64 位压入 stack 中。
(3)返回后,SS 和 32 的 ESP 被加载回 SS 和 ESP,RSP 的高半部分被抛弃。
(4)同样,CS 和 32 的 EIP 被加载回 CS 和 EIP,RIP 的高半部分被抛弃。





6、执行系统服务例程

  processor 从 call gate 处获取 offset 值,加载到 RIP 中,从而执行 RIP 处的代码。










:)

[ 本帖最后由 mik 于 2009-4-10 20:05 编辑 ]

论坛徽章:
0
发表于 2009-04-12 00:44 |显示全部楼层
题外话: long mode 模式下 system descriptor 与 gate descriptor 的疑惑


1、 32 位的 system descriptor 与 64 位的 system descriptor

(1)compatibility 模式下,LDT / TSS descriptor 还是原来的 32 位的 descriptor,与原来 x86 的 LDT / TSS 意义一致。
(2)64 bit 模式下,LDT / TSS descriptor 扩展为 64 位的 descriptor。 descriptor 的 type 被相应改烃。由原来的 32 bit-LDT 改为 64 bit-LDT,available/busy 32bit-TSS 改变 available/busy 64bit-TSS。

  一个全新 64 位 OS 在开启和激活 long mode 后 转入 64 bit 模式,在 64 bit 模式下必须使用 LTR 指令来加载 available 64-TSS 来建立一个 64 位的 TSS 环境。available 64-TSS 加载后,被 processor 置为 busy 64-TSS 且不会再置回为 avalilable 64-TSS。
  所以,无论是在 compatibility 模式还是在 64 bit 模式,使用的都是 64 位 TSS,但是在 compatibility 模式下 TSS 还是被认为是 32 位的 TSS,其行为如同 x86 下,若在 compatibility 下使用 LTR 指令只能加载 32 位的 TSS。64 bit 下使用 LTR 指令加载的是 64 位的 TSS。在 OS 初始化时,实际上的 TSS 环境是 64 位 TSS。
  因此,在 compatibility 下实际上还是使用的是 64 位的 TSS 环境。

  long mode 下不支持 TSS 的任务切换机制,因此,将不支持使用 TSS selector 来 far call 进行任务切换机制。

(3)不能使用 TSS 的任务切换机制,导致了不能加载 LDT 。而 LLDT 指令在 OS 初始化时进行,加载的是 64 位的 LDT。


  
2、64 位的 gate(call-gate、interrupt-gate 及 trap gate)

  long mode 下不支持 task gate。在 long mode 模式下,无论是 compatibility 模式还是 64 bit 模式,所有的 gate 都是 64 位的。gate 指向的 code segment 必须是 64 位的 code segment(L = 1 & D = 0)。
  在 compatibility 模式下使用 call-gate 来 far call 到 code segment 或者 INT n 调用系统例程。processor 将由 compatibility 模式切换到 64 bit 模式。
  在不同权限的 code segment 间执行并改变 CPL 只能通过 gate (call、interrupt/trap)达到,因此 64 位的 OS 核心必定运行在 64 bit 模式下。




:wink:

[ 本帖最后由 mik 于 2009-4-12 00:56 编辑 ]
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP