好长时间没有更新了,最近比较忙。。。。。。
内存分页可以放在C代码中,这样比较方便编写!即loader执行完后进入kernel_main函数之后在分配内存分页!
一、地址
讲到内存必然要讲到计算机中经常提到的一些地址。物理、线性、虚拟、逻辑、有效地址 (点击可查看具体内容)
再需要了解内存的分段和分页机制:内存管理分段和分页机制
保护模式下地址转换原理图:(可以先了解最后在深入)
原理简述过程:
保护模式下(保护模式下必分段,只不过现在寄存器和地址线位数都很大,可以进行平坦模式,即段地址为0!32位地址线可以直接寻到4G,32位寄存器可以直接装下)开始分页后,都是逻辑地址--》线性地址--》物理地址,本质,CPU最终还是会在物理内存中取值和赋值。
如上图中执行命令【mov 0X1A478BFF,%ebx】,我们编写程序的逻辑地址[只是段内偏移地址]如(0X1A478BFF,因为要取这个地址在内存中的值,然后在存储到EBX存储器中) ,要取这个地址在内存中的值,需要找到段基址,那么通过DS寄存器存储的段选择子和GDTR找到GDT表中的对应的段描述符 ,这样就找到了段基址,在加上0X1A478BFF,那么此时 线性地址= 段基址+0X1A478BFF,这样过程就由逻辑地址--》线性地址 了。【当然实际中 CPU内部有缓存寄存器,这样速度就快很多了】
首先逻辑地址--》线性地址,在第五讲中提到GDT,它是全局段描述符表,内部是一个段描述符形成的数组,GDTR存放内存中GDT表的起始地址[实模式下是真实的物理地址,保护模式下则是线性地址,开启了分页才出现了虚拟地址,分页下线性地址和虚拟地址是站在不同角度的相似概念],第五讲中在进入保护模式前,加载了GDTR寄存器,此时存储的是物理地址!本节内存分页后还需要将重新刷新GDTR,使其变为虚拟地址【线性地址】!
需要通过分页机制来映射的线性地址便有了一个高大上的名字,虚拟地址 !
分页机制的作用有两方面
1. 将线性地址转换成物理地址
2. 用大小相等的页代替大小不等的段
总结:X86两种模式下地址到底是怎么回事?(重新在回顾一下)
线性地址给CPU看的 。CPU不需要知道有多少外设,什么种类的外设,反正它都是用地址来访问,线性地址能让CPU把任何设备当成内存。
逻辑地址是给程序的,不需要知道硬件是怎么设计的。
物理地址是给实际的硬件看的。
虚拟地址:内存的思想是程序、数据、堆栈的总大小可以超过实际可用物理内存的大小,操作系统把程序当前使用的那部分保存在物理内存当中,而其他部分保存在硬盘上。虚拟内存的实现基于分页技术。
逻辑地址要经过操作系统转换成线性地址给CPU,CPU发出线性地址给MMU再将线性地址进行转换得到物理地址去访问设备。
当CPU在实模式下时,物理地址 = 段寄存器:偏移地址
当CPU在保护模式下时,必分段,在由实模式转到保护模式前,必须设置段描述符表和GDT,段描述符存在段描述符表中,此时GDTR使用的是肯定是物理地址【实模式下都是段:偏移形成物理地址,GDTR中32位基址可以直接存储实模式下任何地址】;然后在开启CR0寄存器标志位则进入保护模式;因为保护模式要用到段描述符表GDT,用GDTR找到它,最终才能找到段基址。
进入保护模式后:【开启分页后需要再重新更新GDT,此时GDTR使用的是线性地址】
1、未开启分页时,在保护模式下的 线性地址= 段描述符表中的基址【段选择子】 + 偏移【逻辑地址】= 物理地址;
2、开启分页时,在保护模式下的 线性地址= 段描述符中的基址【段选择子】 + 偏移【逻辑地址】,由于大部分段描述符中的基地址为0,所以此时的线性地址=程序的逻辑地址,这时候CPU需要将线性地址经过CPU内部MMU转换为物理地址才能访问内存;
CR3存储页目录地址【物理地址】,如果存储的是线性地址的话,这样的话就会递归,因为线性地址转物理地址必须要用到CR3才能找到页目录首地址。
现代系统中GDT表中基地址都设置为0,那么这样就是平坦模式!这样的话逻辑地址就等于线性地址了!!
二、实模式下内存分布图
因为我们此时编写系统还处在实模式下,那么我们需要简单了解下实模式下的内存分布图。实模式下的内存布局
三、内存分页基础理论
3.1为什么要进行内存分页
目前我们的小kernel还一直在分段机制下工作,因为还只有一个loader在内存中跑,所以不会出现内存不足的问题。假如说此时未开启分页功能,而物理内存空间又不足了,如下图
此时进程C想要执行,但是内存空间已经不足。要么就等待进程A或者进程B执行完成,这样就有连续的内存空间了。要么就讲进程A的A3段或者进程B的B1段换到硬盘上,腾出一部分空间,同样可以容纳进程C执行
等待是极其不好的用户体验,那么只能将段置换到硬盘上了,但是段的大小并不固定,如何段过大,那么IO操作过多,机器的响应速度就会非常慢。
出现这种情况的本质其实是在分段机制下,线性地址等价于物理地址。那么即使在进程B的下面还有10M的可用空间,但因为两块可用空间并不连续,所以进程C无法使用进程B下面的10M可用空间。
按照这种思路,只需要通过某种映射关系,将线性地址映射到任意的物理地址,就可以解决这种问题了。实现线性地址的连续,而物理地址不需要连续,于是分页机制就诞生了。
3.2一级页表
分页机制是工作在分段机制下的,在保护模式下,通过选择子找到段基址,通过段基址:段内偏移的方式组合成线性地址,拿到线性地址之后会根据是否开启分页来找到实际的物理地址,用一副图来解释更加清晰
分页机制的作用有两方面
1. 将线性地址转换成物理地址
2. 用大小相等的页代替大小不等的段
如图所示:
需要通过分页机制来映射的线性地址便有了一个高大上的名字,虚拟地址
假设我们通过逐字节的映射方式
那么页表中会存放4GB个页表项,页表的大小=4GB*4=16GB,这样显然不合理,一页不能只占1B
我们需要平衡页的大小与页的数量的关系,因为页大小*页数量=4GB,想要减少页表的大小,只能增加一页的大小。最终通过数学求极限,定下4KB为最佳页大小
这种情况下,4GB的内存被划分为1MB个内存块,每个内存块的大小为4KB,
页表和内存的映射关系如图
有了页表之后,如何将线性地址转换成物理地址呢?
在一级页表下,线性地址的高20位被用作页表项的索引,也就是类似于数组下标的东西。通过该索引在页表中找到对应页的物理地址,然后将该物理地址+线性地址的低12位组成真正的物理地址。过程如图所示
3.3二级页表
无论是几级页表,标准页的尺寸都是4KB,这个是不会变的。所以4GB的线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中,每个页表包含有1K个页表项。页表项是4字节大小,故页表的大小同样为4KB
既然将原本的一个页表划分出了1K个页表,这些页表就必须进行统一管理。为此,专门有一个页目录表来存放这些页表。页目录表中存储的页表称为页目录项(PDE), 页目录项同样为4KB,且最多有1K个页目录项,所以页目录表也是4KB
二级页表的模型如图
二级页表与一级页表原理虽然相同,但在结构上有了很大的差异,所以虚拟地址到物理地址的转换方式上也发生了很大变化
首先通过虚拟地址的高10位在页目录表中定位一个页表,也就是定位也目录项,然后通过虚拟地址的中间10位在之前定位的页表中找到物理页所在的位置,最后虚拟地址剩下的12位作为找到的物理页的页内偏移
地址转换过程如图所示
3.4PDE与PTE的结构
P位:存在位,为1时表示该页在内存中
图中的存在位确定了一个页表是否可以用于地址转换过程(由线性地址转物理地址),P=1表示该项可用。当目录表项或第二级表项的P=0时,则该表项是无效的,不能用于地址转换过程(由线性地址转物理地址)。
当该表项的所有其他位都可供程序使用,处理器不对这些位进行测试。
当CPU试图使用一个页表项进行地址转换时,如果此时任意一级页表项的P=0,则处理器就会发出页异常信号,此时缺页中断异常处理程序就可以把所请求的页加入到物理内存中,并且导致异常的指令会被重新执行。
RW:读写位,为1时可读可写,为0是可读不可写
US:普通用户/超级用户位,为1时表示处于用户级,也就是3级特权级
RW和US位并不用于地址转换,是用于分页级的保护机制,是由CPU在地址转换过程中同时操作的。
PWT:页级通写位,为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存
PCD:页级高速缓存禁止位,为1表示该页启用高速缓存
A:访问位,为1表示该页被CPU访问过
D:脏页位/已修改位,当CPU对一个页面执行写操作,此为被赋1
A和D位用于提供有关页使用的信息。除了页目录中的已修改位,这些位将由硬件置位,但不复位。
在对一页内存进行读或者写操作之前,CPU将设置有关的目录和二级页表项的已访问位,在向一个二级页表项所涵盖的地址进行写操作之前,处理器将设置该二级页表项的已修改位,而页目录中的已修改位D是不使用的。
当实际使用内存超过物理内存时,内存管理程序可以使用这些位来确定哪些页可以从内存中取走,腾出空间。内存管理程序还需要负责测试和复位这些比特位。
PAT:页属性表位,能够在页面一级的粒度上设置内存属性
G:全局位,为1表示该页在高速缓存TLB中一直保存
四、内存分页实战
启用分页机制需要完成下面三步
1. 准备好页目录表和页表
2. 将页表地址写入控制寄存器Cr3
3. 将寄存器Cr0的PG位置
4. 创建页目录表和页表
这里我们先将其提前,用汇编方式进行简单内存分页。步骤如下:
1.初始化页目录所占的空间为0;
2.给这个页目录所在的空间中所占的第0目录项、第768目录项、第1023目录项赋初值;
3.再将物理地址0~1M内存分给页目录0项的第0~255页表项
汇编源码如下:
;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS ; PAGE_DIR_TABLE_POS = 0x100000
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性
mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。
; 下面将页目录项0和0xc00都存为第一个页表的地址,
; 一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
; 第1个目录项,在页目录表中的第0个目录项写入第一个页表的位置(0x101000)及属性(7)
mov [PAGE_DIR_TABLE_POS + 0x0], eax
; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
;页目录第768项指向第一个页表
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
sub eax, 0x1000
; 使最后一个目录项指向页目录表自己的地址
mov [PAGE_DIR_TABLE_POS + 4092], eax
;下面创建页表项(PTE)
;把1M低端内存存储在第一页目录所指向的页表0~页表255内,每页4K
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte: ; 创建Page Table Entry
mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
add edx,4096
inc esi
loop .create_pte
;创建内核其它页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
指定了物理地址1M的页表 ,具体示意图如下:
程序代码如下;
1.mbr.s跟上一节中一样。
2.loader.s
;-------------- gdt描述符属性 -----------
DESC_G_4K equ 1_00000000000000000000000b
DESC_D_32 equ 1_0000000000000000000000b
DESC_L equ 0_000000000000000000000b ; 64位代码标记,此处标记为0便可。
DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暂置为0
DESC_LIMIT_CODE2 equ 1111_0000000000000000b
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b
DESC_P equ 1_000000000000000b
DESC_DPL_0 equ 00_0000000000000b
DESC_DPL_1 equ 01_0000000000000b
DESC_DPL_2 equ 10_0000000000000b
DESC_DPL_3 equ 11_0000000000000b
DESC_S_CODE equ 1_000000000000b
DESC_S_DATA equ DESC_S_CODE
DESC_S_sys equ 0_000000000000b
;x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0.
DESC_TYPE_CODE equ 1000_00000000b
;x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0.
DESC_TYPE_DATA equ 0010_00000000b
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b
;-------------- 选择子属性 ---------------
RPL0 equ 00b
RPL1 equ 01b
RPL2 equ 10b
RPL3 equ 11b
TI_GDT equ 000b
TI_LDT equ 100b
;--------------------------------------------
;---------------- 页表相关属性 --------------
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
PAGE_DIR_TABLE_POS equ 0X100000
;----------------------------------------------
section loader vstart=0x900
jmp loader_start
;构建gdt及其内部的描述符===============================================
GDT_BASE: dd 0x00000000
dd 0x00000000
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
VIDEO_DESC: dd 0x80000007 ; limit=(0xbffff-0xb8000)/4k=0x7
dd DESC_VIDEO_HIGH4 ; 此时dpl为0
GDT_SIZE equ $ - GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0 ; 此处预留60个描述符的空位(slot)
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 8[1000] 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 16[10000] 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 24[11000] 同上
;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
;======================================================================
loader_start:
;----------------- 准备进入保护模式 -------------------
;1 打开A20
;2 加载gdt
;3 将cr0的pe位置1
;----------------- 打开A20 ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;----------------- 加载GDT ----------------
lgdt [gdt_ptr]
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
[bits 32]
p_mode_start:
mov ax, SELECTOR_DATA
mov ds, ax
mov es, ax
mov ss, ax
mov esp,0x900
mov ax, SELECTOR_VIDEO
mov gs, ax
; 创建页目录及页表并初始化页内存位图
call setup_page
;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
sgdt [gdt_ptr] ; 存储到原来gdt所有的位置
;将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2]
or dword [ebx + 0x18 + 4], 0xc0000000 ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
;段描述符的高4字节的最高位是段基址的31~24位
;将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000
add esp, 0xc0000000 ; 将栈指针同样映射到内核地址
; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载
;160=80个字符[80*2]
mov byte [gs:160*8], 'J'
mov byte [gs:160*8+1],0x02
mov byte [gs:160*8+2], 'a'
mov byte [gs:160*8+3],0x02
mov byte [gs:160*8+4], 'd'
mov byte [gs:160*8+5],0x02
mov byte [gs:160*8+6], 'e'
mov byte [gs:160*8+7],0x02
mov byte [gs:160*8+8], ' '
mov byte [gs:160*8+9],0x00
mov byte [gs:160*8+10], 'O'
mov byte [gs:160*8+11],0x02
mov byte [gs:160*8+12], 'S'
mov byte [gs:160*8+13],0x02
;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr] ; 重新加载
mov byte [gs:320], 'V' ;视频段段基址已经被更新,用字符v表示virtual addr
mov byte [gs:321], 0x02
mov eax, 0xb8142 ;0XB8142=gs:322 段基址是0XB8000 偏移为0X142=322
mov byte [eax], 'S'
jmp $
;------------- 创建页目录及页表 ---------------
setup_page:
;先把页目录占用的空间逐字节清0
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性
mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。
; 下面将页目录项0和0xc00都存为第一个页表的地址,
; 一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址
;下面创建页表项(PTE)
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte: ; 创建Page Table Entry
mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
add edx,4096
inc esi
loop .create_pte
;创建内核其它页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret
程序说明图:
(1)是将硬盘中的MBR加载到内存0X7C00开始的位置,由BIOS完成
(2)mbr.s中将loader加载到内存0X900开始的位置
执行流程 BIOS[CS:IP 0XFFF0:FFF0]--》CS:IP[0:0X7C00]----(MBR将loader加载到0X900)--->CS:IP[0:0X900]---(loader中)
最后进行汇编编译
nasm mbr.S -o mbr 编译产生mbr文件
nasm loader.S -o loader 编译产生loader文件
编译后产生的两个文件放入C++工程中,在用上节中的C++代码进行编译结果生成boot.img文件,最后直接用Bochs进行测试,前面章节都有介绍如何操作,就不做讲解了!
实验结果: