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