Linux教程

操作系统真象还原实验记录之实验六:内存分页

本文主要是介绍操作系统真象还原实验记录之实验六:内存分页,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

操作系统真象还原实验记录之实验五:内存分页

对应书P199页 5.2

1.相关基础知识总结

页目录 页目录项 页表 页表项 物理页 虚拟地址 物理地址

概念略
页目录项及页表项
在这里插入图片描述
低3字节都是属性。高20位都是物理地址。

本次实验
虚拟地址:32位=4GB
物理地址:32MB (但是分页机制下能访问的实地址只有低1MB)32MB是上次实验内存容量的检测结果。
一块物理页大小:4kB
一个页表大小:4kB
一个页表项大小:4B
一个页表有1024个页表项
所以一个页表可表示1024*4kB=4MB的虚拟内存

一个页目录大小:4kB
一个页目录项大小:4B
一个页目录有1024个页目录项
所以该页目录可表示4MB*1024=4GB虚拟内存

根据物理页大小4B 页表项个数2^10个
页目录项个数2^10个,
可以将虚拟地址分成

10位10位12位
页目录项索引页表项索引块内偏移量

由上述表格,虚拟地址转化成物理地址的公式也就很清晰了。
高10位代表该虚拟地址会访问页目录的第几个页目录项,从而获得该页目录项的前20位即对应的页表的首地址。
中间10位代表该虚拟地址会访问页表的第几个页表项,从而获得该页表项前20位,即对应的物理页的首地址。
最后12位代表该物理页的偏移量
获得的物理页首地址加上最后12位偏移量就是虚拟地址对应的物理地址。

2.实验记录

2.1实验目的

1.构造页目录及页表,完成从虚拟地址3GB~3GB+1MB到实地址0 ~1MB的映射以及虚拟地址0 ~ 1MB向物理地址0 ~ 1MB的映射。

[注]你没有看错,这意味着本次实验虚拟地址3GB和虚拟地址0将会转化成相同的物理地址0

2.开启保护模式,并开启保护模式的分页机制
3.修改GDTR中的段基址,以及显存段段描述符中的段基址,确保代码

mov byte [gs:160],'V' 

中的[gs:160]在保护模式的分段机制和分页机制下,最后会拼出虚拟地址0xc00b800+160,显然此地址在3GB~3GB+1MB的虚拟地址之间。

4.执行上句代码,等价于向虚拟地址0xc00b800+160处写入字符‘V’的ASCLL码,
由于第三步我们开起了保护模式的分页机制,cpu会自动将提供的虚拟地址转化成物理地址再去访存。
所以本次实验可以检测在保护模式分页机制下,根据我们自己定义的页表、页目录,该虚拟地址能不能自动转化为实地址中低1MB中的0xb800(即显存文本模式首地址)

若用bochs模拟,显示屏可以打印字符V,说明虚拟地址成功转化为物理地址,试验成功。

2.2 本次实验注意事项

本次实验页目录的页目录项可以构造1024个
每个页表的页表项也可以构造1024个
但是需要注意的是
1…构造页目录时,我们的代码只构造了第0个页目录项和第768~1023个页目录项。其中第0和第768个目录项装的是第0个页表的首地址。第1023个页目录项装的是页目录的首地址。第769到1022个页目录项装的是第1到254个页表的首地址。

2.构造页表时,我们只构造了第0个页表中的前256个页表项。由于一块物理页4KB,因此分页机制下能访问的实地址只有低1MB,256*4KB=1MB。

根据上面这两点,我们还可以得到一个需要注意的信息
1个页目录项代表4MB虚拟内存,
其中第0和第768个目录项装的是第0个页表的首地址,
第768个页目录项代表4MB768=4MB256*3=3GB
所以本次实验只是完成了虚拟地址3GB~3GB+1MB向
实地址0~1MB的映射以及虚拟地址0 ~1MB向实地址0 ~1MB的映射。

3.本次实验页目录的首地址为0x100000即1MB。
第0个页表的首地址为0x101000即1MB+4KB
如此设置理由:低1MB用来装mbr.s、loader.s以及内核。

4.本次实验,3GB~3GB+1MB的虚拟地址是依次映射到低1MB的物理地址的,也就是说,3GB ~ 3GB+1MB的虚拟地址中,小的虚拟地址对应的物理地址一定小。

2.3实验代码

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 

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;
   
 ; 创建页目录及页表并初始化页内存位图
   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';
   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)及属性(3)
   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  ; 页目录项的属性RW和P位为1,US为0
   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

代码功能总结:
setup_page函数
构造了第0个页目录项和第768~1023个页目录项
构造了第0个页表中的前256个页表项。其中页表项的地址依次是实地址低1MB.

先开启了保护模式,然后调用了setup_page建立好页表以及页目录。
修改GDTR中的段基址,以及显存段段描述符中的段基址,
又把页目录地址赋给cr3并打开cr0的pg位(第31位)
最后执行
mov byte [gs:160], ‘V’;
来验证实验结果。

分析一下此句代码寻址过程。
里面涉及了两次虚拟地址的转换。

gs里面是显存段的选择子
在下述代码修改后

sgdt [gdt_ptr]

;;;;;修改了显存段段描述符的段基址;;;;
mov ebx, [gdt_ptr + 2]  
or dword [ebx + 0x18 + 4], 0xc0000000

;;;;修改了GDT基址;;;;
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000

;;;;修改后的GDT起始地址赋值给gdtr;;;;;;
lgdt [gdt_ptr] 

GDT的基址在实地址低1MB下仍然没变,仍然为0x900
显存段段描述符的基址在实地址低1MB下也没变0x900+8*3字节=0x918
gs里的描述符索引值值也没变,为3
变得是GDTR里的GDT基址,变为0xc0000900,也就是3GB+0x900,这显然是个虚拟地址,实地址只有32MB.
因此获得的显存段段描述符基址也是个虚拟地址为
0xc0000900+3 * 8个字节=0xc0000918
由于分页机制已经开启,所以这个虚拟地址会转换为物理地址0x918,
转化过程如下
0xc0000918

10位10位12位
11_0000_000000_0000_00000x918
十进制为76800x918

第768个页目录项是我们设定好了的,里面高20位的是第0个页表的首地址
中间10位是0,也就是第0个页表第0个页表项,里面的前20位是0。
所以这个虚拟地址对应的物理地址是0x00000918,
这就是显存段段描述符的基址。
显存段段描述符里记录的段基址也是个虚拟地址为0xc00b8000
同理,转化成物理地址也就是0x000b8000
这就是显存文本模式的首地址。
160是偏移,这个偏移是合法的。
所以显示屏第二行会出现‘V’。

2.4 实验结果

1.编译loader.s

nasm -o loader.bin loader.s

2.编译mbr.s

nasm -o mbr.bin mbr.s 

3.将mbr.bin刻入第0扇区

dd if=/home/Seven/bochs2.68/bin/mbr.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=1 seek=0 conv=notrunc

4.将loader.bin刻入第2扇区(注意count=3又加了一)

dd if=/home/Seven/bochs2.68/bin/loader.bin of=/home/Seven/bochs2.68/bin/Seven.img bs=512 count=3 seek=2 conv=notrunc

5.模拟bochs

./bochs -f bochsrc.disk

效果图
效果图

3.补充

由于在构建页目录项时,我们还把第1023个页目录项高20位设定成了页目录的首地址

根据分页机制虚拟地址转换物理地址过程
如果虚拟地址高20位为0xfffff,则高10位全为1,找到第1023个页目录项,获得页目录首地址,中间10位也全是1,页目录被当成页表,找到第1023个页目录项被当成页表项,获得的还是页目录首地址。

所以只要虚拟地址高20位为0xfffff,就可以获得页目录首地址

我们就可以通过特定的虚拟地址来获取
1.获取页目录物理地址:虚拟地址0xffffff000
2.获取第N个页目录项的物理地址:虚拟地址:0xfffffxxx
xxx是页目录索引乘以4的积
举例,获得第1个页目录项首地址(编号从0开始),那我给的虚拟地址就应该是0xfffff004,页目录首地址+4

获得第N个页目录项首地址也就获得了第N个页表的首地址。
3.访问页表中的页表项,即获得第M个页表中第N个页表项的首地址:我给的虚拟地址前10位全是是1即0x3ff,中间10位等于M,最后12位等于N*4,这样最后转化得到的物理地址就是第M个页表中第N个页表项的首地址

这篇关于操作系统真象还原实验记录之实验六:内存分页的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!