首页 > 代码库 > [转载]通过 call gate 访问目标 code segment
[转载]通过 call gate 访问目标 code segment
直接 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 处的代码。
[转载]通过 call gate 访问目标 code segment