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

本文详细解析了在不同模式下,直接调用/跳转指令的处理流程,包括索引代码段描述符、权限检查、加载描述符以及执行目标代码段的过程,并特别对比了64位模式下的差异。

转自:点击打开链接

直接调用/跳转的形式是:

  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 处的指令。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值