(6)打造简单OS-内存分页

好长时间没有更新了,最近比较忙。。。。。。

   内存分页可以放在C代码中,这样比较方便编写!即loader执行完后进入kernel_main函数之后在分配内存分页!

一、地址

   讲到内存必然要讲到计算机中经常提到的一些地址。物理、线性、虚拟、逻辑、有效地址 (点击可查看具体内容)

   再需要了解内存的分段和分页机制:内存管理分段和分页机制

   保护模式下地址转换原理图:(可以先了解最后在深入)

  

  原理简述过程:

       保护模式下(保护模式下必分段,只不过现在寄存器和地址线位数都很大,可以进行平坦模式,即段地址为0!32位地址线可以直接寻到4G,32位寄存器可以直接装下)开始分页后,都是逻辑地址--》线性地址--》物理地址,本质,CPU最终还是会在物理内存中取值和赋值。

       如上图中执行命令【mov 0X1A478BFF,%ebx】,我们编写程序的逻辑地址[只是段内偏移地址]如(0X1A478BFF,因为要取这个地址在内存中的值,然后在存储到EBX存储器中)  ,要取这个地址在内存中的值,需要找到段基址,那么通过DS寄存器存储的段选择子和GDTR找到GDT表中的对应的段描述符 ,这样就找到了段基址,在加上0X1A478BFF,那么此时 线性地址= 段基址+0X1A478BFF,这样过程就由逻辑地址--》线性地址 了。【当然实际中 CPU内部有缓存寄存器,这样速度就快很多了

       首先逻辑地址--》线性地址,在第五讲中提到GDT,它是全局段描述符表,内部是一个段描述符形成的数组,GDTR存放内存中GDT表的起始地址[实模式下是真实的物理地址,保护模式下则是线性地址,开启了分页才出现了虚拟地址,分页下线性地址和虚拟地址是一个概念],第五讲中在进入保护模式前,加载了GDTR寄存器,此时存储的是物理地址!本节内存分页后还需要将重新刷新GDTR,使其变为虚拟地址【线性地址】!

         需要通过分页机制来映射的线性地址便有了一个高大上的名字,虚拟地址 !

分页机制的作用有两方面
1. 将线性地址转换成物理地址
2. 用大小相等的页代替大小不等的段

总结:X86两种模式下地址到底是怎么回事?(重新在回顾一下)

线性地址给CPU看的 。CPU不需要知道有多少外设,什么种类的外设,反正它都是用地址来访问,线性地址能让CPU把任何设备当成内存。

逻辑地址是给程序的,不需要知道硬件是怎么设计的。

物理地址是给实际的硬件看的。

虚拟地址:内存的思想是程序、数据、堆栈的总大小可以超过实际可用物理内存的大小,操作系统把程序当前使用的那部分保存在物理内存当中,而其他部分保存在硬盘上。虚拟内存的实现基于分页技术

逻辑地址要经过操作系统转换成线性地址给CPU,CPU发出线性地址给MMU再将线性地址进行转换得到物理地址去访问设备。

 

当CPU在实模式下时,物理地址 = 段寄存器:偏移地址

 

当CPU在保护模式下时,必分段,在由实模式转到保护模式前,必须设置段描述符表和GDT,段描述符存在段描述符表中,此时GDTR使用的是肯定是物理地址【实模式下都是段:偏移形成物理地址,GDTR中32位基址可以直接存储实模式下任何地址】;然后在开启CR0寄存器标志位则进入保护模式;因为保护模式要用到段描述符表GDT,用GDTR找到它,最终才能找到段基址。

 

进入保护模式后:【开启分页后需要再重新更新GDT,此时GDTR使用的是线性地址

1、未开启分页时,在保护模式下的 线性地址= 段描述符表中的基址【段选择子】 + 偏移【逻辑地址】= 物理地址

2、开启分页时,在保护模式下的 线性地址= 段描述符中的基址【段选择子】 + 偏移【逻辑地址】,由于大部分段描述符中的基地址为0,所以此时的线性地址=程序的逻辑地址,这时候CPU需要将线性地址经过CPU内部MMU转换为物理地址才能访问内存

     CR3存储页目录地址【物理地址】,如果存储的是线性地址的话,这样的话就会递归,因为线性地址转物理地址必须要用到CR3才能找到页目录首地址。

    现代系统中GDT表中基地址都设置为0,那么这样就是平坦模式!这样的话逻辑地址就等于线性地址了!!

 

二、实模式下内存分布图

 因为我们此时编写系统还处在实模式下,那么我们需要简单了解下实模式下的内存分布图。实模式下的内存布局

 

三、内存分页   

这里我们先将其提前,用汇编方式进行简单内存分页。步骤如下: 

   1.初始化页目录所占的空间为0;

   2.给这个页目录所在的空间中所占的第0目录项、第768目录项、第1023目录项赋初值;

   3.再将物理地址0~1M内存分给页目录0项的第0~255页表项

汇编源码如下:

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS   ; PAGE_DIR_TABLE_POS = 0x100000
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。
 
;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备 
 
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   ; 第1个目录项,在页目录表中的第0个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       
   ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
   ;页目录第768项指向第一个页表
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     
	; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   ; 使最后一个目录项指向页目录表自己的地址
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     
 
;下面创建页表项(PTE) 
  
   ;把1M低端内存存储在第一页目录所指向的页表0~页表255内,每页4K
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				     ; 创建Page Table Entry
   mov [ebx+esi*4],edx			 ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte
 
;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

指定了物理地址1M的页表 ,具体示意图如下:

程序代码如下;

1.mbr.s跟上一节中一样。

2.loader.s

 
;--------------   gdt描述符属性  -----------
DESC_G_4K   equ	  1_00000000000000000000000b   
DESC_D_32   equ	   1_0000000000000000000000b
DESC_L	    equ	    0_000000000000000000000b	;  64位代码标记,此处标记为0便可。
DESC_AVL    equ	     0_00000000000000000000b	;  cpu不用此位,暂置为0  
DESC_LIMIT_CODE2  equ 1111_0000000000000000b
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b
DESC_P	    equ		  1_000000000000000b
DESC_DPL_0  equ		   00_0000000000000b
DESC_DPL_1  equ		   01_0000000000000b
DESC_DPL_2  equ		   10_0000000000000b
DESC_DPL_3  equ		   11_0000000000000b
DESC_S_CODE equ		     1_000000000000b
DESC_S_DATA equ	  DESC_S_CODE
DESC_S_sys  equ		     0_000000000000b
;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_CODE  equ	      1000_00000000b	  
;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
DESC_TYPE_DATA  equ	      0010_00000000b	
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b
	
;--------------   选择子属性  ---------------
RPL0  equ   00b
RPL1  equ   01b
RPL2  equ   10b
RPL3  equ   11b
TI_GDT	 equ   000b
TI_LDT	 equ   100b
;--------------------------------------------

;----------------   页表相关属性    --------------
PG_P  equ   1b
PG_RW_R	 equ  00b 
PG_RW_W	 equ  10b 
PG_US_S	 equ  000b 
PG_US_U	 equ  100b 
PAGE_DIR_TABLE_POS equ 0X100000
;----------------------------------------------
section loader vstart=0x900
 
   jmp loader_start
   
;构建gdt及其内部的描述符===============================================
   GDT_BASE:   dd    0x00000000 
	       	   dd    0x00000000
 
   CODE_DESC:  dd    0x0000FFFF 
	       	   dd    DESC_CODE_HIGH4
 
   DATA_STACK_DESC:  dd    0x0000FFFF
		     		 dd    DESC_DATA_HIGH4
 
   VIDEO_DESC: dd    0x80000007	       ; limit=(0xbffff-0xb8000)/4k=0x7
	           dd    DESC_VIDEO_HIGH4  ; 此时dpl为0
 
   GDT_SIZE    equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -	1 
   times 60 dq 0					 ; 此处预留60个描述符的空位(slot)
   SELECTOR_CODE  equ (0x0001<<3) + TI_GDT + RPL0     ; 8[1000] 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA  equ (0x0002<<3) + TI_GDT + RPL0	 ; 16[10000] 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 24[11000] 同上 
 
 
   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 
	    	dd  GDT_BASE
;======================================================================
			
loader_start:
 
;-----------------   准备进入保护模式   -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
 
   ;-----------------  打开A20  ----------------
   in al,0x92
   or al,0000_0010B
   out 0x92,al
 
   ;-----------------  加载GDT  ----------------
   lgdt [gdt_ptr]
 
   ;-----------------  cr0第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax
 
   jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。
 
[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,0x900
   mov ax, SELECTOR_VIDEO
   mov gs, ax

   ; 创建页目录及页表并初始化页内存位图
   call setup_page
   
   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
   sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

   ;将gdt描述符中视频段描述符中的段基址+0xc0000000
   mov ebx, [gdt_ptr + 2]  
   or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
					      ;段描述符的高4字节的最高位是段基址的31~24位

   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
   add dword [gdt_ptr + 2], 0xc0000000

   add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

   ; 把页目录地址赋给cr3
   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载
   
   ;160=80个字符[80*2]
   mov byte [gs:160*8], 'J'
   mov byte [gs:160*8+1],0x02
   
   mov byte [gs:160*8+2], 'a'
   mov byte [gs:160*8+3],0x02
   
   mov byte [gs:160*8+4], 'd'
   mov byte [gs:160*8+5],0x02
   
   mov byte [gs:160*8+6], 'e'
   mov byte [gs:160*8+7],0x02
   
   mov byte [gs:160*8+8], ' '
   mov byte [gs:160*8+9],0x00
   
   mov byte [gs:160*8+10], 'O'
   mov byte [gs:160*8+11],0x02
   
   mov byte [gs:160*8+12], 'S'
   mov byte [gs:160*8+13],0x02
   
   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:320], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr
   mov byte [gs:321], 0x02
   mov eax, 0xb8142           ;0XB8142=gs:322   段基址是0XB8000 偏移为0X142=322
   mov byte [eax], 'S'
 
   jmp $
   
   
   ;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				     ; 创建Page Table Entry
   mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

程序说明图:

(1)是将硬盘中的MBR加载到内存0X7C00开始的位置,由BIOS完成

(2)mbr.s中将loader加载到内存0X900开始的位置

    执行流程 BIOS[CS:IP 0XFFF0:FFF0]--》CS:IP[0:0X7C00]----(MBR将loader加载到0X900)--->CS:IP[0:0X900]---(loader中)

 

最后进行汇编编译 

        nasm mbr.S -o mbr                  编译产生mbr文件
        nasm loader.S -o loader          编译产生loader文件

       编译后产生的两个文件放入C++工程中,在用上节中的C++代码进行编译结果生成boot.img文件,最后直接用Bochs进行测试,前面章节都有介绍如何操作,就不做讲解了!

实验结果:

posted @ 2019-09-01 20:50  jadeshu  阅读(305)  评论(0编辑  收藏  举报