Loading... # 页式存储 ## 基础概念 ### 逻辑地址、物理地址、线性地址 逻辑地址可以通过分段机制转换为物理地址 物理地址可以通过分页机制转换为线性地址  线性地址中任意一个页都能映射到物理地址中的任何一个页,这无疑使得内存管理变得相当灵活 ### 页 所谓“页”,就是一块内存,在80386中,页的大小是固定的4096字节(4KB)。 ### `PTE`表 `PTE`是页表中的一个表项,用于指向实际的物理页。`PTE`表的结构如下:  - `PAGE-TABLE BASE ADDRESS`:**页的高二十位地址,因为页具有4k对齐机制,因此低12位一定为0** - `p`存在位: 表示当前条目所指向的页或页表是否在物理内存中。P=0表示页不在内存中,如果处理器试图访问此页,将会产生页异常;P=1表示页在内存中。 - `R/W`: 指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的读写权限。此位与`U/S`位和寄存器`cr0`中的`WP`位相互作用。`R/W`=0表示只读;`R/W`=1表示可读并可写。 - `U/S`: 指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的特权级。此位与`R/W`位和寄存器`cr0`中的`WP`位相互作用。`U/S`=0表示系统级别(Supervisor Privilege Level),如果`CPL`为0、1或2,那么它便是在此级别;`U/S`=1表示用户级别(User Privilege Level),如果`CPL`为3,那么它便是在此级别。如果`cr0`中的`WP`位为0,那么即便用户级(User P.L.)页面的R/W=0,系统级(Supervisor P.L.)程序仍然具备写权限;如果`WP`位为1,那么系统级(Supervisor P.L.)程序也不能写入用户级(User P.L.)只读页。 - `PWT`: 用于控制对单个页或者页表的缓冲策略。`PWT`=0时使用Write-back缓冲策略;`PWT`=1时使用Write-through 缓冲策略。 - `PCD`: 用于控制对单个页或者页表的缓冲。`PCD`=0时页或页表可以被缓冲;`PCD`=1时页或页表不可以被缓冲。 - `A`: 指示页或页表是否被访问。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次访问此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。 - `D`:指示页或页表是否被写入。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次写入此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。 - `PAT`:选择PAT(Page Attribute Table)条目 - `G`: 指示全局页。如果此位被设置,同时cr4中的PGE位被置,那么此页的页表或页目录条目不会在TLB中变得无效,即便cr3被加载或者任务切换时也是如此。 - `AVAIL`: 程序员可用 ### 页表 所谓"页表",是由1024个`PTE`表组成,每个`PTE`表项对应一个物理页  ### `PDE`表 `PDE`是页目录表中的一个表项,用于指向页表。`PDE`表的结构如下:  - `PAGE-TABLE BASE ADDRESS`:**页的高二十位地址,因为页具有4k对齐机制,因此低12位一定为0** - `p`存在位: 表示当前条目所指向的页或页表是否在物理内存中。P=0表示页不在内存中,如果处理器试图访问此页,将会产生页异常;P=1表示页在内存中。 - `R/W`: 指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的读写权限。此位与`U/S`位和寄存器`cr0`中的`WP`位相互作用。`R/W`=0表示只读;`R/W`=1表示可读并可写。 - `U/S`: 指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的特权级。此位与`R/W`位和寄存器`cr0`中的`WP`位相互作用。`U/S`=0表示系统级别(Supervisor Privilege Level),如果`CPL`为0、1或2,那么它便是在此级别;`U/S`=1表示用户级别(User Privilege Level),如果`CPL`为3,那么它便是在此级别。如果`cr0`中的`WP`位为0,那么即便用户级(User P.L.)页面的R/W=0,系统级(Supervisor P.L.)程序仍然具备写权限;如果`WP`位为1,那么系统级(Supervisor P.L.)程序也不能写入用户级(User P.L.)只读页。 - `PWT`: 用于控制对单个页或者页表的缓冲策略。`PWT`=0时使用Write-back缓冲策略;`PWT`=1时使用Write-through 缓冲策略。 - `PCD`: 用于控制对单个页或者页表的缓冲。`PCD`=0时页或页表可以被缓冲;`PCD`=1时页或页表不可以被缓冲。 - `A`: 指示页或页表是否被访问。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次访问此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。 - `PS`: 决定页大小。PS=0时页大小为4KB,PDE指向页表。 - `G`: 指示全局页。如果此位被设置,同时cr4中的PGE位被置,那么此页的页表或页目录条目不会在TLB中变得无效,即便cr3被加载或者任务切换时也是如此。 - `AVAIL`: 程序员可用 ### 页目录 所谓页目录,是由1024个PDE表项组成,每个PDE表项对应一个页表  ### `cr3`寄存器  r3又叫做PDBR(Page-Directory Base Register)。它的高20位将是页目录表首地址的高20位. ## 页式存储示意图  分页机制会将保护模式下的物理地址转换为线性地址,其中: - cr3寄存器指向的页目录表的基地址,线性地址的高十位指向了`PDE`的偏移地址 - `PDE`指向页表的基地址,线性地址的中间十位指向`PTE`的偏移地址 - `PTE`指向物理页的基地址,线性地址的后12位指向物理页的偏移地址\ ## 代码分析 ### `GDT`及`selector`定义 ```nasm PageDirBase0 equ 200000h ; 页目录0开始地址: 2M PageTblBase0 equ 201000h ; 页表0开始地址: 2M + 4K PageDirBase1 equ 210000h ; 页目录1开始地址: 2M + 64K PageTblBase1 equ 211000h ; 页表1开始地址: 2M + 64K + 4K [SECTION .gdt] ; GDT ; 段基址, 段界限, 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 ... LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW|DA_LIMIT_4K ; 0~4G ... ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 ... SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT ... ; END of [SECTION .gdt] ``` 这段代码定义了两段不同的页式存储机制,代码中并没有同时在内存中分配两套存储,而是在中途进行了页式存储的变换,因此这里以第一套存储机制分析: - 页目录0开始于`2M`内存,共含有`1024`个`GDE`表项,每个`GDE`表项的长度为4个字节,因此页目录0的起始内存地址为`2M`~`2M+4K` - 第0号页表开始于`2M`~`2M+4K`,即页目录结束后下一个内存单元就是页表,共有`1024`个页表,每个页表有`1024*4`个字节的数据,因此共占用`4M`内存.因此第一个页表的起始内存地址为`2M+4K`,最后一个页表结束地址为`6M+4K` ### 启动分页机制 ```nasm ; 启动分页机制 -------------------------------------------------------------- SetupPaging: ; 根据内存大小计算应初始化多少PDE以及多少页表 xor edx, edx mov eax, [dwMemSize] mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小 div ebx mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数 test edx, edx jz .no_remainder inc ecx ; 如果余数不为 0 就需增加一个页表 .no_remainder: mov [PageTableNumber], ecx ; 暂存页表个数 ; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞. ; 首先初始化页目录 mov ax, SelectorFlatRW mov es, ax mov edi, PageDirBase0 ; 此段首地址为 PageDirBase0 xor eax, eax mov eax, PageTblBase0 | PG_P | PG_USU | PG_RWW .1: stosd add eax, 4096 ; 为了简化, 所有页表在内存中是连续的. loop .1 ; 再初始化所有页表 mov eax, [PageTableNumber] ; 页表个数 mov ebx, 1024 ; 每个页表 1024 个 PTE mul ebx mov ecx, eax ; PTE个数 = 页表个数 * 1024 mov edi, PageTblBase0 ; 此段首地址为 PageTblBase0 xor eax, eax mov eax, PG_P | PG_USU | PG_RWW .2: stosd add eax, 4096 ; 每一页指向 4K 的空间 loop .2 mov eax, PageDirBase0 mov cr3, eax mov eax, cr0 or eax, 80000000h mov cr0, eax jmp short .3 .3: nop ret ; 分页机制启动完毕 ---------------------------------------------------------- ``` 1. 程序首先先根据页表的大小计算出来一共有多少个页表`PageTableNumber` 2. 然后初始化页目录 1. 先将选择子赋值为`es`寄存器,页目录基址赋值给`di`寄存器,将`eax`指向零号页表的基地址 2. 然后进入循环: 1. 调用`stosd`指令,将`eax`寄存器中的内容赋值给`es:di`指向的内存空间 2. 将`eax`的值加`4096`(每个页表的大小为`4K`,`GDT`的低12位不用,`4096`=$2^{12}$,相当于指向了下一个页表的页基址) 3. 然后初始化所有页表 1. 由于页表在内存中连续存放,首先先算出页表中具有的`PTE`个数存入`ecx`作为循环计数 2. 然后进入循环: 1. 调用`stosd`指令,将`eax`寄存器中的内容赋值给`es:di`指向的内存空间 2. 将`eax`的值加`4096`(每页的大小位`4K`,`GDT`的低12位不用,`4096`=$2^{12}$,相当于指向了下一个页的页基址) 4. 最后设置`cr3`寄存器指向页表的基地址并设置`cr0`寄存器的`PG`位为1 ### 测试分页机制 测试分页机制时是通过同一个线性地址在不同的分页机制下尝试调用不同的函数来完成不同的功能实现的, 首先先将以下几个函数复制到内存的指定地点 ```nasm LinearAddrDemo equ 00401000h ProcFoo equ 00401000h ProcBar equ 00501000h ProcPagingDemo equ 00301000h ; 测试分页机制 -------------------------------------------------------------- PagingDemo: mov ax, cs mov ds, ax mov ax, SelectorFlatRW mov es, ax push LenFoo push OffsetFoo push ProcFoo call MemCpy add esp, 12 push LenBar push OffsetBar push ProcBar call MemCpy add esp, 12 push LenPagingDemoAll push OffsetPagingDemoProc push ProcPagingDemo call MemCpy add esp, 12 ``` 其中函数定义如下 ```nasm PagingDemoProc: OffsetPagingDemoProc equ PagingDemoProc - $$ mov eax, LinearAddrDemo call eax retf LenPagingDemoAll equ $ - PagingDemoProc foo: OffsetFoo equ foo - $$ mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'F' mov [gs:((80 * 17 + 0) * 2)], ax ; 屏幕第 17 行, 第 0 列。 mov al, 'o' mov [gs:((80 * 17 + 1) * 2)], ax ; 屏幕第 17 行, 第 1 列。 mov [gs:((80 * 17 + 2) * 2)], ax ; 屏幕第 17 行, 第 2 列。 ret LenFoo equ $ - foo bar: OffsetBar equ bar - $$ mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'B' mov [gs:((80 * 18 + 0) * 2)], ax ; 屏幕第 18 行, 第 0 列。 mov al, 'a' mov [gs:((80 * 18 + 1) * 2)], ax ; 屏幕第 18 行, 第 1 列。 mov al, 'r' mov [gs:((80 * 18 + 2) * 2)], ax ; 屏幕第 18 行, 第 2 列。 ret LenBar equ $ - bar ``` 这里不用去纠结于函数具体实现 ```nasm mov ax, SelectorData mov ds, ax ; 数据段选择子 mov es, ax call SetupPaging ; 启动分页 call SelectorFlatC:ProcPagingDemo call PSwitch ; 切换页目录,改变地址映射关系 call SelectorFlatC:ProcPagingDemo ret ``` 这里实现了分页机制的切换 由于分页0属于恒等映射,因此**物理地址就是线性地址**,可以通过直接调用`ProcPagingDemo`函数找到内存中`00401000h`对应的函数`Foo` 然后切换分页机制,解析`00401000h`对应的物理地址,并将其重新映射到函数`Bar` 以下是切换分页机制的实现 ```nasm PSwitch: ; 初始化页目录 mov ax, SelectorFlatRW mov es, ax mov edi, PageDirBase1 ; 此段首地址为 PageDirBase1 xor eax, eax mov eax, PageTblBase1 | PG_P | PG_USU | PG_RWW mov ecx, [PageTableNumber] .1: stosd add eax, 4096 ; 为了简化, 所有页表在内存中是连续的. loop .1 ; 再初始化所有页表 mov eax, [PageTableNumber] ; 页表个数 mov ebx, 1024 ; 每个页表 1024 个 PTE mul ebx mov ecx, eax ; PTE个数 = 页表个数 * 1024 mov edi, PageTblBase1 ; 此段首地址为 PageTblBase1 xor eax, eax mov eax, PG_P | PG_USU | PG_RWW .2: stosd add eax, 4096 ; 每一页指向 4K 的空间 loop .2 ; 在此假设内存是大于 8M 的 mov eax, LinearAddrDemo shr eax, 22 mov ebx, 4096 mul ebx mov ecx, eax mov eax, LinearAddrDemo shr eax, 12 and eax, 03FFh ; 1111111111b (10 bits) mov ebx, 4 mul ebx add eax, ecx add eax, PageTblBase1 mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW mov eax, PageDirBase1 mov cr3, eax jmp short .3 ``` 注意,此时线性地址`00401000h`对应的物理地址已经发生了变化! 需要重新根据线性地址解析物理地址 着重关注最后的代码实现: 1. 首先通过线性地址`LinearAddrDemo`的前10位找到页表的偏移地址(页目录索引*4096) 2. 然后通过中间十位找到页表项的偏移地址(页表项索引*4) 3. 然后将`PageTblBase1`与页表的偏移地址相加,得到当前页表的地址 4. 然后将当前页表的基址与页表项的偏移地址相加,得到页表项的地址 5. 然后将页表项对应的地址的内存空间填充`Bar`函数的地址 因此,再次调用`LinearAddrDemo`线性地址位置的函数,是在调用`Bar`函数而不是`Foo`函数 最后修改:2024 年 10 月 01 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏