Paging

来自osdev
Zhang3讨论 | 贡献2022年1月17日 (一) 08:28的版本 (创建页面,内容为“{{Disputed}} right|thumb|600x350px|x86 Paging Structure 分页是一种内存管理设计,它允许每个进程看到完整的虚拟地址空间,而实际上不需要完整的物理内存可用或存在。 32位的x86处理器支持32位虚拟地址和4-GiB虚拟地址空间,当前64位处理器支持48位虚拟寻址和256-TiB虚拟地址空间。 英特尔发布了 [https://en.wikipedia.org/wiki/Intel_5-level_paging 文…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

本文或本节的事实准确性是有争议的
请看 讨论页 上的相关讨论。

x86 Paging Structure

分页是一种内存管理设计,它允许每个进程看到完整的虚拟地址空间,而实际上不需要完整的物理内存可用或存在。 32位的x86处理器支持32位虚拟地址和4-GiB虚拟地址空间,当前64位处理器支持48位虚拟寻址和256-TiB虚拟地址空间。 英特尔发布了 文档,扩展到57位虚拟寻址和128-PiB虚拟地址空间。 当前,x86-64的实现具有物理地址空间的4 GiB和256 TiB之间的限制 (以及物理地址空间的4 PiB的体系结构限制)。

除此之外,分页还引入了页面级保护的好处。 在该系统中,用户进程只能查看和修改在其自己的地址空间上分页的数据,从而提供基于硬件的隔离。 系统页面也会受到保护,不受用户进程的影响。 在x86-64架构上,页面级保护现在完全取代 Segmentation 作为内存保护机制。 在IA-32体系结构上,分页和分段都存在,但是分段现在被认为是一种“遗留”。

一旦操作系统具有分页,它还可以利用其他好处和解决方法,例如用于内存映射IO的线性帧缓冲区模拟和分页到磁盘,将磁盘存储空间用于代替物理RAM。

32-bit 分页 (保护模式)

MMU

分页是通过使用 Memory Management Unit (MMU) 来实现的。 在x86上,MMU通过一系列 tables 映射内存,准确地说是两个。 它们是分页目录 (paging directory PD) 和分页表 (paging table PT)。

PD和PT tables 都包含1024个4字节的条目,使它们每个4 KiB。 在页面目录PD中,每个条目都指向一个页表。 在页表PT中,每个条目都指向一个4 KiB物理页帧。 此外,每个条目都具有控制其所指向的结构的访问保护和缓存功能的位。 由页面目录和页表组成的整个系统表示线性4-GiB虚拟内存映射。(译者注:1K个PD条目 * 1K个PT条目 * 每页4K物理内存空间)

将虚拟地址转换为物理地址首先涉及将虚拟地址分为三个部分: 最高有效的10位 (位22-31) 指定页目录条目的索引,接下来的10位 (位12-21) 指定页表条目的索引,最低有效的12位 (位0-11) 指定页偏移。 然后MMU从页面目录(PD)开始遍历分页结构,并使用页面目录条目来定位页面表(PT)。 页表(PT)条目用于定位物理页帧的基地址,并在物理基地址中添加页偏移量以产生物理地址。 如果由于某种原因翻译失败 (例如,条目被标记为不存在),则处理器会发出页面错误。

页面目录

最顶层的分页结构是页面目录。 它本质上是一个页面目录条目数组,采用以下形式:

A Page Directory Entry

当PS=0时,页表地址字段表示在该点上管理4兆字节的页表的物理地址。 请注意,此地址必须4-KiB对齐,这一点非常重要。 这是必需的,因为32位值的最后12位被访问位等覆盖。 同样,当PS=1时,地址必须为4-MiB对齐。

  • PAT, or Page Attribute Table. 如果支持 Page_attribute_table PAT,则PAT与PCD和PWT一起应指示内存缓存类型。 否则,它是保留的,必须设置为0。
  • G, or 'Global 告诉处理器不要在MOV到CR3指令上使对应于页面的TLB条目无效。 必须将CR4中的位7 (PGE) 设置为启用全局页面。
  • PS, or 'Page Size' 存储该特定条目的页面大小。 如果设置了位,则PDE映射到大小为4 MiB的页面。 否则,它将映射到4 KiB页表。 请注意,4-MiB页面需要启用PSE。
  • D, or 'Dirty' 用于确定页面是否已写入。
  • A, or 'Accessed' 用于发现在虚拟地址转换过程中是否读取了PDE或PTE。 如果有,则设置该位,否则不是。 请注意,CPU不会清除此位,因此负担落在OS上 (如果它根本需要此位)。
  • PCD, is the 'Cache Disable' bit. 如果设置了位,则不会缓存页面。 否则,它将是。
  • PWT, controls Write-Through' abilities of the page. 如果位已设置,则启用通过写入缓存。 如果没有,则改为启用回写。
  • U/S, the 'User/Supervisor' bit, 根据权限级别控制对页面的访问。 如果设置了该位,则所有人都可以访问该页面; 但是,如果未设置该位,则只有主管可以访问它。 对于页面目录条目,用户位控制对页面目录条目引用的所有页面的访问。 因此,如果您希望使页面成为用户页面,则必须在相关页面目录项以及页表项中设置用户位。
  • R/W, the 'Read/Write' 权限标志。 如果位被设置,则页面是读/写的。 否则,当未设置时,页面是只读的。 CR0中的WP位确定这是否仅应用于userland,始终给予内核写访问权限 (默认值) 或userland和内核两者 (请参阅英特尔手册3A 2-20)。
  • P, or 'Present'. 如果设置了位,则该页目前实际上位于物理内存中。 例如,当页面被换出时,它不在物理内存中,因此不在 “当前” 中。 如果调用了一个页面,但不存在,则会发生页面错误,操作系统应该对其进行处理。 (见下文。)

处理器不使用剩余的位9到11 (如果PS = 0,也可以使用位6和8),并且OS可以自由地存储其自己的一些计算信息。 此外,当未设置P时,处理器忽略条目的其余部分,您可以将所有剩余的31位用于额外信息,例如记录页面在交换空间中结束的位置。 当条目被标记为存在时,将访问的位或脏位从1更改为0时,建议使关联页面无效。 否则,由于TLB高速缓存,处理器可能不会在随后的读/写时设置那些位。

A Page Table Entry

设置PS位使得页面目录入口直接指向一个4-MiB页面。 地址转换中没有涉及分页表。 注意: 对于4-MiB页,是否保留位20到13取决于启用PSE以及处理器支持多少PSE位 (PSE、PSE-36、PSE-40)。 CPUID 应该用来确定这一点。 因此,物理地址也必须是4-MiB对齐的。 4 GiB以上的物理地址只能使用4 MiB PDE映射。

Page Table

在每个页表中,也有1024条目。 这些被称为页表条目,与页面目录条目相似。

第一项再次是4-KiB对齐的物理地址。 但是,与以前不同,该地址不是页表的地址,而是物理内存的4 KiB块,然后将其映射到页表和目录中的该位置。 请注意,PAT位是位7,而不是4 MiB PDE中的位12。

示例

假设内核加载到0x100000。但是,它需要重新映射到0xC0000000。 加载内核后,它将启动分页并设置适当的表。 (参见 Higher Half Kernel) 在 Identity Paging 第一兆字节之后,它需要创建第二个表 (即分页目录中的条目 #768) 以将0x100000映射到0xC0000000。 代码可能如下:

 mov eax, 0x0
 mov ebx, 0x100000
 .fill_table:
      mov ecx, ebx
      or ecx, 3
      mov [table_768+eax*4], ecx
      add ebx, 4096
      inc eax
      cmp eax, 1024
      je .end
      jmp .fill_table
 .end:

64-Bit Paging

Page map table entry structure (non-page-sized)

长模式 中的分页类似于32位分页,除了需要 物理地址扩展 (PAE)。 寄存器CR2和CR3扩展到64位。 不仅仅是使用3个级别的页面映射: 页面目录指针表,页面目录和页面表,而是使用第四个页面映射表: 4级页面映射表 (PML4)。这允许处理器将48位虚拟地址映射到52位物理地址。 如果支持并启用了5级页面映射,则第五个页面映射表 (5级页面映射表 (PML5)) 允许处理器将57位虚拟地址映射到52位物理地址。 PML4和PML5都包含512 64位条目,其中每个条目可以指向较低级别的页面映射表。 请注意,每增加一次分页级别,虚拟寻址就会变慢,尤其是在TLB缓存未命中的情况下。

64位模式下的虚拟地址必须是 'canonical' ,即地址的高位必须是全部0s或全部1s。 对于支持48位虚拟地址空间的系统,高16位必须相同,而对于支持57位虚拟地址的系统,高7位必须匹配。 虽然运行在 长模式 (兼容模式) 中的32位代码仍然限于32位虚拟地址,但它们仍然可以映射到52位物理地址。

Page Map Table Entries

Page map table entry structure (page-sized)

新的位已添加到用于长模式分页的页面映射表条目中:

  • XD, or 'Execute Disable'. If the NXE bit (bit 11) is set in the EFER register, then instructions are not allowed to be executed at addresses within the page whenever XD is set. If EFER.NXE bit is 0, then the XD bit is reserved and should be set to 0.
  • PK, or 'Protection Key'. The protection key is a 4-bit corresponding to each virtual address that is used to control user-mode and supervisor-mode memory accesses. If the PKE bit (bit 22) in CR4 is set, then the PKRU register is used for determining access rights for user-mode based on the protection key. If the PKS bit (bit 24) is set in CR4, then the PKRS register is used for determining access rights for supervisor-mode based on the protection key. A protection key allows the system to enable/disable access rights for multiple page entries across different address spaces at once.

M signifies the physical address width supported by a processor using PAE. Currently, up to 52 bits are supported, but the actual supported width may be less.

Bits marked as reserved must all be set to 0, otherwise, a page fault will occur with a reserved error code.

Support for 1 GiB pages, (NX) execute disable, (PKS/PKU) protection keys for supervisor-mode and user-mode pages, shadow stack pages, (M) physical address width, virtual address width, (PAT) page attribute table, (PCID) process context identifiers, and (LA57) 5-level paging can be determined with the CPUID instruction (EAX:0x01; EAX:0x07, ECX=0x00; EAX:0x80000001; EAX:0x80000008).

进程上下文标识符

If process context ids (PCID) are supported, then bits 0-11 of CR3 specify the process context id. Otherwise, bit 3 is PWT for PML4, and bit 4 is PCD for PML4. PCIDs are used to control TLB caching across multiple address spaces. The INVPCID instruction uses PCIDs to allow more control over page invalidation.

启用分页

32位分页

启用分页实际上非常简单。 所需要做的就是用页面目录的地址加载CR3,并设置cr0的分页 (PG) 和保护 (PE) 位。

 mov eax, page_directory
 mov cr3, eax
 
 mov eax, cr0
 or eax, 0x80000001
 mov cr0, eax

注意: 当保护标志清除时设置分页标志会导致 一般保护异常。 此外,一旦启用了分页,通过设置 EFER寄存器 的LME (位8) 来启用长模式的任何尝试都将触发 GPF。 必须先清除CR0.PG,然后才能设置EFER.LME。

如果要将userspace和supervisor的页面设置为只读,请将上面的0x80000001替换为0x80010001,这也设置了WP位。

要启用PSE (4个MiB页面),需要以下代码。

 mov eax, cr4
 or eax, 0x00000010
 mov cr4, eax

64位分页

在长模式下启用分页需要更多的步骤。 由于在PAE激活的情况下不分页就不可能进入长模式,因此启用位的顺序很重要。 首先,分页必须不处于活动状态 (即必须清除CR0.PG。)然后,设置CR4.PAE (位5) 和EFER.LME (MSR 0xC0000080的位8)。 如果要启用57位虚拟地址,则设置CR4.LA57 (位12)。最后,将CR0.PG设置为启用分页。

  ; Skip these 3 lines if paging is already disabled
  mov ebx, cr0
  and ebx, ~(1 << 31)
  mov cr0, ebx

  ; Enable PAE
  mov edx, cr4
  or  edx, (1 << 5)
  mov cr4, edx

  ; Set LME (long mode enable)
  mov ecx, 0xC0000080
  rdmsr
  or  eax, (1 << 8)
  wrmsr

  ; Replace 'pml4_table' with the appropriate physical address (and flags, if applicable)
  mov eax, pml4_table
  mov cr3, eax

  ; Enable paging (and protected mode, if it isn't already active)
  or ebx, (1 << 31) | (1 << 0)
  mov cr0, ebx

  ; 现在用适当的段选择器重新加载段寄存器 (CS,DS,SS等)...

  mov ax, DATA_SEL
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax
  
  ; Reload CS with a 64-bit code selector by performing a long jmp

  jmp CODE_SEL:reloadCS

[BITS 64]
reloadCS:
  hlt   ; Done. Replace these lines with your own code
  jmp reloadCS

启用分页后,您将无法直接从4级分页切换到5级分页 (反之亦然)。 切换到传统的32位分页也是如此。 在进行更改之前,您必须先通过清除CR0.PG来禁用分页。 否则,将导致 general protection fault

物理地址扩展

自Pentium Pro以来的所有Intel处理器 (400 Mhz的Pentium M除外) 和自Athlon系列以来的所有AMD都实现了 物理地址扩展 (PAE)。 此功能使您最多可以访问4 PiB (252) 的RAM。 您可以使用 CPUID 检查此功能。 一旦选中,您可以通过在CR4中设置位5来激活此功能。

对于传统的32位PAE,CR3寄存器指向4个64位条目的页面目录指针表 (PDPT),每个指向由4096个字节组成的页面目录 (就像在普通分页中一样),分为512 64位条目,每个指向4096字节的页面表,分为512 64位页面条目。 请记住,虚拟地址仍限制为4 GiB (232 字节)。

对于兼容模式和 长模式 中使用的4级和5级PAE,CR3寄存器分别指向顶级页面映射表: PML4表和PML5表。 每个页面映射表: PML5表、PML4表、页面目录指针表、页面目录、页面表包含512 64位条目。

如果启用了分页,则在进入长模式之前也必须启用PAE。 尝试在设置CR0.PG并清除CR4.PAE的情况下进入长模式将触发一般保护故障。

用法

由于分页设计简单,它有很多用途。

虚拟地址空间

在一个有页的系统中,每个进程可以在其自己的内存区域中执行,而不会影响任何其他进程的内存或内核的内存。 两个或多个进程可以通过将相同的物理页映射到它们自己的地址空间中的地址来选择共享内存。 每个映射的虚拟地址不需要相同。因此,通常,一个地址空间中的虚拟地址不会指向其他地址空间中的相同数据。

paging illustrated: two process with different views of the same physical memory

虚拟内存

由于分页允许动态处理未分配的页表,因此操作系统可以将整个页面 (当前未使用) 交换到硬盘驱动器,在那里它们可以等待直到调用它们。 但是,与此同时,他们使用的物理内存可以在其他地方使用。 这样,操作系统可以操纵系统,使程序实际上似乎具有比实际更多的RAM。

More...

操纵

CR3值,即包含页面目录地址的值,是物理形式。 然后,计算机处于分页模式,仅识别映射到分页表中的那些虚拟地址,如何编辑和动态更改表?

许多人更喜欢将最后一个PDE映射到自己。 页面目录在系统中看起来像一个页表。 要获取范围为0x00000000-0xFFFFF000的任何虚拟地址的物理地址,则只是以下问题:

void *get_physaddr(void *virtualaddr) {
    unsigned long pdindex = (unsigned long)virtualaddr >> 22;
    unsigned long ptindex = (unsigned long)virtualaddr >> 12 & 0x03FF;

    unsigned long *pd = (unsigned long *)0xFFFFF000;
    // Here you need to check whether the PD entry is present.

    unsigned long *pt = ((unsigned long *)0xFFC00000) + (0x400 * pdindex);
    // Here you need to check whether the PT entry is present.

    return (void *)((pt[ptindex] & ~0xFFF) + ((unsigned long)virtualaddr & 0xFFF));
}

要将虚拟地址映射到物理地址,可以执行以下操作:

void map_page(void *physaddr, void *virtualaddr, unsigned int flags) {
    // Make sure that both addresses are page-aligned.

    unsigned long pdindex = (unsigned long)virtualaddr >> 22;
    unsigned long ptindex = (unsigned long)virtualaddr >> 12 & 0x03FF;

    unsigned long *pd = (unsigned long *)0xFFFFF000;
    // Here you need to check whether the PD entry is present.
    // When it is not present, you need to create a new empty PT and
    // adjust the PDE accordingly.

    unsigned long *pt = ((unsigned long *)0xFFC00000) + (0x400 * pdindex);
    // Here you need to check whether the PT entry is present.
    // When it is, then there is already a mapping present. What do you do now?

    pt[ptindex] = ((unsigned long)physaddr) | (flags & 0xFFF) | 0x01; // Present

    // Now you need to flush the entry in the TLB
    // or you might not notice the change.
}

取消映射条目基本上与上面相同,但是您没有将 pt[ptindex] 分配一个值,而是将其设置为0x00000000 (即不存在)。 当整个页面表为空时,您可能希望将其删除并将页面目录条目标记为 “不存在”。 当然,你不需要 'flags' 或 'physaddr' 来取消映射。

页面故障

当进程试图访问未映射到任何物理内存的虚拟内存区域时,当在只读页面上尝试写入时,会导致 page fault 异常。使用保留位访问PTE或PDE或权限不足时。 page fault 可以是Pure的,这是在错误过程具有访问页面的权限时发生的,或者是无效的,这是由于保护违规造成的。 Pure page fault 不是错误,而是通过页面故障处理程序通过执行适当的映射操作和/或页面交换来解决。

处理

CPU在触发 page fault exception 之前,会在堆栈上推送错误代码。 错误代码必须由异常处理程序进行分析,以确定如何处理异常。 以下位元是唯一使用的,所有其他位元都是保留的。

Bit 0 (P) is the Present flag.
Bit 1 (R/W) is the Read/Write flag.
Bit 2 (U/S) is the User/Supervisor flag.
Bit 3 (RSVD) indicates whether a reserved bit was set in some page-structure entry
Bit 4 (I/D) is the Instruction/Data flag (1=instruction fetch, 0=data access)
Bit 5 (PK) indicates a protection-key violation
Bit 6 (SS) indicates a shadow-stack access fault
Bit 15 (SGX) indicates an SGX violaton

这些标志的组合指定页面错误的详细信息,并指示要采取的操作:

US RW  P - Description
0  0  0 - Supervisory process tried to read a non-present page entry
0  0  1 - Supervisory process tried to read a page and caused a protection fault
0  1  0 - Supervisory process tried to write to a non-present page entry
0  1  1 - Supervisory process tried to write a page and caused a protection fault
1  0  0 - User process tried to read a non-present page entry
1  0  1 - User process tried to read a page and caused a protection fault
1  1  0 - User process tried to write to a non-present page entry
1  1  1 - User process tried to write a page and caused a protection fault

当CPU触发页面不存在的异常时,将使用导致异常的线性地址填充CR2寄存器。 上面的10位指定页目录条目 (PDE),中间的10位指定页表条目 (PTE)。 首先检查PDE并查看是否设置了当前位,如果未设置页表并将PDE指向页表的基地址,则设置当前位和iretd。 如果存在PDE,则PTE的当前位将被清除。 您需要将一些物理内存映射到页表,设置当前位,然后iretd继续处理。

= INVLPG =

INVLPG是自i486以来可用的指令,该指令使TLB中的单个页面无效。 英特尔指出,在将来的流程中可能会以不同的方式实现此指令,但是必须显式启用此替代行为。INVLPG不修改任何标志。

NASM示例:

 invlpg [0]

GCC的内联程序集 (来自linux内核源):

static inline void __native_flush_tlb_single(unsigned long addr) {
   asm volatile("invlpg (%0)" ::"r" (addr) : "memory");
}

这只会使当前处理器上的页面无效。 如果您使用的是SMP,则需要向其他处理器发送IPI,以便它们也可以使页面无效 (这称为TLB shootdown; 它非常慢),以确保避免任何讨厌的比赛条件。 您可能只想在删除映射时执行此操作,并且只是使页面错误处理程序无效页面,如果它没有通过查看页面目录来使该处理器上的映射添加无效,则再次避免竞争条件。

当您修改页面目录中的条目,而不仅仅是页表时,您需要使表中的每个页面无效。 或者,您可以重新加载CR3,这将使整个目录无效,但这可能会更慢。(TODO time这个)

分页技巧

无论地址如何,当PDE或PTE中清除当前位时,处理器始终会触发页面故障异常。 这意味着PTE或PDE的内容可用于指示保存在大容量存储器上的页面的位置并快速加载它。 当页面被交换到磁盘时,使用这些条目来标识分页文件中可以快速加载它们的位置,然后将当前位设置为0。 同样,来自磁盘的块可以通过这种方式映射到内存。 当进程访问内存映射区域时,会发生页面错误。 故障处理程序读取适当的表,将磁盘块加载到页面中,并对其进行映射。 然后,该进程可以像直接访问设备一样读取/写入内存。 然后,页面的内容将被写回磁盘以保存更改。

为了提高内存效率,两个或多个进程可以作为只读共享页面。 如果一个进程要写入其页面,则会发生页面错误,系统可以复制该页面,然后将其标记为读写。 这就是所谓的写复制 (COW)。 写时复制允许系统延迟内存分配,直到一个进程实际需要它,防止不必要的复制。

另见

Articles

外部链接

de:Paging