首页 > 代码库 > [转载]使用 call/jmp 直接调用/跳转目标 code segment

[转载]使用 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 处的指令。

[转载]使用 call/jmp 直接调用/跳转目标 code segment