《操作系统真象还原》第五章——开启分页机制

本文详细阐述了32位系统中分页机制的工作原理,包括页表结构、物理内存映射、控制寄存器CR3的作用,以及如何通过二级页表优化内存管理,降低内存消耗。同时介绍了GDT和描述符在内存布局中的关键作用。
摘要由CSDN通过智能技术生成

一些概念

  • 一个进程一个页表
  • 一个物理页大小为4kB
  • 一级页表1k个表项,每个表项存储页面的物理地址
  • 二级页表(页目录表)也对应着1k个表项,每个表现存储一级页表的物理地址
  • 一个表项4B

有了上述概念之后,我们知道,在32位地址空间下

如果不采用二级页表机制,由于一个物理页的大小为4KB,因此页表的大小为2^(32-12)*4B=4MB,这意味着每加载一个进程就需要首先将一个4MB的页表加载进来,这显然违背了我们分页的初衷

而如果采用二级页表机制,一个页表和一个页目录表的大小都为1k*4B=4KB,而我们每加载一个进程只需要加载一个页目录表和一个一级页表,因此只需要4KB+4KB=8KB即可

分页机制启动步骤

  • 准备页目录表和页表
  • 将页表地址写入控制寄存器cr3
  • 寄存器cr0的PG位置为1

页目录项与页表项

  • P,表示存在位
    • 1表示该页位于物理内存中
  • RW:读写位
    • 1表示可读可写
    • 0表示可读不可写
  • US:表示普通用户/系统用户
    • 1,处于user级别,任意级别(1,2,3,4)特权的程序都可以访问该页
    • 0,处于supervisor级别,只允许特权级位(1,2,3)的程序可以访问
  • PWT:页级通写位
    • 1,表示此项采用通写方式表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式本位用来间接决定是否用此方式改善该页的访问效率。
  • PCD:页级高速缓存禁止位
    • 1表示该页启用高速缓存
    • 0表示该页禁用高速缓存
  • A:访问位
    • 1表示该页已经被CPU访问过了
  • D:脏页位
    • 当CPU对一个页面进行写操作时,就会将对应页表项D位置为1
    • 此项仅对页表项有效,并不会修改页目录项中的D位
  • PAT:页属性表位
  • G,全局位
    • 1表示该页为全局页,也就是该页将在TLB高速缓存中一直存在
    • 0表示该页不是全局页
  • AVL:可用位,操作系统是否可用

控制寄存器cr3

用途:存储页表物理位置,又称页目录基址寄存器PDBR

内存规划

虚拟空间共4G

  • 3GB~4GB:操作系统
  • 0~3GB:用户进程自己的虚拟地址空间 

  • 页目录表的位置:0x100000
  • 第一个页表位置:0x101000

创建页表和页目录表

;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表

;逐字节清空页目录表
setup_page:
    mov ecx,4096                                    ;页目录表的大小为4KB,ecx是loop指令的循环计数器
                                                    ;由于初始化页表是逐字节置0的,因此ecx的值为4096
    mov esi,0                                       ;页目录表的偏移量
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS+esi],0             ;逐字节清空页目录表
                                                    ;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
    inc esi                                         ;递增偏移量,清空下一个字节
    loop .clear_page_dir

;初始化创建页目录表
.create_pde:
    mov eax,PAGE_DIR_TABLE_POS                      ;eax保存页目录表的起始地址
    add eax,0x1000                                  ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
    mov ebx,eax                                     ;ebx保存第一张页表的起始地址,后续会用到
    or eax,PG_US_U|PG_RW_W|PG_P                     ;eax已经有了第一张页表的起始地址
                                                    ;此处再加上属性,即可表示为页目录表的一个表项,
                                                    ;该表项代表的是第一张页表的物理地址及其相关属性

    mov [PAGE_DIR_TABLE_POS+0x0],eax                ;页目录表的第一个表项指向第一张页表
    mov [PAGE_DIR_TABLE_POS+0xc00],eax              ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
                                                    ;因此该语句是将3GB的虚拟空间映射到内核空间 
                                                    ;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
    
    sub eax,0x1000          
    mov [PAGE_DIR_TABLE_POS+4092],eax               ;最后一个页表项指向自己,为将来动态操作页表做准备


;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
    mov ecx,256                                     ;循环计数器
    mov esi,0                                       ;偏移量
    mov edx,PG_US_S|PG_RW_W|PG_P                    ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
                                                    ;且物理地址为0的物理页的页表项
.create_pte:
    mov [ebx+esi*4],edx                             ;此前ebx已经保存了第一张页表的起始地址
    add edx,4096                                    ;edx指向下一个物理页(一个物理页4KB)
    inc esi                                         ;esi指向页表的下一个偏移
    loop .create_pte

; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x2000                                  ;此时的eax表示第二张页表的起始地址
    or eax,PG_US_U|PG_RW_W|PG_P                     ;为eax表项添加属性

    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254                                     ;要设置254个页表项
    mov esi,769                                     ;从第769个页表项开始设置
.create_kernel_pde:
    mov [ebx+esi*4],eax                             ; 设置页目录表项
    inc esi                                         ; 增加要设置的页目录表项的偏移
    add eax,0x1000                                  ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
    loop .create_kernel_pde                         ; 循环设定254个页目录表项
    ret

开启分页机制

[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA                    ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
    mov ds,ax
    mov es,ax                      
    mov ss,ax

    mov esp,LOADER_STACK_TOP                ;初始化栈指针,将栈指针地址放入bsp寄存器
    mov ax,SELECTOR_VIDEO                   ;初始化显存段寄存器,显存段的选择子放入gs寄存器
    mov gs,ax

    call setup_page                         ;创建页目录表和页表,并初始化页内存位图
    mov ebx,[gdt_ptr+2]                     ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
    or dword [ebx+0x18+4],0xc0000000        ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
                                            ;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
                                            ;表示将显存段的起始地址增加了3G

    add dword [gdt_ptr+2],0xc0000000        ;同理将GDT_BASE的起始地址也增加3G
    add esp,0xc0000000                      ;同理将esp栈指针的起始地址也增加3G

    mov eax,PAGE_DIR_TABLE_POS
    mov cr3,eax

    mov eax,cr0                             ;打开cr0的PG位
    or eax,0x80000000
    mov cr0,eax

    lgdt [gdt_ptr]                          ;开启分页后,用gdt的新地址重新加载

    mov byte [gs:160],'V'

    jmp $

编译loader

nasm -o ./osCode/build/loader.bin -I ./osCode/include/ ./osCode/loader.S

写入磁盘

dd if=./osCode/build/loader.bin of=./bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc

运行

./bin/bochs -f boot.disk

完整loader

%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
;初始化栈指针地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;------------- 构建gdt及其内部的描述符 -------------
    GDT_BASE: 
        dd 0x00000000
        dd 0x00000000

    ;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
    ;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
    CODE_DESC:  
        dd 0x0000FFFF
        dd DESC_CODE_HIGH4;段描述符的高4字节部分
    DATA_STACK_DESC: 
        dd 0x0000FFFF
        dd DESC_DATA_HIGH4

    ;定义显存段的描述符
    ;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
    ;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
    ;段粒度位4k,因此段界限的值为0x7fff/4k=7
    VIDEO_DESC: 
        dd 0x80000007
        dd DESC_VIDEO_HIGH4

    GDT_SIZE equ $-GDT_BASE
    GDT_LIMIT equ GDT_SIZE-1
    times 60 dq 0 ;此处预留60个描述符的空位
    
;------------- 构建选择子 -------------
    SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0

    total_mem_bytes dd 0    ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
                            ;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
                            ;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
                            ;故total_mem_bytes在内存中的地址为0x900+0x200=0xc00
                            ;该地址将来在内核中会被用到


;------------- 定义gdtr(指向GDT的寄存器) -------------
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE

    ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
                            ;该定义语句事实上是定义了一个数组ards_buf[244]
                            ;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
                            ;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300

    ards_nr dw 0            ;用于记录ards结构体数量

;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
    ;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
    ;EBX:ARDS后续值
    ;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
    ;ECX:ARDS结构的字节大小,20
    ;EDX:固定为签名标记,0x534d4150

;返回值
    ;CF:若cf为0,表示未出错,cf为1,表示调用出错
    ;EAX:字符串SMAP的ASCII码值,0x534d4150
    ;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
    ;ECX:ARDS结构的字节大小,20
    ;EBX:ARDS后续值,即下一个ARDS的位置。
    ;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
    ;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构

loader_start:
    xor ebx,ebx             ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
    mov edx,0x534d4150
    mov di,ards_buf         ;di存储缓冲区地址,即指向缓冲区首地址

.e820_mem_get_loop:
    mov eax,0x0000e820
    mov ecx,20              ;一个ards结构体的大小
    int 0x15                ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
    add di,cx               ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
    inc word [ards_nr]      ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
    cmp ebx,0               ;比较ebx中的值是否为0
    jnz .e820_mem_get_loop  ;若ebx不为0,则继续进行循环获取ARDS,
                            ;若为0说明已经获取到最后一个ards,则退出循环

    mov cx,[ards_nr]        ;cx存储遍历到的ards结构体个数
    mov ebx,ards_buf        ;ebx指向缓冲区地址
    xor edx,edx             ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0

.find_max_mem_area:
    mov eax,[ebx]           ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
    add eax,[ebx+8]         ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
    add ebx,20              ;遍历下一个ards
    cmp edx,eax             ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
    jge .next_ards
    mov edx,eax             ;否则就是更新edx
.next_ards:
    loop .find_max_mem_area

    mov [total_mem_bytes],edx   ;将最终结果保存到total_mem_bytes


;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1

    ;------------- 打开A20 -------------
    in al,0x92
    or al,0000_0010B
    out 0x92,al
    
    ;------------- 加载gdt -------------
    lgdt [gdt_ptr]

    ;------------- 置cr0的PE位为1 -------------
    mov eax,cr0
    or eax,0x00000001
    mov cr0,eax

    jmp dword SELECTOR_CODE:p_mode_start    ;刷新流水线

.error_hlt:
    hlt                                     ;出错则挂起


[bits 32]
p_mode_start:
    mov ax,SELECTOR_DATA                    ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
    mov ds,ax
    mov es,ax                      
    mov ss,ax

    mov esp,LOADER_STACK_TOP                ;初始化栈指针,将栈指针地址放入bsp寄存器
    mov ax,SELECTOR_VIDEO                   ;初始化显存段寄存器,显存段的选择子放入gs寄存器
    mov gs,ax

    call setup_page                         ;创建页目录表和页表,并初始化页内存位图
    mov ebx,[gdt_ptr+2]                     ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
    or dword [ebx+0x18+4],0xc0000000        ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
                                            ;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
                                            ;表示将显存段的起始地址增加了3G

    add dword [gdt_ptr+2],0xc0000000        ;同理将GDT_BASE的起始地址也增加3G
    add esp,0xc0000000                      ;同理将esp栈指针的起始地址也增加3G

    mov eax,PAGE_DIR_TABLE_POS
    mov cr3,eax

    mov eax,cr0                             ;打开cr0的PG位
    or eax,0x80000000
    mov cr0,eax

    lgdt [gdt_ptr]                          ;开启分页后,用gdt的新地址重新加载

    mov byte [gs:160],'V'

    jmp $


;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表

;逐字节清空页目录表
setup_page:
    mov ecx,4096                                    ;页目录表的大小为4KB,ecx是loop指令的循环计数器
                                                    ;由于初始化页表是逐字节置0的,因此ecx的值为4096
    mov esi,0                                       ;页目录表的偏移量
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS+esi],0             ;逐字节清空页目录表
                                                    ;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
    inc esi                                         ;递增偏移量,清空下一个字节
    loop .clear_page_dir

;初始化创建页目录表
.create_pde:
    mov eax,PAGE_DIR_TABLE_POS                      ;eax保存页目录表的起始地址
    add eax,0x1000                                  ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
    mov ebx,eax                                     ;ebx保存第一张页表的起始地址,后续会用到
    or eax,PG_US_U|PG_RW_W|PG_P                     ;eax已经有了第一张页表的起始地址
                                                    ;此处再加上属性,即可表示为页目录表的一个表项,
                                                    ;该表项代表的是第一张页表的物理地址及其相关属性

    mov [PAGE_DIR_TABLE_POS+0x0],eax                ;页目录表的第一个表项指向第一张页表
    mov [PAGE_DIR_TABLE_POS+0xc00],eax              ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
                                                    ;因此该语句是将3GB的虚拟空间映射到内核空间 
                                                    ;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
    
    sub eax,0x1000          
    mov [PAGE_DIR_TABLE_POS+4092],eax               ;最后一个页表项指向自己,为将来动态操作页表做准备


;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
    mov ecx,256                                     ;循环计数器
    mov esi,0                                       ;偏移量
    mov edx,PG_US_S|PG_RW_W|PG_P                    ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
                                                    ;且物理地址为0的物理页的页表项
.create_pte:
    mov [ebx+esi*4],edx                             ;此前ebx已经保存了第一张页表的起始地址
    add edx,4096                                    ;edx指向下一个物理页(一个物理页4KB)
    inc esi                                         ;esi指向页表的下一个偏移
    loop .create_pte

; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
    mov eax,PAGE_DIR_TABLE_POS
    add eax,0x2000                                  ;此时的eax表示第二张页表的起始地址
    or eax,PG_US_U|PG_RW_W|PG_P                     ;为eax表项添加属性

    mov ebx,PAGE_DIR_TABLE_POS
    mov ecx,254                                     ;要设置254个页表项
    mov esi,769                                     ;从第769个页表项开始设置
.create_kernel_pde:
    mov [ebx+esi*4],eax                             ; 设置页目录表项
    inc esi                                         ; 增加要设置的页目录表项的偏移
    add eax,0x1000                                  ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
    loop .create_kernel_pde                         ; 循环设定254个页目录表项
    ret

 

  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值