分页机制
这一部分在手册第四章
视频讲解可以看这一个课程
在不使用分页机制的时候, 我们看到的是物理内存, 物理内存有多大, 我们就可以使用多大的内存
使用内存分页机制, 我们就可以扩充访问的地址范围, 也可以实现权限的细分, 实际上就是实现虚拟内存, 将地址进行映射, 看到的内存更大了, 但是实际上可以使用的内存的大小还是不变的
访问的内存==>从页表里面找物理内存==>访问实际的物理内存
开启以后得访问过程: 根据段寄存器找到对应的记录的GDT表, 之后根据表找到自己的使用的内存, 加上偏移量之后就是实际的地址, 这一个地址会通过分页机制里面的页表去获取实际的物理地址, 页表的地址放在CR3的寄存器里面
这样可以实现用户使用内存的时候感觉是一块连续的内存, 但是实际的内存可能是不连续的
处理器在访问数据、获取指令时,使用的都是线性地址,只要它是连续的就可以了,最终都能够通过映射表找到实际的物理地址。
实现虚拟内存
在使用多任务的时候不同任务感觉自己访问的地址是相同的, 但是实际是不同的, 这是通过页表实现的, 不同任务可以有不同的页表, 把一个相同的地址定位到不同的物理地址
这一个表记录在CR3里面, 在任务切换的时候可以进行切换
使用TSS进行任务切换
实际配置
实际使用的时候可以使用一级页表(每一块是4MB)和二级页表(每一块是4KB大小)
- 使用4KB的模式, 使用二级页表
使用这一个地址的不同段去获取实际的页表
- 使用4M的模式: 使用一级页表
会根据传过来的数据的地址(逻辑地址)分段之后进行访问不同的页表, 获得一个4KB的空间的地址, 最后通过偏移量进行实际的访问
这一个表的地址在CR3里面
CR3以及使用4M模式的一级页表格式(只有这一个表, 不需要二级页表)
使用4KB模式的时候的一二级页表格式
使用CR4以及CR0控制实际使用的模式以及页表的开启
这一个位控制使用分页
这一个是实际的打开时候的寄存器状态
使用这一个位开启4MB的模式
同时满足这两个条件的时候可以使用4MB的一级页表
实际的实现
第一级映射(页目录表PDE)有两种的格式, 一种是4MB的映射, 一种是4KB的映射使用4MB模式的时候, 就不需要二级页表了, 只有一个表, 最后可以使用的内存实际上是4MB, 使用4KB模式的时候会使用两级页表, 最后实际控制的内存大小是4GB
第二级映射(页表PTE)
实际实现一级映射(4MB)
需要在打开页表之前实现映射, 否则CPU会找不到对应的内存, 直接映射到0地址的位置
//这个表是否有效
#define PDE_P (1<<0)
//是否可写
#define PDE_W (1<<1)
//是否可以被低权限访问
#define PDE_U (1<<2)
//设置使用的模式(4M模式)
#define PDE_PS (1<<7)
//定义一个页表的结构体,需要设置低0的表项
//这一个设置的是逻辑地址0地址的分页, 是一个恒等的映射, 使得代码的访问正常, 映射 的地址还是0
uint32_t pg_dir[1024] __attribute__((aligned(4096))) = {
[0] = (0) | PDE_P | PDE_W | PDE_U | PDE_PS;
};
_start_32:
//在这里设置段地址
mov $KERNEL_DATA_SEG, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov %ax, %gs
mov %ax, %fs
mov $_start, %esp
//打开页表, 记录位置
mov $pg_dir, %eax
mov %eax, %cr3
//CR4里面有一个位控制是否允许这一个模式
mov %cr4, %eax
orl $(1<<4), %eax
mov %eax, %cr4
//还需要控制PR0最高位w为1
mov %cr0, %eax
orl $(1<<31), %eax
mov %eax, %cr0
jmp .
分页打开后, 可以使用这一个命令查看映射关系, 权限是u: 用户 r: 读 w: 写
实际实现二级映射
一级表, bit7为0
二级表, bit7为1
//这个表是否有效
#define PDE_P (1<<0)
//是否可写
#define PDE_W (1<<1)
//是否可以被低权限访问
#define PDE_U (1<<2)
//设置使用的模式4KB/4MB
#define PDE_PS (1<<7)
//新建另一个映射的地址
#define MAG_ADDR 0x80000000
//使用二级表进行控制内存测试, 这里是实际上的地址
uint8_t map_phy_buffer[4096] __attribute__((aligned(4096))) = {0x36};
//创建一个二级表项,随便给一个值,在后面会进行设置,随便初始化一个值连接器会把其他的位置设置为0,否则会为随机的
static uint32_t page_table[1024] __attribute__((aligned(4096))) = {PDE_U};
//定义一个页表的结构体,需要设置低0的表项
uint32_t pg_dir[1024] __attribute__((aligned(4096))) = {
[0] = (0) | PDE_P | PDE_W | PDE_U | PDE_PS,
};
void os_init(void){
//设置一级表,使用的是表的高10位,这里会找到想要的虚拟地址所在的位置,设置为二级表的位置
/********************************************************************************/
//计算一下4KB的话对应的表项 = 二级表项地址+权限(这里没有使用4M的映射(PDE_PS))
pg_dir[MAG_ADDR>>22] = (uint32_t)page_table | PDE_P | PDE_W | PDE_U;
/********************************************************************************/
//初始化表的二级,这里是实际的地址,之后需要设置对应的位置,这里会设置二级表指向的是上面的数组
page_table[(MAG_ADDR>>12)&0x3ff] = (uint32_t)map_phy_buffer | PDE_P | PDE_W | PDE_U;
}
实际的地址计算
_start_32:
//在这里设置段地址
mov $KERNEL_DATA_SEG, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov %ax, %gs
mov %ax, %fs
mov $_start, %esp
//在这里调用设置4KB的分页表
call os_init
# 打开页表机制
mov $pg_dir, %eax
mov %eax, %cr3
//CR4里面有一个位控制是否允许这一个模式(这一个没用上)
mov %cr4, %eax
orl $(1<<4), %eax
mov %eax, %cr4
//还需要控制PR0最高位w为1
mov %cr0, %eax
orl $(1<<31), %eax
mov %eax, %cr0
jmp .
使用这一个命令查看现有的映射
在修改之后发现两个位置是同步的, 可以直接操控第二个映射地址或者采用第一个映射的地址
总结
也就是说,在没有开启分页机制时,由程序员给出的逻辑地址,需要先通过分段机制(GDT表)转换成物理地址。但在开启分页机制后,逻辑地址仍然要先通过分段机制进行转换,只不过转换后不再是最终的物理地址,而是线性地址,然后再通过一次分页机制转换,得到最终的物理地址。
GDT表使用