Welcome to A!Die Software Studio |
说明:
S16 表示 16 位段寄存器
P16 表示 16 位的普通寄存器, 立即数, 结果为 16 位的表达式等等.
P32 同上, 只是扩展到 32 位.
一. CPU 概况
1. 8086: 8 位数据线, 16 位地址线. 8 位数据线和前8位地址线合用.
2. 8088: 16位数据线, 16位地址线. 数据线和地址线完全分时合用.
3. 80186: 16位数据线, 16位地址线. 数据线和地址线完全分时合用.
4: 80188: 16位数据线, 16位地址线. 数据线和地址线完全分时合用.
5. 80286: 16位数据线, 24位地址线. 数据线和地址线是完全分开的. 转到保护模式的过渡 CPU.
6. 80386: 32位数据线, 32位地址线. 数据线和地址线是完全分开的(其中80386SX像80286).
7. 80486: 32位数据线, 32位地址线.
8. Pentium: 64位数据线, 32位地址线.
9. Pentium Pro: 64位数据线, 36位地址线.
二. 实模式: 分段内存
1. 支持的 CPU: 8086 以上
2. 启用方式: 启动后自动进入
3. 地址长度: 20
4. 寻址能力: 1M
5. ALU宽度: 16
6. 寻址过程:
引入了 CS, DS, SS, ES 这 4 个 16 位的段寄存器. 寻址时将段寄存器左移 4 位后再加上 16 位的偏移, 既: (S16 << 4) + P16. 得到 20 位的地址.
省略段寄存器时, 会使用默认的段寄存器:
段 | 偏移 | 用途 |
CS | IP | 指令地址 |
DS | AX, BX, SI, DI, Disp8/16 | 数据地址 |
SS | SP, BP | 堆栈地址 |
ES | DI | 串操作目的地址 |
三. 保护模式: 分段内存
1. 支持的 CPU: 80286 以上
2. 启用方式: 将 CR0 寄存器的 PE 位置 1.
3. 地址长度: 32
4. 寻址能力: 4GB
5. ALU宽度: 32
6. 寻址过程:
从 32 位的数据宽度, 寻 32 位的地址, 看起来似乎是非常简单的一件事情. 不过由于对内存保护的加入, 这个过程其实更为复杂. 而 Intel 又选择了兼容之前的分段内存, 且分段机制在进入保护模式后是必须的, 不能关闭. 其实大多数的操作系统实现的时候都选择绕过分段机制, 只使用分页机制来进行内存管理.
保护模式的分段机制保留了以前的段寄存器, 并且增加了两个 FS, GS. 这些段寄存器仍然为 16 位, 但是里面保存的不再是段的基地址了, 而是一个段选择子, 段选择子是一个如下的结构:
struct SegSelector {
unsigned int RPL : 2;
unsigned int PI : 1;
unsigned int Selector : 13;
};
既, 0-1 位表示请求的权限, 共有 0 - 3 四种. 但几乎所有的操作系统都只使用 2 种. 0 表示内核的, 3 表示用户空间的. 第 2 位决定是使用 GDT 还是 LDT, 为 0 使用 GDT, 为 1 使用 LDT. 选择绕过分段机制的操作系统是不会创建 LDT 的, 永远只使用 GDT. 最后高 13 位表示在 GDT/LDT 中的索引号.
GDT/LDT 是段描述符的数组, 段描述符是描述段的基址, 边界, 属性的一个结构, 共 64 bit, 8 个字节. GDT 是全局的, 只有一个. LDT 可以有多个. Intel 设想的是, GDT 供操作系统内核使用, LDT 每个进程一个, 但是基本上没有人按这种方式来使用. 段描述符的结构如下:
struct SegDesc {
unsigned short limitLow;
unsigned int baseLow : 24;
unsigned int type : 4; // 根据 S 字段的不用, 有不同的含义. 包含读写权限等等.
unsigned int S : 1; // 系统段标志, 为 0 表示系统段
unsigned int DPL : 2; // 段的权限等级. 0 为内核 3 位用户
unsigned int P : 1; // 段是否在内存中, 为 1 表示在内存. 当 P 为 0 时, base 和 limit 都是无效的, 操作系统可以用来保存自己的数据.
unsigned int limitHigh : 4;
unsigned int AVL : 1; // CPU 不使用, 软件自己决定表示什么.
unsigned int zero : 1; // 为 0
unsigned int DB : 1; // 根据段的使用方式, 有不同的含义.
unsigned int G : 1; // 粒度, 决定 Limit 的单位. 为 0, 单位为字节. 为 1, 单位是 4KB
unsigned char baseHigh;
};
之所以这么复杂, 是因为在 80286 中, 高 16 位是没有使用的. 新的 CPU 为了兼容这个过渡产品, base 和 limit 只能扩展到高 16 位去.
GDT/LDT 这个数组是放在内存里面的, 所以还需要记录它们在内存中的起始位置, 为此, 又增加了 2 个寄存器: GDTR 和 LDTR. GDTR 是一个 48 位的寄存器, 高 32 位是一个线性地址, 低 16 位是边界. LDTR 是一个 16 位寄存器, 里面保存的是在 GDT 中的索引号.
struct {
unsigned short limit;
unsigned int base;
} GDTR;
这样, 段寄存器通过查询 GDT/LDT 表获得段的基址后, 再加上偏移得到一个 32 位的地址. 这个地址被称为线性地址. 如果没有启用下面说的分页机制, 那么线性地址就是物理地址. 如果启用了分页机制, 线性地址还需要经过一次映射才能得到物理地址. 综上, 虚拟地址到线性地址的映射关系可用如下伪代码来描述:
if(S16 & 0x04 == 0) // 取段寄存器的第 3 位, 判断是使用 GDT 还是 LDT
{ // 使用 GDT
gdt = (GDTR >> 16) & 0xFFFFFFFF; // 取 GDTR 寄存器的高 32 位, 既 GDT 表的起始地址.
sd = gdt + S16 & 0xFFF8; // 根据段寄存器中的索引号找到段描述符
sbase_l = (sd >> 12) & 0x00FFFFFF; // 取基址的低 24 位
sbase_h = (sd >> 32) & 0xFF000000; // 取基地址高 8 位
sbase = sbase_h | sbase_l;
return sbase + P32 // 基址加上偏移得到最终的线性地址
}
else
{ // 使用 LDT
gdt = (GDTR >> 16) & 0xFFFFFFFF;
sd_ldt = gdt + LDTR & 0xFFF8;
ldt = (sd_ldt >> 12) & 0x00FFFFFF + (sd_ldt >> 32) & 0xFF000000;
sd = ldt + S16 & 0xFFF8;
sbase = (sd >> 12) & 0x00FFFFFF + (sd >> 32) & 0xFF000000;
return sbase + P32;
}
或者, 我们使用上面定义的结构来表述:
if(SegSelector(S16)->PI)
{ // PI 为 1 使用 LDT
SegDesc* gdt = GDTR->base;
SegDesc* ldt= &gdt[LDTR >> 3];
SegDesc* sd = &ldt[SegSelector(S16)->Selector];
void* base = sd->baseHigh << 24 + sd->baseLow;
return base + P32
}
else
{ // PI 为 0 使用 GDT
SegDesc* gdt = GDTR->base;
SegDesc* sd = &gdt[SegSelector(S16)->Selector];
void* base = sd->baseHigh << 24 + sd->baseLow;
return base + P32
}
同样, 在没有指明段寄存器的情况下, 会使用一个默认的段寄存器:
段 | 偏移 | 用途 |
CS | EIP | 指令地址 |
DS | EAX, EBX, ECX, EDX, ESI, EDI, Disp8/16/32 |
数据地址 |
SS | ESP, EBP | 堆栈地址 |
ES | EDI | 串操作目的地址 |
GS | 无 | 一般地址 |
FS | 无 | 一般地址 |
以下是网上搜集的描述分段内存的图表:
四. 保护模式: 分页内存
1. 支持的 CPU: 80386 以上, PSE 需要 Pentium 以上
2. 启用方式: 将 CR0 寄存器的 PG 位置 1
3. 地址长度: 32
4. 寻址能力: 4GB
5. ALU宽度: 32
6. 寻址过程:
分页机制是现代操作系统实现内存管理的主要方式. 它将线性地址空间划分为固定大小的页面, 每个页面可以被映射到物理内存或外部存储器的虚拟内存文件中, 并且进行权限检查. 在没有启用 PAE 时, 内存页面大小可以是 4KB 或 4MB. 如果 CR4 中的 PSE 位是 0, 则只支持 4KB 的内存页. 如果 PSE 位是 1, 则根据 PDE 中的 PS 位来决定内存页的大小.
1# 4KB 页面寻址
对于 4KB 的页面, 32 位的线性地址不再表示物理地址, 而是变成如下的含义了:
struct LineAddress {
unsigned int offset : 12;
unsigned int table : 10;
unsigned int directory : 10;
};
其中高 10 位表示页目录的下标, 中间 10 位表示页表中的下标. 在系统中, 每一个进程有一个页目录, 其基址为 4KB 对齐, 低 12 位为 0, 高 20 位存放在 CR3 寄存器中的高 20 位里. 因此, CR3 寄存器又被称为页目录基址寄存器(PDBR). CR3 的结构为:
struct CR3 {
unsigned int nouse1 : 3;
unsigned int PWT : 1; // Page Write Through 标志, CR0.CD 为 1 时忽略, CR0.CD 为 0 时: PWT = 1 使用 Write-Through 的缓存类型, PWT = 0 使用 Write-Back 的缓存类型.
unsigned int PCD : 1; // Page Cache Disable 标志, CR0.CD 为 1 时忽略, CR0.CD 为 0 时: PCD = 1 表示该物理页不能被缓存
unsigned int nouse2 : 7;
unsigned int pdt_base : 20;
};
页目录(Page Directory)是一个 4KB 大小的数组, 里面包含 1024 个 4 字节的页目录表项(PDE, Page Directory Entry). 对于 4KB 的页面, PDE 的结构如下:
struct PDE {
unsigned int P : 1; // Present, 是否在物理内存中, 1 在, 0 不在
unsigned int R/W: 1; // Read/Write, 为 1 表示可读写, 为 0 表示只读
unsigned int U/S : 1; // User/Supervisor 为 0 表示管理权限, 为 1 表示用户权限
unsigned int PWT: 1; // Write-through
unsigned int PCD : 1; // Cashe-Disabled
unsigned int A : 1; // Accessed 是否被访问过, 1 表示访问过
unsinged int ZERO: 1; // 固定为 0
unsigned int PS : 1; // PageSize, 页大小, 0 表示 4KB, 1 表示 4MB. 当 CR4 的 PSE 为 0 时, 忽略该项, 页大小始终为 4KB.
unsigned int G : 1; // Global Page, 全局页.
unsigned int nouse : 3; // CPU 未使用, 供系统程序员使用.
unsigned int base : 20; // 页表基址的高 20 位, 低 12 位固定为 0. 所以, 页表基址一定是按 4KB 对齐的.
};
PDE 中最重要的, 是高 20 位表示的页表基址, 它指向的是一个 4KB 的页表(Page Table), 页表包含了 1024 个 4 字节的页表表项(Page Table Entry, PTE). PTE 的结构如下:
struct PTE {
unsigned int P : 1;
unsinged int R/W : 1;
unsigned int U/S : 1;
unsigned int PWT: 1;
unsigned int PCD : 1;
unsigned int A : 1;
unsigned int D : 1; // Dirty 表示内存页是否被修改过, 1 修改过 0 未修改过
unsigned int PAT : 1; // Page Attribute Table, 在全局属性表中的索引
unsigned int G : 1; // 全局页
unsigned int nouse : 3; // 供系统程序员使用
unsigned int base : 20; // 内存页起始物理地址的高 20 位, 低 12 位固定为 0, 所以必须按 4KB 对齐.
};
根据上面的结构, 线性地址到物理地址的映射过程, 可以用如下伪代码来表示:
LineAddress laddr = P32;
PDE* pde = CR3.pdt_base << 12;
PTE* pte = pde[laddr.direcotry].base << 12;
char* pageStart = pte[laddr.table].base << 12;
return pageStart + laddr.offset;
2# 4MB 页面寻址 (PSE 模式)
对于 4MB 的页面, 不需要使用页表, 只需要页目录的一层映射. 要配置 4MB 的页面, 需要设置 CR4 中的 PSE 位. 并在 PDE 中设置 PS 位. 使用 4MB 页面时, 线性地址的结构如下:
struct LineAddress {
unsigned int offset : 22;
unsigned int directory : 10;
};
此时的 PDE 结构如下:
struct PDE {
/* 0 */unsigned int P : 1;
/* 1 */unsinged int R/W : 1;
/* 2 */unsigned int U/S : 1;
/* 3 */unsigned int PWT: 1;
/* 4 */unsigned int PCD : 1;
/* 5 */unsigned int A : 1;
/* 6 */unsigned int D : 1;
/* 7 */unsigned int PS : 1; // 4MB 页面该位为 1
/* 8 */unsigned int G : 1;
/* 9-11 */unsigned int nouse : 3; // 供系统程序员使用
/* 12 */unsigned int PAT : 1;
/* 13-21 */unsigned int reserved : 9; // 保留未使用, 必须为 0
/* 22-31 */unsigned int base : 10; // 内存页起始物理地址的高 10 位, 低 22 位固定为 0, 所以必须按 4MB 对齐.
};
此时的寻址过程伪代码如下:
LineAddress laddr = P32;
PDE* pde = CR3.pdt_base << 12;
char* pageStart = pde[laddr.direcotry].base << 22;
return pageStart + laddr.offset;
以下是网上搜集的描述分页内存的图表:
1. 内存的二级分页结构
2. 4KB 页面寻址过程
3. 4MB 页面寻址过程
五. 虚拟 86 模式 (V8086, V86)
1. 支持的 CPU: 80386 以上
2. 启用方式: 在保护模式下, 将标志寄存器中的 VM 位置 1
3. 地址长度: 20
4. 寻址能力: 1MB
5. ALU 宽度: 16
6. 寻址过程:
虚拟 86 模式是保护模式下, 某些任务的一种工作模式. 此模式是为了能够在保护模式下运行实模式软件. 在虚拟 86 模式下, 软件的工作环境和实模式类似, 使用 (S16 << 4) + P16 的方式访问 1M 的内存, 但是得到的地址不再是物理地址了, 而是由系统的虚拟 86 管理程序分配的内存. 虚拟 86 模式下也可以使用内存分页(实模式下不行), 让没有使用的内存空间不占用物理内存. 虚拟 86 模式下的中断和特殊指令的访问也由系统软件进行模拟, 不能直接访问硬件.
在虚拟 86 模式下, 是不能直接更改标志寄存器的 VM 位的, 所以进入和退出虚拟 86 模式是通过任务切换或中断来完成的.
以下是网上搜集的描述 V86 模式的相关图表:
1. 保护模式和 V86 模式的切换
六. PSE-36: Page Size Extension 36
1. 支持的 CPU: Pentium III 以上 (另说为 Pentium II)
2. 启用方式: 开启 PSE 的情况下, 如果 CPU 支持即可使用
3. 地址长度: 36
4. 寻址能力: 64GB
5. ALU 宽度: 32
6. 寻址过程:
在前面的分页内存中, 处于 PSE 模式时, PDE 结构只使用了高 10 位作为基址. 在 PSE-36 模式里, 将使用其中的 14 位来作为基址, 这样, 最后的地址位数将达到 36 位, 寻址能力提高到 64GB. 在 PSE 36 模式下, 4MB 页面的 PDE 结构变为:
struct PDE {
/* 0 */unsigned int P : 1;
/* 1 */unsinged int R/W : 1;
/* 2 */unsigned int U/S : 1;
/* 3 */unsigned int PWT: 1;
/* 4 */unsigned int PCD : 1;
/* 5 */unsigned int A : 1;
/* 6 */unsigned int D : 1;
/* 7 */unsigned int PS : 1; // 4MB 页面该位为 1
/* 8 */unsigned int G : 1;
/* 9-11 */unsigned int nouse : 3; // 供系统程序员使用
/* 12 */unsigned int PAT : 1;
/* 13-16 */unsigned int baseHigh : 4; // 内存起始地址的 32-35 位
/* 17-21 */unsigned int reserved : 5; // 保留未使用, 必须为 0
/* 22-31 */unsigned int baseLow : 10; // 内存页起始物理地址的 22-31 位.
};
此时寻址过程伪代码为:
LineAddress laddr = P32;
PDE* pde = CR3.pdt_base << 12;
INT64 pageStart = (INT6)pde[laddr.direcotry].baseLow << 22 + (INT64)pde[laddr.directory].baseHigh << 32;
return pageStart + laddr.offset;
对于 4KB 的页面, 仍然和普通的分页内存相同, 它可以表示的内存仍然只要 4GB. 所以, 在 PSE 36 模式下, 4KB 的页面只能位于前 4GB 物理内存中. 4GB 以上的内存只能通过 PSE 方式访问, 页面大小只能为 4MB.
七. PAE: Physical Address Extension 物理地址扩展
1. 支持的 CPU: Pentium Pro 以上
2. 启用方式: 设置 CR4 中的 PAE 位
3. 地址长度: 36
4. 寻址能力: 64GB
5. ALU 宽度: 32
6. 寻址过程:
在 PAE 模式中, 应用程序仍然为 32 位, 只能使用 4GB 的内存空间. 但是系统可以把不同的进程映射到 64GB 的物理内存中. 应用程序如果需要使用大于 4GB 的内存, 则需要操作系统的特殊支持(Windows 为 AWE, Address Windowing Extensions; Unix 存在多种, 比如 mmap ).
启用 PAE 后, CR3 寄存器不再指向页目录基址, 而是指向一个页目录指针表 PDPT (Page Directory Pointer Table), 即包含 4 个页目录指针的表. 页目录项和页表项从原来的 4 字节变为 8 字节, 占用的内存大小仍然为 4KB, 所以其中的表项从 1024 个变为 512 个. 因此, 现在一共有 4 个页目录表, 页目录表和页表的下标也只需要 9 位了, 于是线性地址的划分也有了变化.
1# 4KB 页面寻址
在 4KB 页面下, 线性地址被描述为:
struct LineAddress {
unsigned int offset : 12;
unsigned int table : 9;
unsigned int directory : 9;
unsigned int ptrindex : 2;
};
其中的高 2 位表示在页目录指针表 PDPT 内的索引, CR3 寄存器里保存了 PDPT 的地址, CR3 结构为:
struct CR3 {
unsigned int nouse : 5;
unsigned int pdpt_base : 27;
};
因此, PDPT 的地址是 32 字节对齐的, 因为 PDPT 的大小是 32 字节. PDPT 的表项 PDPTE 是 64 位的, 下面是 PDPTE 的结构:
struct PDPTE {
unsigned int P : 1;
unsigned int reseved1 : 2; // 保留, 必须为 0
unsigned int PWT : 1;
unsigned int PCD : 1;
unsigned int reserved2 : 4; // 保留, 必须为 0
unsigned int nouse : 3;
unsigned int pdt_base : 52;
};
PDPTE 从第 12 位开始的高位是 PDT 的基地址的高位, 随着物理地址位数的不同, 使用的位也不同, 未使用的位需保持为 0. PDT 的低 12 位固定为 0, 按 4KB 对齐. 比如果物理地址是 52 位, 则 PDT 的高 40 位由 PDPTE[51:12] 提供, PDPTE[63:52] 必须为 0. 当物理地址是 40 位时, PDT[39:12] = PDPTE[39:12], 物理地址 36 位时 PDT[35:12] = PDPTE[35:12].
PDE 在 PAE 模式下是 64 位了, 结构中的 PS = 0 时表示 4 KB 的页面, 此时的 PDE 结构如下:
struct PDE_4k {
unsigned int P : 1;
unsigned int R/W : 1;
unsigned int U/S : 1;
unsigned int PWD : 1;
unsigned int PCD : 1;
unsigned int A : 1;
unsigned int nouse1 : 1; // 忽略
unsigned int PS : 1; // 4KB 页面这里是 0
unsigned int nouse2 : 4;
unsigned int pt_base : 51; // PDT 的基址, 和 PDPTE 类似, 随着物理地址位数不同, 该结构中有效的位数也不同, 无效的位需要为 0
unsigned int XD : 1; // Execution Disable: 当寄存器 IA32_EFER.NXE 置位后有效, 否则为保留必须为 0 的位. 开启 XD 功能后, PDE.XD = 1 或 PTE.XD = 1 则该页面是数据页, 不可执行.
};
PDE 中从第 12 位开始的高位表示 PT 的基地址, 随着物理地址的位数不同, 使用的 PDE 结构中的位数也不同. PT 基地址的低 12 位固定为 0, 以 4 KB 对齐. PT 中存放的 PTE 结构也是 64 位的了:
struct PTE {
unsigned int P : 1;
unsigned int R/W : 1;
unsigned int U/S : 1;
unsigned int PWD : 1;
unsigned int PCD : 1;
unsigned int A : 1;
unsigned int D : 1;
unsigned int PAT : 1;
unsigned int G : 1;
unsigned int nouse : 3;
unsigned int page_frame : 51; // 12~62 位是 4KB 页面的基地址了, 和 PDPTE 一样, 随着物理地址位数的不同, 有效位数不同.
unsigned int XD : 1;
};
在上述扩展下, 寻址过程和 Non-PAE 模式下是类似的, 只是多了 PDPT 一个层次, 线性地址到物理地址转换的伪代码表示如下:
LineAddress laddr = P32;
PDPTE* pdpt = CR3.pdpt_base << 5;
PDE_4k* pde = pdpt[laddr.ptrindex].pdt_base << 12;
PTE* pt = pde[laddr.direcotry].pt_base << 12;
char* pageStart = pt[laddr.table].page_frame << 12;
return pageStart + laddr.offset;
其中的指针类型的位数不再是 32 位, 可以认为是 64 位或物理地址的位数.
2# 2MB 页面寻址
在 PDE 中, PS 位是 1 的话, 将会使用 2MB 的页面, 由于不再使用 PTE 结构, 线性地址的含义如下:
struct LineAddress {
unsigned int offset : 21;
unsigned int directory : 9;
unsigned int ptrindex : 2;
};
CR3 以及 PDPTE 的结构都和 4KB 模式下相同, PDE 的结构有些区别:
struct PDE_2M {
unsigned int P : 1;
unsigned int R/W : 1;
unsigned int U/S : 1;
unsigned int PWT : 1;
unsigned int PCD : 1;
unsigned int A : 1;
unsigned int D : 1;
unsigned int PS : 1 // 2M 页面该位是 1
unsigned int G : 1;
unsigned int nouse1 : 3;
unsigned int PAT : 1;
unsigned int reserved : 8; // 保留必须为 0
unsigned int frame_base : 43; // 21~63, 随物理地址位数不同有效位不同, 无效的位必须为 0
unsigned int XD : 1;
};
其中页面地址的低 21 位固定为 0, 基址按 2MB 对齐, 高位由 PDE_2M 的高位提供. 此时寻址过程如下:
LineAddress laddr = P32;
PDPTE* pdpt = CR3.pdpt_base << 5;
PDE_2M* pde = pdpt[laddr.ptrindex].pdt_base << 12;
char* pageStart = pde[laddr.directory].frame_base << 21;
return pageStart + laddr.offset;
附图:
1. PAE 模式下 4KB 页面的寻址
2. PAE 模式下 2MB 页面的寻址
八. 长模式 (long-mode, IA-32e 模式)
1. 支持的 CPU: x86-64 的 CPU
2. 启用方式:
同时满足以下条件:
(1). 开启保护模式 CR0.PE = 1
(2). 开启分页机制 CR0.PG = 1
(3). 开启 PAE CR4.PAE = 1
(4). IA32_EFER.LME = 1 (Long Mode Enable)
(5). IA32_EFER.LMA = 1 (Long Mode Active)
3. 地址长度: 48
4. 寻址能力: 256 TB
5. ALU 宽度: 64
6. 寻址过程:
x86-64 体系, 也被称为 x64 体系, 还被叫做 AMD 64 和 Intel 64 体系. 他们是 x86 体系向 64 位的扩展, 有别于纯 64 位架构的 IA64 体系. x64 体系兼容 x86 的运行模式, 并增加一种新的长模式. x64 的运行模式如下:
x64 体系 | 子模式 | 资源 |
long-mode (IA-32e) | 64-bit mode | 64 位执行环境 |
compatibility mode | 内核为 64 位, 应用为 32 位的 legacy mode | |
legacy mode | protected mode | 32 位 |
real mode | 16 位 |
在长模式下, 内核只能为 64 位, 应用可以为 64 位或 32 位. 兼容模式(compatibility mode)和保护模式基本相同.
在 64 位模式下, 寄存器被扩展为 64 位, 默认的地址大小也是 64 位(可以使用 67H 前缀来使用 32 位地址, 但是不能使用 16 位地址), 并增加了 RIP 相对寻址方式. 兼容模式下代码段描述符中的 D 标志位决定了默认的地址大小: D = 0 默认为16位地址, D = 1 默认为32位地址, 可通过 67H 前缀来改变默认值.
1. 分段机制
长模式下的分段机制被进一步的弱化, 但是仍然被保留下来.
在 64 位模式下, 六个段寄存器仍然为 16 位, 其含义和保护模式下相同, 包含 Index, TI, RPL. 除 CS 寄存器外, 其余寄存器允许加载 Null selector(SS 只能在 Ring 0/1/2 下加载 Null selector).
GDTR/LDTR 的 base 扩展为 64 位, 所以 GDTR/LDTR 是 80 位的寄存器了:
struct {
unsigned short limit;
unsigned int64 base;
} GDTR;
段描述符仍然为 8 字节 64 位, 但是里面的大多数字段都已经无效了. 因为在 64 位模式下, 只有 FS 和 GS 可以使用非 0 的段基址, 其余的段的基址都被固定为 0, 长度被固定为 0XFFFFFFFF. 对于 FS 和 GS 的段 base 值, 新增了两个 MSR 寄存器来表示, 分别是 IA32_FS_BASE, IA32_GS_BASE . 代码段的描述符格式如下:
struct CodeSegDesc {
0 - 15: unsigned short limitLow; // 无效
16-39: unsigned int baseLow : 24; // 无效
40: unsigned int A : 1; // 无效
41: unsigned int R : 1; // 无效
42: unsigned int C : 1;
43: unsigned int C/D: 1; // 固定为 1
44: unsigned int S : 1; // 固定为 1
45-46: unsigned int DPL : 2;
47: unsigned int P : 1;
48-51: unsigned int limitHigh : 4; // 无效
52: unsigned int AVL : 1; // 无效
53: unsigned int L : 1;
54: unsigned int D : 1;
55: unsigned int G : 1; // 无效
56-63: unsigned char baseHigh; // 无效
};
从上面可见, 64位模式下段描述符只有 C, DPL, P, L, D 五个可用的标志:
C 代码一致性, 影响权限的检查
DPL 标识段的权限
P 段是否在内存中
L 用于长模式下的子模式选择, L=1 表示 64 位模式, L=0 表示兼容模式.
D 用于标识默认操作数的大小
数据段的描述符格式如下:
struct DataSegDesc {
0 - 15: unsigned short limitLow; // 无效
16-39: unsigned int baseLow : 24; // 无效
40: unsigned int A : 1; // 无效
41: unsigned int W : 1;
42: unsigned int E : 1; // 无效
43: unsigned int C/D: 1; // 固定为 0
44: unsigned int S : 1; // 固定为 1
45-46: unsigned int DPL : 2;
47: unsigned int P : 1;
48-51: unsigned int limitHigh : 4; // 无效
52: unsigned int AVL : 1; // 无效
53: unsigned int L : 1; // 无效
54: unsigned int D : 1; // 无效
55: unsigned int G : 1; // 无效
56-63: unsigned char baseHigh; // 无效
};
其中, 只有 P 和 DPL 标志有效. W 标志只有作为堆栈段时要求必须为 1.
由于段的基址被强制为 0, 所以虚拟地址和线性地址是等价的, 这个地址进行分页转换后成为物理地址.
2. 分页
Long Mode 下, 分页是必须的. 而且只有一种模式, 使用 4 层映射, 在 PAE 的上面又增加了一层. 页面的大小可以是 4K, 2M, 1G. 线性地址长度为 64 位, 目前使用的最高地址为 48 位. 对 4KB 的页面, 线性地址结构如下:
struct LineAddress {
0-11: unsigned int offset : 12; // 偏移
12-20: unsigned int pte_index : 9;
21-29: unsigned int pde_index : 9;
30-38: unsigned int pdpte_index : 9;
39-47: unsigned int pml4te_index : 9;
48-63: unsigned int sign : 16; // 符号扩展位
};
其中的 48-63 位是符号扩展位, 要么全 0, 要么全 1, 必须与第 47 位相同. 这种地址被称为规范化地址(canonical address), 是为了方便以后将 48 位的地址扩展到更高位数时无需进行修改. 4 个 9 位的 index 分别是 4 种表结构的下标, 这些表结构的元素都是 8 字节 64 位的, 由于索引为 9 位, 所以这些表结构的大小都是 4KB. 第一个索引 pml4te_index 用于索引 PML4T(Page Map Level-4 Table, 表元素称为 PML4E), PML4T 的基址由 CR3 寄存器提供. CR3 被扩展为 64 位, 在不支持 PCIDE 功能时, CR3 的结构为:
struct NormalCR3 {
0- 2: unsigned int no_use1 : 3;
3: unsigned int PWT : 1; // Page-level Write-Through
4: unsigned int PCD : 1; // Page-level Cache Disable
5-11: unsigned int no_use2: 7;
12-47: unsigned int pml4t_base : 36;
48-63: unsigned int receved : 16; // 保留为 0
};
PML4T 的基地址低 12 设置为 0, 按 4 KB 对齐. 高位从 CR3 的 12 位开始. 随着物理地址长度不同, 使用的位数也不同, 没有使用的位数则保留为 0.
启用 PCID (CR4.PCIDE = 1, PCID = Process Context ID, 仅 Intel64 支持, AMD64 不支持) 后, CR3 的低 12 位表示 PCID 值, 第 63 位控制 CR3 切换时缓存的处理. 详情从略.
根据 CR3 和 pml4t_index 可以寻到 PML4E 结构, 该结构描述如下:
struct PML4E {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int no_use1 : 1;
7: unsigned int receved : 1; // PS 位 必须为 0
8-11: unsigned int no_use2 : 4;
12-47: unsigned int pdpt_base : 36;
48-62: unsigned int receved : 15; // 保留为 0, 随物理地址大小扩展
63: unsigned int XD : 1; // Execution Disable
};
其中的 pdpt_base 和 pml4t_base 一样, 第 12 位为 0, 高位可向保留位扩展, 以后的结构也都类似. XD 位和 PAE 模式中的 XD 位含义相同. 通过 PML4E 中的 pdpt_base 以及线性地址中的 pdpte_index 可以寻址到 PDPTE 结构, PDPTE 结构将控制 1G 页面的转换, 所以第 7 位 PS 位有效, 当 PS = 1 时直接通过 PDPTE 结构转换 1G 的页面. 此时 PDPTE 结构如下:
struct PDPTE_1G {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int D : 1;
7: unsigned int PS : 1; // PS 位, 为 1
8: unsigned int G : 1;
9-11: unsigned int no_use1 : 3;
12: unsigned int PAT : 1;
13-29: unsigned int receved : 17; // 保留, 必须为 0
30-47: unsigned int page_base : 18;
48-62: unsigned int receved : 15;
63: unsigned int XD : 1; // Execution Disable
};
其中的 page_base 的低 30 位为 0, 按 1GB 对齐, 高位可向保留位扩展. 线性地址中的 pde_index 和 pte_index 也用于表示偏移, 一个是 30 位的偏移. 由基址 + 偏移得到物理地址.
当 PS = 0 是, 页面是 4K 或 2M 的页面, PDPTE 需要提供 PDT 的基址, 此时的 PDPTE 结构如下:
struct PDPTE {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int no_use1 : 1;
7: unsigned int PS : 1; // PS 位, 为 0
8-11: unsigned int no_use2 : 4;
12-47: unsigned int pdt_base : 36;
48-62: unsigned int receved : 15; // 保留为 0, 随物理地址大小扩展
63: unsigned int XD : 1; // Execution Disable
};
由其中的 pdt_base 和线性地址中的 pde_index 可寻找到 PDE 结构, PDE 中的 PS 位决定是 4KB 页面还是 2MB 页面, 当 PS = 1 时页面为 2M, 此时 PDE 结构如下:
struct PDE_2M {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int D : 1;
7: unsigned int PS : 1; // PS 位, 为 1
8: unsigned int G : 1;
9-11: unsigned int no_use1 : 3;
12: unsigned int PAT : 1;
13-20: unsigned int receved : 8; // 保留, 必须为 0
21-47: unsigned int page_base : 27;
48-62: unsigned int receved : 15;
63: unsigned int XD : 1; // Execution Disable
};
page_base 的低 21 位为 0, 按 2M 对齐, 线性地址 pte_index 用于表示偏移, 一共 21 位偏移, 由基址 + 偏移得到物理地址.
PDE.PS = 0 时, 页面为 4K, 继续寻找 PTE 结构, 此时的 PDE 为:
struct PDE {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int no_use1 : 1;
7: unsigned int PS : 1; // PS 位, 为 0
8-11: unsigned int no_use2 : 4;
12-47: unsigned int pt_base : 36;
48-62: unsigned int receved : 15; // 保留为 0, 随物理地址大小扩展
63: unsigned int XD : 1; // Execution Disable
};
由 pt_base + pte_index 最终得到 PTE 结构, 此时的 PTE 结构与 PAE 模式下是完全一致的:
struct PTE {
0: unsigned int P : 1;
1: unsigned int R/W : 1;
2: unsigned int U/S : 1;
3: unsigned int PWT : 1;
4: unsigned int PCD : 1;
5: unsigned int A : 1;
6: unsigned int D : 1;
7: unsigned int PAT : 1; // PS 位变为 PAT 标志
8: unsigned int G : 1;
9-11: unsigned int no_use1 : 3;
12-47: unsigned int page_base : 36;
48-62: unsigned int receved : 15;
63: unsigned int XD : 1; // Execution Disable
};
最终由 PTE 的 page_base 加上线性地址的 offset 得到物理地址, 漫长的寻址过程终于画上了句号.
以下是网上搜集的描述长模式下寻址相关的图表:
1. 线性地址到物理地址转换, 灰色路线是 2M 页面, 深灰色路线是 1G 页面, 黑色路线是 4K 页面的转换.
好东西
转了,很全面