Loading... 进入保护模式后,我们对内存的访问仍然是基于物理地址的,我们运行的程序,大多是希望自己有一段连续的地址空间的,这样方便寻址。如果使用物理地址来访问内存,就必须真的给每个进程都分配大段地连续物理内存空间,这可能造成内存碎片难以处理的问题。为了解决这个问题,可以引入**分页模式**(**Paging mode**),好处非常多,此处不再赘述,可以看 [WIKI](https://zh.wikipedia.org/wiki/%E5%88%86%E9%A0%81)。 直观地来看,分页模式做的就是将地址虚拟化,将物理地址以页(一页为 4KB)的粒度映射到虚拟地址上。这样可以给每个进程一个连续的虚拟地址,但是该进程实际使用的物理地址不是连续的,而是以 4K 为粒度的离散的空间,这样最大的碎片就只会有 4KB,基本解决了内存碎片的问题。 ### 一级页表 为了实现这样的映射,自然的想法是就建立一张页表,把虚拟地址全部映射到物理地址上。由于一页为 4KB,32 位系统能够寻到的地址上限为 4GB,所以为了实现完全的映射,这个页表就需要有 1024 * 1024 个项,共计 1M,如果用一个 32 位数做为一个项,一个页表的大小就是 4M,考虑到一台电脑中同时会运行多个进程,进程一多,页表就会占用大量空间,非常的浪费,同时页表是需要有连续的物理地址空间的,这就又回到了物理地址时代的内存碎片的问题上了。 总结一下,一级页表存在这两个问题: 1. 1M 个页表项常驻于内存,占用内存空间 2. 页表需要连续的物理地址空间,会造成内存碎片过大的问题 通过二级页表可以解决这两个问题。 ### 二级页表 二级页表就是通过一个**页目录表**(**Page Directory Entry**,**PDE**)来管理之前的一级页表的。 一级页表有 1024 * 1024 个项,每个项大小 4B,也就是 1024 * 4KB,那么用一个内存页可以存下 1024 个页表项,1024 个这样的**页表**(在二级页表中,**页表**特指有 1024 个项的页表,英文为**Page Table Entry**,**PTE**)就可以将虚拟地址映射到物理地址上了,那么我们用一个目录存下这 1024 个页表的地址,就可以实现映射了,这一个目录,即**页目录表**也只需要占用一个页,原来的 4M 的空间就被拆分成了 1024 + 1 个页,解决了页表产生内存碎片的问题。 另外,由于我们往往不会在程序中使用全部的虚拟地址,所以没有必要把所有的虚拟地址都做好映射,只要把需要的段做上映射就可以了,通过二级页表,我们不需要一次性建好所有的页表项。 这里的不需要一次性建好可以多说两句,一级页表其实也是可以不一次性建好的,打个比方,不需要映射的地方清零就行了,代表不需要映射,但是要知道这里仍然占用了连续空间,存储这个项没有被建立的形象。而二级页表可以在页目录表直接表示这 4M 空间都没有映射,对应的 1024 个页表也不需要存在于内存中了。换句话说,就是二级页表的页目录表一项可以表示 4M 空间的状态,比一级页表一项表示 4KB 高到不知道哪里去了。这样就不需要让所有的页表存在于内存中,只需要让需要的存在,减少了内存占用。 ### 宏观实现 实现的方法就是用 1025 个页来映射虚拟地址,其中只有页目录表一个页是必须存在且存在于固定的物理地址上。开启分页机制前,需要把页目录表基址存到控制寄存器 CR3 中。 然后硬件页部件就可以根据 32 位虚拟地址可以计算物理地址。虚拟地址的高10位(22 ~ 31 位)作为页目录表的索引,从目录中找到对应的页表,然后以虚拟地址的 12 ~ 21 作为页表的索引,找到虚拟地址的物理地址基址,以虚拟地址的低 12 位作为偏移,就可以访问到这整个页中的数据。这个计算过程是由硬件完成的。 从上面可以看出,由于页表和目录都是页对齐的,所以低 12 位都不需要使用,可以在其中存储一些控制信息,每一位对应如下 页目录表: | 31 ~ 12 | 11 ~ 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | | - | - | - | - | - | - | - | - | - | - | - | | 页表物理地址高 20 位 | AVL | G | 0 | 0 | A | PCD | PWT | US | RW | P | 页表: | 31 ~ 12 | 11 ~ 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | | - | - | - | - | - | - | - | - | - | - | - | | 物理页地址高 20 位 | AVL | G | PAT | D | A | PCD | PWT | US | RW | P | AVL:未被硬件使用 G:Global 位,若该位为 1,TLB(Translation Lookaside Buffer)中会缓存这个虚拟地址的映射。 PAT:页属性表位,在页粒度上设置内存属性 D:Dirty 位,脏位,CPU 在对一个页执行写操作时,会置该位为 1。该位仅对页表有效。 A:Accessed 位,访问位,为 1 表示被访问过。 PCD:Page-level Cache Disabled,页级高速缓冲禁止位,设置为 0 禁用高速缓存。 PWT:Page-level Write-Through,页级通写位,和高速缓存有关。 US:User/Supervisor 位,该位为 1 时,表示为 User 级,所有特权级都可以访问,为 0 时仅有 Ring 0 可以访问。 RW:设置读写属性,1 表示可读写,0 表示只读。 P:Present 位,**特别的**,与页中的 P 位不同,对于 PDE,1 表示对应的页目录表已存在,对于 PTE 表示对应的页已分配。 接下来我们在 loader 中设置并开启页表。 ### 设置页表 使用一个函数来设置页表。由于现在还没有用户进程,所以暂时不需要设置内核地址之外的页表。使用类似于 Linux 的内核布局,内核代码处于虚拟地址 3G - 4G。不过其映射的物理地址,我们希望它处于低段。也就是把 3G - 4G 这个虚拟地址映射到低 0 - 1G 上。当然实际上我们的内核完全不需要这么多的空间,1M 的数量级都远远不到,所以就提前约定,内核使用物理地址 0 - 1M 的空间(也不是这里面的全部,有些硬件也映射到了这里)。那么我们的页目录表就建立在 0x10000 上(物理地址 1M 处)。 内核的虚拟地址处于 3G-4G 即 0xC0000000 ~ 0xFFFFFFFF,对应到页目录表中,就是 0x300 ~ 0x3FF 项(取高十位),换算成偏移地址(左移 2 位)就是 0xC00 ~ 0xFFC 这 256 项,每一项对应的页表可以映射 4M 的物理内存。之前也说了我们的内核只是用 0 - 1M 的空间,所以我们首先建立好 PDE[0x300] 对应的页表。 然后还需要建立 PDE[0],这是因为当前运行的 loader 原先是使用物理地址的,进入分页模式后,要保证物理地址转为虚拟地址后仍然能够映射到物理地址上,即自己映射自己,这样 loader 才能继续工作。loader 的基址是 0x600,对应的页目录表索引位 0,所以我们需要建立 PDE[0],不过由于 PDE[0] 和 PDE[0x300] 指向的是同一个页表,建立也不麻烦。 PDE 的位置固定,内核所使用的页表完全可以直接建立在 PDE 之后,反正这些页表是一直要使用的,不存在碎片的问题,所以就直接建立在 PDE 之后。 建立的过程就是下面的代码这样: ``` SetupPage: ; ---------- this function setup the Page Directory Entry and Page Table Entry ---------- ; clear PTE mov ecx,0x1000 ; 4K PDE mov esi,0 ; use this reg the clear .SetupPage_ClearPDE: mov byte [PAGE_DIR_TABLE_POS + esi],0 inc esi loop .SetupPage_ClearPDE ; setup PDE .SetupPage_CreatePDE: mov eax,PAGE_DIR_TABLE_POS add eax,0x1000 ; addr of the first PTE mov ebx,eax ; ebx is the base addr of PTEs ; make the PDE[0] and PDE[0xC00] point to the first PTE or eax,PG_US_U | PG_RW_RW | PG_P ; set user page status mov [PAGE_DIR_TABLE_POS + 0x0],eax ; the first PTE's place mov [PAGE_DIR_TABLE_POS + 0xC00],eax ; the first PTE used by kernel, point ot the fisrt PTE ; 0xC0000000 ~ 0xFFFFFFFF belongs to kernel sub eax,0x1000 mov [PAGE_DIR_TABLE_POS + 0xFFC],eax ; make the last Entry point to PDE itself ; creat PTE for kernel mov ecx,256 ; 1M / 4K = 256 mov esi,0 mov edx,PG_US_U | PG_RW_RW | PG_P ; User, RW, P .SetupPage_CreatePTE: mov [ebx + esi * 4],edx add edx,0x1000 inc esi loop .SetupPage_CreatePTE mov eax,PAGE_DIR_TABLE_POS add eax,0x2000 ; second PTE or eax,PG_US_U | PG_RW_RW | PG_P mov ebx,PAGE_DIR_TABLE_POS mov ecx,254 ; 1022 - 769 + 1 mov eax,769 ; start from 769,the second PTE of kernel .SetupPage_CreateKernelPDE: mov [ebx + esi * 4],eax inc esi add eax,0x1000 loop .SetupPage_CreateKernelPDE ret ; ---------- end of function SetupPage ---------- ``` 这里需要说明几点: * 页属性设置位 User 的原因是以后要在内核段中执行 init 进程,该进程是 Ring 3 的,所以需要这样设置 * 最后把内核的需要的所有页目录都建立好了,看似没有意义,因为我们的内核明明只占用 1M 空间,但是实际上这是为了未来将内核共享给所有用户进程所做的处理。具体的,每个用户进程页表的内核段都是直接从 0x10000 的 PDE 拷被过去的,如果不提前建好页目录,当内核为一个用户进程建立了超过 4M 的资源后,其他用户由于页表对应索引处为零,就无法访问到这里的资源。 ### 进入分页模式 进入分页模式,需要完成以下 4 步: 1. 建立页表(之前已经完成) 2. 预更新 GDT,使进入分页模式后能够立刻修改 GDTR,使各段使用正确的虚拟地址 3. 设置 CR3 为 PDT 的地址 4. 设置 CR0 的最高位为 1,正式开启分页模式 进入分页模式后,更新 GDTR,让各段可以使用上虚拟地址。 最后完成的 loader.S: ``` %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR jmp LoaderStart ; 3 bytes db 0 dd 0,0,0 ; addr align ; offset 0x10 ; set up GOT and descriptor GDT_BASE: dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF ; low 32 bits dd DESC_CODE_HIGH4 ; high 32 bits DATA_STACK_DESC: dd 0x0000FFFF ; used by stack and data seg dd DESC_DATA_HIGH4 ; text-mode display ; limit = (0xBFFFF - 0xB8000) / 4K = 0x7 VIDEO_DESC: dd 0x80000007 dd DESC_VIDEO_HIGH4 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 60 dq 0 ; reserve 60 GDTs TOTAL_MEM_BYTES dd 0 ; memory of the machine ; addr: LOADER_BASE_ADDR + 0x10 + 0x200 = 0x800 SELECTOR_CODE equ ((CODE_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 SELECTOR_DATA equ ((DATA_STACK_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 SELECTOR_VIDEO equ ((VIDEO_DESC - GDT_BASE) / 8) << 3 + TI_GDT + RPL0 ; pointer point to GDT gdt_ptr: dw GDT_LIMIT ; low 16 bits of GDT reg dd GDT_BASE ; high 32 bits of GDT reg ; end of GDT setup LoaderStart: ; ---------- first, get the total memory of the machine ---------- ; ---------- we must do it before enter the PE mode as we need the BIOS int ---------- ; use bios int 0x15 sub 0xE801 .LoaderStart_E801FailedRetry: mov ax,0xE801 int 0x15 jc .LoaderStart_E801FailedRetry ; calculate low 15MB memory mov cx,0x400 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx,0x100000 ; add 1MB, this is caused by the memory hole mov esi,edx xor eax,eax mov ax,bx mov ecx,0x10000 ; 64 * 1024 mul ecx add esi,eax ; esi store the mov [TOTAL_MEM_BYTES],esi ; now TOTAL_MEM_BYTES stores the total memory ; ---------- ready to enter Proctection mode ---------- ; 1 open A20 address line ; 2 load GDT reg ; 3 set pe of cr0 to 1 ; open A20 in al,0x92 or al,0000_0010B ; save existed status out 0x92,al ; load GDT reg lgdt [gdt_ptr] ; set cr0, let's roll! mov eax,cr0 or eax,0x00000001 ; save existed status mov cr0,eax ; enter Protection mode jmp dword SELECTOR_CODE:ProctectionModeStart ; reflesh assembly line ; ---------- end of function LoaderStart ---------- ; ---------- now we are in 32-bits PE mode ---------- [bits 32] ProctectionModeStart: ; set selectors mov ax,SELECTOR_DATA mov ds,ax mov es,ax mov ss,ax mov esp,LOADER_STACK_TOP mov ax,SELECTOR_VIDEO mov gs,ax mov byte [gs:160],'P' ; first thing we do is start the page mode ; 1 setup PDE and related PTE call SetupPage ; 2 modify the GDT to make it work in paging mode sgdt [gdt_ptr] mov ebx,[gdt_ptr + 2] or dword [ebx + 0x18 + 4],0xC0000000 ; modify the VIDEO_DESC add dword [gdt_ptr + 2],0xC0000000 ; pre modify the GDTR value add esp,0xC0000000 ; also modify the stack mov eax,PAGE_DIR_TABLE_POS mov cr3,eax mov eax,cr0 or eax,0x80000000 ; save existed status mov cr0,eax ; enable paging mode lgdt [gdt_ptr] ; change GDTR mov byte [gs:162],'V' jmp $ SetupPage: ; ---------- this function setup the Page Directory Entry and Page Table Entry ---------- ; clear PTE mov ecx,0x1000 ; 4K PDE mov esi,0 ; use this reg the clear .SetupPage_ClearPDE: mov byte [PAGE_DIR_TABLE_POS + esi],0 inc esi loop .SetupPage_ClearPDE ; setup PDE .SetupPage_CreatePDE: mov eax,PAGE_DIR_TABLE_POS add eax,0x1000 ; addr of the first PTE mov ebx,eax ; ebx is the base addr of PTEs ; make the PDE[0] and PDE[0xC00] point to the first PTE or eax,PG_US_U | PG_RW_RW | PG_P ; set user page status mov [PAGE_DIR_TABLE_POS + 0x0],eax ; the first PTE's place, mapping loader's addr to itself mov [PAGE_DIR_TABLE_POS + 0xC00],eax ; the first PTE used by kernel, mapping to low 1M ; 0xC0000000 ~ 0xFFFFFFFF belongs to kernel sub eax,0x1000 mov [PAGE_DIR_TABLE_POS + 0xFFC],eax ; make the last Entry point to PDE itself ; creat PTE for kernel mov ecx,256 ; 1M / 4K = 256 mov esi,0 mov edx,PG_US_U | PG_RW_RW | PG_P ; User, RW, P .SetupPage_CreatePTE: mov [ebx + esi * 4],edx add edx,0x1000 inc esi loop .SetupPage_CreatePTE mov eax,PAGE_DIR_TABLE_POS add eax,0x2000 ; second PTE or eax,PG_US_U | PG_RW_RW | PG_P mov ebx,PAGE_DIR_TABLE_POS mov ecx,254 ; 1022 - 769 + 1 mov esi,769 ; start from 769,the second PTE of kernel .SetupPage_CreateKernelPDE: mov [ebx + esi * 4],eax inc esi add eax,0x1000 loop .SetupPage_CreateKernelPDE ret ; ---------- end of function SetupPage ---------- ``` 更新后的 boot.inc: ``` ;---------- loader and kernel ---------- LOADER_BASE_ADDR equ 0x600 ; 0x500 ~ 0x7BFF LOADER_START_SECTOR equ 0x2 ;---------- gdt related ---------- ; G, D, L, AVL sign DESC_G_4K equ 1_00000000000000000000000b ; set grid 4K DESC_D_32 equ 1_0000000000000000000000b ; set 32 bit text mode DESC_L equ 0_000000000000000000000b ; turn off 64 bit text mode DESC_AVL equ 0_00000000000000000000b ; unused by CPU ; segment limit high 4 bits DESC_LIMIT_CODEH equ 1111_0000000000000000b ; LIMIT 0xF(FFFF) DESC_LIMIT_DATAH equ DESC_LIMIT_CODEH ; LIMIT 0xF(FFFF) DESC_LIMIT_VIDEOH equ 0000_0000000000000000b ; Present sign DESC_P_IN equ 1_000000000000000b ; this segment is in RAM ; Descriptor Privilege Level (DPL sign) DESC_DPL_RING_0 equ 00_0000000000000b ; set RING 0 DESC_DPL_RING_1 equ 01_0000000000000b ; set RING 1 DESC_DPL_RING_2 equ 10_0000000000000b ; set RING 2 DESC_DPL_RING_3 equ 11_0000000000000b ; set RING 3 ; CPU segment status (S sign) DESC_S_CODE equ 1_000000000000b ; code segment DESC_S_DATA equ DESC_S_CODE ; data segment DESC_S_SYS equ 0_000000000000b ; sys segment (to cpu) ; OS segment status (type sign) DESC_TYPE_CODE equ 1000_00000000b ; code segment (r-x) DESC_TYPE_DATA equ 0010_00000000b ; data segment (rw-) ; normalized Descriptor DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_L + \ DESC_D_32 + DESC_AVL + DESC_P_IN + DESC_LIMIT_CODEH + \ DESC_DPL_RING_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_L + \ DESC_D_32 + DESC_AVL + DESC_P_IN + DESC_LIMIT_DATAH + \ DESC_DPL_RING_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_L + \ DESC_D_32 + DESC_AVL + DESC_P_IN + DESC_LIMIT_VIDEOH + \ DESC_DPL_RING_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B ;---------- selector status ---------- ; Request Privilege Level RPL0 equ 00b ; Ring 0 RPL1 equ 01b ; Ring 1 RPL2 equ 10b ; Ring 2 RPL3 equ 11b ; Ring 3 ; Table Indicator TI_GDT equ 000b ; set GDT selector TI_LDT equ 100b ; set LDT selector ; ---------- page related ---------- PAGE_DIR_TABLE_POS equ 0x10000 ; PDT start at 1M PG_P equ 1 ; Present sign PG_RW_RW equ 10 ; page type:rw- PG_RW_R equ 00 ; page type:r-- PG_US_U equ 100 ; User level PG_US_S equ 000 ; Supervisor level ``` 最后的效果: <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/05/364815485.png "></div> 由于已经将视频段 GDT 更新为虚拟地址,这里成功输出代表页表建立正确,成功进入分页模式。 ### 未来对页表的修改 在 bochs 中使用 `info tab` 指令可以显示出当前的虚拟地址映射情况。我们先注释掉循环 `.SetupPage_CreateKernelPDE`,这样在显示映射时不会有太多无关信息,输入 info tab 就可以查看当前的映射情况,如下 ``` <bochs:2> info tab cr3: 0x000000010000 0x00000000-0x000fffff -> 0x000000000000-0x0000000fffff 0xc0000000-0xc00fffff -> 0x000000000000-0x0000000fffff 0xffc00000-0xffc00fff -> 0x000000011000-0x000000011fff 0xfff00000-0xfff00fff -> 0x000000011000-0x000000011fff 0xfffff000-0xffffffff -> 0x000000010000-0x000000010fff ``` 可见有虚拟地址被直接映射到了页目录表上,通过对这些虚拟地址进行读写操作,我们可以容易的建立、修改新的页表。其中的具体原理这里不写了,其实没什么好说的,根据建立目录方式就可以看出来。 最后修改:2021 年 05 月 25 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧