一、查看内存分布情况
设置“页目录表”和“页表”之前,最好先查看下内存分布情况,根据“OS可用内存的大小”来设置她们;否则可能设置了太大的“页目录表”和“页表”而浪费了宝贵的内存。
如果傻乎乎地设置“页目录表”和“页表”,我们来看看后果:
假设内存的一个物理块是4KB(=4096B)。我们知道,“页目录表”占用一个内存物理块,而“页目录表”中的每项PDE占32bit(4B),因此“页目录表”中最多有4096B/4B=1024个PDE;1024个PDE对应1024个页表,而每个“页表”又占用一个物理内存块。综上,
“页目录表”占用内存 =4KB
“页表”占用内存 =1024*4KB =4MB
∴仅仅是“页目录表”和“页表”这种索引性质的东西(还不是真正要加载到内存的代码和数据)就占用了4MB+4KB
%include "pm.inc" ; 常量, 宏, 以及一些说明
PageDirBase equ 200000h ; 页目录开始地址: 2M
PageTblBase equ 201000h ; 页表开始地址: 2M + 4K
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4096-1, DA_DRW ; Page Directory
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 4096 * 8 - 1, DA_DRW ; Page Tables
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen - 1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址,等一会儿在.s16这个实模式段中将GDT的基地址放到[GdtPtr+2]开头的4个字节中的,现在先不着急,先用0初始化即可。
; 等一会儿会使用命令 lgdt [GdtPtr] 加载GdtPtr这6个字节到寄存器GDTR中
; GDT 选择子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .data1] ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
; 实模式下使用这些符号 (i.e. _szPMMessage)
; 字符串
_szPMMessage: db "In Protect Mode now. ^-^", 0Ah, 0Ah, 0 ; 进入保护模式后显示此字符串
_szMemChkTitle: db "BaseAddrL BaseAddrH LengthLow LengthHigh Type", 0Ah, 0 ; 进入保护模式后显示此字符串
_szRAMSize db "RAM size:", 0
_szReturn db 0Ah, 0
; 变量
_wSPValueInRealMode dw 0
_dwMCRNumber: dd 0 ; Memory Check Result --> 放置ARDS结构数组的结构元素个数
_dwDispPos: dd (80 * 6 + 0) * 2 ; 屏幕第 6 行, 第 0 列。
_dwMemSize: dd 0
_ARDStruct: ; Address Range Descriptor Structure
_dwBaseAddrLow: dd 0
_dwBaseAddrHigh: dd 0
_dwLengthLow: dd 0
_dwLengthHigh: dd 0
_dwType: dd 0
_MemChkBuf: times 256 db 0
; 保护模式下使用这些符号(i.e. szPMMessage)
szPMMessage equ _szPMMessage - $$
szMemChkTitle equ _szMemChkTitle - $$
szRAMSize equ _szRAMSize - $$
szReturn equ _szReturn - $$
dwDispPos equ _dwDispPos - $$
dwMemSize equ _dwMemSize - $$
dwMCRNumber equ _dwMCRNumber - $$
ARDStruct equ _ARDStruct - $$
dwBaseAddrLow equ _dwBaseAddrLow - $$
dwBaseAddrHigh equ _dwBaseAddrHigh - $$
dwLengthLow equ _dwLengthLow - $$
dwLengthHigh equ _dwLengthHigh - $$
dwType equ _dwType - $$
MemChkBuf equ _MemChkBuf - $$
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
; END of [SECTION .gs]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h ;注:(软件)堆栈由程序人员在存储器中划出的一块存储区,向地址减小的方向堆积。
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [_wSPValueInRealMode], sp
; 得到内存数-------------------------------->代码1
mov ebx, 0
mov di, _MemChkBuf
.loop:
mov eax, 0E820h
mov ecx, 20
mov edx, 0534D4150h
int 15h ;调用这个中断,下一块内存的信息会被放入[es:di]
; BIOS中断 和 DOS中断:
; bios中断(int 1~20h)是主板预装好的BIOS提供的功能;dos中断(int 21H)则需要使用操作系统。调用方式都是int #. 例如,例如,
; INT 17H是打印机I/O调用的BIOS中断,(1)当AH=0时,把AL中的字符在打印机上打印出来;(2)当AH=1时,把AL中的初始化控制命令传送给打印机;(3)当AH=2时,把打印机的状态读至AL寄存器。这里17H是中断号,AH是功能号,AL是调用参数,(1)可以简记为“INT 17H/0”.
; 注:BIOS,实际上就是微机的基本输入输出系统(Basic Input-Output System),其内容集成在微机主板上的一个ROM芯片上,主要保存着有关微机系统最重要的基本输入输出程序,系统信息设置、开机上电自检程序和系统启动自举程序等。
; BIOS功能主要包括以下方面:一是BIOS中断服务程序,即微机系统中软件与硬件之间的一个可编程接口,主要用于程序软件功能与微机硬件之间实现衔接。操作系统对软盘、硬盘、光驱、键盘、显示器等外围设备的管理,都是直接建立在BIOS系统中断服务程序的基础上,操作人员也可以通过访问 INT 5、INT 13等中断点而直接调用BIOS中断服务程序。二是BIOS系统设置程序,前面谈到微机部件配置记录是放在一块可读写的CMOS RAM芯片中的,主要保存着系统基本情况、CPU特性、软硬盘驱动器、显示器、键盘等部件的信息。在BIOS ROM芯片中装有“系统设置程序”,主要用来设置CMOS RAM中的各项参数。这个程序在开机时按下某个特定键即可进入设置状态,并提供了良好的界面供操作人员使用。事实上,这个设置CMOS参数的过程,习惯上也称为“BIOS设置”。第三是POST上电自检程序,微机按通电源后,系统首先由POST(Power On Self Test,上电自检)程序来对内部各个设备进行检查。通常完整的POST自检将包括对CPU、640K基本内存、1M以上的扩展内存、ROM、主板、 CMOS存贮器、串并口、显示卡、软硬盘子系统及键盘进行测试,一旦在自检中发现问题,系统将给出提示信息或鸣笛警告。第四为BIOS系统启动自举程序,系统在完成POST自检后,ROM BIOS就首先按照系统CMOS设置中保存的启动顺序搜寻软硬盘驱动器及CD—ROM、网络服务器等有效地启动驱动器,读入操作系统引导记录,然后将系统控制权交给引导记录,并由引导记录来完成系统的顺利启动。
; 这么看来,int 15h是一个BIOS中断调用咯! 关于int 15h, 详见这里http://blog.csdn.net/gxfan/article/details/2962549
jc LABEL_MEM_CHK_FAIL ;若CF==0,则出错
add di, 20
inc dword [_dwMCRNumber]
cmp ebx, 0
jne .loop ;若CF==0, ebx==0,则还有下一块内存的信息
jmp LABEL_MEM_CHK_OK
LABEL_MEM_CHK_FAIL:
mov dword [_dwMCRNumber], 0
LABEL_MEM_CHK_OK:
; 初始化 16 位代码段描述符
;;;;;;;;;;;;将16位代码段段基址放到eax中;;;;;;;;;;;;
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
;;;;;;;;;;;将段基址放到相应位置;;;;;;;;;;;
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20 (将92端口一个字节的第1位置为1)
in al, 92h --> 从92端口读一个字节到al
or al, 00000010b
out 92h, al --> 将al持有的数据写到92端口
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [_wSPValueInRealMode]
in al, 92h ; ┓
and al, 11111101b ; ┣ 关闭 A20 地址线
out 92h, al ; ┛
sti ; 开中断
mov ax, 4c00h ; ┓
int 21h ; ┛回到 DOS
; 上面是一个DOS中断调用(不是BIOS中断调用), int 21h中的21h是中断号,AH=4ch是“功能号”,AL=00h是“中断调用参数”。这里的功能是“带返回码结束,AL=返回码”。
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorData
mov es, ax
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
; 下面显示一个字符串,用法详见后面“保护模式下显示字符串”
push szPMMessage call DispStr add esp, 4
push szMemChkTitle
call DispStr
add esp, 4
call DispMemSize ; 显示内存信息
call SetupPaging ; 启动分页机制
; 到此停止
jmp SelectorCode16:0
; 启动分页机制 --------------------------------------------------------------
SetupPaging:
;1、计算出最大地址为[dwMemSize]对应的内存共有多少个页表(也即PDE个数,不是页面个数),将个数(ecx,32bit)压栈
xor edx, edx
mov eax, [dwMemSize] ;根据自己机器的实际内存大小来设置页目录表和页表
mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小(一个页面是4k,一个页表有1024个页面)
div ebx ; edx:eax / ebx ==> eax ...... edx
mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
test edx, edx
jz .no_remainder
inc ecx ; 如果余数不为 0 就需增加一个页表
.no_remainder:
push ecx ; 暂存页表个数
; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
;2、首先初始化“页目录表”(即初始化每个PDE)
mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
mov es, ax
xor edi, edi
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
.1:
stosd ; eax中的4个字节 -> es:edi ,每循环一次是一个PDE
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .1 ; 循环次数为ecx=PDE个数
;3、再初始化所有“页表”,即初始化每个PTE
mov ax, SelectorPageTbl ; 此段首地址为 PageTblBase
mov es, ax
pop eax ; 页表个数,也是PDE个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx ; eax*ebx==>积edx:eax
mov ecx, eax ; PTE个数 = 页表个数 * 1024
xor edi, edi
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd ; eax==>es:edi
add eax, 4096 ; 每一页指向 4K 的空间
loop .2 ; 循环次数为ecx=PTE个数
;4、开启页表机制
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
; 分页机制启动完毕 ----------------------------------------------------------
DispMemSize:
push esi
push edi
push ecx
mov esi, MemChkBuf
mov ecx, [dwMCRNumber];for(int i=0;i<[MCRNumber];i++)//每次得到一个ARDS
;注意到, ds:esi ==> MemChkBuf ,是一个线性结构; es:edi ==> ARDStruct,是二维数组
.loop: ;{
mov edx, 5 ; for(int j=0;j<5;j++) //每次得到一个ARDS中的成员
mov edi, ARDStruct ; {//依次显示BaseAddrLow,BaseAddrHigh,LengthLow,
.1: ; LengthHigh,Type
push dword [esi] ;
call DispInt ; DispInt(MemChkBuf[j*4]); //显示一个成员,一个成员有四个字节dword
pop eax ; db-> BYTE, dw->WORD, dd->DWORD
stosd ; ARDStruct[j*4] = MemChkBuf[j*4]; stosd指令:将eax的四个字节放到es:edi(ARDStruct)中
add esi, 4 ;
dec edx ;
cmp edx, 0 ;
jnz .1 ; }
call DispReturn ; printf("\n");
cmp dword [dwType], 1 ; if(Type == AddressRangeMemory) ; 通过上面stosd,已经填充好了dwType
jne .2 ; {
mov eax, [dwBaseAddrLow] ;
add eax, [dwLengthLow] ;
cmp eax, [dwMemSize] ; if(BaseAddrLow + LengthLow >= MemSize)
jb .2 ; jb==jump below
mov [dwMemSize], eax ;MemSize = BaseAddrLow + LengthLow; 等号右边是当前这个内存块最后一个字节的地址
.2: ; }
loop .loop ;}
;
call DispReturn ;printf("\n");
push szRAMSize ;
call DispStr ;printf("RAM size:");
add esp, 4 ;
;
push dword [dwMemSize] ;
call DispInt ;DispInt(MemSize);
add esp, 4 ;
pop ecx
pop edi
pop esi
ret
%include "lib.inc" ; 库函数
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]
**********************************************************************************************************************************
代码1执行结果如下图:
**********************************************************************************************************************************
程序效果示意图:
可以看到,我的电脑可以使用的内存确实能达到4G(FFFC0000 h + 00040000 h=1 0000 0000 h ==> 4GB),但可供OS使用的内存大小范围仅到32GB(type=1, 00100000 h+01EF0000 h = 1FF0000 h ==> 32MB).
二、初始化页目录表、页表、开启分页机制
知道哪些内存可以使用后,我们就可以节省地 构造自己的“页目录表”和“页表”,并开启分页机制了。代码在上面已经给出,即SetupPaging 这个函数。
三、一个细节——保护模式下显示字符串
这里讲“保护模式下实现字符串”主要是分析一下上面程序中调用到的一个函数DispStr,并无涉及对本节课程的理解。
DispStr调用方法:
[SECTION .data1] [BITS 32] ... _szPMMessage: db "In Protect Mode now. ^-^",0Ah,0Ah,0 ;保护模式显示 szPMMessage equ _szPMMessage - $$ ... push szPMMessage ;将要显示的字符串指针入栈 call DispStr add esp, 4 ;将显示了的字符串指针出栈
DispStr实现:
DispStr:
push ebp
mov ebp, esp ;-->此后,esp中放的是TopOfStack指针,指向栈顶
push ebx
push esi
push edi
mov esi, [ebp+8] ;取出在函数DispStr调用前被压栈的那个”字” szPMMessage
mov edi, [dwDispPos]
;初始时_dwDispPos: dd (80 * 6 + 0) * 2; 屏幕第6行, 第0列,是屏幕上下一个显示的位置
;dwDispPos equ _dwDispPos - $$
mov ah, 0Fh
.1:
lodsb ;lodsb-->将esi中的一个字节放到AL中
test al, al
jz .2 ;是结束字符0
cmp al, 0Ah;
jnz .3 ;是“非回车的字符”
;;;;;;;;;;;;;;;;是“回车”的时候;;;;;;;;;;;;;;;;
;1) 求得下一行的行数
push eax
mov eax, edi ;ax(=edi),为被除数(16bit)
mov bl, 160 ;bl,为除数(8bit)
div bl ;ah为余数,al为商
and eax, 0FFh ;将余数去掉,eax为商(即当前字符的“行数”)
inc eax ;eax为下一行的行数
;2) 将edi指向显存中下一行的第一个字符处
mov bl, 160 ;一行80个字符,一个字符对应显存中的2byte
mul bl ;bl寄存器中的值乘上al寄存器中的值,积在ax中
mov edi, eax
pop eax
jmp .1
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.3: ;是“非回车的字符”
mov [gs:edi], ax
add edi, 2
jmp .1
.2: ;是结束字符0
mov [dwDispPos], edi
pop edi
pop esi
pop ebx
pop ebp
ret
注意:
1) mov esi, [ebp+8] 取出字符串指针szPMMessage
;
; |-----------------------|
; | edi | <== ebp
; |-----------------------|
; | esi | <== ebp+2
; |-----------------------|
; | ebx | <== ebp+4
; |-----------------------|
; | ebp | <== ebp+6
; |-----------------------|
; | szPMMessage | <== ebp+8
; |-----------------------|
; | *** |
; |-----------------------|
其中,edi,esi,ebx,ebp是在函数DispStr中压入的;szPMMessage是在调用这个函数之前压栈的,这个字符串就是要被显示的字符串
; | ... |
; |---------------|
; | 'I' | <==szPMMessage/esi均指向这个位置
; |---------------|
; | 'n' |
; |---------------|
; | ' ' |
; |---------------|
; | 'P' |
; |---------------|
; | 'r' |
; |---------------|
; | ... |
; |---------------|