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的页,而不是4KB
uint32_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 page
uint32_t write_disable : 1; // 1 (R/W) Read/write, if 0, writes may not be allowed
uint32_t user_mode_acc : 1; // 2 (U/S) if 0, user-mode accesses are not allowed t
uint32_t write_through : 1; // 3 (PWT) Page-level write-through,直写法
uint32_t cache_disable : 1; // 4 (PCD) Page-level cache disable
uint32_t accessed : 1; // 5 (A) Accessed
uint32_t : 1; // 6 Ignored;
uint32_t ps : 1; // 7 (PS)
uint32_t : 4; // 11:8 Ignored
uint32_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 page
uint32_t write_disable : 1; // 1 (R/W) Read/write, if 0, writes may not be allowe
uint32_t user_mode_acc : 1; // 2 (U/S) if 0, user-mode accesses are not allowed t
uint32_t write_through : 1; // 3 (PWT) Page-level write-through
uint32_t cache_disable : 1; // 4 (PCD) Page-level cache disable
uint32_t accessed : 1; // 5 (A) Accessed;
uint32_t dirty : 1; // 6 (D) Dirty
uint32_t pat : 1; // 7 PAT
uint32_t global : 1; // 8 (G) Global
uint32_t : 3; // Ignored
uint32_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系统还没有设置内核态和用户态,目前就是最高的系统态,后面再来隔离用户态和系统态,然后实现变态
太肝了,加油吧!!!