当前位置: 首页 > 知识库问答 >
问题:

x86分页是如何工作的?

明越
2023-03-14

我相信一个好的答案将适合于一个大的SO答案,或者至少在几个答案中。

主要目标是给完全的初学者提供足够的信息,使他们能够自己使用手册,并且能够理解与分页相关的基本OS概念。

建议的准则:

    null

x86虚拟化是如何工作的:body只要求提供源代码。

共有1个答案

邓焱
2023-03-14

这个答案的版本有一个很好的TOC和更多的内容。

我将纠正任何报告的错误。如果你想要做大的修改或者增加一个缺失的方面,让他们在你自己的答案上得到应得的代表。小的编辑可以直接合并。

最小示例:https://github.com/cirosantilli/x86-bare-metal-examples/blob/5c672f73884a487414b3e21bd9e579c67cd77621/paging.s

这是一个“难”的主题,因为最小的例子很大,因为您需要制作自己的小OS。

虽然没有实例在头脑中是不可能理解的,但尽量尽快熟悉手册。

英特尔在《英特尔手册》第3卷系统编程指南-325384-056US 2015年9月第4章“分页”中介绍了分页。

首先分段将它们转换为线性地址,然后分页再将线性地址转换为物理地址。

(logical) ------------------> (linear) ------------> (physical)
             segmentation                 paging

大多数时候,我们可以认为物理地址是索引实际的RAM硬件存储单元,但这并不是100%正确的,因为:

  • 内存映射I/O区域
  • 多通道存储器
    null

这是分页的主要优点,因为相同大小的块使事情更易于管理。

分页已经变得越来越流行,以至于在x86-64的64位模式(新软件的主要操作模式)中放弃了对分段的支持,在64位模式中,分页只存在于模拟IA32的兼容模式中。

分页用于在现代操作系统上实现进程虚拟地址空间。通过虚拟地址,操作系统可以在单个RAM上容纳两个或多个并发进程,其方式如下:

  • 两个程序都不需要了解其他程序
  • 两个程序的内存可以根据需要增长和收缩
  • 程序之间的切换非常快
  • 一个程序永远不能访问另一个进程的内存

在历史上,分页是在分段之后出现的,在现代开放源码软件(如Linux)中,由于管理固定大小的分页内存块而不是可变长度的段更容易,分页在很大程度上取代了分段,用于虚拟内存的实现。

与保护模式下的分段(修改段寄存器会触发GDT或LDT的加载)类似,分页硬件使用内存中的数据结构来完成其工作(页表、页目录等)。

这些数据结构的格式是由硬件固定的,但由操作系统来正确设置和管理RAM上的数据结构,并告诉硬件在哪里找到它们(通过CR3)。

其他一些体系结构几乎完全将分页放在软件手中,因此TLB miss运行一个操作系统提供的函数来遍历页表并将新映射插入TLB。这使得页表格式由操作系统选择,但硬件不太可能像x86那样将页遍历与其他指令的无序执行重叠。

这是一个分页如何在x86体系结构的简化版本上操作以实现虚拟内存空间的示例。

操作系统可以为它们提供以下页表:

操作系统给进程1的页表:

RAM location        physical address   present
-----------------   -----------------  --------
PT1 + 0       * L   0x00001            1
PT1 + 1       * L   0x00000            1
PT1 + 2       * L   0x00003            1
PT1 + 3       * L                      0
...                                    ...
PT1 + 0xFFFFF * L   0x00005            1

操作系统给进程2的页表:

RAM location       physical address   present
-----------------  -----------------  --------
PT2 + 0       * L  0x0000A            1
PT2 + 1       * L  0x0000B            1
PT2 + 2       * L                     0
PT2 + 3       * L  0x00003            1
...                ...                ...
PT2 + 0xFFFFF * L  0x00004            1

present:指示页面存在于内存中。

页表位于RAM上。例如,它们可以定位为:

--------------> 0xFFFFFFFF


--------------> PT1 + 0xFFFFF * L
Page Table 1
--------------> PT1


--------------> PT2 + 0xFFFFF * L
Page Table 2
--------------> PT2

--------------> 0x0

两个页表在RAM上的初始位置是任意的,由操作系统控制。由操作系统来确保它们不重叠!

在这个简化的示例中,页表条目仅包含两个字段:

bits   function
-----  -----------------------------------------
20     physical address of the start of the page
1      present flag

因此,在本例中,硬件设计人员可以选择L=21

大多数真正的页表条目都有其他字段。

如果进程1希望访问线性地址0x00000001,则寻呼硬件电路自动为OS执行以下操作:

>

  • 将线性地址分成两部分:

    | page (20 bits) | offset (12 bits) |
    

    因此在这种情况下,我们会:

      null

    硬件知道此条目位于RAM地址PT1+0*L=PT1处。

    因为它存在,所以访问是有效的

    在页表中,页码0x00000的位置在0x00001*4k=0x00001000处。

      00001 000
    + 00000 001
      -----------
      00001 001
    

    然后硬件获得该物理位置的内存。

    以同样的方式,进程1将发生以下转换:

    linear     physical
    ---------  ---------
    00000 002  00001 002
    00000 003  00001 003
    00000 FFF  00001 FFF
    00001 000  00000 000
    00001 001  00000 001
    00001 FFF  00000 FFF
    00002 000  00002 000
    FFFFF 000  00005 000
    

    例如,当访问地址00001000时,页面部分是00001硬件知道它的页表条目位于RAM地址:PT1+1*L(1,因为是页面部分),并且它将在那里查找它。

    现在,进程2将发生以下转换:

    linear     physical
    ---------  ---------
    00000 002  00001 002
    00000 003  00001 003
    00000 FFF  00001 FFF
    00001 000  00000 000
    00001 001  00000 001
    00001 FFF  00000 FFF
    00003 000  00003 000
    FFFFF 000  00004 000
    

    对于不同的进程,相同的线性地址转换为不同的物理地址,这仅取决于cr3内部的值。

    这样,每个程序都可以期望其数据以0开始,以FFFFFFFF结束,而不必担心确切的物理地址。

    然后通常由OS注册一个异常处理程序来决定必须执行的操作。

    访问不在表上的页可能是编程错误:

    int is[1];
    is[2] = 1;
    

    但在某些情况下,它是可以接受的,例如在Linux中,当:

    >

  • 程序希望增加其堆栈。

    它只是尝试访问给定可能范围内的某个字节,如果操作系统愿意,它会将该页添加到进程地址空间中。

    操作系统可以根据页表项其余部分的内容发现这种情况,因为如果当前标志是清楚的,则页表项的其他部分完全由操作系统来执行它想要的操作。

    例如,在Linux上,当present=0:

    >

  • 如果页表条目的所有字段都为0,则为无效地址。

    在任何情况下,OS都需要知道哪个地址生成了页面错误,以便能够处理该问题。这就是为什么每当出现页面错误时,nice IA32开发人员都会将cr2的值设置为该地址的原因。然后,异常处理程序只需查看cr2以获取地址。

    使本例更容易理解的对现实的简化:

    >

  • 所有实际的寻呼电路都使用多级寻呼以节省空间,但这显示了一个简单的单级方案。

    页表只包含两个字段:一个20位的地址和一个1位的存在标志。

    真正的页表总共包含12个字段,因此其他功能都被省略了。

    单级分页方案的问题是它会占用太多的RAM:4G/4K=1M个进程条目。如果每个条目都是4字节长,那么每个进程就会有4M,这对于台式计算机来说也太多了:ps-a wc-l说我现在正在运行244个进程,所以这将占用我大约1GB的RAM!

    为此,x86开发人员决定使用减少RAM使用量的多级方案。

    这个系统的缺点是访问时间稍长。

    在用于不带PAE的32位处理器的简单3级分页方案中,32个地址位的划分如下:

    | directory (10 bits) | table (10 bits) | offset (12 bits) |
    

    cr3现在指向当前进程的页目录的RAM位置,而不是页表。

    单级方案中的页表条目根本不会改变。

    页表从单层方案更改是因为:

    • 每个进程最多可以有1k个页表,每个页目录条目一个。
    • 每个页表正好包含1k个条目,而不是1m个条目。

    在前两个级别上使用10位(而不是12812)的原因是每个页表条目的长度为4字节。则页目录和页表的2^10项将很好地适合于4KB页。这意味着为此目的分配和释放页面会更快、更简单。

    操作系统提供给进程%1的页目录:

    RAM location     physical address   present
    ---------------  -----------------  --------
    PD1 + 0     * L  0x10000            1
    PD1 + 1     * L                     0
    PD1 + 2     * L  0x80000            1
    PD1 + 3     * L                     0
    ...                                 ...
    PD1 + 0x3FF * L                     0
    
    RAM location      physical address   present
    ---------------   -----------------  --------
    PT1 + 0     * L   0x00001            1
    PT1 + 1     * L                      0
    PT1 + 2     * L   0x0000D            1
    ...                                  ...
    PT1 + 0x3FF * L   0x00005            1
    
    RAM location      physical address   present
    ---------------   -----------------  --------
    PT2 + 0     * L   0x0000A            1
    PT2 + 1     * L   0x0000C            1
    PT2 + 2     * L                      0
    ...                                  ...
    PT2 + 0x3FF * L   0x00003            1
    
    • PD1:进程1的页目录在RAM上的初始位置。
    • PT1PT2:进程1的页表1和页表2在RAM上的初始位置。

    因此,在本例中,页面目录和页面表可以存储在RAM中,如下所示:

    ----------------> 0xFFFFFFFF
    
    
    ----------------> PT2 + 0x3FF * L
    Page Table 1
    ----------------> PT2
    
    ----------------> PD1 + 0x3FF * L
    Page Directory 1
    ----------------> PD1
    
    
    ----------------> PT1 + 0x3FF * L
    Page Table 2
    ----------------> PT1
    
    ----------------> 0x0
    

    让我们一步一步地翻译线性地址0x00801004

    0    0    8    0    1    0    0    4
    0000 0000 1000 0000 0001 0000 0000 0100
    

    10 10 12分组:

    0000000010 0000000001 000000000100
    0x2        0x1        0x4
    

    它给出:

    • 页面目录项=0x2
    • 页表项=0x1
    • 偏移=0x4

    转换地址的其他示例有:

    linear    10 10 12 split   physical
    --------  ---------------  ----------
    00000001  000 000 001      00001001
    00001001  000 001 001      page fault
    003FF001  000 3FF 001      00005001
    00400000  001 000 000      page fault
    00800001  002 000 001      0000A001
    00801008  002 001 008      0000C008
    00802008  002 002 008      page fault
    00B00001  003 000 000      page fault
    

    如果页目录项或页表项不存在,则会出现页错误。

    如果操作系统希望并发运行另一个进程,它将给第二个进程一个单独的页目录,并将该目录链接到单独的页表。

    如果采用2级方法,最好的拆分是两个18位级别。

    但这意味着页面目录将有2^18=256K条目,这将占用太多的RAM:接近于32位体系结构的单级分页!

    因此,64位体系结构创建甚至更多的页面级别,通常是3或4。

    使用32位,只能寻址4GB的RAM。

    这开始成为大型服务器的一个限制,因此Intel将PAE机制引入Pentium Pro。

    为了缓解这个问题,Intel增加了4条新的地址线,这样64GB就可以寻址了。

    页面大小扩展。

    允许页面长度为4M(如果PAE处于开启状态,则为2M)而不是4K。

    PSE通过CR4PAE位打开和关闭。

    >

  • 无PAE和无PSE:10 10 12

    无PAE和PSE:10 22

    22是4MB页面内的偏移量,因为22位地址为4MB。

    原因是20位已经不足以代表页表的地址了:现在需要24位,因为处理器增加了4条额外的线。

    因此,设计人员决定将条目的大小增加到64位,并且为了使它们适合于单页表,必须将条目的数量减少到2^9而不是2^10。

    开始的2是一个新的页级,称为页目录指针表(PDPT),因为它指向页目录并填充32位线性地址。PDPTs也是64位宽。

    PAE和PSE:2 9 21

    设计人员决定保留一个9位宽的字段,以使其适合于一个页面。

    这就剩下23位。为PDPT留出2页,以保持与PAE盒的一致性,而不留出PSE盒留出21页用于偏移,这意味着页面是2m宽的,而不是4m宽的。

    本节将描述具有4个单个地址条目的简化的完全关联TLB。注意,与其他缓存一样,真正的TLB通常不是完全关联的。

    在线性地址和物理地址之间发生转换后,它被存储在TLB上。例如,4个条目的TLB以以下状态启动:

      valid   linear   physical
      ------  -------  ---------
    > 0       00000    00000
      0       00000    00000
      0       00000    00000
      0       00000    00000
    

    >指示要替换的当前条目。

      valid   linear   physical
      ------  -------  ---------
      1       00003    00005
    > 0       00000    00000
      0       00000    00000
      0       00000    00000
    
      valid   linear   physical
      ------  -------  ---------
      1       00003    00005
      1       00007    00009
    > 0       00000    00000
      0       00000    00000
    

    现在,如果00003需要再次转换,硬件首先查找TLB,并通过单个RAM访问找到其地址00003->00005

    当然,00000不在TLB上,因为没有任何有效条目包含00000作为密钥。

    当TLB填满时,旧地址将被覆盖。就像对于CPU缓存一样,替换策略是一个潜在的复杂操作,但一个简单合理的启发式是删除最近最少使用的条目(LRU)。

    对于LRU,从状态开始:

      valid   linear   physical
      ------  -------  ---------
    > 1       00003    00005
      1       00007    00009
      1       00009    00001
      1       0000B    00003
    

    添加0000D->0000A将得到:

      valid   linear   physical
      ------  -------  ---------
      1       0000D    0000A
    > 1       00007    00009
      1       00009    00001
      1       0000B    00003
    

    例如,一个地图,其中:

    • 键和值都有20位(简单分页方案的情况)
    • 每次最多需要存储4个值

    可以存储在包含4个条目的TLB中:

    linear   physical
    -------  ---------
    00000    00001
    00001    00010
    00010    00011
    FFFFF    00000
    
    linear   physical
    -------  ---------
    00000    00001
    00001    00010
    00010    00011
    ... (from 00011 to FFFFE)
    FFFFF    00000
    

    有些x86 CPU超出了x86规范的要求,在修改页表条目和使用页表条目之间提供了比它所保证的更多的一致性,而页表条目还没有缓存在TLB中。显然Windows9x依赖于它的正确性,但现代AMD CPU不提供连贯的页面遍历。英特尔的CPU可以做到这一点,尽管他们必须检测到错误的猜测才能做到这一点。利用这一点可能不是一个好主意,因为这样做可能不会有太多好处,而且很有可能导致难以调试的、对时间敏感的微妙问题。

    Linux内核广泛使用了x86的分页特性,以允许以较小的数据碎片进行快速进程切换。

    V4.2中,查看arch/x86/下:

    • include/asm/pgtable*
    • 包括/asm/page*
    • mm/pgtable*
    • mm/page*

    似乎没有定义用来表示页面的结构,只有宏:include/asm/page_types.h特别有趣。节选:

    #define _PAGE_BIT_PRESENT   0   /* is present */
    #define _PAGE_BIT_RW        1   /* writeable */
    #define _PAGE_BIT_USER      2   /* userspace addressable */
    #define _PAGE_BIT_PWT       3   /* page write through */
    

    arch/x86/include/uapi/asm/processor-flags.h定义CR0,特别是PG位位置:

    #define X86_CR0_PG_BIT      31 /* Paging */
    

    >

  • Rutgers-PXK-416章“内存管理:课堂讲义”

    对旧操作系统使用的内存组织技术进行了良好的历史回顾。

    非免费:

  •  类似资料:
    • 我正在阅读《实用恶意软件分析》一书,其中出现了以下示例代码: 作者接着说: 返回的COM对象将存储在堆栈中IDA Pro标记为ppv的变量中,如图所示。 我的问题是,这是为什么?既然我们做了一个mov eax,[esp 24h ppv],这难道不是将[esp 24h ppv]内部的数据移动到eax并覆盖返回值,而不是将返回值存储在变量中吗?我认为在Intel格式中,mov操作数1、操作数2总是将第

    • 问题内容: 我正在通过Sequelize寻找查询,但似乎找不到任何文档。 问题答案: 问题:https : //github.com/sequelize/sequelize/issues/348 我使用sequelize@2.0.0-dev9

    • 问题内容: 因此,我想在Oracle数据库中选择一系列行。我需要这样做是因为表中有数百万行,并且我想将结果分页给用户(如果您知道另一种在客户端执行此操作的方法,那么我正在使用JavaFX,但我没有认为通过网络发送所有数据以在客户端对它们进行分页是个好主意)。 我有以下查询: 该和只是例子。在应用程序中,我只是要求下限并添加10_000的大小以获取接下来的10_000行。 现在rownum列出现在结

    • 所以我想在Oracle DB中选择一系列行。我需要这样做,因为我在表中有数百万行,我想将结果分页给用户(如果您知道在客户端执行此操作的另一种方法,如果重要的话,我正在使用JavaFX,但我认为通过网络发送所有数据以在客户端分页它们不是一个好主意)。 所以读完这篇文章:SQLROWNUM如何在特定范围之间返回行,我有以下疑问: 和就是一个例子。在应用程序中,我只需要求下限,然后添加一个10\u 00

    • 本文向大家介绍Intel x86 Assembly& Microarchitecture 80486分页,包括了Intel x86 Assembly& Microarchitecture 80486分页的使用技巧和注意事项,需要的朋友参考一下 示例 80486 Paging Subsystem与80386非常相似。它向后兼容,唯一的新功能是允许逐页控制内存缓存-操作系统设计人员可以将特定页面标记为

    • 本文向大家介绍Intel x86 Assembly& Microarchitecture 80386分页,包括了Intel x86 Assembly& Microarchitecture 80386分页的使用技巧和注意事项,需要的朋友参考一下 示例 高级设计 80386是一个32位处理器,具有32位可寻址存储空间。Paging子系统的设计者注意到,一个4K页面设计以一种非常整洁的方式映射到这32位