免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: mik
打印 上一主题 下一主题

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

论坛徽章:
0
11 [报告]
发表于 2009-02-24 02:38 |只看该作者
5.6、 观察 xp 实际的 segmentation 情景



使用 bochs 可以很容易很直观地观察调试系统。下面选取一个 xp 启动的实际片断,如下:

<bochs:7> sreg
cs:s=0x001b, dl=0x0000ffff, dh=0x00cffa00, valid=1
ds:s=0x0023, dl=0x0000ffff, dl=0x00cff300, valid=31
ss:s=0x0023, dl=0x0000ffff, dl=0x00cff300, valid=31
ss:s=0x0023, dl=0x0000ffff, dl=0x00cff300, valid=31
fs:s=0x003b, dl=0xe000ffff, dl=0x7f40f3fd, valid=7
gs:s=0x0000, dl=0x00000000, dl=0x00000000, valid=0
ldtr:s=0x0000, dl=0x00000000, dh=0x00000000, valid=0
tr:s=0x0028, dl=0x200020ab, dh=800008b04, valid=1
gdtr:base=0x8003f000, limit=0x3ff
idtr:base=0x8003f400, limit=0x7ff



1、GDTR.base 是 0x8003f000, GDTR.limit 是 0x3ff
2、IDTR.base 是 0x8003f400, IDTR.limit 是 0x7ff
3、LDTR.selector  为 0x0000

这里没有建立 LDT,它的 selector 是 0x0000,也就是 NULL descriptor。



5.6.1、  观察 cs register

观察 cs 的信息:
1、cs 使用的 selector 正是前面提到的 0x1b
2、接下来的 dl=0x0000ffff, dh=0x00cffa00 其实就是 descriptor 信息。

来看一看 cs 的 descriptor 的什么:

<bochs:8> x/2 0x8003f000+3*8
0x8003f018 <bogus+    0>:  0x0000ffff   0x00cffa00


  cs 使用的 selector 是 0x1b,因此:selector.RPL = 3  使用的权限 3   selector.TI = 0,使用 GDT,selector.SI = 3

  descriptor 的地址在:gdtr.base + 3 * 8 = 0X8003f018。

  它的值按 64 位显示是:0x00cffa00_0000ffff。


那么,descriptor 的信息:
1、 base = 0x00000000,这是 32 位值。
2、 limit = 0xffffff,这是一个 20 位的值。
3、 DPL = 11b,也就是 3 级。
4、 S 位是 1,它是一个非系统的 descriptor,也就是属于 segment descriptor。
5、 type 是 1010b,显示它是一个 execute/readable  non-conforming 类型的 code segment descriptor。
6、 limit 的粒度位 G 位是 1,显示它是 4K 粒度的。
7、 最后缺省位 D 位是 1, 表明目标 code segment 的 32 位代码。

  对这个 descriptor 描述的信息,归纳一下为:segment 是 32 位的代码段,基地址是 0x00000000,访问权限是 3 级,limit 是 0xFFFFF * 0x1000 + 0xFFF = 4G。
  物理上这个 descriptor 被加载到 cs register 里。




5.6.2、 观察 ds register

  我们看看 ds 加载的 descriptor 的又是怎样的。
  ds 使用的 selector 是 0x23:TI = 0,SI = 4 以及 RPL = 3。

获取 descirptor :
<bochs:9> x/2 0x8003f000+4*8
0x8003f020 <bogus+    0>:  0x0000ffff   0x00cff300


这个 descriptor 的值是:0x00cff300_0000ffff  (64 位值)


1、 base = 0x00000000,这是 32 位值。
2、 limit = 0xffffff,这是一个 20 位的值。
3、 DPL = 11b,也就是 3 级。
4、 S 位是 1,它是一个非系统的 descriptor,也就是属于 segment descriptor。
5、 type 是 0011b,显示它是一个具有 R/W 权限的 data segment descriptor。
6、 limit 的粒度位 G 位是 1,显示它是 4K 粒度的。
7、 最后缺省位 D 位是 1, 同 code segment descriptor 意义一致

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

这个 descriptor 与上面 cs 的 descriptor 不同之处仅是 type 不同。这个 descriptor 是个 data segment descriptor。



5.6.3、 平坦的内存模式

  现在的操作系统绝大部分使用平坦的内存模式,这种模式下,所有 segment 的基址是 0x00000000,但是 windows 使用了 FS 来定义非零基址的段。 FS 描述的段的基地址是:0x7f3de000,使用 FS 来管理一些系统信息。
  使用了平坦模式,导致可以使用 ds 读取 cs 的数据,或者可以执行 cs 以外的如:ds 或 ss 的代码。在 segmentation 这个阶段里 processor 无法阻止 stack 里的代码可以执行这一情况。直至在 paging 保护措施上才得到解决。

  既然使用了平坦模式,逻辑地址与线性地址是一致的。导致现代操作系统已经弱化逻辑地址这个概念,虚拟地址一般就指线性地址

因此:对于两条指令
  mov eax, dword ptr cs:[0x8012100]
      mov eax, dword ptr ds:[0x8012100]
      -------------------------------------
      结果是完全一致的。当然这是有提前的。提前是:cs 装的这个 code segment 是可读的。



但是,对于这两条指令,情况就不同了:

  mov dword ptr cs:[0x8012100],eax
      mov dword ptr ds:[0x8012100],eax
     -------------------------------------
     第 1 条是会出错的。这里 cs 装的 code segment 是不可写的。

论坛徽章:
0
12 [报告]
发表于 2009-02-24 15:02 |只看该作者
5.7、 segmentation 下的 protected 核心




segment descriptors 构建保护模式下的最基本、最根本的执行环境。system descriptors 则构建保护模式下的核心组件:

1、TSS descriptor 提供硬件级的进程切换机制
2、LDT descriptor 供进程使用多个 descriptor
3、Gate descriptor 提供 processor 权限级别的切换机制。



5.7.1、 TSS 提供的进程切换机制


TSS 是一段内存区域,存放进程相关的执行环境信息。初始化的 TSS 是由用户提供,进程切换时的保存信息由 processor 执行。


5.7.1.1、  三个元素构成 TSS 环境:

1、 TSS descriptor:这个 descriptor 属于 system descriptor 类型,它的 S (system)位是 0。

  下面列出 TSS descriptors 的类型值:

0001:  16-bit TSS
0011: busy 16-bit TSS
1001:  32-bit TSS
1011: busy 32-bit TSS
---------------------------------------------------
  以上是 x86 下的 TSS descriptor 类型,分为 16 和 32 位 TSS,x64 的 long mode 下 32 位的 TSS descriptor 将变为 64 位 TSS descriptor。


情景提示:

关于 TSS 的 busy 与 available 状态:
  1、 TSS descriptor 的类型指明是 busy 与 available 状态。
  2、 TSS descriptor 的 busy 与 available 状态由 processor 去控制。即:由 processor 置为 busy 或 avaibable。 除了初始的 TSS descriptor 外。

  TSS 的 busy 状态主要用来支持任务的嵌套。TSS descriptor 为 busy 状态时是不可进入执行的。同时防止 TSS 进程切换机制出现递归现象。


2、 TSS selector 以及 TR(Task Register)寄存器

  TR 寄存器的结构与 segment registers 是完全一致的,即:由软件可见的 selector 部分与 processor 可见的隐藏部分(信息部分)构成。
  TR.selector 与 CS.selector 中的 selector 意义是完全一样的。其 descriptor 的加载也是一样的。

  即: TR.selector 在 GDT / LDT 中索引查找到 TSS descriptor 后,该 TSS descriptor 将被加载到 TR 寄存的隐藏部分。当然在加载到 TR 寄存器之前,要进行检查通过了才可加载到 TR 寄存器。若加载 non-TSS descriptor 进入 TR 则会产生 #GP 异常。


3、 TSS 块(Task Status Segment)
  像 code segment 或 data segments 一样,最终的 TSS segment 由 TSS descriptor 来决定。 TSS descriptor 指出 TSS segment 的 base、limit 及 DPL 等信息。
  TSS segment 存放 eflags 寄存器、GPRs 寄存器及相关的权限级别的 stack pointer (ss & sp)、CR3 等等信息。




5.7.1.2、 TSS 机制的建立

  对于多任务 OS 来说,TSS segment 是必不可少的,系统至少需要一个 TSS segment,但是现在的 OS 系统不使用 TSS 机制来进行任务的切换。

情景提示:
  TSS 存在的唯一理由是:需要提供 0 ~ 2 权限级别的 stack pointer,当发生 stack 切换时,必须使用 TSS 提供的相应的 stack pointer。
  但是:若提供空的 TSS segment,或者可以考虑以直接传递 stack pointer 的方式实现 stack 切换,即便是这样设计 processor 要读取 TSS segment 这一工作是必不可少的。



下面的指令用来建立初始的 TSS segment:
  LTR word ptr [TSS_Selector]                /* 在 [TSS_selector] 提供 TSS selector */
或:LTR ax                                               /* 在 ax 寄存器里提供 TSS selector  */

  ltr 指令使用提供的 selector 在 GDT / LDT 里索引查找到 TSS descriptor 后,加载到 TR 寄存器里。初始的 TSS descriptor 必须设为 available 状态,否则不能加载到 TR。processor 加载 TSS descriptor 后,将 TSS descriptor 置为 busy 状态。




5.7.1.3、  TSS 进程切换的过程

  当前进程要切换另一个进程时,可以使用 2 种 selector 进行:使用 TSS selector 以及 Task gate selector(任务门符)。

如:
  call 0x2b:0x00000000          /* 假设 0x2b 为 TSS selector */
     call 0x3b:0x00000000          /* 假设 0x3b 为 Task-gate  selector */


  TSS 提供的硬件级进程切换机制较为复杂,大多数 OS 不使用 TSS 机制,是因为执行的效能太差了。上面的两条指令的 TSS 进程切换的过程如下:


1、使用 TSS selector  

  call 0x2b:0x00000000        /* 0x2b 为 TSS selector */

  这里使用 jmp 指令与 call 指令会有些差别,call 允许 TSS 进程切换的嵌套,jmp 不允许嵌套。


(1)processor 使用 TSS selector (0x2b) 在 GDT 索引查找第 5 个 descriptor

(2)processor 检查找到的 descriptor 类型是否是 TSS descriptor,不是的话将产生 #GP 异常。是否为 available TSS,若目标 TSS descriptor 是 busy 的话,同样将产生 #GP 异常。

(3)processor 进行另一项检查工作:权限的检查,CPL <= DPL 并且 selector.RPL <= DPL 即为通过,否则产生 #GP 异常。

(4)当前进程的执行环境被保存在当前进程的 TSS segment 中。

情景提示:
  在这一步里,此时还没发生 TSS selector 切换,processor 把当前进程的环境信息保存在当前的 TSS segment 中。


(5)这里发生了 TSS selector 切换。新的 TSS selector 被加载到 TR.selector,而新的 TSS descriptor 也被加载到 TR 寄存的隐藏部分。

情景提示:
  (1)这里,processor 还要将旧的 TSS selector 保存在当前的 TSS segment(新加载的 TSS)中的 link 域。这个 TSS segment 中的 link 其实就是 old TSS selector 域,用来进程返回时获得原来的 TSS selector 。从而实现任务的嵌套机制。
  (2)processor 将当前 eflags 寄存器的 NT(Nest Task)标志位置为 1,表明当前的进程是嵌套内层。


(6)processor 从当前的 TSS segment 取出新进程的执行环境,包括:各个 selector registers(segment registers)、GPRs、stack pointer (ss & sp)、CR3 寄存器以及 eflags 寄存器等。
  在这一步,在加载 selectors 进入 segment registers 之前,还必须经过相关的 selector & descriptor  的常规检查以及权限检查。通过之后才真正加载。否则同样产生 #GP 异常。

情景提示:
  processor 还要做另一项工作,就是:将新进程的 TSS descriptor 置为 busy 状态。使得新进程不能重入。


(7)processor 从当前的 CS:RIP 继续往下执行,完成这个 TSS 进程的切换。这个 CS: RIP 就是新加载的新进程的 cs : rip

---------------------------------------------------------------------------------------
  从上面的过程可以看出,使用 TSS 进程切换机制异常复杂,导致进程切换的效能太差了。比使用 call gate 以及 trap gate 慢上好多。若当中发生权限的改变,还要发生 stack 切换。
  进程的返回同样复杂。同样需要这么多步骤,总结来说,主要的时间消耗发生在新旧进程的信息保存方面。




2、 使用 task gate selector
  
  另一种情况是使用 task gate selector 进行 TSS 进程切换,使用 task gate selector 除了可以 call/jmp 外,还可用在中断机制上,下面的两种情况:

  call 0x3b:0x00000000            /* 0x3b 为 task gate selector */
或:int 0x3e                              /* 假设 0x3e 是 task gate 的向量 */


  这两种情形差不多,除了一些细微的差别外,不过是触发的机制不同而已。

  processor 在 GDT 索引查找到的是一个 task gate descriptor,这个 task gate descriptor 中指出了目标的 TSS selector 。processor 从 task gate descriptor 里加载 TSS selector,剩下的工作和使用 TSS selector  进行切换进程是一致。
  processor 访问 task gate descriptor 仅需对 task gate descriptor 作出权限检查:CPL <= DPL 并且 RPL <= DPL。在这里不需要作出对 TSS descriptor 的 DPL 权限进行检查。


  gate descriptor 机制提供了一层间接的访问层,主要用来控制权限的切换。

实际上:
  对于 call 0x2b:0x00000000 这条指令,processor 并不知道这个 selector 是 TSS selector 还是 task gate selector,在查找到 descriptor 后,processor 查看这个 descriptor 的 type 才能确定是 TSS selector 还是 task gate selector。



最后需注意:
  (1)使用 jmp 指令进行转移,processor 不会将 eflags 中 NT 标志位置 1
  (2)使用中断机制下的 TSS 进程切换,processor 将不作任务的权限检查动作。





5.7.1.4、 TSS 进程的返回

  从 TSS 机制切换的进程在执行完后使用 iret 指令返回原进程进,同样会发生新旧 TSS segment 的更新动作。

  进程通过使用 ret 返回时,processor 将不会从嵌套内层返回到的嵌套外层进程,也就是不会返回原进程。processor 对 ret 指令的处理,只会从 stack  pointer(ss : esp) 取返回地址。



对于使用进程使用了 iret 中断返回指令时:

(1)processor 将会检查当前的 eflags.NT 标志位是否为 1,也就是检查当前进程是否处于嵌套的内层。

(2)eflags.NT = 1,processor 将从当前 TSS segment 中的 link(old TSS selector)域中取出原来进程的 TSS selector。

(3)processor 将不作任何的权限检查,TSS selector 被加载到 TR.selector,TSS descriptor 同时被加载到 TR 的隐藏部分。

(4)processor 将清除当前的 eflags.NT 为 0。若是使用 ret 指令返回的,processor 是不会清 eflags.NT 为 0

(5)从 TSS segment 中加载新的进程执行环境,从新的 CS:EIP 处继续执行。 将原来的 TSS descriptor 重新置为 available 状态,使得可以再次进入。


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

  由上可得,processor 遇到 ret 指令时,是不会对 eflags.NT 进行检查的。而使用 iret 指令,processor 将对 eflags.NT 进行检查。





:wink:

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

论坛徽章:
0
13 [报告]
发表于 2009-02-25 16:13 |只看该作者
TSS 的题外话:使用 TSS 机制在任何权限下进行随意的切换


  
  利用 TSS  机制可以进行任意权限级别的随意切换是基于:

(1)在任何权限下可以更改 eflags.NT 标志位。

(2)使用中断返回指令 iret 进行切换到更高级或更低级权限的代码。


如下:
  pushfd                                       /* 这条指令可以在 3 级执行 */
      or dword ptr [esp], 0x4000           /* 置 eflags.NT 为 1 */
      popfd                                         /* 写 eflags */
      ... ...
      iret                                          /* 使用 iret 进行切换到高级或低级代码 */

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

基于以上所述成立的条件是:

(1)在当前 TSS segment 中的 link(old TSS selector)指出另一个 TSS selector

(2)从这个 TSS selector 所索引出 TSS descriptor ,最终从这个 TSS descriptor 加载的 TSS segment 中加载权限高或权限低的 selectors,包括 cs selector 、ds selector 以及 ss selector ,从而实现任何权限的切换。


这里唯一的障碍是:
  需要在 0 级权限下首先建立一个可用的 TSS segment 环境,而 TSS segment 的 link 域里有个可用的 TSS selector。LTR 指令需要在 0 级权限下使用。



  

   

论坛徽章:
0
14 [报告]
发表于 2009-03-03 23:55 |只看该作者
5.7.2、  LDT descriptor



  显然,GDT(global descriptor table)是系统与所有进程共用的。相对于 LDT(local descriptor table)则是进程独享的。GPRs、selector registers、stack pointers、eflags、LDT descriptor、CR3 构成了一个进程的执行环境。
  TSS segment 包含了上述几个环境资源。LDT 是进程独享的一个资源,是基于进程生命周期内该 LDT 进程外不可见的。每个进程有自己的 LDT (前提是系统使用了 LDT 机制)。
  实际上,使用了 LDT 机制等于使用了 TSS 机制。TSS 机制早已被绝大多数 OS 所抛弃了。这无疑是 LDT 机制被绝大多数 OS 舍弃。然而,LDT 虽然是进程独享的,但另一方面,使得多个进程可以共享内存。通过传递同个的 LDT descriptor 就可以在多个进程中共享内存区域。如:每个进程的 TSS segment 中的 LDT descriptor 设为同一个 descriptor。



5.7.2.1、 LDT 的使用

1、LDT 的初始化
  操作系统在初始化时,可以使用 LLDT 指令建立一个 LDT。 LLDT 须要 0 级权限,所以在用户进程方面不能使用 LLDT 指令建立 LDT。
 
 
用法示例:
 
   LLDT ax                                     /* ax 是 LDT descriptor selector*/
或:
   LLDT word ptr [LDT_desc]        /*  内存 [LDT_desc] 存放着 LDT descriptor selector */


很显然,这里的 LDT descripotr selector 结构应该是:

  XXXXXXXXXXXXX 0 00

在 GDT 中对应的 LDT descriptor entry。


2、用户进程中的 LDT
  通过 TSS 切换的进程可以在 TSS segment 中指出 LDT descriptor selector。在进程切换后,processor 会自动执行 load LDT descriptor 的动作。这个 LDT descriptor selector 结构和上述的一样的。同样要在 GDT 查找 LDT descriptor。

  在这里,如果多个进程的 TSS segment 中的 LDT descriptor selector 设为同一个 selector 的话,等同于多个进程共享同一个 LDT,起共享内存作用。



5.7.2.2、 LDT descriptor 的加载
  
  LDT descriptor 的加载模式和 TSS descriptor 的加载模式完全一样。

  如同 TSS descriptor 加载到 TR 寄存器一样,LDT descriptor 被加载到 LDTR 寄存器。LDTR 寄存器与 TR 寄存器的结构也完全一致。包括:selector 域、attribute 域、limit 域以及 base 域。

情景提示:
  1、实际上,segment registers、LDTR 以及 TR 的结构都是一样的。而 GDTR 与 IDTR 则是一样的。
  2、segment registers、LDTR 以及 TR 的加载方式也是一样的。而 GDTR 与 IDTR 的加载方面是一样的。GDTR 与 IDTR 是需要操作系统初始化,代表 GDT 与 IDT 两个 descriptor table 结构必须要建立。





:wink:

[ 本帖最后由 mik 于 2009-3-12 17:17 编辑 ]

论坛徽章:
0
15 [报告]
发表于 2009-03-12 17:37 |只看该作者
5.7.3、 使用 Gate 构建保护模式下 protected 核心


  x86 下 segment 保护核心就是 gate 机制,x64 的 long 模式下 segment 保护有所弱化,AMD 号称 long 模式取消了 segment 机制,实际上,现阶段的的 x64 体系中,segment 机制是不可取消的。只不过是将平坦内存模式从软件层移到硬件层,等取于取消了 segment 机制。
  现在的操作系统都将 protected 重心放在了 paging 阶段,x64 体系从硬件层体现了这个思想。



5.7.3.1、 gate 是什么

  gate 机制体现在 gate descriptor 的设置,显然,gate descriptor 像是一道通关令符。顾名思义,门是通过另一片天地的必经之路。这片天地通常会得到更多的资源,这片天地通常就是系统的核心代码部分。
  gate descripotr 门符就是指引通往这片天地的钥匙。有权限获得门符就可以访问更底层的代码。


  gate descriptor 的作用相当于 C 语言里的指针,并且这个指针就是函数指针。

如下:

void foo()
{
        ... ...
}

int main()
{
        void (*pf)() = foo;
        ... ...
        
        pf();             /* call gate 类似 */
        ... ...
}


  gate 机制是一种间接的转移控制方式。gate 的主要作用是用来检查权限、切换权限。上面的 c 代码中,函数指针 pf 相当一个 call gate descritpor。gate descriptor 结构里指出了真正要跳转代码的地方。


有以下几种 gate 类型:
● call gate
● interrupt gate
● trap gate
● task gate



上面的图是这几种 gate descriptor 的格式。


1、在 task gate descriptor 里,关键的元素就是 TSS selector,这个 TSS selector 给出目标进程的 TSS selector,通过使用 task gate descriptor 达到使用 TSS 来进行进程的切换。

2、可见 interrupt gate descriptor 与 trap gate descriptor 的格式是完全一致的,它们的匹别只是 type 的不同。

3、而 call gate descriptor 与 interrupt/trap gate descriptor 的区别只是,使用 call gate 可以传递总共 31 个参数。




5.7.3.2、  如何使用 call gate 传递参数

  在 call gate descriptor 的 parameter count 部分里指出了参数的个数。这个参数是告诉目标代码将要有多少个参数传递过来。

看下面的例子:

/* caller */

push param1                     /* parameter */
push param2
push param3

call 0x20:00000000              /* call gate */

... ...


在调用方里使用 call gate 进行切换。caller 先压入了 3 个参数,然后通过 call gate 来调用子例程


/* callee */

... ...

ret 0x0c


子例程 callee 完成工作后,很正常地 ret 返回。

那么,当发生 stack 切换时,即:caller 在 CPL =3 下, callee 在 CPL = 0 下,这时会发生 stack 的切换。

此时,processor 会做相当多的必要工作,在假设通过权限的检查下:

1、首先从 TSS segment 里得到 0 级下的 stack 指针(即:SS 和 ESP)加载到 SS 以及 ESP。这时属于 0 级下的 stack 空间了。

2、将原来的 SS 及 ESP 保存到新的 stack 里,也就是压入旧的 SS 及 ESP。

3、然后,这里根据 call gate descriptor 里的 parameter count 参数个数,从旧的 stack 里复制参数到新的 stack 里。在这个例子里是 3 个参数,也就是依次压入 param1、param2 及 param3 在 stack 上。
  在这一步里就实现了传递参数。


4、然后,再保存返回指针(即:CS 及 EIP)。

5、在 callee 返回时执行 ret 0x0c 时,pop 出 CS 及 EIP,ret 0x0c 会消去压入的 12 个字节的。

6、pop 出 原来的 stack 指针(SS 及 ESP)。从而 0 级的 stack 恢复正常。


  使用 call gate 传递参数,是一种 caller 与 callee 的约定方式,caller 压入参数,并明确告诉 callee 有多少个参数。而 callee 必须负责做消除 stack 工作。
  这个例程的使用参数一旦定下来,caller 与 callee 就不能改变。若一方改变了这种约定,就会造成 stack 混乱。






:wink:

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

论坛徽章:
0
16 [报告]
发表于 2009-03-22 00:44 |只看该作者
5.7.3.3、 gate 的使用方法

  gate descriptor 给出了目标代码的访问途径。



上面给出一个直观的访问途径图。这个途径就是:

1、gate descriptor 的 selector 给出目标代码的 code segment descriptor
2、由目标代码 code segment descriptor 的 base address 得出代码的 base
3、这个 base 加上 gate descriptor 的 offset 值,最终得到代码的入口点。

用 C 代码描述为:

void do_call_with_gate(selector_t call_gate_descriptor)
{
        code_descriptor = get_descriptor(call_gate_descriptor.selector);
        base = code_descriptor.base;

        void (*pf)() = (void(*)()) base + call_gate_descriptor.offset;

        pf();

}




在用户代码中使用如下指令:

  call  0x20:00000000                 /*  通过 call gate 调用 */
或:
  jmp 0x20:00000000               

---------------------------------------------------------------
  上面这条指令是 far call 指令格式:call cs:eip 这种形式。 0x20 为 call gate descriptor selector,在这里 0x000000000 是无意义的。只是为了完整这条指令格式,可以使用任意一个值。
  在 gate descriptor 格式里,高 16 位和低 16 位组成一个 32 位的 offset 值。这个值就是入口地址。在绝大多数的 OS 里使用的平坦内存模式下,base 为 0,那么,这个 offset 就是最终的服务例程的入口点。

论坛徽章:
0
17 [报告]
发表于 2009-03-22 20:49 |只看该作者
话题 6、 x64 体系下的 segment 情形


  

6.1、 x64 下的物理资源及系统数据结构


6.1.1、 segment registers

  x64 体系在硬件级上最大限度地削弱了 segmentation 段式管理。采用平坦内存管理模式,因此体现出来的思想是 base 为 0、limit 忽略。
  但是,x64 还是对 segmentation 提供了某种程度上的支持。体现在 FS 与 GS 的与众不同。
  segment registers 的 selector 与原来的 x86 下意义不变。


在 64 bit 模式下:

(1)code register(CS)
  ● CS.base = 0(强制为 0,实际上等于无效)
  ● CS.limit = invalid
  ● attribute:仅 CS.L 、CS.D、CS.P、CS.C 以及 CS.DPL 属性是有效的。

情景提示:
  64 bit 模式下的 code segment descriptors 中的 L 位、D 位、P 位、C 位以及 DPL 域是有效的。code segment descriptor 加载到 CS 后仅 CS.L 、CS.D、CS.P、CS.C 以及 CS.DPL 属性是有效的。


  在 compatibility 模式下 code segment descriptor 和 CS 寄存器与原来 x86 意义相同。


(2)data registers (DS、ES 以及 SS)
  ● DS.base = 0(强制为 0,实际上等于无效)
  ● DS.limit = invalid
  ● DS.attribute = invalid:所有的属性域都是无效的。

  data registers 的所有域都是无效的。data segment 的 attribute 是无效的,那么也包括 DPL、D/B 属性。

  在 64 bit 模式下,所有的 data segment 都具有 readable/writable 属性,processor 对 data segment 的访问不进行权限 check 以及 limit 检查。


(3)FS 与 GS
  ● FS.base 是完整是的 64 位。
  ● FS.limit = invalid
  ● FS.attribute = invalid

  与其它 data registers 不同的是,FS 与 GS 的 base 是有效的。支持完整的 64 位地址。但是 limit 和 attribute 依旧无效的。


1、为 FS 和 GS 加载非 0 的 64 位 base 值,使用以下指令:
  
  mov fs, ax

  pop fs

--------------------------------------------  
  这条指令只能为 fs 提供 32 位的 base 值,这根本的原因是:data segment descriptor 提供的 base 是 32 位值。在 x64 里的 segment descriptor 是 8 个字节。也就是 base  是 4 个字节。通过 selector 加载 base 值,只能获取 32 位地址值。


2、为 fs 和 gs 提供 64 位地址值,可以使用以下指令:

  mov ecx, C0000100                   /* FS.base msr 地址 */
  mov edx, FFFFF800
  mov eax, 0F801000                    
   wrmsr                                        /* 写 FS.base */
--------------------------------------------------
  上面代码为 FS.base 提供 0xFFFFF8000F801000 地址。


  mov ecx, C0000101                   /* GS.base msr 地址 */
  mov edx, FFFFF800
  mov eax, 0F801000                    
   wrmsr                                        /* 写 GS.base */
--------------------------------------------------
  上面代码为 GS.base 提供 0xFFFFF8000F801000 地址。


  另一种方法是使用 swapgs 指令,这条指令将 kernelGS 地址与 GS.base 交换。
  



6.1.2、 descriptors 结构

  x64 体系已经不提供对 segmentation 的支持(或者说最大程度削弱了),对于 segment descriptor 来说,还是停留在 x86 的阶段,绝大部分的功能已经去掉。但是对于 system descriptor 来说,它是被扩展为 16 个字节,是 128 位的数据结构。
  因此,descriptors 结构要分两部分来看。

1、segment descriptors
  包括 code segment descriptor 和 data segment descriptor,code segment descriptor 除这几个属性: P 位、C 位、D 位、L 位以及 DPL 外,其它都是无效的(当然 code segment descriptor 的 S 位为 0 表示 segment descriptor,code / data 位为 1 表示 code segment)。
  data segment descriptor 除了 P 属性外,其它都是无效的(当然 data segment descriptor 的 S 属性为 0,code/data 属性为 0)。
  segment descriptor  还是 8 个字节 64 位的数据结构,没有被扩展为 16 个字节,根本的原因是 base 域是无效的。



2、 system descriptors
  包括 LDT descriptor、TSS descriptor 。这些 descriptor 被扩展为 16 个字节共 128 位。descriptor 的 base 域被扩展为 64 位值。用来在 64 位的线性地址空间中定位。

  在 64 bit 模式下,LDT / TSS descriptor 被扩展为 64 位的 descriptor,base 是 64 位值。在 compatibility 模式下,LDT / TSS 依旧是 32 位的 descriptor。 
  


3、 gate descriptor
  long mode 下不存在 task gate。所有的 gate(call、interrupt / trap) 都 64 位的。gate 所索引的 code segment 是 64 位的 code segment(L = 1 && D = 0)



情景提示:
  1、long mode 下的 segment descriptor 与 x86 原有的 segment descriptor 格式完全一致,只是在 64 bit 模式中 descriptor 的大部分域是无效的。
  2、64 bit 模式下的 system descriptor 被扩展为 16 个字节。由于 system descriptor 中的 base 是有效的,base 被扩展为 64 位,故 system descriptor 被扩展为 128 位。





6.1.3、 descriptor table

1、long mode 下的 GDT / LDT 表

试想一下,以下情景:
  当 processor 处于 long mode 下,而要执行的目标代码是 32 位的,也就是说 processor 将要进入 compatibility mode(兼容模式)去执行 x86 的 32 位代码,此时变得有玄妙 ... ...


  1、首先,processor 处于 long mode,这表明此时:processor 既可以执行 64 位代码也可以进入 compatibility mode 去执行 32 位代码。什么情形下进入 compatibility mode 呢?这是根据目标代码的 descriptor 的 L 属性,目标代码的 descriptor 加载进入 CS 后,即 CS.L 决定是 64 bit mode 还是 compatibility mode。
  当 CS.L = 1 时,processor 处于 64 bit mode, CS.L = 0 时,processor 处于 compatibility mode。

  2、其次,一个既可执行 64 位代码,又可 32 位代码的 64 位 OS ,它的核心 kernel 及系统服务例程、相关的系统库是运行在 64 bit 模式下的。原有的 32 位代码运行 compatibility 模式下。
  所以,在 GDT 表中可能同时存在 32 位的 descriptor 和 64 位的 LDT/TSS descriptor、64 位的 call gate descriptor。

  3、最后,processor 是怎样识别哪个是 32 位的 descriptor?哪个是 64 位的 call gate descriptor ?在 32 与 64 位 descriptor 相互存在的 descriptor table 里又是如何去正确读取所要的 descriptor ?


-----------------------------------------------------------------------------------------------
  在 long mode 下所有的 gate 都是 64 位的,所有的 segment descriptors 都是 32 位的。processor 根据 descriptor 的 types 来判断哪些是 64 位,哪些是 32 位。


  这样需要解决一个 32 位 descriptor 与 64 位 descriptor 重叠的问题,思考以下的指令:

call [call_gate]             /* call gate */


  若 [call_gate] 里放着的 selector 是 0x20,若它是 64 位的 call gate descriptor selector。那么 processor 将进入 64 位模式执行 64 位代码。这个 selector 中的 SI 值是 4,所以 GDT 表中的第 4 项是个 64 位的 call gate descirptor。
  这个 64 位的 call gate descriptor 结构有 128 位宽。64 位的 base 中,低 32 位 base 在前 8 个字节里,高 32 位 base 在后 8 个字节里的前2 个字节。


在这种情形下,当程序中,使用了以下指令,意图去访问 32 位的 code segment descriptor 时,就会产生问题:
  
  call 0x28:00000000                  /* 意图访问 32 位的 code segment */

  这个 selector 的 SI 是 5,所以 processor 刚好访问到 64 位的 call gate descriptor 的高半部分。这个高半部分是 base 的高 32 位值。当程序中以 32 位 code segment descriptor 形式访问 64 位的 call gate descriptor 高半部分,这时 32 位的 code segment descriptor 与 64 位 call gate 的高半部分重叠了,这根本不是想要访问的 32 位 code segment descriptor,这就产生了不可预测的结果。

  也就是说:以 64 位 gate 的高半部分作为 32 位的 descriptor 访问将现出不可预制的结果。

情景提示:
  这个原因产生的根本原因是:GDT 的索引是固定为以 8 字节宽度进行索引。在 64 位模式下,不会以 16 字节为宽度进行索引,还是以 8 字节宽度进行索引,这导致 32 位的 descriptor 可能会与 64 位 descriptor 部分重叠了。


  解决这个问题的设计思想是,将 64 位的 gate descriptor 或 LDT descriptor、TSS descriptor 的高半部分的对应 descriptor 的 type 的域置为 0000。
  当 type = 0000 时,表示这个 descriptor 是非法的,这样会引发 #GP 异常。

  所以,当上述的情形下,64 位的 call gate desciptor 的高半部分的 type 域定义为 0000 时,以 32 位 descriptors 方式访问 64 位 call gate 的高半部分时,将产生 #GP 异常。这样将避免产生不可预测的结果改变产生 #GP 异常,从而提醒软件设计人员进行更改。


这种实施策略相当于:

char *p = 0;           
*p = 'a';                      /* 产生异常 */
--------------------------------------------
  OS 将反馈给程序员这将产生异常,目的是将不可预测的结果改为可预测的错误结果。从而避免错误。




2、 long mode 下的 IDT 表

  前面提到,在 long mode 下:GDT/LDT 中是以 8 个字节进行索引的。那么:在 IDT 中则是以 16 个字节索引的。

情景提示:
  1、GDT / LDT 是以 8 个字节进行索引 descriptors。
  2、IDT 是以 16 个字节进行索引 descriptors。



造成这种在不同 descriptor table 中按不同 size 进行索引的根本原因是:

  1、在 GDT / LDT 中,可以同时存放 32 位与 64 位的 descriptors,这些 descriptors 包括:32 位的 segment descriptors、64 位的 system descriptors(LDT descriptors、TSS descriptors),还有就是 64 位的 call gate descriptors。
  所以,在 GDT / LDT 中只能以原来的 8 个字节进行索引,那么在 64 位的 system descriptors 中,必须在其高 8 字节的 type 属性里设为: type = 0000 以防止 32 位 descriptors 与 64 位 descriptors 重叠而产生问题。

  2、在 IDT 中,只能存放 64 位的 descriptors,所以不像 GDT / LDT 那样需要兼容 32 位的 descriptors。因此,在 IDT 中固定以 16 个字节进行索引 descriptor entrys。

  IDT 是不能存放 call gate descriptors 的,只能存放 interrupt /trap descriptors 。task gate 在 long mode 下不存在。














:wink:

[ 本帖最后由 mik 于 2009-4-11 02:54 编辑 ]

论坛徽章:
0
18 [报告]
发表于 2009-03-29 01:19 |只看该作者
6.1.4、  long mode 下的 call gate


6.1.4.1、 64 位的 call gate descriptor 中已经不支持 parameter count 域,offset 域被扩展至 64 位 base 值。

情景提示:
  64 位的 call gate descriptor 的中 selector 域必须是一个 64 位代码的 selector,也就是说:这个 selector 指向的 code segment descriptor 中的 L 属性必须是 1,并且 default operand size 为 32 位,即:D 属性为 0。否则将产生 #GP 异常。(L = 1 && D = 0)
  若要转到 32 位代码,须使用 32 位的 call gate descriptor。


  由于 64 位的 call gate descriptor 中不支持 parameter count 域,所以在 long mode 下,processor 不支持自动复制参数的行为(从 caller 的 stack 复制到 callee 的 stack 中)。

那么,在 long mode 下如何使用 call gate 调用例程时,如果确实需要传递参数,又是如何传递参数呢?  

  在 stack 不进行切换的情况下,和一般的例程调用别无两样。在要进行 stack 切换的情形下,原来的 stack 的 ss 和 rsp 值被保存至新的 stack 中(也就是 caller 的 ss 和 rsp 会保存至 callee 的 stack 中)。那么,例程 callee 将直将使用原来的 rsp 去获取参数。
  这样做的根本原因是,在 x64 的 long mode 下在硬件级下使用的是平坦内存管理模式,忽略了 segmentation 管理,callee 将可以直接使用 rsp 来获取参数。


看看下面的指令的情形:

caller: 
    ... ...
  push param1
  push param2 
    call [call_gate]            /* call gate */
  ... ...


  caller 中用 call gate 进行调用例程 callee, 假设这里需要进行 stack 的切换。


callee:

   ret         


  callee 中仅使用 ret 进行返回,无需进行清栈处理。


那么:
  caller 的 ss 和 rsp 将被保存至 callee 的 stack 中,如下:

------------------
     caller's SS       + 24
------------------
    caller's RSP      + 16
------------------
    caller's CS       + 8
------------------
    caller's RIP         <------------   callee's  rsp
------------------
 ... ...
------------------

  在例程 callee 中 [rsp+16] 处获取 caller's rsp 值,callee 中如下处理:

  push rbp
      mov rbp, rsp
      mov rax, [rbp + 24]           /* get caller's rsp */
      mov rbx, [rax]                   /* get param1  */
      ... ...

      和一般的调用例程,使用参数无异,不像 x86 下使用 call gate 调用例程,processor 会自动产生复制参数行为。



6.1.4.2、 前面提过,call gate 只能存放在 GDT / LDT 中,不能放在 IDT 中
  这造成 64 位的 call gate descriptors 的高 8 字节的 type 属性必须为 0000,以避免 32 位的 descriptor 与 64 位的 descriptors 重叠在一起。为 0 的 type 是属于无效的 descriptors 类型,processor 会检测这个 type 是否为 0,为 0 则是无效的 descriptors。
  这样的话,想提取 64 位的 descriptors 的高半部分作为 32 位 descriptors 使用就会产生一个 #GP 异常。
  

论坛徽章:
0
19 [报告]
发表于 2009-03-29 01:59 |只看该作者
6.1.5、  long mode 下的 TSS segment 以及 task gate

  long mode 已经不支持使用 TSS 机制进行任务切换。TSS 必须存在的唯一原因依然是:需要提供每个权限级别的 stack 指针。但是,由于不支持 TSS 任务切换机制,所以,long mode 下的 TSS segment 和原来的 TSS segment 有很大的改变。


1、long mode 下的 TSS segment 去掉了必要的执行环境,包括:GRPs 集、Selector Registers 集、eflags 寄存器、CR3 寄存器、指令指针 EIP、LDT selector 以及 Task link。
  这些执行环境在 x86 环境中是必须的。但是在 x64 的 long mode 下的 TSS segment 中这些环境要素已经没用处了,x64 的 Tss segment 唯一的作用就是提供 stack pointer。

2、long mode 下的 TSS segment 中的 stack pointer 被扩展至 64 位,SS selector 被抛弃。同时又增加了 7 个 stack pointer。这些新增的 7 个 stack pointer 和原来的 stack pointer 意义和用途都不同 。
  原来的 3 个 stack pointer 意义是代表权限级别为 0、1、2 的通用 stack pointer,在任务切换时,若发生了 stack 切换时相应级别的 stack pointer。
  新增的 7 个 stack pointer 是特定用途的 stack pointer,仅用于发生中断和异常时使用。这 7 个 stack pointer 意即:Interrupt Stack Table(IST)IST ~ IST7。使用于 interrupt 或 trap gate descriptor 中。在 interrupt/trap gate descriptors 里指出相应的 stack pointer 号,从而进入相应的 stack。



  由于在 long mode 下不支持 TSS segment 任务切换机制,所以,在 long mode 下没有 task gate 存在。当然也不支持使用 TSS selector 进行任务切换
  





:wink:

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

论坛徽章:
0
20 [报告]
发表于 2009-04-06 22:37 |只看该作者
6.1.6、 long mode 下的 interrupt/trap gate


  interrupt 与 trap gate 格式是一样的,只是 descriptor type 不一样而已。与 x86 的 interrupt / trap gate 的所不同的是:base 扩展为 64 位、额外增加了 IST 域。
  前面提过,由于 interrupt / trap gate 只能放在 IDT 表中,不存在 64/32 位代码共存的现象,所以,interrupt / trap gate 的高半部分的 type 域不需为 0000,而保留未用。



6.1.6.1、 interrupt gate 与 trap gate 的细微区别

  interrupt 与 trap 是在格式上是完全一致,区别在于:当响应 interrupt gate 时,processor 将 Rflags.IF 清为 0,将中断标志清为 0,表示在 interrupt 例程执行完毕之前是不能响应其它的中断(可屏敝中断)。
  当响应 trap gate 时,processor 不会对 Rflags.IF 标志进行修改。表示在执行 trap 例程时,可以响应其它的中断。



6.1.6.2、 IST(Intrrupt Stack Table)的使用
  
  新增的 IST 域共 3 位,可表示 1 ~ 7 个 ID 值,代表 IST1 ~ IST7。当 IST = 0 时,表示无 IST,或者不使用 IST 功能。
  IST1 ~ IST7 是索引 ID 值,在 long mode 下的 64 位 TSS segment 的 IST1 ~ IST7 中进行索引,前面已经提过在 TSS segment 中新增了 IST ~ IST7 共 7 个域,每个域为 64 位值,每个 64 位值是 stack pointer (RSP)。
  假如在 interrupt/trap gate descriptor 中的 IST 设为 2,那么当响应这个 interrupt/trap 例程时,processor 据此在 TSS 中索引到 IST2,将 TSS 的 IST2 加载到 rsp 中,从而使用这个特定的 stack pointer 。
  但是,若发生了权限的改变,即发生了 stack 的切换,最终仍是从相应权限的 stack pointer 加载 rsp 值,如:目标 interrupt 例程是 0 级的代码,而调用者为 3 级代码。这时发生 stack 的切换,processor 那么将从 RSP0 中加载 rsp。
  
  IST 的价值是为 interrupt / trap 例程提供一个特定的 stack 环境。这是在未发生 stack 切换的前提下。IST 为 0 则表示不使用特定的 stack 环境。



6.1.6.3、 interrupt / trap gate 调用流程

  long mode 下的 interrupt / trap gate 与 32 位下的调用流程有很大差别:

1、在 x86 下 32 位的 interrpt / trap gate 的 base(offset) 并不是真正的 interrupt / trap 例程的执行入口,执行入口首需经 interrupt/trap gate 的 selector 域得出目标 code segment 的 descriptor,然后,目标 code segment descriptor 的 base 值加上 interrupt / trap gate 的提供 offset 值,最终才形成真正的执行入口。
  当然,现在绝大部分的 OS 已经在 code segment descriptor 的 base 设为 0,实际上等于 interrupt / trap gate 的 offset 就是例程的执行入口。

2、在 long mode 下的 intterupt / trap gate 的 base(offset)提供的是真正的例程执行入口地址。而 interrupt / trap gate 的 selector 仅仅只是起了权限检查的作用。当过通了权限检查后,直接将 offset 加载到 rip 进行执行。
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

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

清除 Cookies - ChinaUnix - Archiver - WAP - TOP