免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: mik

【x86 & x64 沉思录】(6.7 更新) [复制链接]

论坛徽章:
0
发表于 2009-04-12 02:29 |显示全部楼层
题外话: 选择 conforming 还是 non-conforming

  code segment descriptor 的 C 位表示 code segment 是 conforming 还是 non-conforming 类型,C = 1 代表 conforming 类型,C = 0 代表 non-conforming。conforming 类型可以允许低权限向高权限转移,而不需要通过 call-gate。低权限向高权限的 non-conforming 转移则必须通过 call-gate。

显然,使用 non-conforming 比使用 conforming 更为安全,因为:

原因 1、当 far call/jmp 到 code segment 直接跳转时(使用 code segment selector):

(1)选择 conforming 类型仅需要 CPL >= DPL 权限就可以了,也就是说:可以直接从低权限向高权限的 code segment 转移,但是 CPL 是不改变的。
  这样存在着隐患,由于 CPL(当前的 processor 权限)不改变,但是代码却从低权限转到了高权限,导致:若高权限的代码之中使用了高权限的指令,而 CPL 还停留在低权限上,执行这样的指令会导致异常发生,这是其一。
  其二:高权限的代码使用的是高权限的 stack,但由于 CPL 不变,导致不能发生正确的 stack 切换。

(2)而选择 non-conforming 类型需要更严格的权限,需:CPL == DPL 且 RPL <= DPL。
  很明显,使用 non-conforming 类型不能从低权限转向高权限,必须 CPL == DPL,只能要同级权限之中转移。这样就避免了不必要的隐患。



原因 2:当通过 call-gate 来 far call/jmp 到 code segment 时:

(1)选择 conforming 类型的需要权限:(CPL <= DPLg) && (RPL <= DPLg) 并且 (CPL >= DPLs)
(2)选择 non-conforming 虽然需要同样的权限:(CPL <= DPLg) && (RPL <= DPLg) 并且 (CPL >= DPLs)。
  但是,在 non-conforming 下仅允许使用 call 指令转移到高权限,不允许使用 jmp 指令转移到高权限代码。
使用 jmp call-gate 导致产生两个问题:
其一:jmp 指令不能正确返回到原调用的代码。由于没有将原 CS:RIP 入栈保存,系统服务例程 ret 时,将会出错。
其二:由于不能正确返回,可能导致不能从高权限代码返回低权限代码,processor 将一直运行在高权限代码中。

  使用 jmp call-gate 指令到 non-conforming 仅允许同级转移,即:CPL <= DPLg && RPL<= DPLg 并且 CPL == DPLs
  jmp 指令只能同级转移(不能调用系统服务例程),使用 non-conforming 类型将不允许 jmp 指令转移到高权限代码,避免隐患。  

-------------------------------------------------------------------------------

  综上所述两个原因,使用 non-conforming 类型更符合 x86 / x64 的保护机制。




:wink:

[ 本帖最后由 mik 于 2009-4-12 23:37 编辑 ]

论坛徽章:
0
发表于 2009-04-12 17:24 |显示全部楼层
7.1.3.4、 使用 TSS selector 进行任务切换

指令:
  call 0x20:00000000
  jmp 0x20:00000000
-----------------------------------
  selector 0x20 是个 TSS descriptor 的 selector。


  TSS descriptor 与 segment descriptor 的格式一致,不同的是 type,在 x86 和 long mode 的 compatibility 模式下有: available/busy 16-TSS 和 available/busy 32-TSS。
  当成功将 TSS descriptor 加载到 TR 寄存器后,processor 将 available TSS 置为 busy TSS,在返回时将 busy TSS 置为 available TSS。


1、索引查找 TSS descriptor
  TSS selector 在 GDT 里索引 TSS descriptor,其方法和 segment / gate dscriptor 的查找一致。



2、TSS 的 limit & type check

  TSS segment 大小必须能容纳所有的 processor state,整个 TSS segment 的大小是 104 个字节,在加载 TSS descriptor 进入 TR 之前,processor 检查 TSS descriptor 中的 limit 必须大于或等于 67h(104 字节)。若否,则产生 #TS 异常。
  processor 也检查 TSS descriptor 的 type 是否为 available 类型。若为 busy 类型,则产生 #GP 异常。


if (temp_descriptor.type == BUSY)
{
  goto do_#GP_exception;
}

if (temp_descriptor.limit < 0x67)
{
  goto do_#TS_exception;
}




3、权限的 check

  所需的权限是:CPL <= DPL && RPL <= DPL

if (RPL <= DPL && CPL <= DPL) {
   /* 通过,允许访问 */
} else {
   /* 失败,#GP 异常 */
}





4、保存 old-task 的 processor  state

  在执行加载 TSS descriptor 到 TR 之前,processor 将执行一项很重要的工作:保存 old-task 的执行环境(processor 的状态)在当前的 TSS segment 中。
  当前的 TSS segment 就是 old-task 的 TSS segment,在没加载 new-task 的 TSS 进入 TR 之前。

执行环境包括:
(1)GPRs
(2)segment registers
(3)Eflags
(4)LDTR 和 CR3
(5)EIP



5、加载 TSS descriptor

  通过一系列的 processor 检查后,processor 加载 TSS descriptor 进入 TR 寄存器。

TR.selector = TSS_selector;
TR.limit = temp_descriptor.limit;
TR.base = temp_descriptor.base;
TR.attribute = temp_descriptor;

TSS_descriptor.type = BUSY_32;

CR0.TS = 1;


TSS descriptor 加载到 TR 寄存器相应的域。
并且,processor 将 TSS descriptor 的 type 置为 BUSY 状态,防止再调用该 TSS descriptor
CR0.TS 也被置为 1,防止在 task 切换时执行 x87 or SSE(media)指令。



6、加载 processor state 信息

  TSS descriptor 加载进 TR,processor 将加载新任务保存在 TSS 的 processor 状态信息。
包括:
(1)GPRs 加载
(2)segment registers 加载
(3)Eflags 加载
(4)LDTR 和 CR3 加载



7、设置 EFLAGS.NT 和 TSS.link 域

  使用 call 指令调用 TSS selector,processor 将置 Eflags.NT 为 1,表示当前的 task 被嵌套调用。而使用 jmp 指令则不会置 Eflags.NT 为 1。

if (opcode == CALL) {           /* 使用 call 指令 */
  Eflags.NT = 1;                 /* 置 NT 位为 1 */
  TSS.link = old_TSS;         /* link 域为 old task 的 TSS selector */
}


  jmp 指令不支持 Task-Nesting 因此不改变 Eflags.NT 域,使用 call 指令还将当前 TSS 的 link 域设为 old-task 的 TSS selector,这样,当前使用 iret 指令返回时,将通过 old-task 的 TSS selector 返回到 old-task。





7、执行新 task

  同样,processor 加载新的 task EIP 进入 EIP 寄存器,执行新的 task 代码。新的 CS 被加载时,CPL 就是新的 CS.RPL。




8、返回 old-task

  new-task 执行完毕通过 ret 或 iret 指令返回到 old-task。

在 caller 中:

... ...
  call 0x20:0x0000000
next:
... ...

使用 call TSS_selector 来切换到 new-task。

(1)使用 ret 指令返回
  在 new-task 中使用 ret 指令返回和一般的 caller / callee 之间返回并无两样。程序将返回到 next 处。

(2)使用 iret 指令返回
  使用 Eflags.NT 的目的是让 iret 来返回,当使用 iret 指令返回时,processor 将检查 Eflags.NT 是否为 1 ,若 Eflags.NT = 1 则表示当前 task 被嵌套。
  程序将返回到 TSS.link 的 TSS selector 指向的 TSS descriptor,这个 TSS descriptor 就是 old-task 的 TSS descriptor,这里又进行另一次的任务切换到 old-task。
----------------------------------------------------------
  使用 ret 与 iret 的不同就是:用 ret 返回时,当前的 processor 还是当前的 state,使用 iret 返回时,将加载保存在 TSS 的 processor state。
  也就是说,ret 返回 old-task 还是沿用 new-task 的 processor 状态。iret 返回到 old-task 使用的是原来保存在 old-task 的 TSS 的 processor 状态。


使用的 iret 的另一个功能:
  当 Eflags.NT = 1 时:由于 iret 将返回到 TSS.link 的 TSS selector,进行一次任务切换。若这个 TSS selector 是刻意构造的别有用途的 TSS 时,可以进行一次别有用途的 task 切换。






7.1.3.4.1、 long mode 下的 TSS 切换情形

  在 long mode 下已经不支持使用 TSS 来切换 task ,包括 TSS selector 和 task gate。使用 call / jmp TSS_selector 将产生 #GP 异常。








:wink:

[ 本帖最后由 mik 于 2009-4-17 23:52 编辑 ]

论坛徽章:
0
发表于 2009-04-18 00:10 |显示全部楼层
7.1.3.5、 通过 task gate 进行 task 切换


指令:
  call 0x20:00000000
  jmp 0x20:00000000
-----------------------------------
  selector 0x20 是个 task gate 的 selector


  这里使用 task gate 任务切换与使用 TSS selector 的情形基本一样。


值得注意的是,使用 task gate 在权限的 check 方面与 call gate 不同的是:

(1)call-gate 的权限 check 中:CPL <= DPLg && RPL <= DPLg  并且 CPL >= DPLs(或 CPL == DPLs)
(2)task-gate 的权限 check 中:CPL <= DPLg && RPL <= DPLg,忽略和 DPLs 的校验。


task-gate 的权限 check:
  CPL <= DPLg && RPL <= DPLg
 

论坛徽章:
0
发表于 2009-04-18 00:58 |显示全部楼层
7.1.3.6、 使用 INT n  调用系统例程


  IDT(Interrupt Descriptor Table)仅能存放 interrupt-gate、trap-gate 和 task-gate。


指令:
  int 0x80
-----------------------------------
  0x80 是 vector (中断向量号)

  在 x86 下,gate-descriptor 是 8 个字节,所以:gate = IDTR.base + vector * 8,在 long mode 下,gate-descrptor 是 16 字节,所以:gate = IDTR.base + vector * 16。

  在 real-mode 下,IVT(Interrupt Vector Table)是 IDT 在 real-mode 下的表现形式。IVT entry 是 4 个字节,构成 CS:offset 这种形式,offset 在低端,CS 在高端。所以: entry = IDTR.base + vector * 4。 IDTR.base 初始为 0。




1、索引 gate 和 descriptor

  在 IDT 中查找 gate descriptor 是以 vector 为索引,不存在 selector,vector 从 0 ~ 255 共 1 个字节。

gate = IDTR.base + vector * 8;

selctor = gate.selector;

temp_descriptor = get_descriptor(selector, GDT /* or LDT);


  用 IDTR.base + vector * 8 方法得取 gate-descriptor,再用 gate_descriptor 的 selector 来获取目 code segment descriptor。



2、权限 check

  在访问 IDT 中不使用 selector,所以不存在 RPL 检查,仅需要检查 CPL 、DPLg 和 DPLs 就行了。DPLg 代表 gate-descriptor 的 DPL,DPLs 代表 code segment descriptor 的 DPL。

  若 gate 是个 task-gate 仅需判断 CPL <= DPLg 就行了


if (gate.type == TASK_GATE) {                         /* task gate */
  if (CPL <= DPLg) {
     /* 通过,允许访问 */
  } else {
     /* 失败,#GP 异常 */
  }

} else if (CPL <= DPLg && CPL >= DPLs) {           /* interrupt / trap gate */

  /* 通过,允许访问 */
} else {
  /* 失败,#GP 异常 */
}


  无论 code segment 是 conforming 还是 non-conforming 都是可以的。





3、stack 切换

  interrupt / trap 服务例程必然在运行在 supervisor 权限下,若切入 interrupt/trap 例程的代码运行在 user 权限下,这将导致权限改变,stack 也发生切换。


DPL = temp_descriptor.DPL;                          /* code segment descriptor's DPL */

old_SS = SS;
old_ESP = ESP;

SS = TSS.stack[DPL].SS;                                /* 加载目标 stack */
ESP = TSS.stack[DPL].ESP;


push(old_SS);
push(old_ESP);

push(Eflags);

push(CS);
push(EIP);

if (ERROR_CODE) {
  push(error_code);
}


stack 切换时,processor 做以下工作:
(1)以目标 code segment 的 DPL 为索引在 TSS 获取相应权限级别的 stack pointer 加载到 SS & ESP
(2)将旧的 SS & ESP 保存在当前 stack (新 stack) 中。
(3)EFLAGS 寄存器入栈。
(4)返回地址(CS & EIP)入栈。
(5)由异常引发的 interrupt/trap 例程,还将异常码入栈,以供 interrupt/trap 例程使用。

 SS.RPL 会被更新为 DPL,表示当前 stack 的权限级别就是 SS.RPL。


当发生同级权限的转移时,不会发生 stack 切换,使用的是当前的 stack:

push(Eflags);
push(CS);
push(EIP);

if (ERROR_CDOE)
  push(error_code)





4、Eflags 寄存器的处理

  processor 将旧的 Eflags 入栈保存,将修改当 Eflags 的标志

if (gate == TASK_GATE)                    /* task gate */
  Eflags.NT = 1;
else                                    /* interrupt/trap gate */
  Eflags.NT = 0;

Eflags.TF = 0;
Eflags.VM = 0;
Eflags.RF = 0;

if (gate == INTERRUPT_GATE) {                 /* interrupt gate */
  Eflags.IF = 0;
} else if (gate == TRAP_GATE) {        /* trap gate */
  /* do nothing */
}                                            /* task gate */


(1)若是 task gate,processor 将置 Eflags.NT = 1 进入 Nest Task 模式,在 Iret 指令返回时将检查 Eflags.NT = 1 进入 task 切换。若是 interrupt/trap 的话,将 Eflags.NT 清 0,防止 IRET 指令回时进行 task 切换。
(2)Eflags.TF 将被清 0,将 Eflags.TF = 1 将引发 #DB 异常,processor 将进入单步调试模式,在进入响应 #DB 异常的例程前,processor 会将 Eflags.TF 清 0。
(3)Eflags.RF 清 0,在 IRET 指令返回时,processor 将 Eflags.RF 置为 1,使得 #DB 异常断点下一条指令能顺利执行。




5、加载 code segment descriptor

  同样,processor 会加载 code segment selector & descriptor 到 CS 寄存器

CS.selector = gate.selector;
CS.base = temp_descriptor.base;
CS.attribute = temp_descriptor.attribute;
CS.limit = temp_descriptor.limit;


  加载后,CS.RPL 就是新的 CPL,


  
6、执行服务例程

  processor 将加载 EIP = CS.base + gate.offset,然后执行 CS:EIP 处的中断服务例程。




7、中断例程返回

  中断例程使用 iret 指令返回时,processor 同样会进行一系统的权限检查,中断例程不能向高权限的代码返回。


if (Eflags.NT == 1) {
  /* 发生 task 切换 */
}


pop(temp_EIP);
pop(temp_CS);

if (temp_CS.RPL == CPL) {                        /* CPL == RPL */

  goto return_same;       /* 同级返回 */

} else if (temp_CS.RPL > CPL)                  /* CPL < RPL */

  goto return_less;        /* 向低权限代码返回

else {                                         /* CPL > RPL */
  /* 异常,#GP 异常 */
  /* 拒绝向高权限代码返回 */
}



return_same:
  CS = temp_CS;
  EIP = temp_EIP;

  pop(Eflags);

  return;


return_less:

  pop(temp_Eflags);

  pop(temp_ESP);
  pop(temp_SS);

  if (temp_SS.RPL == temp_CS.RPL) {
    Eflags = temp_Eflags;

    SS = temp_SS;
    ESP = temp_ESP;

      CS = temp_CS;
    EIP = temp_EIP;

  } else {
    /* 失败,#GP 异常 */
  }

  return;



(1)iret 指令检查 Eflags.NT 是否为 1,为 1 时,直接进行任务切换出去,这个 task 就是 TSS.link 里的 TSS selector。

(2)pop 出旧的 CS & EIP,并进行权限检查,temp_CS.RPL 代表原来的 CPL 运行级别,必须 CPL <= temp_CS.RPL,若 pop 出的 CS.RPL 是低于 CPL 的,则会产生 #GP 异常,processor 防止向高权限的代码返回。

  当向同级的代码返回时,仅需加载回原来的 CS & EIP 以及 EFLAGS 寄存器就行了。

  当向低权限代码返回时,processor 会检查 pop 出来的 SS.RPL 是否等于 temp_CS.RPL(原来的 CS.RPL),不相同则产生 #GP 异常,在 x86 下,无论什么情况 CPL.RPL 必须等于 SS.RPL。通过后,processor 相应地加载 SS & ESP、CS & EIP 以及 EFLAGS 寄存器,然后返回原程序。










:)

[ 本帖最后由 mik 于 2009-4-22 15:13 编辑 ]

论坛徽章:
0
发表于 2009-04-22 16:29 |显示全部楼层
7.1.3.7、 long mode 下的中断服务例程切换

  在 long mode 下,gate 是 16 字节的,并取消了对 task gate 的支持。即 IDT 的 entry 是 16 字节的,所以:gate = IDTR.base + vector * 16。
  在 long mode 下,code segment descriptor 的 L、D、C 以及 DPL 有效,其它域无效。由于 base 强制为 0,中断服务例程的入口地址就是 gate.offset,这个 offset 是 64 位值。code segment descriptor 的 L = 1 && D = 0,代表 code segment 是 64 位代码。



1、索引 gate 和 code segment descriptor

gate = IDTR.base + vector * 16;

temp_descriptor = get_descriptor(gate.selector, GDT /* LDT */);


同样,以 gate.selector 可以找到 code segment descriptor。



2、gate descriptor 和 code segment descriptor 的检查

  processor 将对 gate 和 code segment descriptor 进行检查

if (gate.type == INTERRUPT_GATE || gate.type == TRAP_GATE) {

} else {
  /* #GP 异常 */
}  

if (IS_CANONICAL(gate.offset)) {                /* is canonical-address ? */

} else {
  /* #GP */
}

if (temp_descriptor.L == 1 && temp_descriptor.D == 0) {            /* 64 bit */

} else {
  /* #GP 异常 */
}


  processor 对 gate 额外检查 offset 是否是 canonical-address 地址形式。processor 还将对 code segment descriptor 的 L 和 D 属性进行检查,是否为 64 位代码。



3、权限 check

  DPLg 是 gate.DPL,DPLs 是 code segment descripotr.DPL
if (CPL <= DPLg && D >= DPLs) {
  /* 通过,允许访问 */
} else {
  /* 失败, #GP 异常 */
}





4、stack 切换
  
  在 long mode 下的 interrupt/trap gate 增加了 IST 域,代表 1 ~ 7 stack pointer ID 值。允许在 interrupt/trap 提供自定义的 stack pointer,分别为:IST1 ~ IST7。


old_SS = SS;
old_RSP = RSP;

if (gate.IST)
{
  SS = NULL;
  RSP = TSS.IST[gate.IST].RSP;         /* Intrrupt Stack Table */

} else {

  SS = NULL:
  RSP = TSS.stack[temp_descriptor.DPL].RSP;       /* stack pointer */
}

RSP |= 0xFFFFFFFF_FFFFFFF0;                /* 调整 stack 为 16 字节对齐 */

push64(old_SS);
push64(old_RSP);

push64(RFlags);

push64(CS);
push64(RIP);

if (ERROR_CODE)
{
  push64(error_code);
}


(1)如果,interrupt/trap gate 的 IST 不为 0 无论是否有权限的改变,都将发生 stack 切换,以便使用 gate 提供的 IST 指针,若 gate.IST 为 0 的话,将沿有以前的的 stack 切换模式:在权限改变时发生 stack 切换。

(2)long mode 下的 stack 以 16 字节对齐,processor 额外将 RSP 调整为 16 字节对齐。

(3)SS 将被加载为 NULL selector,即:0x0000(selector),在 long mode 下的 stack 切换中 SS 被加载为 NULL-selector 是不会产生 #GP 异常的。

(4)旧的 SS & RSP、Eflags、CS & RIP 将被入栈,SS、CS 在 8 字节边界上,多余补 0 。Rflags 的高 32 位补 0 入栈。

(5)同样若是 exception 例程则入栈 error code

---------------------------------------------------------------------------------
  在 long mode 下的 TSS segment 是 64 位的,其中有 6 个 RSP 指针域,构成一个 Interrupt Stack Table 表,分别为:IST1 ~ IST7,这 6 个 RSP 由 interrupt / trap gate descriptor 中的 IST 域来索引获取。
  gate 中的 IST 域值为 001 ~ 111,相应地指出在 TSS 中的 IST 值。当 gate.IST 为 0 时,表示在 gate 中不提供对 IST 的索引,还是按原来的获取 stack pointer 方式。




5、Rflags 寄存器的处理

Rflags.NT = 0;
Rflags.RF = 0;
Rflags.TF = 0;

if (gate == INTERRUPT_GATE) {
  Rflags.IF = 0;
} else if (gate == TRAP_GATE) {

}

同样,processor 需将 NT、TF、RF 清为 0,在 interuupt gate 下 IF 清 0, trap-gate 下不修改 IF 标志。long mode 下不支持 virtual-8086 mode,对 VM 标志不修改。




6、加载 code segment descriptor

  和 x86 下一样,code segment selector 和 descriptor 被加载到 CS 中,但是仅 CS.L、CS.D、CS.P、CS.C 和 CS.DPL 有效,其它域无效,CS.base 被强制为 0,CS.limit 被强制为 FFFFFFFF_FFFFFFFF
  CS.RPL 被更新为 code segment descriptor 的 DPL,即为当前的 CPL。  


7、执行中断服务例程
  
  由于 CS.base 被强制为 0,因此 gate.offset 就是实际的中断服务例程入口地址。

  RIP = gate.offset 然后执行 CS:RIP 处理的中断服务例程。



8、中断服务例程的返回

  若返回到 64 bit 模式时,processor 处理将和 x86 的情形一样。但是在 64 bit 的中断服务例程里需使用 iretq 指令。

情景提示:
  由于 iret 指令在 64 bit 模式下 default operand-size 是 32 位的,这不同于 ret 指令。ret 指令的 default operand-size 是 64 位。所以,中断返回时需使用 iretq 指令,即:使用指令前缀 REX.W(48) 将 iret 调整为 iretq








7.1.3.7.1、 compatibility 模式与 64 bit 模式之间的切换

  由于 long mode 下有 compatiblity 与 64 bit 两种子模式,那么在 x64 版本的 64 位 OS 里就有可能存在 compatibility 与 64 bit 模式之间的切换情况发生。
  64 位 OS 核心运行在 64 bit 模式下,当系统加载的是原 x86 下的 32 位用户程序,processor 将从 64 bit 切换到 compatiblity 模式。32 位软件返回 OS 时,从 compatibility 模式切换回 64 bit 模式。









----- 待续

[ 本帖最后由 mik 于 2009-5-2 00:47 编辑 ]

论坛徽章:
0
发表于 2009-05-31 00:30 |显示全部楼层
34、使用 sysenter/sysexit 指令快速切换



  通过 GDT / IDT 表索引查找 target code-segment descriptor 显得不够迅速。
* 原因1:经过多重的索引查找
* 原因2:经过相关的权限检查

  因此 Intel 推出了 sysenter 指令来快速陷入 0 层,同时免去了相关的权限检查环节。sysexit 指令快速返回。



1、sysenter/sysexit 的使用前提

* sysenter/sysexit 必须在 protected 模式下执行。在 real 模式下使用将产生 #GP 异常。
* sysenter/sysexit 必须执行在 flat 内存模式,即:CS.base = 0 & SS.base = 0
* 在 AMD 的 processor 中,sysenter/sysexit 在 long mode 是无效的,在 long mode 下使用将产生 #UD 异常。
  在 Intel 的 processor 中,sysenter/sysexit 在 long mode 是有效的。



2、sysenter 的使用环境

  前面说过,sysenter/sysexit 须使用在 flat 内存模式下。

  虽然,在相应的 MSR 寄存器中提供了 CS 和 SS selector,但实际上这个 CS 和 SS selector 将被忽略。而以硬性的 code segment descriptor 和 stack segment descriptor 来代替。processor 自动负责这个具体工作的实施。

processor 会重设 code / stack segment descriptor
* CS.base = 0  &  SS.base = 0
* CS.limit = FFFFF  &  SS.limit = FFFFF    (4G 的 limit,实际等于 CS.limit = FFFFFFFF)
* CS.G = 1  &  SS.G = 1
* CS.DPL = 0  &  SS.DPL = 0  (强制为 0 级权限)
* CS.type = 1011 (execute/read  & accessed)
* SS.type = 0011   (read/write & accessed)
* CS.P = 1  &  SS.P = 1

无论提供的 code/stack segment descriptor 是怎样,processor 将硬性按上面的重设 CS 与 SS 环境。




3、sysenter 执行环境的获取

  processor 会按规则去获取执行环境,包括:CS 和 SS selector、eip 和 esp(代码入口和栈顶入口)。

* MSR_SYSENTER_CS[15:0]
    获取 CS selector。这个 MSR 的 bit15 ~ bit0 提供了 CS selector,而 bit64 ~ bit16 须为 0。MSR_SYSENTER_CS[15:0] 将载入 CS 中。

* MSR_SYSENTER_EIP[31:0]
  提供 target code segment 的入口 EIP 值,MSR_SYSENTER_EIP[31:0] 将载入 EIP 中。

* MSR_SYSENTER_ESP[31:0]
  提供 target stack segment 的入口 ESP 值,MSR_SYSENTER_ESP[31:0] 将载入 ESP 中。

* SS selector 的获取
  没有直接 MSR 寄存器去提供 SS selector,但是,Intel 将视提供的 CS descriptor 的下一个 descriptor 为 SS descriptor。
  也就是说:由 MSR_SYSENTER_CS[15:0] 提供的 CS selector 的下一个就是 SS selctor。

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 1(加上8)


由上面可见,CS seelctor 的下一个 selector 就是 SS selector。

其结果就是:SS selector = MSR_SYSENTER_CS + 8
---------------------------------------------------------
  前面说过,MSR 虽然提供了 CS 和 SS selector,但最终会被 processor 重设。




4、 sysenter 执行环境的设置

  由于 CS & SS selector、EIP 以及 ESP 在相应的 MSR 寄存器中获取,所以使用前须由指令 WRMSR 指令来设定相应的值。

* MSR_SYSENTER_CS 的地址在 174H
* MSR_SYSENTER_ESP 的地址在 175H
* MSR_SYSENTER_EIP 的地址在 176H


实例:
  xor edx, edx                              /* MSR_SYSENTER_EIP[63:32] 为 0 */
  mov eax, 0x80100400                  /* eip 为 0x80100400 */
  mov ecx, 0x176                          /* MSR_SYSENTER_EIP 地址 */
  wrmsr                                       /* 写 MSR */
-------------------------------------
  上面这段代码将设置 MSR_SYSENTER_EIP[31:0] 值为 0x80100400





5、 sysexit 执行环境的获取

  当 sysenter 指令执行到的 0 级代码使用 sysexit 返回时,sysexit 的执行,processor 同样按规则去获取返回执行环境。


* 返回 CS selector 的获取
  MSR_SYSENTER_CS[15:0] 提供的 CS slector 的下第二个 selector 为返回的 CS selector。

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 2(加上16)


MSR_SYSENTER_CS[15:0] 的下第二个 selector 就是 CS selector。

其结果就是:返回 CS selector = MSR_SYSENTER_CS + 16



* 返回 SS selector 的获取
  同样根据规则 MSR_SYSENTER_CS[15:0] 提供的 selector 的下第三个 selector 就是返回的 SS selector。

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 3(加上24)


MSR_SYSENTER_CS[15:0] 接下来的第 3  个 selector 就是返回的 SS selector。

其结果就是:返回的 SS selector = MSR_SYSENTER_CS+24



* 返回的 EIP 和 ESP

EIP 和 ESP 简单得多:
  EIP 放在 EDX 寄存器中。
  ESP 放在 ECX 寄存器中。



6、 sysexit 的使用环境

  和 sysenter 指令一样, sysexit 指令的使用环境 CS 和 SS 同样会被 processor 重新设定,虽然给出了返回的 CS 和 SS。

除了 DPL 外,sysexit 和 sysenter 的环境是一样的。

* CS.DPL = 3(CS.CPL = 3)  &   SS.DPL =3

-------------------------------------------------------------
  区别就是:sysenter 目标代码必须是 0 级,sysexit 目标代码必须是 3 级。





7、 long mode 下的 sysenter/sysexit 指令

  仅在 Intel 的 processor 下才有效,AMD 的 processor 在 long mode 下使用 syscall/sysret 来代替。

  在 long mode 下,MSR_SYSENTER_ESP[63:32] 和 MSR_SYSENTER_EIP[63:32] 是有效的,扩展成 64 位,变成了 MSR_SYSENTER_RSP 和 MSR_SYSENTER_RIP。

* sysenter 进入的 target code segment descriptor 的 L 必须是 1,D = 0
  即:目标的 CS.L = 1 & CS.D = 0 表示目标为 64 位代码。

* MSR_SYSENTER_RSP 和 MSR_SYSENTER_RIP 的值必须的 canonical address 形式。





8、sysenter 的执行

if (CR0.PE == 0)                                   /* non-proected mode */
        goto do_GP_exception;        

if (MSR_SYSENTER_CS[15:2] == 0)         /* NULL-selector */
        goto do_GP_exception;               

eflags.VM = 0;
eflags.IF = 0;
eflags.RF = 0;


if (EFER.LMA == 0)                               /* x86-legacy mode */
         goto x86_sysenter;
else
         goto x64_sysenter;                     /* long mode */



x86_sysenter:

        CS.selector = MSR_SYSENTER_CS;               /* load CS selector with MSR_SYSENTER_CS[15:0] */
        CS.selector.RPL = 0;
        CS.base = 0x00000000;
        CS.base = 0xFFFFFFFF;
        CS.type = 0x0B;                                              /* type = 1011 */
                                                                            /* non-conforming, excecute/readable, accessed */
        CS.D = 1;                                                       /* 32 bit code segment */
        CS.P = 1;
        CS.G = 1;
        CS.S = 1;                                                       /* non-system descriptor */


        SS.selector = MSR_SYSENTER_CS + 8;               /* next-selector with MSR_SYSENTER_CS + 8 */
        SS.selector.RPL = 0;
        SS.base = 0x00000000;
        SS.limit = 0xFFFFFFFF;
        SS.type = 0x03;                                            /* type = 0011 */
                                                                          /* expand-up, write/read, accessed */
        SS.D = 1;                                                     /* 32 bit stack pointer */
        SS.P = 1;
        SS.G = 1;
        SS.S = 1;


        esp = MSR_SYSENTER_ESP;
        eip = MSR_SYSENTER_EIP;




x64_sysenter:

        CS.selector = MSR_SYSENTER_CS;               /* load CS selector with MSR_SYSENTER_CS[15:0] */
        CS.selector.RPL = 0;
        CS.base = 0x00000000_00000000;
        CS.base = 0xFFFFFFFF_FFFFFFFF;
        CS.type = 0x0B;                                              /* type = 1011 */
                                                                            /* non-conforming, excecute/readable, accessed */
        CS.L = 1;
        CS.D = 0;                                                       /* 64 bit code segment */
        CS.P = 1;
        CS.G = 1;
        CS.S = 1;                                                       /* non-system descriptor */


        SS.selector = MSR_SYSENTER_CS + 8;               /* next-selector with MSR_SYSENTER_CS + 8 */
        SS.selector.RPL = 0;
        SS.base = 0x00000000_00000000;
        SS.limit = 0xFFFFFFFF_FFFFFFFF;
        SS.type = 0x03;                                            /* type = 0011 */
                                                                          /* expand-up, write/read, accessed */
        SS.D = 1;                                                     /* 64 bit stack pointer */
        SS.P = 1;
        SS.G = 1;
        SS.S = 1;


        rsp = MSR_SYSENTER_RSP;
        rip = MSR_SYSENTER_RIP;

---------------------------------------------------------------------------------------------
  processor 设置必要的执行环境:CS 和 SS 都是 0 级的 flat 内存模式。




9、sysexit 的执行

if (CR0.PE == 0)                                   /* non-proected mode */
        goto do_GP_exception;        

if (MSR_SYSENTER_CS[15:2] == 0)         /* NULL-selector */
        goto do_GP_exception;               

if (CS.RPL != 0)                                   /* CPL != 0 */
        goto do_GP_exception;               


if (EFER.LMA == 0)                               /* x86-legacy mode */
         goto x86_sysexit;
else
         goto x64_sysexit;                     /* long mode */



x86_sysexit:

        CS.selector = MSR_SYSENTER_CS + 16;           /* MSR_SYSENTER_CS[15:0] + 16 */
        CS.selector.RPL = 3;
        CS.base = 0x00000000;
        CS.base = 0xFFFFFFFF;
        CS.type = 0x0B;                                              /* type = 1011 */
                                                                            /* non-conforming, excecute/readable, accessed */
        CS.D = 1;                                                       /* 32 bit code segment */
        CS.P = 1;
        CS.G = 1;
        CS.S = 1;                                                       /* non-system descriptor */


        SS.selector = MSR_SYSENTER_CS + 24;               
        SS.selector.RPL = 3;
        SS.base = 0x00000000;
        SS.limit = 0xFFFFFFFF;
        SS.type = 0x03;                                            /* type = 0011 */
                                                                          /* expand-up, write/read, accessed */
        SS.D = 1;                                                     /* 32 bit stack pointer */
        SS.P = 1;
        SS.G = 1;
        SS.S = 1;


        esp = ecx;
        eip = edx;



x64_sysexit:

        if (opcode[0] == REX.W)                          /* return to 64 bit */
                goto 64bit_sysexit;
        else
                goto 32bit_sysexit;


64bit_sysexit:

        CS.selector = MSR_SYSENTER_CS + 32;               /* MSR_SYSENTER_CS[15:0] + 32 */
        CS.selector.RPL = 3;
        CS.base = 0x00000000_00000000;
        CS.base = 0xFFFFFFFF_FFFFFFFF;
        CS.type = 0x0B;                                              /* type = 1011 */
                                                                            /* non-conforming, excecute/readable, accessed */
        CS.L = 1;
        CS.D = 0;                                                       /* 64 bit code segment */
        CS.P = 1;
        CS.G = 1;
        CS.S = 1;                                                       /* non-system descriptor */


        SS.selector = MSR_SYSENTER_CS + 40;               /* next-selector with MSR_SYSENTER_CS + 40 */
        SS.selector.RPL = 0;
        SS.base = 0x00000000_00000000;
        SS.limit = 0xFFFFFFFF_FFFFFFFF;
        SS.type = 0x03;                                            /* type = 0011 */
                                                                          /* expand-up, write/read, accessed */
        SS.D = 1;                                                     /* 64 bit stack pointer */
        SS.P = 1;
        SS.G = 1;
        SS.S = 1;


        rsp = rcx;
        rip = rdx;



32bit_sysexit:

         CS.L = 0
         goto x86_sysexit;





:wink:

[ 本帖最后由 mik 于 2009-6-7 00:31 编辑 ]

论坛徽章:
0
发表于 2009-05-31 23:51 |显示全部楼层
35、使用 syscall/sysret 指令快速切换



  syscall/sysret 由 AMD 提出替代 sysenter/sysret 指令用来解决 64 位下的 fast call。其原理和 sysenter/sysexit 差不多。



1、syscall/sysret 与 sysenter/sysexit 不同之处

* 在 AMD 的 processor 中,syscall/sysret 在 long mode 和 legacy mode 都是有效的。sysenter/sysexit 仅在 legacy mode 才有效。
  在 Intel 的 processor 中,syscall/sysret 仅在 long mode 才有效,sysenter/sysexit 在 long mode 和 legacy mode 都是有效的。

* 在 Intel processor 的 long mode 中,sysenter/sysexit 的使用其原理和 x86 模式中一样,区别仅在于 MSR_SYSENTER_EIP[63:0] 和 MSR_SYSENTER_ESP[63:0] 都是有效的。
 在 AMD processor 的 long mode 中,syscall/sysret 新增了 LSTAR 、CSTAR 以及 SFMASK 寄存器。用来存放目标的 RIP 值。



2、syscall/sysret 使用环境

  和 sysenter/sysexit 一样,syscall 的目标代码是 0 级的平坦模式代码。sysret 的目标代码是 3 级的平坦模式代码。


(1) MSR_STAR[63:0] 寄存器

  这个寄存器分为 3 部分:MSR_STAR[31:0]、MSR_STAR[47:32] 以及 MSR_STAR[63:48]
  MSR_STAR 寄存器的地址在 C000_0081

* MSR_STAR[31:0]
  STAR 寄存器的低 32 位提供目标代码的 EIP 值。将被加载到 EIP 寄存器。

* MSR_STAR[47:32]
  这部分的 16 位为 syscall 的目标代码提供 CS selector
  
  syscall 指令获取目标代码的 CS selector 由 MSR_STAR[47:32] 直接提供,而 SS selector 将是 CS selector 的下一个 selector,也就是 CS selector + 8

下一个 selector 是这样形成的:

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 1(即:selector 加上 8 )



* MSR_STAR[63:48]
  STAR 寄存器的高 16 位,为 sysret 提供目标代码的 CS selector

  sysret 指令获取目标代码的 CS selector 与 SS selector 稍为复杂些,这个复杂的情况是由模式的切换产生。


当 sysret 切换到 x86 或 compatiblity 模式时:

  返回的目标代码的 CS selector 由 MSR_STAR[63:48] 直接提供,SS selector 由 CS selector 的下一个 selector 提供,即:SS selector = CS selector + 8


SS selector 是这样产生的:

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 1(即:selector 加上 8 )




当 sysret 切换到 64 bit 模式时:

  返回的目标代码的 CS selector 则是 MSR_STAR[63:48] + 16,也就是接着的第 2 个 selector。


此时,CS selector 是这样产生的:

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 2(即:selector 加上 16 )




SS selector 则是:MSR_STAR[63:48] + 8,即:

XXXXXXXXXXXXX   0  00
-----------------
         |
         +---------------> CS_selector.SI + 1(即:selector 加上 8 )



---------------------------------------------------------------------

  因此,MSR_STAR[63:48] 实则表示为:MSR_STAR.SYSRET_CS。

  无论什么时候,MSR_STAR.SYSRET_CS 提供的是 x86/compatibility 模式的 CS selector,64 bit 模式的 CS selector 则是 MSR_STAR.SYSRET_CS + 16。
  x86/compatibility 与 64 bit 模式的 SS selector 都为 MSR_STAR.SYSRET_CS + 8





(2) MSR_LSTAR[63:0] 寄存器

  MSR_LSTAR 寄存器 64 位宽,使用在 64 bit 模式下。当 processor 处于 64 bit 模式下,syscall 指令从 MSR_LSTAR 寄存器读取 RIP 值。
  MSR_LSTAR 寄存器的地址在 C000_0082



(3) MSR_CSTAR[63:0] 寄存器

  MSR_CSTAR 寄存器 64 位宽,使用在 compatibility 模式下。当 processor 处于 compatibility 模式下,syscall 指令将从 MSR_CSTAR 寄存器读取 RIP 值。
  MSR_CSTAR 寄存器的地址在 C000_0083


(4) MSR_SFMASK[63:0] 寄存器

  MSR_SFMASK 寄存器是 RFLAGS 寄存器的掩码。MSR_SFMASK 寄存器每一位对应于 RFLAGS 寄存器的每一位。
* 当 MSR_SFMASK 寄存器的位被置 1 ,相应的 RFLAGS 寄存器位为 0
* 当 MSR_SFMASK 寄存器的位被置 0 ,相应的 RFLAGS 寄存器位没改变

  MSR_SFMASK 寄存器的地址在 C000_0084
  




3、syscall 执行

下面 syscall 执行流程的 C 代码:
------------------------------------------------------------------------------------------------

if (EFER.SCE == 0)                        /* syscall support ? */
        goto do_UD_exception         /*  #UD exception */


if (EFER.LMA == 0)                         /* x86-legacy mode */
        goto x86_syscall;                    /* do x86-legacy syscall */
else
        goto x64_syscall;                    /* do x64-long mode syscall */



x86_syscall:

        eflags.VM = 0;
        eflags.IF = 0;
        eflags.RF = 0;

        ecx = eip;                                                    /* save EIP for sysret return */

        CS.selector = MSR_STAR.SYSCALL_CS;             /* load CS selector with MSR_STAR[47:32] */
        CS.selector.CPL = 00;                                    /* target code-segment's CPL = 00 */
        CS.base = 0x00000000;
        CS.limit = 0xFFFFFFFF;
        CS.type = 0x0B;                                           /* CS.type = 1011 */
                                                                         /* non-conforming, execute/readable, accessed */

        SS.selector = MSR_STAR.SYSCALL_CS + 8;       /* CS selector next selector */
        SS.selector.RPL = 00;                                    /* target stack-segment's DPL = 00 */
        SS.base = 0x00000000;
        SS.limit = 0xFFFFFFFF;
        SS.type = 0x03;                                           /* SS.type = 0011 */
                                                                         /* expand-up, read/write, accessed */

        eip = MSR_STAR.EIP;                                    /* load into EIP with MSR_STAR[31:0] */



x64_syscall:

        rcx = rip;                              /* save rip for sysret return */        
        r11 = rflags;                          /* save rflags for syscall routine */


        rflags = rflags | ~MSR_SFMASK;        /* mask with MSR_SFMASK */
        rflags.RF = 0;


        if (CS.L == 1 && CS.D == 0) {            /* it's 64 bit mode */

                 temp_rip = MSR_LSTAR;

        } else if (CS.L == 0) {                      /* it's compatibility mode */
                 temp_rip = MSR_CSTAR;
        }



        CS.selector = MSR_STAR.SYSCALL_CS;           /* load CS.selector with MSR_STAR[47:32] */
        CS.selector.CPL = 00;                                  /* CPL = 00 */
        CS.base = 0x00000000_00000000;                 
        CS.limit = 0xFFFFFFFF_FFFFFFFF;
        CS.type = 0x0B;                                         /* CS.type = 1011 */
        CS.L = 1;
        CS.D = 0;


        SS.selector = MSR_STAR.SYSCALL_CS + 8;     /* next selector */


/* the follow is ignored and imlicit processor set follow force to ... ... */

        SS.selector.DPL = 00;
        SS.base = 0x00000000_00000000;
        SS.limit = 0xFFFFFFFF_FFFFFFFF;
        SS.type = 0x03;                                         /* SS.type = 0011 */



        rip = temp_rip;


-----------------------------------------------------------------------------------------------------
  syscall 指令执行目标的 CPL = 0,processor 强制将 CPL = 0 以及 CS 和 SS 的属性全部强制为 flat 内存模式。在 long mode 下,syscall 目标的 CS & SS 绝大部分属性都是无效的。processor 会自动设为 flat 的 64 位地址模式。
  值得注意的是:syscall 指令并没提供目标的 esp/rsp 值,这将导致沿用当前的 esp/rsp 值。而 sysenter 指令提供了目标的 esp/rsp 值。




4、sysret 的执行

  sysret 当前执行 CPL = 0,返回目标的 CPL = 3,返回的 eip/rip 将从 ecx/rcx 中获取。


if (EFER.SCE == 0)
        goto do_UD_exception;

if (CS.RPL != 0)                           /* CPL != 0 */
        goto do_GP_exception;


if (EFER.LMA == 0)                        /* x86-legacy mode */
        goto x86_sysret;
else
        goto x64_sysret;                  /* x64-long mode */



x86_sysret:

        CS.selector = MSR_STAR.SYSRET_CS;                  /* load CS selector with MSR_STAR[63:48]    */
        CS.selector.RPL = 3;                                          /* target's CPL = 3 */
        CS.base = 0x00000000;
        CS.limit = 0xFFFFFFFF;
        CS.type = 0x0B;                                           /* CS.type = 1011 */
                                                                         /* non-conforming, execute/readable, accessed */

        SS.selector = MSR_STAR.SYSRET_CS + 8;              /* load SS selector with next selector */

        eflags.IF = 1;
        eip = ecx;



x64_sysret:

        if (Opcode[0] == REX.W) {                     /* return to 64 bit mode */

                CS.selector = MSR_STAR.SYSRET_CS + 16;            
                CS.selector.RPL = 3;         
                CS.base = 0x00000000_00000000;
                CS.limit = 0xFFFFFFFF_FFFFFFFF;
                CS.type = 0x0B;
                CS.L = 1;
                CS.D = 0;

        } else {                                                  /* return to compatibility */

                CS.selector = MSR_STAR.SYSRET_CS;                 
                CS.base = 0x00000000;
                CS.limit = 0xFFFFFFFF;
                CS.type = 0x0B;
        }

        SS.selector = MSR_STAR.SYSRET_CS + 8;

        temp_rip = rcx;
        rflags = r11;                                 /* restore rflags */


        rip = rcx;


------------------------------------------------------------------------------------
  sysret 指令执行在 long mode 和 x86 mode 之间存在差异:

在 x86-legacy mode 下:
  返回的 CS selector 从 MSR_STAR[63:48](即上述的 MSR_STAR.SYSRET_CS)处加载,SS selector 由 MSR_STAR[63:48] + 8 所得。


在 x64-long mode 下:
  processor 将视乎返回到 64 bit 模式还是 compatibility 模式?返回 64 bit 模式时,CS selector 将由 MSR_STAR[63:48] + 16 所得。SS selector 还是由 MSR_STAR[63:48] + 8 所得。

  由于 sysret 指令缺省的 operand size 是 32 位。
  当 sysret 指令使用 REX prefix 来返回到 64 bit 模式,当 REX.W = 1 时,sysret 指令的 operand size 是 64 位,表示 sysret 将返回到 64 位代码。
  当 sysret 指令不使用 REX prefix 或者 REX.W = 0 时,sysret 指令将返回到 32 位或 compatibility 模式。






:wink:

[ 本帖最后由 mik 于 2009-6-6 23:20 编辑 ]

论坛徽章:
0
发表于 2009-06-07 03:24 |显示全部楼层
36、使用 ret/iret 指令进行切换


  使用 ret 和 iret 指令只能切换到同级或低级权限的代码,若使用 ret 和 iret 切换到高权限代码,将产生 #GP 异常。


一、far ret

















--- 待续

论坛徽章:
0
发表于 2009-07-11 00:41 |显示全部楼层
37、TSS 详解


  这里以 xp 的实例 TSS 来观察


1、TSS 的地址和长度

  TSS descriptor 指出 TSS 块的地址和长度。

<bochs:4> info gdt 0x14
Global Descriptor Table <base=0x000000008003f000, limit=1234>:
GDT[0x14]=32-Bit TSS <Available> at 0x863b02d8, length=0x00068

在 GDT 的 0x14 entry 是 32-TSS descriptor,它描述 TSS 块在 0x863b02d8 地址上,长度为 0x68

TSS 的长度必须不低于 0x68,否则产生 #GP 异常。



2、观察 TSS 块的内容

<bochs:7> x /26wx 0x863b02d8
[bochs ]:
0x00000000863b02d8 <bogus+        0>: 0x00000000  0x80547100  0x00000010  0x00000000
0x00000000863b02e8 <bogus+       16>: 0x00000000  0x00000000  0x00000000  0x00ad7000
0x00000000863b02f8 <bogus+       32>: 0x806cfbe8  0x00000000  0x00000000  0x00000000
0x00000000863b0308 <bogus+       48>: 0x00000000  0x00000000  0x80547100  0x00000000
0x00000000863b0318 <bogus+       64>: 0x00000000  0x00000000  0x00000023  0x00000008
0x00000000863b0328 <bogus+       80>: 0x00000010  0x00000023  0x0000030  0x00000000
0x00000000863b0338 <bogus+       96>: 0x00000000  0x20da0000


从上面可以看出:

Link  = 0x0000
esp0 = 0x80547100
ss0   = 0x00000010
esp1 = 0x00000000
ss1   = 0x00000000
esp2 = 0x00000000
ss2   = 0x00000000
cr3   = 0x00ad7000
eip   = 0x806cfbe8
eflags = 0x00000000
eax    = 0x00000000
ecx    = 0x00000000
edx    = 0x00000000
ebx    = 0x00000000
esp    = 0x80547100
ebp    = 0x00000000
esi    = 0x00000000
edi    = 0x00000000
es    = 0x0023
cs    = 0x0008
ss    = 0x0010
ds    = 0x0023
fs    = 0x0030
gs    = 0x0000
ldt    = 0x0000
IOPBBA  = 0x20da

----------------------------------------------
ss1/esp1 和 ss2/esp2 都设为 0

IOPB 的 base 地址在 0x863b02d8+0x20da

[ 本帖最后由 mik 于 2009-7-20 01:34 编辑 ]

论坛徽章:
0
发表于 2012-03-14 21:22 |显示全部楼层
:wink:
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP