前言
本博客记录《操作系统真象还原》第五章第2个实验的操作~
实验环境:ubuntu18.04+VMware , Bochs下载安装
实验内容:启动内存分页机制
实验原理:内存分页机制
前置知识
前置知识可食用内存分页机制
代码
include/boot.inc
PAGE_DIR_TABLE_POS equ 0x100000 ;二级页目录表,页表放在内存中1M起始位置连续存放,尽可能简单...(省略)
;---------------- 页表相关属性 --------------
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;
:PAGE_DIR_TABLE_POS 用于定义页目录表的物理地址,把页目录表放置到物理内存 0x100000。- 注释下方的代码是用于页目录项 PDE 和页表项 PTE 中的属性,是用二进制直接定义的。
boot/loader.S
%include "boot.inc"section loader vstart=LOADER_BASE_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDR;构建gdt及其内部的描述符GDT_BASE: dd 0x00000000 dd 0x00000000CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4DATA_STACK_DESC: dd 0x0000FFFFdd DESC_DATA_HIGH4VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7dd DESC_VIDEO_HIGH4 ; 此时dpl为0GDT_SIZE equ $ - GDT_BASEGDT_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 + RPL0SELECTOR_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 0ards_nr dw 0 ;用于记录ards结构体数量loader_start:;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------xor ebx, ebx ;第一次调用时,ebx值要为0mov 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 0x15jc .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_lowadd eax, [ebx+8] ;length_lowadd ebx, 20 ;指向缓冲区中下一个ARDS结构cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量jge .next_ardsmov edx, eax ;edx为总内存大小
.next_ards:loop .find_max_mem_areajmp .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,0xe801int 0x15jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位mov cx,0x400 ;cx和ax值一样,cx用做乘数mul cx shl edx,16and eax,0x0000FFFFor edx,eaxadd edx, 0x100000 ;ax只是15MB,故要加1MBmov esi,edx ;先把低15MB的内存容量存入esi寄存器备份;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量xor eax,eaxmov ax,bx mov ecx, 0x10000 ;0x10000十进制为64KBmul 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, 0x88int 0x15jc .error_hltand eax,0x0000FFFF;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位mul cxshl 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,0x92or al,0000_0010Bout 0x92,al;----------------- 加载GDT ----------------lgdt [gdt_ptr];----------------- cr0第0位置1 ----------------mov eax, cr0or eax, 0x00000001mov cr0, eaxjmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,; 这将导致之前做的预测失效,从而起到了刷新的作用。
.error_hlt: ;出错则挂起hlt[bits 32]
p_mode_start:mov ax, SELECTOR_DATAmov ds, axmov es, axmov ss, axmov esp,LOADER_STACK_TOPmov ax, SELECTOR_VIDEOmov gs, ax; 创建页目录及页表并初始化页内存位图call setup_page;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载sgdt [gdt_ptr] ; 存储到原来gdt所有的位置;将gdt描述符中视频段描述符中的段基址+0xc0000000mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 ;视频段是第3个段描述符,每个描述符是8字节,故0x18。;段描述符的高4字节的最高位是段基址的31~24位;将gdt的基址加上0xc0000000使其成为内核所在的高地址add dword [gdt_ptr + 2], 0xc0000000add esp, 0xc0000000 ; 将栈指针同样映射到内核地址; 把页目录地址赋给cr3mov eax, PAGE_DIR_TABLE_POSmov cr3, eax; 打开cr0的pg位(第31位)mov eax, cr0or eax, 0x80000000mov cr0, eax;在开启分页后,用gdt新的地址重新加载lgdt [gdt_ptr] ; 重新加载mov byte [gs:160], 'V' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:162], 'i' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:164], 'r' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:166], 't' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:168], 'u' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:170], 'a' ;视频段段基址已经被更新,用字符v表示virtual addrmov byte [gs:172], 'l' ;视频段段基址已经被更新,用字符v表示virtual addrjmp $;------------- 创建页目录及页表 ---------------
setup_page:
;先把页目录占用的空间逐字节清0mov ecx, 4096mov esi, 0
.clear_page_dir:mov byte [PAGE_DIR_TABLE_POS + esi], 0inc esiloop .clear_page_dir;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entrymov eax, PAGE_DIR_TABLE_POSadd 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, 0x1000mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址;下面创建页表项(PTE)mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256mov esi, 0mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte: ; 创建Page Table Entrymov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 add edx,4096 ; edxinc esiloop .create_pte;创建内核其它页表的PDEmov eax, PAGE_DIR_TABLE_POSadd eax, 0x2000 ; 此时eax为第二个页表的位置or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1mov ebx, PAGE_DIR_TABLE_POSmov ecx, 254 ; 范围为第769~1022的所有目录项数量mov esi, 769
.create_kernel_pde:mov [ebx+esi*4], eaxinc esiadd eax, 0x1000loop .create_kernel_pderet
实验操作
1.修改文件
(base) user@ubuntu:/home/cooiboi/bochs/include$ sudo vim boot.inc
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo vim loader.S
2.编译loader.S
sudo nasm -I include/ -o boot/loader.bin boot/loader.S
(base) user@ubuntu:/home/cooiboi/bochs$ sudo nasm -I include/ -o boot/loader.bin boot/loader.S
3.将load写入磁盘中
sudo dd if=/home/cooiboi/bochs/boot/mbr.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=1 conv=notrunc
sudo dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=3 seek=2 conv=notrunc
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo dd if=/home/cooiboi/bochs/boot/mbr.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=1 conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000156073 s, 3.3 MB/s
(base) user@ubuntu:/home/cooiboi/bochs/boot$
(base) user@ubuntu:/home/cooiboi/bochs/boot$ sudo dd if=/home/cooiboi/bochs/boot/loader.bin of=/home/cooiboi/bochs/boot/hd60M.img bs=512 count=3 seek=2 conv=notrunc
2+1 records in
2+1 records out
1237 bytes (1.2 kB, 1.2 KiB) copied, 0.000296966 s, 4.2 MB/s
4.启动Bochs
sudo bin/bochs -f boot/bochsrc.disk
(base) user@ubuntu:/home/cooiboi/bochs$ sudo bin/bochs -f boot/bochsrc.disk
info gdt
(info
是用来查看各种数据的命令)
info tab
(tab
表示页表)查看虚拟地址映射情况。
说明: cr3 寄存器显示的是页目录表的物理地址。按->分成左右两列,左边列出的是 32 位虚拟地址范围,右边是虚拟地址对应的物理地址
总结
- 获取页目录表物理地址:让虚拟地址的高 20 位为 0xfffff,低 12 位为 0x000,即 0xfffff000,这也
是页目录表中第 0 个页目录项自身的物理地址。 - 访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为 0xfffffxxx,其中 xxx 是页目录
项的索引乘以 4 的积。 - 访问页表中的页表项:要使虚拟地址高 10 位为 0x3ff,目的是获取页目录表物理地址。中间 10 位为页表的索引,因为是 10 位的索引值,所以这里不用乘以 4。低 12 位为页表内的偏移地址,用来定位页表项,它必须是已经乘以 4 后的值。
参考资料
- 《操作系统真象还原》
- 《操作系统真象还原》第四章 ---- 剑指Loader 刃刺GDT 开启新纪元保护模式 解放32位