直接调用/跳转的形式是:
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 处的指令。