x64 内核(二)IA-32e 分段研究

😄

1 分段架构

1.1 IA-32 分段架构

IA-32 架构中,内存管理功能分为:段(Segment)、页(Page)。

段地址格式,也叫做逻辑地址或远指针(Far point):Sengment-Selector: Offset(如 CS:Offset)。

如果不启用分页,如下图,处理器的线性地址将直接映射到处理器的物理地址空间。

17.png

IA-32 支持三种分段方式:

  1. 基础平坦模式(Basic Flat Model)

    这种模式下,至少存在两种段描述符:代码段描述符、数据段描述符(段基址都为 0,Limit 都为 4GB)。任何尝试访问不存在的内存时不会产生异常。

    18.png

  2. 受保护的平坦模式(Protected Flat Model)

    定义了 R0 权限的代码段和数据段,以及 R3 权限的代码段和数据段。任何尝试访问不存在的内存时都会生成通用保护异常 (#GP)。

    19.png

  3. 多分段模式(Multi-Segment Model)

    Windows 使用该分段机制,访问段内存时会执行段权限检查。

    20.png

分段模式下,将段地址(逻辑地址)转换为线性地址如下图:

21.png

1.2 IA-32e 分段架构

IA-32e 模式下,分段的效果取决于处理器是以兼容模式(Compatibility-mode)还是 64 位模式(64-bit mode)运行。

  • 兼容模式:不会禁用,依然存在分段,分段的方式和 IA-32 下方式一样(请注意并不是直接使用 IA-32 的分段)。
  • 64 位模式:分段通常被禁用(但不是完全禁用),从而创建一个平坦的 64 位线性地址空间。处理器将CS、DS、ES、SS的段基址视为零,创建一个等于有效地址的线性地址。FS 段和 GS 段基址不为 0。处理器在 64 位模式下运行时不执行段Limit检查。由于 64 位模式下的平坦地址空间,线性地址等于有效地址。 如果 FS 或 GS 段与非零段基址一起使用,则此规则不成立。

段基址(Segment.Base)有区别:

  • IA-32:段基址为 32 位。
  • IA-32e:段基址为 64 位(其实主要是兼容模式下使用)。

IA-32e 模式下将逻辑地址转换为线性地址的方式相同,如下图:

21.png

在 IA-32e 模式下的代码段描述符的 L 位用来标志一个代码段按何种模式来运行代码:

  • L = 1:64 位代码段。
  • L = 0:
    • D/B = 1:32 位代码段。
    • D/B = 0:16 位代码段。

1.3 物理地址空间

  1. IA-32 架构保护模式下:

    • 物理地址是一个平坦的地址空间,未进行分段。

    • 物理地址范围为 4GB,启用物理地址扩展(PAE)或页面大小扩展 (PSE-36) 功能后,物理地址空间为范围 64 GB。

  2. IA-32e 架构处理器:物理地址范围的大小在不同 CPU 上支持不同。大小由 CPUID.80000008H:EAX[bits 7-0] 指出,如 2.2 实验中我的电脑最大就是 36 位(64GB)。

1.4 段寄存器

CPU 处理器提供了 6 个段寄存器:CS、DS、ES、FS、GS、SS。

段寄存器分为可见部分(段选择子)和隐藏部分:

  • IA-32:共 96 位
    • 可见部分:16 位
    • 隐藏部分:80 位
  • IA-32e:共 128 位
    • 可见部分:16 位
    • 隐藏部分:112 位

变化是 IA-32e 下段基址变为 64 位。

22.png

注意:在段寄存器内 Limit 占 32 位,在段描述符内只有 20 位。根据 G、D/B 标志来将段描述符的 20 位扩展到段寄存器的 32 位。

IA-32e 的 64 位模式下不使用 ES、DS 和 SS 段寄存器,所以它们在段描述符寄存器中的字段(Base、Limit和Attribute)被忽略。某些形式的段加载指令也是无效的(例如, LDS,POP ES)。 引用 ES、DS 或 SS 段的地址计算被视为段基址为零。

FS.base 和 GS.base 的隐藏描述符寄存器字段物理映射到 MSR,以便加载 64 位实现支持的所有地址位。

R3:CS = 33, DS = 2B, ES = 2B, FS = 53, GS = 2B, SS = 2B

R0:cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b

1.5 段选择子

段选择子是一个 16 位数,该数决定了取 GDT 表中查哪一个条目数据。

5.png

注意:处理器不使用 GDT 的第一个条目,第一项是全 0 的值。 指向 GDT 第一项的段选择子(即 Index = 0 且 TI = 0)被称为“空段选择子” 。段寄存器加载空段选择子时,处理器不会产生异常寄存器(除了 CS 或 SS 段寄存器)。但是,当使用包含空选择子的段寄存器访问内存时,它会生成异常。空选择子可用于初始化未使用的段寄存器,但不能加载来访问内存。 使用空段选择子加载 CS 或 SS 寄存器会导致生成通用保护异常(#GP)。

1
2
3
4
5
6
7
8
9
10
11
kd> r gdtr
gdtr=fffff8025a003fb0
kd> dq fffff8025a003fb0
fffff802`5a003fb0 00000000`00000000 00000000`00000000
fffff802`5a003fc0 00209b00`00000000 00409300`00000000
fffff802`5a003fd0 00cffb00`0000ffff 00cff300`0000ffff
fffff802`5a003fe0 0020fb00`00000000 00000000`00000000
fffff802`5a003ff0 5a008b00`20000067 00000000`fffff802
fffff802`5a004000 0040f300`00003c00 00000000`00000000
fffff802`5a004010 00000000`00000000 00000000`00000000
fffff802`5a004020 00000000`00000000 00000000`00000000

2 段描述符

2.1 段描述符表

段描述符表(Segment Descriptor Tables):是一个数组,每个元素为 8 字节,表中的每个元素叫做段描述符。一共有两种段描述符表:

  • 全局描述符表 GDT(Global Descriptor Table)
  • 局部描述符表 LDT(Local Descriptor Table)

段描述符表结构如下:

24.png

LDT 一般是用来帮助进行任务切换,但是在 32 位的 Windows 中使用线程切换并不使用任务,所以基本没有使用到 LDT。在 IA-32e 模式下,Intel 直接就取消了任务门,所以 LDT 表基本也就没啥研究的意义了。

段描述符是 GDT 表和 LDT 表的元素表示,它为段寄存器提供隐藏的字段:Base、Limit、Attribute。(注意:在段寄存器内 Limit 占 32 位,在段描述符内只有 20 位。)

在 IA-32e 模式下,除了系统描述符为 16 字节 128 位以外,其余的段描述符都是 8 字节 64 位。

系统段描述符包括:调用门描述符、IDT 描述符、LDT 描述符、TSS 描述符。

下图为段描述符的通用格式:

23.png

注意:GDT、IDT 不被视为段,它只是用来描述段描述符。段描述符寄存器分为两部分:base、limit。(不是段寄存器的Base和Limit,注意区分段寄存器和段描述符寄存器)

  • IA-32:GDTR 寄存器为 48 位,低 16 位表明 GDT 表的字节总数,高 32 位为 GDT 表在内存中的起始线性地址。
  • IA-32e:GDTR 寄存器为 80 位,低 16 位表明 GDT 表的字节总数,高 64 位为 GDT 表在内存中的起始线性地址。

26.png

2.2 Limit、G 位

逻辑地址格式为:Seg.Selector:Offset。

一、IA-32

在 IA-32 下 Offset 取值由段寄存器隐藏部分的 Limit 进行限制,Limit 可以从20 位扩展到 32 位,这是根据段描述符的 G、D/B、以及代码段的 E 位来将 20 位扩展成 32 位的。

Limit:用来指定段的大小范围。

  • G = 1,段的范围是 0x00000FFF <= Limit <= 0xFFFFFFFF(4KB~4GB),以 4KB 大小为增量,增加到 32 位。

  • G = 0,段的范围是 0x00000000 <= Limit <= 0x000FFFFF(0~1MB),以 1Byte 大小为增量,此时只使用段描述符中的 20 位。

G 位只是指定了段的大小范围,但是 D/B 则指定了数据段最大上限:

  • D/B = 1,段的最大上限值为 4GB(0xFFFFFFFF)。
  • D/B = 0,段的最大上限值为 64KB(0x0000FFFF)。

注意:除了代码段 CS 不是数据段,其他 DS、ES、FS、GS、SS 都可以归为数据段。数据段是有方向的,即向下扩展(Expand-down)还是向上扩展(Expand-up)。IA-32 架构使用的是向下扩展。IA-32 架构使用的是向下扩展。

向上扩展、向下扩展、D/B 位的关系:

逻辑地址的表示为:Seg.Selector:Offset。其中 offset 取值范围就是依赖于向上扩展还是向下扩展。

  • 向上扩展:0 <= Offset < Limit
  • 向下扩展:Limit+1 <= Offset < 0xFFFFF/0xFFFFFFFF(最大值取决于D/B位的值)

要使 Offset 取值范围尽可能的大,也就是可使用的内存尽可能的多:

  • 对于向上扩展,需要不断增大 Limit 的值,所以”向上扩展“因此而得名。
  • 对于向下扩展,需要不断减小 Limit 的值,所以”向下扩展“因此而得名。

段权限检查:

  1. 检查 Offset 是否合法:IA-32 使用向下扩展的数据段,有效限制指定了段内不允许访问的最后一个地址。

    • D/B = 1,Limit+1 <= Offset < 0xFFFFFFFF(4GB)
    • D/B = 0,Limit+1 <= Offset < 0x000FFFFF(64KB)

    Limit == 0 时,向下扩展的段有最大范围。

  2. 检查段描述符表的 Limit,即 GDTR 寄存器的 Limit(表述GDT表的字节数)。

  • 向上扩展段:指定最小偏移为 0,最大为 Limit,64-bit 模式使用。
  • 向下扩展的:指定最大偏移量位 0xFFFFFFFF,最小为 Limit,IA-32使用。

二、IA-32e

64 位模式,Limit 为 0xFFFFFFFF。使用向上扩展的 SS 段。

段权限检查:

  • 兼容模式:会像 Legacy 模式一样进行段 Limit 检查。
  • 64-bit 模式:在 4.2 中说过,分段通常被禁用(但不是完全禁用),从而创建一个平坦的 64 位线性地址空间。所以在 IA-32e 模式下不会对段描述符的 Limit 进行越界检查(Limit被忽略,不使用),但是会对段描述符寄存器 GDTR 的 Limit(段描述符表的字节数)进行检查。

2.3 Base 域

一、IA-32

0 <= Base <= 4GB,Base为32位。

二、IA-32e

  • 兼容模式:0 <= Base <= 4GB,Base为32位。
  • 64-bit模式:64 位模式下不使用 ES、DS 和 SS 段寄存器,所以它们在段描述符寄存器中的字段(Base、Limit和Attribute)被忽略。 处理器将CS、DS、ES、SS的段基址视为零,创建一个等于有效地址的线性地址。FS 段和 GS 段基址不为 0。Base为64位。

在 64-bit 模式,数据段描述符为64位,但是 FS、GS 段基址不为 0,所以使用了 MSR 来保存基址,FS 的基址只有切换到兼容模式才能从 MSR 寄存器读到。

2.4 P、DPL 位

P:指示当前段是否已经映射到物理内存。

P = 0 时,将该段描述符加载到段寄存器时会产生 #NP 异常,将转到异常处理。

DPL:指定当前段的特权级别。如一个数据段的 DPL = 3,表示用户模式的数据段。

  • CPL:当前特权级别,当前 CS、SS 的 bit 0 和 bit 1。
  • RPL:我以什么权限级别去访问你,段选择子的 bit 0 和 bit 1。
  • DPL:你应该以什么样的权限来访问我。DPL 是段或门的特权级别,当当前执行的代码段试图访问一个段或门时,段或门的 DPL 会与当前 CPL、段选择子 RPL 进行比较。
    • 数据段:DPL >= CPL。
    • 非一致性代码段(Nonconforming code segment):CPL = DPL 并且 RPL <= DPL。
    • 调用门、中断门:DPL >= CPL(RPL 随意,不检查)。是DPL要为3,否则在3环无法访问到门描述符。
    • TSS:DPL >= CPL。

2.5 S 位、Type 域

段描述符中分为三种类型:代码段描述符、数据段描述符、系统描述符。

  • S = 1,GDT 段描述符属于代码段/数据段。
  • S = 0,GDT 段描述符属于系统描述符。

系统描述符分为两类:系统段描述符、门描述符。

  1. 系统段描述符:LDT、TSS。
  2. 门描述符:调用门、中断门、陷阱门、任务门。

2.5.1 代码段/数据段

S = 1,代码段/数据段

S 位决定 Type 域的取值

  • S = 1,Type 域最高位 = 1,代码段。
  • S = 1,Type 域最高位 = 0,数据段。
段类型 Type 域 备注
代码段(CODE) A:段描述符是否被访问过
1:已访问过
0:未访问过

R:是否可读
1:可读
0:不可读

C:一致位
1:一致代码段(Conforming),也就是共享的段,要求:CPL >= DPL。
特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据。
特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态CS还是用户态CS。
0:非一致代码段(Nonconforming),Windows 使用。要求:CPL = DPL 并且 RPL <= DPL。
只允许同级访问。绝对禁止不同级别的访问:核心态不是用户态,用户态也不是核心态。
保护模式下,代码段不可写。
数据段(DATA) A:段描述符是否被访问过
1:已访问过
0:未访问过

W:是否可写
1:可写
0:不可写

E:拓展方向位
1:向下拓展,Windows 使用
0:向上拓展。
SS 段必须是可读可写的。

13.png

也就是共享的段,要求:CPL >= DPL。特权级高的程序不允许访问特权级低的数据:核心态不允许访问用户态的数据特权级低的程序可以访问到特权级高的数据,但特权级不会改变:用户态CS还是用户态CS。

2.5.2 系统描述符

S = 0,系统描述符。系统描述符分为两类:系统段描述符、门描述符。

  1. 系统段描述符:LDT、TSS。
  2. 门描述符:调用门、中断门、陷阱门、任务门。

在 IA-32e 模式下,系统描述符都是 16 字节 128 位。

15.png

2.5.3 64-bit模式代码段

结构如下:可以看到,Base、Limit 都没有了,被平坦了。

33.png

64 位模式下不使用 ES、DS 和 SS 段寄存器,所以它们在段描述符寄存器中的字段(Base、Limit 和 Attribute)被忽略。如果使用的话, 处理器会将CS、DS、ES、SS的段Base和Limit都视为零,创建一个等于有效地址的线性地址。FS 段和 GS 段基址不为 0。

64-bit 模式下,代码段仍然存在,其存在的两个目的:

  1. 指出处理器的运行模式

    • CS.L = 0,处理器运行在兼容模式。
      • D/B = 1,运行32位代码程序。
      • D/B = 0,运行16位代码程序。
    • CS.L =1,处理器运行在 64-bit 模式。
      • D/B = 0,运行64-bit模式,地址大小为64位。
      • D/B = 1,供将来使用,现在使用会产生#GP异常。
  2. 使用 CS.DPL 执行权限检查,就像在传统模式中检查那样。

2.5.4 64-bit模式数据段

64 位模式下不使用 ES、DS 和 SS 段寄存器,所以它们在段描述符寄存器中的字段(Base、Limit 和 Attribute)被忽略。

2.6 D/B 位

IA-32 下:

D/B 位对数据段来说决定地址空间大小,对代码段来说决定寻址大小。

  1. 情况一:对CS段的影响(加载:MOV CS, AX)

    D = 1 采用32位寻址方式

    D = 0 采用16位寻址方式

  2. 情况二:对SS段的影响(加载:MOV SS, AX)

    D = 1 隐式堆栈访问指令(如:PUSH POP CALL等会修改ESP) 使用32位堆栈指针寄存器ESP

    D = 0 隐式堆栈访问指令(如:PUSH POP CALL等会修改SP) 使用16位堆栈指针寄存器SP

    指令前缀67:改变默认寻址方式,但不是改D/B位,若默认是32寻址,则改为16位寻址。若默认是16寻址,则改为32位寻址。

  3. 情况三:对数据段DS、ES的影响(加载:MOV DS, AX)

    D = 1 段上限为4GB 0xFFFFFFFF

    D = 0 段上限为64KB 0xFFFF

IA-32e 下:D/B = 0。

3 段权限检查

在分段这块,段权限检查主要分为两类:

  1. 直接调用代码段的权限检查。
  2. 门的权限检查(调用门、中断门、陷阱门、任务门)。

直接调用/跳转到代码段权限检查:

类型 段权限检查 备注
调用/跳转到代码段 near call, jmp:段内调用/跳转,不进行权限检查。目标代码段选择子和当前 CS 相同。
far call, jmp:段间调用/跳转,进行权限检查。目标代码段选择子与当前 CS 不相同。
非一致性代码段:CPL = DPL,RPL <= CPL。只允许同权限级别段访问,不能跨权限段访问(门除外)。
一致性代码段:CPL >= DPL,不检查RPL。允许低权限访问高权限,但是 CPL 不会改变,所以堆栈不会切换。

一致代码段又称共享代码段。假设操作系统有一段代码是提供了某些通用功能,这段代码并不会对内核产生影响,并希望这些功能能够被应用层(三环程序)直接使用,即可让一直代码段去修饰这块代码。这也是为什么一致代码段要求:CPL >= DPL(当前权限比描述权限低)就可以了。这段代码就是给低权限的应用使用的。

4 调用门

为了提供对具有不同权限级别的代码段的受控访问,处理器提供了一组称为门描述符的特殊描述符。门描述符有四种:

  • 调用门

  • 中断门

  • 陷阱门

  • 任务门

GDT 表中包含以上 4 种门描述符,IDT 表不包含调用门描述符。

4.1 IA-32 调用门描述符

调用门描述符中包含需要传递的参数的个数,调用门描述符中还指定一个 DPL = 0 的代码段选择子,用来提权。

  • 偏移:32 位。
  • S = 0,DPL = 3,Type = 0xC。
  • 代码段选择子的 DPL = 0。

34.png

35.png

IA-32 远调用:

1
2
3
4
BYTE buff[6] = {0,0,0,0,0x48,0};

call/jmp far ptr [buff];
call/jmp fword ptr [buff];

IA-32e 远调用:

1
2
3
4
BYTE buff[10] = {0,0,0,0,0,0,0,0,0x73,0};

call/jmp far ptr [buff];
call/jmp tbyte ptr [buff];
类型 助记符 简写 字节数 范围
字节 BYTE DB 1 0~255
字 WORD DW 2 0~65535
双字 DWORD DD 4 0~4294967295
远字 FWORD DF 6
四字 QWORD DQ 8
十字 TBYTE DT 10
有符号字节 SBYTE 1 -128~127
有符号字 SWORD 2 -32768~32767
有符号双字 SDWORD 4

4.2 IA-32e 调用门描述符

IA-32e 模式 128 位的调用门结构如下:

  • 偏移值:64 位。
  • S = 0,DPL = 3,Type = 0xC。
  • 代码段选择子的 DPL = 0。

36.png

IA-32e 远调用:

1
2
3
4
BYTE buff[10] = {0,0,0,0,0,0,0,0,0x73,0};

call/jmp far ptr [buff];
call/jmp tbyte ptr [buff];

00007FF6 AB7510F0

0111 0 011 14

00007FF7 3DEC10E0

00000000`00007FF6

AB75EC00`001010F0

4.3 调用门过程

37.png

4.4 调用门段权限检查

IA-32 和 IA-32e 调用门段权限检查都是一样的。

38.png

39.png

4.5 调用门堆栈及EFLAGS

调用门堆栈

一、IA-32

根据调用门描述符指定参数的个数,将 3 环的参数复制到 0 环的堆栈中。注意参数在 SS3、ESP3 和 CS3、EIP3 中间。

注意:CS0 来自代码段描述符、SS0、ESP0 来自 TSS。EIP0 来自调用门的 offset + 代码段描述符的 base。

40.png

二、IA-32e

注意:

  1. SS0 = NULL,SS0.RPL = CS0.CPL, ESP0 来自 TSS。
  2. 调用门描述符中不存在参数个数,不可使用参数。
  3. 如有必要,0 环可以通过引用保存在新进程堆栈上的旧 SS3 和 ESP3 来访问旧堆栈。

堆栈中数据压入和 IA-32 无参时是一样的,只不过每一个都是 8 字节。

41.png

调用门返回

IA-32:retf 指令。

IA-32e:retfq 指令。

关于 RFLAGS.IF

  • 中断门:IF 位清 0。
  • 调用门:IF 位不清 0。
  • 陷阱门:IF 位不清 0。

总的看来,IA-32e 模式下的 IDT 变得更简单了,不再有任务门,只有中断门和陷阱门, 断门和陷阱门都支持通过它们中的 IST 位域来指定是否要切换栈——软件调试第二版卷1 3.5.3。