操作系统真象还原实验记录之实验六:内存分页
作者:互联网
操作系统真象还原实验记录之实验五:内存分页
对应书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_0000 | 00_0000_0000 | 0x918 |
十进制为768 | 0 | 0x918 |
第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个页表项的首地址
标签:虚拟地址,物理地址,地址,实验,内存,1MB,目录,真象,页表 来源: https://blog.csdn.net/mxy990811/article/details/115795590