DIY操作系统(4):内存管理及分页机制

获取物理内存容量

在Linux2.6内核中是使用detect_memory函数来获取内存容量的,其本质是在实模式下调用BIOS的0x15中断实现的,其下有三个子功能,由强到弱依次为:

  • EAX = 0xE820:遍历主机上全部内存。
  • AX = 0xE801:分别检测低15MB和16MB~4GB的内存。
  • AH = 0x88:最多检测出64MB内存,超过也按64MB计。
    给出一个loader.S,由强到弱依次调用三种方法来获取物理内存容量
   %include "boot.inc"
   section loader vstart=LOADER_BASE_ADDR
   LOADER_STACK_TOP equ LOADER_BASE_ADDR
   
;构建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         ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
   SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0	 ; 同上
   SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0	 ; 同上 

   ; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
   ; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
   ; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
   total_mem_bytes dd 0					 
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

   ;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
   gdt_ptr  dw  GDT_LIMIT 
	    dd  GDT_BASE

   ;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
   ards_buf times 244 db 0
   ards_nr dw 0		      ;用于记录ards结构体数量

   loader_start:
   
;-------  int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局  -------

   xor ebx, ebx		      ;第一次调用时,ebx值要为0
   mov edx, 0x534d4150	      ;edx只赋值一次,循环体中不会改变
   mov di, ards_buf	      ;ards结构缓冲区
.e820_mem_get_loop:	      ;循环获取每个ARDS内存范围描述结构
   mov eax, 0x0000e820	      ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
   mov ecx, 20		      ;ARDS地址范围描述符结构大小是20字节
   int 0x15
   jc .e820_failed_so_try_e801   ;若cf位为1则有错误发生,尝试0xe801子功能
   add di, cx		      ;使di增加20字节指向缓冲区中新的ARDS结构位置
   inc word [ards_nr]	      ;记录ARDS数量
   cmp ebx, 0		      ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
   jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
   mov cx, [ards_nr]	      ;遍历每一个ARDS结构体,循环次数是ARDS的数量
   mov ebx, ards_buf 
   xor edx, edx		      ;edx为最大的内存容量,在此先清0
.find_max_mem_area:	      ;无须判断type是否为1,最大的内存块一定是可被使用
   mov eax, [ebx]	      ;base_add_low
   add eax, [ebx+8]	      ;length_low
   add ebx, 20		      ;指向缓冲区中下一个ARDS结构
   cmp edx, eax		      ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
   jge .next_ards
   mov edx, eax		      ;edx为总内存大小
.next_ards:
   loop .find_max_mem_area
   jmp .mem_get_ok

;------  int 15h ax = E801h 获取内存大小,最大支持4G  ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
   mov ax,0xe801
   int 0x15
   jc .e801_failed_so_try88   ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
   mov cx,0x400	     ;cx和ax值一样,cx用做乘数
   mul cx 
   shl edx,16
   and eax,0x0000FFFF
   or edx,eax
   add edx, 0x100000 ;ax只是15MB,故要加1MB
   mov esi,edx	     ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
   xor eax,eax
   mov ax,bx		
   mov ecx, 0x10000	;0x10000十进制为64KB
   mul ecx		;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
   add esi,eax		;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
   mov edx,esi		;edx为总内存大小
   jmp .mem_get_ok

;-----------------  int 15h ah = 0x88 获取内存大小,只能获取64M之内  ----------
.e801_failed_so_try88: 
   ;int 15后,ax存入的是以kb为单位的内存容量
   mov  ah, 0x88
   int  0x15
   jc .error_hlt
   and eax,0x0000FFFF
      
   ;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
   mov cx, 0x400     ;0x400等于1024,将ax中的内存容量换为以byte为单位
   mul cx
   shl edx, 16	     ;把dx移到高16位
   or edx, eax	     ;把积的低16位组合到edx,为32位的积
   add edx,0x100000  ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
   mov [total_mem_bytes], edx	 ;将内存换为byte单位后存入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第0位置1  ----------------
   mov eax, cr0
   or eax, 0x00000001
   mov cr0, eax

   jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
					     ; 这将导致之前做的预测失效,从而起到了刷新的作用。
.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
   mov ax, SELECTOR_VIDEO
   mov gs, ax

   mov byte [gs:160], 'P'

   jmp $

mbr.S如下

;主引导程序 
;------------------------------------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00         
   mov ax,cs      
   mov ds,ax
   mov es,ax
   mov ss,ax
   mov fs,ax
   mov sp,0x7c00
   mov ax,0xb800
   mov gs,ax

; 清屏
;利用0x06号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10   功能号:0x06	   功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
;无返回值:
   mov     ax, 0600h
   mov     bx, 0700h
   mov     cx, 0                   ; 左上角: (0, 0)
   mov     dx, 184fh		   ; 右下角: (80,25),
				   ; 因为VGA文本模式中,一行只能容纳80个字符,共25行。
				   ; 下标从0开始,所以0x18=24,0x4f=79
   int     10h                     ; int 10h

   ; 输出字符串:MBR
   mov byte [gs:0x00],'1'
   mov byte [gs:0x01],0xA4

   mov byte [gs:0x02],' '
   mov byte [gs:0x03],0xA4

   mov byte [gs:0x04],'M'
   mov byte [gs:0x05],0xA4	   ;A表示绿色背景闪烁,4表示前景色为红色

   mov byte [gs:0x06],'B'
   mov byte [gs:0x07],0xA4

   mov byte [gs:0x08],'R'
   mov byte [gs:0x09],0xA4
	 
   mov eax,LOADER_START_SECTOR	 ; 起始扇区lba地址
   mov bx,LOADER_BASE_ADDR       ; 写入的地址
   mov cx,4			 ; 待读入的扇区数
   call rd_disk_m_16		 ; 以下读取程序的起始部分(一个扇区)
  
  ; 此处直接跳到loader_start
   jmp LOADER_BASE_ADDR + 0x300
       
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:	   
;-------------------------------------------------------------------------------
				       ; eax=LBA扇区号
				       ; ebx=将数据写入的内存地址
				       ; ecx=读入的扇区数
      mov esi,eax	  ;备份eax
      mov di,cx		  ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;第2步:将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置7~4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步:向0x1f7端口写入读命令,0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;第4步:检测硬盘状态
  .not_ready:
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;第5步:从0x1f0端口读数据
      mov ax, di
      mov dx, 256
      mul dx
      mov cx, ax	   ; di为要读取的扇区数,一个扇区有512字节,每次读入一个字,
			   ; 共需di*512/2次,所以di*256
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx
      mov [bx],ax
      add bx,2		  
      loop .go_on_read
      ret

   times 510-($-$$) db 0
   db 0x55,0xaa

测试,loader运行后,使用xp指令查看变量total_mem_bytes的地址0xB00处的内容,显示为0x02000000,即为十进制的32MB。
在这里插入图片描述
与配置文件吻合。
在这里插入图片描述

内存分页机制

二级、一级页表结构
请添加图片描述
字段说明:

  • P:存在位。表示该页是否在内存中。
  • RW:可读写属性位(?说的应该不是页本身)。
  • US:页或页表项的特权级位。如:若为0,则表示处于supervisor级,只允许ring0 1 2的程序访问,不允许ring3的程序访问。
  • PWT:write-through位。
  • PCD:高速缓存有效位。
  • A:访问位。
  • D:脏位。仅针对页表项有效,代表对应页是否被修改过。
  • PAT:页属性表位,设置各页的内存属性。
  • G:全局页标志。若为全局页,则可直接使用TLB进行地址转换。
  • AVL:可用标志。软件是否可使用该页。

启用分页机制的必要条件:

  1. 准备好页目录表、页表。
  2. 将页表地址写入控制寄存器CR3。
  3. 将控制寄存器CR0的PG位置1。

在这里插入图片描述

在页表中实现用户进程共享OS:把用户的4GB虚拟地址空间划分为两部分,0~3GB给用户进程,3GB~4GB给操作系统,并让所有用户进程的OS虚拟地址空间指向同一片物理页地址(OS的代码地址)。

页目录表有1024个表项,0号页目录表项指向第一个页表,这是用于保证开启分页模式前后loader都能正常运行的权宜之计,解释见下。768号到1022号表项最终能索引到1GB-4MB的大小,这是本系统支持的内核代码的最大长度,所有进程的页目录表中,这些表项的指向都相同。1023号页目录表项指向页目录自身。

此处将页目录表放在0x100000处,页表紧挨页目录表,创建函数实现如下:

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间(一页4KB)逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   ; 下面的宏定义在boot.inc中
   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

;创建为内核预留1GB-4MB的大小的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

boot.inc中与页表相关的部分:

; 页目录表的物理地址
PAGE_DIR_TABLE_POS equ 0x100000
;----------------   页表相关属性    --------------
PG_P  equ   1b
PG_RW_R	 equ  00b 
PG_RW_W	 equ  10b 
PG_US_S	 equ  000b 
PG_US_U	 equ  100b 

  • 为什么要将0号页目录项存为第一个页表的物理地址:第一个页表映射的是低4M的物理内存空间,包含内核代码所在的低1M。在开启分页之前,内核代码指令的地址都是物理地址,(如0x900可访问变量A),而开启分页之后,指令中的地址被当做虚拟地址来解释,若不作任何处理,继续访问虚拟地址0x900,则不一定是A的地址了。需要做一定的处理,保证内核代码指令中的地址0x900无论分页与否都能访问A,故需要将虚拟地址空间的低1M与物理地址空间的低1M进行映射。

  • 为什么将0xc00号(768号)页目录项设置为第一个页表的物理地址:因为这个页目录项及之后的页目录项对应的页表的页都属于虚拟地址空间的高1G,即内核代码的空间。

  • 为什么将最后一个页目录项指向页目录自身:为了对页表进行动态操作。(留坑)

启用分页机制的全部代码如下:

; 创建页目录及页表并初始化页内存位图
   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]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp $

要注意,开启分页机制后,将显存打印功能等放到内核空间,具体来说,取GDT描述符表的地址,根据偏移量定位显存段描述符,将其段基址加上0xc0000000,即为分页之后的虚拟地址,可作为段基址。GDT的修改操作为:取原GDT的始址,加上0xc0000000,再写回GDTR寄存器中。

测试:
可正常运行并打印字符’V’。
在这里插入图片描述
分页后,虚拟地址到物理地址的映射如下。(为什么有64位?)
在这里插入图片描述

用虚拟地址访问页表

上图中,cr3寄存器显示的是页目录表的物理地址。
地址映射中各行:

  1. 0号页目录表项实现的映射,虚拟空间低端1M对应了物理地址的低端1M。
  2. 768号页目录表项实现的映射,内核空间中的低1M对应了物理地址的低端1M。
  3. FFC00000转二进制为1111111111 0000000000 000000000000,页目录号为10个1,即1023,页号为0。由于1023号页目录项指向自身,所以其对应的“页”实际是页目录表本身所在的页,所谓的页号为0就是页目录号为0,其中的地址就是第一个页表的物理地址0x101000,所以其对应的物理地址就是`0x101000 + 0 = 0x101000。实质是描述了0号页目录表项到第一个页表的映射关系。
  4. 描述了768号页目录表项到第一个页表的映射关系。
  5. 描述了1023号页目录表项到页目录表自身的映射关系。

3、4、5行都是在1023号页目录表项指向页目录表的基础上实现的,有:
在这里插入图片描述

商印通3.0版本隆重推出 为满足市场需求,不断提升商印通的品质和服务,保持在行业内的领先地位,商印网推出DIY个性定制系统平台商印通3.0版本。通过底层技术和设计理念的创新,在模板、流程、功能、界面、性能等各个方面再上层楼。 一、用户体验全面升级 商印通DIY个性定制系统平台3.0版本,结合客户需求,以人为本,使操作更加人性化,让您的客户爱不释手! 1、自由DIY背景、贴图、边框等,更趣味。 2、随意拖动图片位置、大小,新增或删除照片区域,更个性。 3、自由更换图层顺序,更灵活。 4、新增剪切、复制、粘贴功能,更便捷。 5、编缉区域可根据屏幕大小进行比例缩放,更人性化。 二、后台拼版自动化 后台自动拼版,下载文件可直接印刷,无需人工拼版,为您节约人力、物力资源、提高效率。 产品类型拼版:可实现同类别下的产品,不同客户的订单一起自动拼版,节省时间效率高。 组合式拼版:同一订单的两个产品可拆分开来,组合其它的订单的产品一起拼版。(如:A客户的订单有2个台历,B、C客户的订单都有一本台历产品,可以将A客户的台历与B客户的台历组合自动拼版。) 三、模块系列化 商印通3.0版本结合市场调查模块全新升级,分为四大系列:个性冲印、个性印品、个性礼品、个性饰品。四大系列精准定位,吸引不同层次的客户群体,为您带来更多的交易量! 个性冲印系列:网上冲印省时、省事、省钱,备受大家青睐!个性冲印市场大众化,需求量大! 个性印品系列:定制印品个性、趣味、方便,是市场增长点。 个性礼品系列:礼尚往来,中国人的传统。传统的礼品已经过时,个性礼品成了刚性需求。 个性潮品系列:潮品是现在年轻人的最爱,个性潮品成为未来发展的趋势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值