(第三章 11)根据自己内存情况 使用分页机制

一、查看内存分布情况

      设置“页目录表”和“页表”之前,最好先查看下内存分布情况,根据“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'           |

;  |---------------|

;  |      ...           |

;  |---------------|

猜你喜欢

转载自chuanwang66.iteye.com/blog/1076168