局部空间与全局空间

32 位机的虚拟地址位数为 32 位,每个任务都有自己独立的 4GB 虚拟地址空间。所谓独立,指的是不同任务相同的虚拟地址映射到不同的物理地址,本质上是依赖每个任务有自己的 PDT/PT。

这 4GB 虚拟地址空间又分为两部分:低端虚拟地址位于局部空间/用户空间、高端虚拟地址位于全局空间/内核空间。虚拟地址的高低端分界线依赖于操作系统的实现,如 Linux 的分界线位于 3GB 处,0 - 3GB 为低端虚拟地址,3GB - 4GB 为高端虚拟地址。

所有任务共享全局空间(所有任务的高端虚拟地址指向相同的内核页表),有各自的局部空间(LDTR 指向当前任务的 LDT,低端线性地址指向各自的页表)。

下图参考《x86 汇编语言 从实模式到保护模式》绘制,虚拟空间中 0 - 2GB 为局部空间,2 - 4GB 为全局空间,所有段均处于平坦模式。

全局空间_局部空间

用户态与内核态

特权级是处理器用来区分不同执行环境的一种机制。例如,在 x86 架构中,有四个特权级(Ring 0 到 Ring 3),其中 Ring 0(最高特权级)是内核态,操作系统内核在这里运行,拥有全部权限;Ring 3(最低特权级)是用户态,普通应用程序运行于此,权限受到限制。由固件负责实施特权级保护。

  • DPL:在描述符中指定,代表着一个段的特权级。
  • CPL:在 CS 段选择子中指定,代表着当前执行的代码段的特权级。
  • RPL:在段选择子中指定,代表请求者的特权级。
  • IOPL: 在 TSS 的 EFLAGS 中指定,代表着当前任务的输入输出特权级。

在绝大多数时候,请求者都是当前程序自己,因此 CPL = RPL。有时可能用户程序需要传入一个选择子作为参数,让内核例程代为访问,进入内核态后 CPL 变为 0,这时操作系统必须负责把这个选择子的 RPL 设置为用户程序自己的 CPL。

特权级在以下场景中发挥作用:

  • 特权指令使用条件:CPL = 0。
  • 访问数据段条件:CPL 和 RPL $\leq$ 目标 DPL。
  • 访问代码段条件:除从高特权级例程返回外,不允许从高到低转移,因为操作系统不会引用可靠性比自己低的代码。一般来说,控制转移只允许发生在两个特权级相同的代码段之间(CPL 和 RPL = 目标 DPL);若目标代码段是依从的,可以从低到高,但转移后不允许改变 CPL(CPL 和 RPL $\geq$ 目标 DPL);还可以通过调用门从低到高(用 jmp far 不改变 CPL,用 call far 会把 CPL 提升到目标 DPL)。
  • 访问调用门条件:目标 DPL $\leq$ CPL 和 RPL $\leq$ 调用门描述符 DPL。

态 != 空间

态和空间不是一回事,但又紧密关联

区别

  • 局部空间/全局空间是虚拟地址直接决定的:平坦模式下,IP(32 位,范围 0 - 4GB)的值即为虚拟地址大小,若虚拟地址是低端则决定了位于局部空间,若虚拟地址是高端则决定了位于全局空间。

  • 用户态/内核态是 CPL 直接决定的:CS 中段选择子的 CPL 为 3 决定了位于用户态,CPL 为 0 决定了位于内核态。

既然平坦模式下虚拟地址大小只由 IP 决定,那么在用户态下 IP 的值为高端虚拟地址会发生什么呢?在 Linux 下运行 gcc -Wall -O0 -g -m32 -Ttext 0xc0100000 test.c -o test; ./test,能顺利编译但果然无法运行:报错 Segmentation fault,即虚拟地址非法。

联系:用户态 $\leftrightarrow$ 内核态、局部空间 $\leftrightarrow$ 全局空间这两种切换是同时进行的。切换的方式一般是通过门描述符,门描述符中同时指定了目标代码段的段描述符(CS)和段内偏移(IP),因此 CS、IP 是同步变化的,用户态 $\leftrightarrow$ 内核态、局部空间 $\leftrightarrow$ 全局空间这两种切换也就一同进行,导致产生了用户态和局部空间绑定、内核态和全局空间绑定的感觉。

以系统调用为例,解释用户任务陷入内核的过程,即从用户态 CPU 在局部空间执行转变为内核态 CPU 在全局空间执行。

  1. 用户程序将参数传入相应寄存器,如系统调用号传入EAX,再执行int 0x80
  2. CPU 在 IDT 中找到中断向量 0x80 对应的描述符是一个系统门(用户态可以访问的陷阱门),该门描述符 DPL = 3,包含内核代码段 __KERNEL_CS 的段选择子、指向 system_call() 系统调用处理程序的段内偏移。内核代码段的段选择子 TI=0,因此内核代码段描述符位于 GDT 中,内核代码段描述符的 DPL=0 且段基址是一个高端虚拟地址。
  3. CPU 将内核代码段的段选择子赋给 CS,system_call()在内核代码段中的段内偏移赋给 IP。此时 CS、IP 对应的虚拟地址从低端虚拟地址变为高端虚拟地址(即进入全局空间),CS 的 CPL 由 3 变为 0(即进入内核态)。注意:陷入内核并没有发生任务切换(指 LDTR、TR、PDBR 都没变),PDBR 仍然指向用户任务 PDT,且高端虚拟地址通过用户任务 PDT 仍然能找到内核 PT。
  4. 开始执行 system_call()system_call() 根据最开始传入的系统调用号,调用对应的特定服务例程。