对应书P199页 5.2
概念略
页目录项及页表项
低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位偏移量就是虚拟地址对应的物理地址。
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,说明虚拟地址成功转化为物理地址,试验成功。
本次实验页目录的页目录项可以构造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的虚拟地址中,小的虚拟地址对应的物理地址一定小。
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’。
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
效果图
由于在构建页目录项时,我们还把第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个页表项的首地址