虚拟内存管理
虚拟内存就是对每一个进程而言,对它来说它认为它独占所有4G内存,进程内的地址就是以这4G的虚拟内存来表示的,当要执行时,cpu通过分段机制和分页机制将虚拟地址转换成物理内存地址进行访问。同时一个进程也不是所有的页都在内存中,只有部分在内存中,当需要的页不在内存时产生一个缺页中断,然后进行调度,将需要的页调入内存
仿照linux的设计,对于一个进程的4G虚拟空间3G-4G的空间给系统内核,0-3G给用户程序,现在要将内核映射到虚拟地址空间的3G-4G,但是映射完加载内核就需要页表来指示正式的物理内存地址,但是内核不加载就没有页表,所有需要一个临时的页表
内核的映射
修改链接器的脚本script/kernel.ld
1 | /* |
第8行修改了内核的加载地址为3G,然后新增的两个.init段放临时页表和函数,这两个段放在0x100000处给grub加载,然后将当前地址加上0xC0000000的偏移量
后面的部分和原来的区别就是加了AT(ADDR(.xxxx) - 0xC0000000)这些,这些是指明区段所载入内存的实际地址,所以将当前偏移量减去0xC0000000就是实际加载地址
链接器修改了,相应的其他代码也要修改 boot/boot.s
1 | ...... |
第五行修改代码段从.init.text
开始,同时指定kern_entry()
函数在代码段.init.text
处,并且在该函数中定义临时页表,切换到高虚拟地址的kern_init()
执行,并且切换内核栈和multiboot结构体指针
修改include/pmm.h
1 |
|
修改init/entry.c
1 |
|
把原来的内核入口函数改成了kern_init(),kern_entry()函数里定义了临时页表并且开启分页机制,然后修改了内核栈到虚拟地址,然后调用kern_init();attribute((section(“.init.text”)))是gcc提供的指定函数或数据的存储区段
一点点来看:
首先定义的pgt_temp是临时页目录(临时二级页表),pgd_t这个数据类型在vmm.h内定义是uint32_t,它放在.init.data段,起始地址是0x1000
pte_low是在低端地址的页表,pte_high是在3g以上高端地址的页表,起始地址分别为0x2000,和0x3000,所以临时页目录的大小就是0x2000-0x1000=0x1000是4KB,低端页表因为只要映射内核的4MB地址所以一页页表就够了。这些都在临时数据段(.init.data)
kern_entry()函数放在临时代码段(.init.text),该函数的加载地址就是0x100000(在链接器脚本中定义的),该函数第一行代码就是将页目录的第一项进行设置
页目录和页表项的格式如下
将页目录的第一项映射到低端页表,PAGE_PRESENT为0x1代表存在,PAGE_WRITE为0x2,这样构造的页目录第一项就为0x1003
然后函数第二行就是将第一张高端页表映射到页目录中,PGD_INDEX(PAGE_OFFSET)
是获取地址的页目录号,因为一个32位虚拟地址的高10位是页目录中偏移,所以#define PGD_INDEX(x) (((x) >> 22) & 0x3FF)
获取虚拟地址的高10位就是页目录中的偏移,这里就是将高端地址的第一位也就是3G获取它的页目录号,然后继续构造页目录项
然后是映射内核虚拟地址 4MB 到物理地址的前 4MB,4MB也就是1024页,页号左移12位刚好就是每一页的起始地址
映射 0x00000000-0x00400000 的物理地址到虚拟地址 0xC0000000-0xC0400000,也是4MB,对于二级页表来说更上面是一样的,它映射0xC0000000是通过页目录的偏移实现的
然后往CR3寄存器写入页目录的基址,将CR0寄存器的第31位置为1代表开启分页模式
在kern_entry()定义的页表将0x00000000-0x00400000 的物理地址到虚拟地址 0xC0000000-0xC0400000,同时还将映射内核虚拟地址 4MB 到物理地址的前 4MB,这是因为在进入kern_entry()时还没有开启分页机制,开启分页机制映射0x00000000-0x00400000 的物理地址到虚拟地址 0xC0000000-0xC0400000后在1MB处的kern_entry()函数会出错,所以映射一下低位4MB
更新一下include/multiboot.h
1 | ...... |
修改显存地址 drivers/console.c
1 | ...... |
之前的elf_t结构体存储的是低端内存的地址,现在也必须加上页偏移:
kernel/debug/elf.c
1 | ...... |
mm/vmm.c实现内核页表和映射
1 |
|
init_vmm()
函数跟之前的临时页表的部分差不多,同时注册了一个14号中断函数处理页面出错
map函数是将虚拟地址映射到物理地址,函数前两行是获取虚拟地址对应的页目录偏移和页表偏移,分别是高10位和低10位
然后通过页目录取得二级页表的物理地址,若该页表不存在则申请一页内存,然后映射入页目录,获取到二级页表之后要在函数中访问它需要取得它的虚拟地址,所以加上偏移,然后将对应物理页的地址构造成页表项映射到该二级页表中
unmap函数用于取消映射,直接将对应二级页表的页表项设置为0,因为在构建内核页表的时候没有映射第0页就是方便这时候当NULL
get_mapping函数是获取虚拟地址对应的物理地址成功获取返回1并将物理地址写入pa,否则返回0
一些东西的定义mm/vmm.h
1 |
|
实现页错误中断处理函数
1 |
|
debug
测试一下发现无法运行报错了,然后开始寻找问题,先排查了一遍发现代码没有问题
看报错信息显示如下
发现是在0xc0105000这里出错了
然后我用objdump -h time_kernel
得到如下结果
发现多了两个段.text.__x86.get_pc_thunk.ax
和.text.__x86.get_pc_thunk.bx
他们的VMA地址是加了偏移量之后的
然后使用objdump -d time_kernel
发现这两个段的函数在分页开启之前就被调用了,查资料知道这两个函数是传递寄存器的值,所以我们要把它们放在分页之前
修改scripts/kernel.ld
1 | ...... |
在.init.data之后.text之前加入这两个段
现在测试就成功了