一些概念
- 一个进程一个页表
- 一个物理页大小为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