首页 > 代码库 > [转载]通过 call gate 访问目标 code segment

[转载]通过 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 处的代码。

[转载]通过 call gate 访问目标 code segment