12.10开启内存分页机制

2024-03-18 05:28

本文主要是介绍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 = 010-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开启内存分页机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/821302

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

hadoop开启回收站配置

开启回收站功能,可以将删除的文件在不超时的情况下,恢复原数据,起到防止误删除、备份等作用。 开启回收站功能参数说明 (1)默认值fs.trash.interval = 0,0表示禁用回收站;其他值表示设置文件的存活时间。 (2)默认值fs.trash.checkpoint.interval = 0,检查回收站的间隔时间。如果该值为0,则该值设置和fs.trash.interval的参数值相等。

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

【Tools】大模型中的自注意力机制

摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样                      🎵 方芳《摇太阳》 自注意力机制(Self-Attention)是一种在Transformer等大模型中经常使用的注意力机制。该机制通过对输入序列中的每个元素计算与其他元素之间的相似性,

如何通俗理解注意力机制?

1、注意力机制(Attention Mechanism)是机器学习和深度学习中一种模拟人类注意力的方法,用于提高模型在处理大量信息时的效率和效果。通俗地理解,它就像是在一堆信息中找到最重要的部分,把注意力集中在这些关键点上,从而更好地完成任务。以下是几个简单的比喻来帮助理解注意力机制: 2、寻找重点:想象一下,你在阅读一篇文章的时候,有些段落特别重要,你会特别注意这些段落,反复阅读,而对其他部分

【Tools】大模型中的注意力机制

摇来摇去摇碎点点的金黄 伸手牵来一片梦的霞光 南方的小巷推开多情的门窗 年轻和我们歌唱 摇来摇去摇着温柔的阳光 轻轻托起一件梦的衣裳 古老的都市每天都改变模样                      🎵 方芳《摇太阳》 在大模型中,注意力机制是一种重要的技术,它被广泛应用于自然语言处理领域,特别是在机器翻译和语言模型中。 注意力机制的基本思想是通过计算输入序列中各个位置的权重,以确

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

JVM 常见异常及内存诊断

栈内存溢出 栈内存大小设置:-Xss size 默认除了window以外的所有操作系统默认情况大小为 1MB,window 的默认大小依赖于虚拟机内存。 栈帧过多导致栈内存溢出 下述示例代码,由于递归深度没有限制且没有设置出口,每次方法的调用都会产生一个栈帧导致了创建的栈帧过多,而导致内存溢出(StackOverflowError)。 示例代码: 运行结果: 栈帧过大导致栈内存