直接 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 处的代码。