本文主要是介绍12.10开启内存分页机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
12.10开启内存分页机制
王道里面讲的很详细
我们对于loader的内存映射先采用一级映射
一级映射的话一页是4MB
loader部分必须要建立相同的地址映射
CR3的基础知识
CR3含有存放页目录表页面的物理地址,因此CR3也被称为PDBR。因为页目录表页面是页对齐的,所以该寄存器只有高20位是有效的。而低12位保留供更高级处理器使用,因此在往CR3中加载一个新值时低12位必须设置为0。
的主要功能还是用来存放页目录表物理内存基地址,每当进程切换时,Linux就会把下一个将要运行进程的页目录表物理内存基地址等信息存放到CR3寄存器中。
CR3的PCD和PWT位的作用,PWT表示的直写法还是其他的,PCD表示是否开TLB
CR0的基础知识
CR4的基础知识
VME
用于虚拟8086模式。PAE
用于确认是哪个分页,PAE = 1
,是2-9-9-12
四级分页,PAE = 0
是10-10-12
三级分页。PSE
是大页是否开启的总开关,如果置0,就算PDE
中设置了大页你也得是普通的页。
/*** @brief 开启分页机制* 将0-4M空间映射到0-4M和SYS_KERNEL_BASE_ADDR~+4MB空间* 0-4MB的映射主要用于保护loader自己还能正常工作* SYS_KERNEL_BASE_ADDR+4MB则用于为内核提供正确的虚拟地址空间*/
void enable_page_mode (void) {
#define PDE_P (1 << 0) //这个区域存在
#define PDE_PS (1 << 7) //表示单级页表
#define PDE_W (1 << 1) //这个区域可以写
#define CR4_PSE (1 << 4) //开启大页
#define CR0_PG (1 << 31) //CR0里面开启分页// 使用4MB页块,这样构造页表就简单很多,只需要1个表即可。// 以下表为临时使用,用于帮助内核正常运行,在内核运行起来之后,将重新设置static uint32_t page_dir[1024] __attribute__((aligned(4096))) = { //gcc的指示符,将其对齐到4KB地址处[0] = PDE_P | PDE_PS | PDE_W | 0x0, // PDE_PS,开启4MB的页,loader放第0项,从0地址开始}; //因为一级页表的位数为10为,所以为1024// 设置PSE,以便启用4M的页,而不是4KBuint32_t cr4 = read_cr4();write_cr4(cr4 | CR4_PSE);// 设置页表地址write_cr3((uint32_t)page_dir); //CR3本质就是页表基址寄存器// 开启分页机制write_cr0(read_cr0() | CR0_PG); //开启分页
}
void load_kernel(void) {`````````// 开启分页机制enable_page_mode();`````````
}
然后给kernel内核设置分页和权限
怎么实现这种针对性的区分权限呢?需要知道各个段的地址,用前几篇文章那个方法
/* 参考文档: https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html */
SECTIONS
{PROVIDE(kernel_base = 0x0);. = 0x00010000;PROVIDE(s_text = .);.text : {*(.text)}.rodata : {*(.rodata)}PROVIDE(e_text = .);PROVIDE(s_data = .);.data : {*(.data)}.bss : {*(.bss)}PROVIDE(mem_free_start = .);
}
现在要设置内核也就是kernel部分的分页和权限
现在要开启二级页表,二级页表有顶级页表有二级页表
对于顶级页表和二级页表需要一个联合来描述这个数据结构(10+10+12结构)
c语言小知识点
unsigned int abc:1;变量abc在分配空间的时候只分配一个位(二进制)!!比如:int a:2;//占二位int b:3;//占三位int c:33;//错误,int在32位中不可能有32位
//mmu.h
/*** @brief Page-Table Entry*/
typedef union _pde_t { //一级目录,联合体uint32_t v;//32位struct {uint32_t present : 1; // 0 (P) Present; must be 1 to map a 4-KByte pageuint32_t write_disable : 1; // 1 (R/W) Read/write, if 0, writes may not be alloweduint32_t user_mode_acc : 1; // 2 (U/S) if 0, user-mode accesses are not allowed tuint32_t write_through : 1; // 3 (PWT) Page-level write-through,直写法uint32_t cache_disable : 1; // 4 (PCD) Page-level cache disableuint32_t accessed : 1; // 5 (A) Accesseduint32_t : 1; // 6 Ignored;uint32_t ps : 1; // 7 (PS)uint32_t : 4; // 11:8 Ignoreduint32_t phy_pt_addr : 20; // 高20位page table物理地址};
}pde_t;/*** @brief Page-Table Entry*/
typedef union _pte_t { //二级目录uint32_t v;struct {uint32_t present : 1; // 0 (P) Present; must be 1 to map a 4-KByte pageuint32_t write_disable : 1; // 1 (R/W) Read/write, if 0, writes may not be alloweuint32_t user_mode_acc : 1; // 2 (U/S) if 0, user-mode accesses are not allowed tuint32_t write_through : 1; // 3 (PWT) Page-level write-throughuint32_t cache_disable : 1; // 4 (PCD) Page-level cache disableuint32_t accessed : 1; // 5 (A) Accessed;uint32_t dirty : 1; // 6 (D) Dirtyuint32_t pat : 1; // 7 PATuint32_t global : 1; // 8 (G) Globaluint32_t : 3; // Ignoreduint32_t phy_page_addr : 20; // 高20位物理地址};
}pte_t;
此时此刻我们要创建内核页并切换过去(内核是64KB处起始的10000处)
//mmu.h
#define PDE_CNT 1024 //因为采用的是10+10+12模式
//memory.h
typedef struct _memory_map_t {void * vstart; // 虚拟地址void * vend;void * pstart; // 物理地址uint32_t perm; // 访问权限
}memory_map_t;
//memory.c
static pde_t kernel_page_dir[PDE_CNT] __attribute__((aligned(MEM_PAGE_SIZE))); // 内核顶级页目录表,要对齐
/*** @brief 根据内存映射表,构造内核页表*/
void create_kernel_table (void) {extern uint8_t s_text[], e_text[], s_data[], e_data[]; //简化了前面从lds文件里面拿地址的操作extern uint8_t kernel_base[];// 地址映射表, 用于建立内核级的地址映射// 地址不变,但是添加了属性static memory_map_t kernel_map[] = { //二级页表的映射关系{kernel_base, s_text, 0, 0}, // 内核栈区,关于这个0的访问权限后面回用到{s_text, e_text, s_text, 0}, // 内核代码区{s_data, (void *)(MEM_EBDA_START - 1), s_data, 0}, // 内核数据区};// 清空后,然后依次根据映射关系创建映射表for (int i = 0; i < sizeof(kernel_map) / sizeof(memory_map_t); i++) {memory_map_t * map = kernel_map + i;// 可能有多个页,建立多个页的配置// 简化起见,不考虑4M的情况int vstart = down2((uint32_t)map->vstart, MEM_PAGE_SIZE);int vend = up2((uint32_t)map->vend, MEM_PAGE_SIZE);int page_count = (vend - vstart) / MEM_PAGE_SIZE;// 建立映射关系memory_create_map(kernel_page_dir, vstart, (uint32_t)map->pstart, page_count, map->perm);}
}
memory_create_map函数
//memory.c
/*** @brief 将指定的地址空间进行一页的映射*/
int memory_create_map (pde_t * page_dir, uint32_t vaddr, uint32_t paddr, int count, uint32_t perm) { //page_dir为顶级页表基址for (int i = 0; i < count; i++) { //需要分配这么多页// log_printf("create map: v-0x%x p-0x%x, perm: 0x%x", vaddr, paddr, perm);pte_t * pte = find_pte(page_dir, vaddr, 1);//这个1表示要不要分配PTE表项所在的表(二级页表),后面另作他用if (pte == (pte_t *)0) {// log_printf("create pte failed. pte == 0");return -1; //未找到}// 创建映射的时候,这条pte应当是不存在的。// 如果存在,说明可能有问题// log_printf("\tpte addr: 0x%x", (uint32_t)pte);ASSERT(pte->present == 0);pte->v = paddr | perm | PTE_P;//因为是一一映射vaddr += MEM_PAGE_SIZE;paddr += MEM_PAGE_SIZE;}return 0;
}
find_pte函数
pte_t * find_pte (pde_t * page_dir, uint32_t vaddr, int alloc) {pte_t * page_table;pde_t *pde = page_dir + pde_index(vaddr); //顶级页表其中一个表项,拿出其中的地址,然后找到二级页表if (pde->present) { //这个二级页表是否存在page_table = (pte_t *)pde_paddr(pde); //如果存在取出这个表项,就是二级页表的地址} else {// 如果不存在,则考虑分配一个if (alloc == 0) {return (pte_t *)0;} //就算这个二级页表不存在也不需要分配这个二级页表// 分配一个物理页表uint32_t pg_paddr = addr_alloc_page(&paddr_alloc, 1);//因为10位所以一个页表大小刚好为4KB,一个位视图一个点的大小if (pg_paddr == 0) {return (pte_t *)0;} //没有找到空闲空间分配失败// 设置为用户可读写,将被pte中设置所覆盖pde->v = pg_paddr | PTE_P; //分配完了一定记得要写入这个联合体// 为物理页表绑定虚拟地址的映射,这样下面就可以计算出虚拟地址了//kernel_pg_last[pde_index(vaddr)].v = pg_paddr | PTE_P | PTE_W;// 清空页表,防止出现异常// 这里虚拟地址和物理地址一一映射,所以直接写入page_table = (pte_t *)(pg_paddr);kernel_memset(page_table, 0, MEM_PAGE_SIZE);}return page_table + pte_index(vaddr); //返回的是二级页表其中一个表项,也就是目标页的地址
}
分割地址函数
//mmu.h
/*** @brief 返回vaddr在页目录中的索引*/
static inline uint32_t pde_index (uint32_t vaddr) {int index = (vaddr >> 22); // 只取高10位return index;
}
/*** @brief 获取pde中地址*/
static inline uint32_t pde_paddr (pde_t * pde) {return pde->phy_pt_addr << 12;
}/*** @brief 返回vaddr在页表中的索引*/
static inline int pte_index (uint32_t vaddr) {return (vaddr >> 12) & 0x3FF; // 取中间10位
}/*** @brief 获取pte中的物理地址*/
static inline uint32_t pte_paddr (pte_t * pte) {return pte->phy_page_addr << 12;
}
调试
发现报错了
发现最后两项重复了,找问题,因为是对kernel内存进行分区,看kernel反汇编
发现这个地址并没有和4KB对齐,所以提取前面的地址会有重复的情况
解决方案
SECTIONS
{PROVIDE(kernel_base = 0x0);. = 0x00010000;PROVIDE(s_text = .);.text : {*(.text)}.rodata : {*(.rodata)}PROVIDE(e_text = .);. = ALIGN(4096);PROVIDE(s_data = .);.data : {*(.data)}.bss : {*(.bss)}PROVIDE(mem_free_start = .);
}
加. = ALIGN(4096);
就可以让其和4KB对齐了
从qemu看分段情况
第一个info mem显示出来的是loader初始的那个一级页表对应的段
自己终于理解了分段和分页结合一起的巧妙,分段好划分权限,分页能提高内存的利用率
测试分段权限设置是不是成功
一开始test函数地址为0x118df,没有设置页表的权限,看看是不是能改为0x12
成功改变
经过页表的权限设置以后依然是被改变了
因为目前的x86系统还没有设置内核态和用户态,目前就是最高的系统态,后面再来隔离用户态和系统态,然后实现变态
太肝了,加油吧!!!
这篇关于12.10开启内存分页机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!