启动分页的步骤: 1.建立页目录表,2.建立页表,3.将页目录表的地址放到CR3中,4.将CR0中的PG置1表示启动分页。
这里建立一个页目录表(有1024项,每一项对应一个页表),建立1024个页表(每个页表有1024项,每一项对应一个物理页)。
下面是程序的代码, 参考了《自己动手写操作系统》
;;nasm 2.04
;;nasm fenye.asm -o fenye.com
org 0100H
%macro Descriptor 3 ;定义Descriptor结构体宏
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro ; 共 8 字节
DA_C EQU 98h ; 只读代码段的属性值
DA_DRW EQU 92h ; 允许读写的数据段的属性值
DA_32 EQU 4000h ;32位段的属性值
DA_LIMIT_4K EQU 8000h ; 段界限粒度为 4K 字节
PG_P EQU 1 ; 页存在属性位
PG_RWR EQU 0 ; R/W 属性位值, 读/执行
PG_RWW EQU 2 ; R/W 属性位值, 读/写/执行
PG_USS EQU 0 ; U/S 属性位值, 系统级
PG_USU EQU 4 ; U/S 属性位值, 用户级
CatalogPageTableBase equ 200000h ; 页目录表的基址:2M
PageTableBase equ 201000h ;页表的基址 :2M+4k
jmp LABEL_BEGIN
;现在进行第一步:建立GDT表
[SECTION .gdt] ;用于放GDT表的段
;GDT
LABEL_GDT: Descriptor 0,0,0 ;空描述符,当一个任务没有LDT时,会把LDTR清空,这时,LDTR便指向空描述符。
LABEL_CODE32: Descriptor 0,SegCode32Len-1,DA_C + DA_32 ; 代码段的描述符
LABEL_VIDEO: Descriptor 0B8000h,0ffffh,DA_DRW ;显存的描述符,0b8000h是显存的首地址
;GDT 结束
LABEL_CATALOG_TABLE: Descriptor CatalogPageTableBase, 4095 ,DA_DRW ;目录表的描述符,大小为4k
LABEL_PAGE_TABLE: Descriptor PageTableBase , 1023 , DA_DRW | DA_LIMIT_4K ;页表的描述符,大小为1024*4K=4M
GdtLenth equ $ - LABEL_GDT ;计算GDT表的长度
GdtPtr dw GdtLenth - 1 ; 准备存入GDTR的信息
dd 0 ; 用于存放GDT表的物理地址,后面会计算
;GDT选择子
SelectorCatalogTable equ LABEL_CATALOG_TABLE - LABEL_GDT ;页目录表的选择子
SelectorPageTable equ LABEL_PAGE_TABLE - LABEL_GDT ;页表的选择子
SelectorCode32 equ LABEL_CODE32 - LABEL_GDT ;代码段的选择子
SelectorVideo equ LABEL_VIDEO - LABEL_GDT ;显存所在段的选择子
;[SECTION .gdt]结束
[SECTION .s16]
[BITS 16] ;目标处理器模式
LABEL_BEGIN:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,0100h
;初始化32位段的描述符
xor eax,eax ; 同或运算,这里是将eax清零
mov ax,cs
shl eax,4 ;左移四位,相当于乘以16,实模式下计算物理地址
add eax,LABEL_SEG_CODE32 ;eax中为32位段的物理地址
mov word [LABEL_CODE32 + 2],ax ;将32位段的物理地址存入段的描述符中
shr eax,16
mov byte [LABEL_CODE32 + 4],al
mov byte [LABEL_CODE32 + 7],ah
;计算GDT表的物理地址,将GDT表的物理地址信息存入GdtPtr中
xor eax,eax
mov ax,cs
shl eax,4
add eax,LABEL_GDT
mov dword [GdtPtr + 2],eax
;现在进行第二步:将GDT表的信息加载到GDTR中
lgdt [GdtPtr] ;装载GDTR
;关中断
cli
;现在进行第三步:打开地址线
in al,92h
or al,00000010b
out 92h,al
;现在进行第四步:置PE为1,表示CPU处于保护模式下
mov eax,cr0
or eax,1
mov cr0,eax
;现在进入第五步:跳到保护模式的代码
jmp dword SelectorCode32:0 ;这里将SelectorCode32存入CS中作为选择子
;[SECTION .s16] 结束
;保护模式下的代码,32位代码
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
call SetupPaging ;启动分页
mov ax,SelectorVideo ;视频选择子,用于找到显存段的描述符
mov gs,ax
mov edi,(80 * 10 + 0)*2 ;屏幕的第10行,第0列
mov ah,0Ch
mov al,'p'
mov [gs:edi],ax ;将字符‘p’输出到屏幕
jmp $
SetupPaging:
;为简单,这里让线性地址等于物理地址
;初始化页目录表
mov ax,SelectorCatalogTable ;将页目录表的选择子放到es中
mov es,ax
mov ecx,1024 ;1024送到cx,表示循环1024次
xor edi,edi ;edi清零
xor eax,eax ;ax清零
mov eax,PageTableBase | PG_P | PG_USU | PG_RWW ;将第一个页表的信息(页目录表中的一个项)放到eax中,准备放到页目录表中
s1:
stosd
add eax,4096 ;eax加4096表示下一个页表的地址(一个页表有4096的大小)
loop s1
;初始化页表的信息
mov ax,SelectorPageTable ;将第一个页表的选择符放到es
mov es,ax
mov ecx,1024*1024 ;循环1024*1024次,因为一个目录表有1024个项,每一个项对应一个页表,而每个页表有1024个项,每一个项
xor edi,edi ;对应一个物理页
xor eax,eax
mov eax,PG_P | PG_USU | PG_RWW
s2:
stosd
add eax,4096 ;每一页有4k的大小
loop s2
mov eax,CatalogPageTableBase ;将页目录表的地址存入cr3中
mov cr3,eax
mov eax,cr0 ;将cr0的最高位置1,表示启动分页
or eax,80000000h
mov cr0,eax
ret
SegCode32Len equ $ - LABEL_SEG_CODE32 ;计算32位代码段的长度