x86 系列处理器上的页式内存管理
硬件层直接支持内存分页机制 ,我们需要做的工作仅仅变为配置页表
默认情况下不使用 分页机制(段式内存管理)
分页机制启动后,使用二级页表 对内存进行管理
x86 系列处理器的分页方式 (32位)
x86分页机制示意图
页目录大小=2^10项=1024项,共计2^10*4字节=2^12字节=4K
子页表大小=2^10项=1024项,共计2^10*4字节=2^12字节=4K
页大小=2^12字节=4K
—些重要结论(针对 32 位 x86 处理器)
页目录占用 1 内存页4K (1内存页 可访问1024 个子页表)
单个子页表占用1 内存页(内存页又可以可访问1024 个页面)
页面起始地址按4K 字节对齐 (每个内存页大小4K,页面起始地址总是4096 整数倍)
32位系统分页后可访问的虚拟内存空间为: 4K *( 1024 * 1024 ) = 4G
x86系列处理器上的页属性 由于物理页面的起始地址按照4K(4096)字节对齐 ,而我们页表存储的是每个物理页的起始地址,因此页表里每一项的低12位理论上应该全为0(恰被4K整除)
既然低12位在寻址上派不上用场,而我们可以使用低12位进行属性描述,描述物理页的属性,比如这个物理页是否加载到内存里面,是否可读可写,页面特权级等
实战构建 最简单的分页构建大小 这里是顺序排列的,一个for循环即可完成构建
页目录每项之间就刚好差一个内存页的大小4K,因此后一项只需比前一项大4096
同样地,子页表每项存储的是最终物理页的地址,每个物理页(内存页)的大小是4K,因此后一项只需比前一项大4096
x86对分页的硬件支持 只需要在保护模式开启一些选项就可以让硬件支持x86的分页机制了,我们需要做的仅仅变为配置好页表,将cr3寄存器指向我们配置好的页表即可
将cr3指向页目录地址(可切换不同页目录)
将cr0最高位置为1(硬件级开启分页机制)
1 2 3 4 5 mov eax, PageDirBase mov cr3, eax mov eax, cr0 or eax, 0x80000000 ;最高位置为1 mov cr0, eax
loop指令:循环指令,将cx减1,若cx不为0,则执行标签处代码
1 2 3 4 5 6 mov ax, 0 mov cx, 10 label: add ax, cx loop label
stosb/stosw/stosd
把al/ax/eax中的值存储到 edi 指向的内存单元(ES:DI)中
同时 edi 的值根据方向标志增加或者减少 (cld/ std)
1 2 3 4 5 mov es, ax mov edi, 0 mov eax, 0xFF ; 0xFF ---> [es : 0] cld ; edi: 0->4 stosd
二级页表构建最终实现 这里简单的定义了二级页目录表和子页表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 PageDirBase equ 0x200000 PageTblBase equ 0x201000 org 0x9000 jmp ENTRY_SEGMENT [section .gdt] ; GDT definition ; 段基址, 段界限, 段属性 PAGE_DIR_DESC : Descriptor PageDirBase, 4095, DA_DRW + DA_32 PAGE_TBL_DESC : Descriptor PageTblBase, 1023, DA_DRW + DA_LIMIT_4K + DA_32 ; GDT end ; GDT Selector PageDirSelector equ (0x0005 << 3) + SA_TIG + SA_RPL0 PageTblSelector equ (0x0006 << 3) + SA_TIG + SA_RPL0 SetupPage: push eax push ecx push edi push es mov ax, PageDirSelector mov es, ax mov ecx, 1024 ; 1K sub page tables ;1024个目录条目 mov edi, 0 mov eax, PageTblBase | PG_P | PG_USU | PG_RWW cld ; 构建二级页表目录 stdir: stosd ; PageTbIBase + 4096 * x( x>=0 && x<1024 ) ---> [es : edi] add eax, 4096 loop stdir ;构建子页表 mov ax, PageTblSelector mov es, ax mov ecx, 1024 * 1024 ; 1M pages ;1024*1024总共这么多个条目 mov edi, 0 mov eax, PG_P | PG_USU | PG_RWW ;eax初始值是0 cld ;子页表赋值 sttbl: stosd add eax, 4096 loop sttbl ;开启x86的分页机制 mov eax, PageDirBase mov cr3, eax mov eax, cr0 or eax, 0x80000000 mov cr0, eax pop es pop edi pop ecx pop eax ret
引出的问题
进入32位实模式代码后, 应该什么时候调用call SetupPage 启动分页机制, 并建立页表?
在位置 <1> 和位置 <2> 调用 SetupPage的执行效果是否相同? 为什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ;<1> call SetupPage mov ax, Data32Selector mov ds, ax mov ebp, DTOS_OFFSET mov bx, 0x0C mov dh, 12 mov dl, 33 call PrintString mov ebp, HELLO_WORLD_OFFSET mov bx, 0x0C mov dh, 13 mov dl, 31 call PrintString ;<2> call SetupPage
经过编译运行测试发现并无任何不同,那么是什么原因呢?
这里假设要访问的虚拟地址为32位0x00804ABC,试着计算出物理地址是什么 高0~9
位和10~19
位代表查找页目录的第几目录和查找子页表第几项
1 2 3 4 5 0x00804 0000000010 0000000100 2 4 页目录第2项 子页表的第4项 最终物理地址=4096*(1024*2+4)+0xABC=0x00804ABC
当前的分页方式使得: 任意虚地址都被直接映射为物理地址 ,物理地址的值==虚地址的值,做了无用之功
因此, SetupPage是否调用, 以及调用位置不影响执行结果。
页表验证实验 根据虚拟地址计算: k,j,offset
读取内存: PageDirBase + k * 4 得到PageTbIBase
读取内存: PageTbIBaseJ + j*4 得到 PageBase
计算物理内存: PageBase + offset
bochs调试
x /1wx 0x200008
查看物理地址0x200008的4个字节(每个条目占用4个字节)的内容,可知子页表的起始物理地址0x00203000(低12位忽略不计)
x /1wx 0x00203010
查看物理地址0x00203010的4个字节的内容,可知虚拟地址0x00804ABC对应的物理页的起始地址0x00804000(低12位忽略不计)
1 2 3 4 5 6 7 <bochs:5> x /1wx 0x200008 [bochs]: 0x0000000000200008 <bogus+ 0>: 0x00203007 <bochs:6> x /1wx 0x00203010 [bochs]: 0x0000000000203010 <bogus+ 0>: 0x00804007
验证实验:0x00804ABC
计算得到k=2,j=4,offset=0xABC
读取内存:0x200000 + 2 * 4 = 0x200008得到PageTbIBase0x203000
读取内存: 0x203000+ 4*4 = 0x203010得到PageBase0x00804000
计算物理内存: 00804007+ 0xABC = 0x00804ABC
页表映射实验 实验内容
拥有多个任务,因此需要创建多个页表,可自由切换当前使用的页表
不同的页表会将同一个虚拟地址映射到不同的物理地址
加载不同的页表,并且读取同一个虚拟地址中的内容并且观察不同
创建InitPageTable函数,通过页目录地址,页表起始地址构建二级页表
创建SwitchPageTable函数,根据页目录起始地址切换页表,开启分页机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 ; eax --> page dir base selector ;页目录选择子 ; ebx --> page table base selector ;页表选择子 ; ecx --> page table base ;页表起始地址 InitPageTable: push es push eax ; [esp + 12] push ebx ; [esp + 8] push ecx ; [esp + 4] push edi ; [esp] mov es, ax ; es = 页目录选择子 mov ecx, 1024 ; 1024次 mov edi, 0 mov eax, [esp + 4] ; eax = ecx = 页表起始地址 or eax, PG_P | PG_USU | PG_RWW ; or添加属性 cld stdir: ; 循环初始化好页目录1024项目录 stosd ; eax = PageTbIBase + 4096 * x( x>=0 && x<1024 ) ---> [es : edi] add eax, 4096 loop stdir mov ax, [esp + 8] ; es = 页表选择子 mov es, ax mov ecx, 1024 * 1024 ; 1M pages ;1024个页表 x 1024个项 mov edi, 0 mov eax, PG_P | PG_USU | PG_RWW ;eax初始值是0 cld sttbl: stosd ; 循环初始化1024个子页面1024项物理地址 add eax, 4096 ; eax --> [es : edi] loop sttbl pop edi pop ecx pop ebx pop eax pop es ret ;eax --> page directory base 页目录起始地址 ;x86硬件支持页表,配置好页表后,将cr3寄存器指向我们配置好的页表即可 SwitchPageTable: push ax mov eax, cr0 and eax, 0x7FFFFFFF ;暂时关闭分页机制 mov cr0, eax ;cr0最高位置为0 mov eax, [esp] ;页目录基地址放入eax mov cr3, eax ;cr3是指向页目录的指针,重新赋值 mov eax, cr0 or eax, 0x80000000 ;cr0最高位置为1 mov cr0, eax pop eax ret
指定物理内存写入数据 x86早期寄存器只有16位,2的16次方即64K,为了访问1M内存就需要两个寄存器段基址+偏移地址方式访问。
后来支持32位了,一个寄存器就可以直接访问4G内存,仅仅使用一个寄存器就可以访问4G的物理内存,但是段式内存管理仍然保留下来了,而如何实现直接4G物理内存直接访问达到“指哪打哪”的效果呢?
可以添加一个选择子,选择子起始地址为0,选择子段界限为整个物理内存空间大小,这个物理内存看作一个段处理,这样就可以“指哪打哪”,这就是平坦内存模型
1 2 3 4 5 6 7 8 1.段描述符 FLAT_MODE_RW_DESC : Descriptor 0, 0xFFFFF, DA_DRW + DA_LIMIT_4K + DA_32 2.选择子 FLatModeRMSeLector equ (0x0009 << 3) + SA_TIG + SA_RPL0 3.内存访问 mov ax, FLatModeRMSeLector mov es, ax mov ecx, [es:0x00FF00FF] ; 注意直接访问,不要打开x86的页表访问功能
实现思路
准备数据 在地址 0x501000处写入字符串 (“D.T.OS!”) 在地址 0x601000处写入字符串 (“HelloWorld!”)
改写页表 在页表0 中将 0x401000 映射到 0x501000 在页表1 中将 0x401000 映射到 0x601000
SwitchPageTable : Page0 ==> Printstring: 0x401000 “D.T.OS!” SwitchPageTable : Page1 ==> Printstring : 0x401000 “Hello World!”
MemCpy32(32位内存拷贝函数) 实现其实和先前实现的16位下内存拷贝函数十分接近,区别在于寄存器换成32位大小的寄存器,并且传入的选择子应该是平坦模式的全局内存访问的选择子,注意调用此函数不可开启x86系统下页表机制,此函数才能够“指哪打哪”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 ; es --> flat mode selector ; ds:esi --> source ; es:edi --> destination ; ecx --> length MemCpy32: push esi push edi push ecx push ax cmp esi, edi ja btoe add esi, ecx add edi, ecx dec esi dec edi jmp etob btoe: cmp ecx, 0 jz done mov al, [ds:esi] mov byte [es:edi], al inc esi inc edi dec ecx jmp btoe etob: cmp ecx, 0 jz done mov al, [ds:esi] mov byte [es:edi], al dec esi dec edi dec ecx jmp etob done: pop ax pop ecx pop edi pop esi ret
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 PageDirBase0 equ 0x200000 PageTblBase0 equ 0x201000 PageDirBase1 equ 0x300000 PageTblBase1 equ 0x301000 ObjectAddrX equ 0x401000 TargetAddrY equ 0x501000 TargetAddrZ equ 0x601000 DATA32_SEGMENT: DTOS db "D.T.OS!", 0 DTOS_LEN equ $ - DTOS DTOS_OFFSET equ DTOS - $$ HELLO_WORLD db "Hello World!", 0 HELLO_WORLD_LEN equ $ - HELLO_WORLD HELLO_WORLD_OFFSET equ HELLO_WORLD - $$ CODE32_SEGMENT: ;...... mov ax, FlatModeRWSelector mov es, ax mov esi, FYOS_OFFSET mov edi, TargetAddrY mov ecx, FYOS_LEN call MemCpy32 mov esi, HELLO_WORLD_OFFSET mov edi, TargetAddrZ mov ecx, HELLO_WORLD_LEN call MemCpy32
可以看到MemCpy32还是正确执行了的
MapAddress(改写虚拟地址映射目标物理地址) 以虚拟地址为0x401000修改两个页表映射的物理地址为0x501000和0x601000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 ; es --> flat mode selector ; eax --> virtual address ; ebx --> target address ; ecx --> page directory base MapAddress: push edi push esi push eax push ebx push ecx ;0000000001 0000000001 000000000000 ;k = 1 页表位置===> [PageDir + 1*4] ;j = 1 目标物理页地址===> [[PageDir + 4] + 1 * 4] ;1. 取虚地址高10位,计算子页表的页目录位置 mov eax, [esp + 8] shr eax, 22 and eax, 1111111111b shl eax, 2 ;偏移位置要乘以4才是 ;2. 取虚地址中间10位,取出子页表内容,计算物理地址在子页表中的位置 mov ebx, [esp + 8] shr ebx, 12 and ebx, 1111111111b shl ebx, 2 ;3. 取子页表起始地址 mov esi, [esp] ;页目录起始地址 pageDirBase add esi, eax ;加上eax偏移地址 目标偏移地址pageDirBase+1*4 mov edi, [es : esi] ;取偏移地址四个字节即子页表起始地址放到edi and edi, 0xFFFFF000 ;低12位清0 目标偏移地址[pageDirBase+1*4] ;4. 将目标地址写入子页表的对应位置 add edi, ebx ;加上偏移地址[PageDirBase + 4] + 1 * 4 mov ecx, [esp + 4] ;取出想要写入的物理地址 and ecx, 0xFFFFF000 or ecx, PG_P | PG_USU | PG_RWW mov [es:edi], ecx ;目标物理地址写入到虚拟地址映射位置[[PageDirBase + 4] + 1 * 4] pop ecx pop ebx pop eax pop esi pop edi ret
主函数内调用修改内存映射函数
1 2 3 4 5 6 7 8 9 10 11 mov eax, ObjectAddrX ; 0x401000 mov ebx, TargetAddrY ; 0x501000 mov ecx, PageDirBase0 call MapAddress mov eax, ObjectAddrX ; 0x401000 mov ebx, TargetAddrZ ; 0x601000 mov ecx, PageDirBase1 call MapAddress
读取0号页目录的1号页的物理内存,PageDirBase0 + 4=0x200000 + 0x4=0x200004得到0x202004
读取目标物理页0x00202004的物理内存得到0x00501000符合预期,是我们想要修改的数据
1 2 3 4 5 6 <bochs:3> x /1wx 0x200004 [bochs]: 0x0000000000200004 <bogus+ 0>: 0x00202007 <bochs:4> x /1wx 0x00202004 [bochs]: 0x0000000000202004 <bogus+ 0>: 0x00501007
读取1号页目录的1号页的物理内存PageDirBase1 + 4=0x300000 + 0x4=0x300004得到0x302004
读取目标物理页0x302004的物理内存得到0x00601000符合预期,是我们想要修改的数据
1 2 3 4 5 6 <bochs:6> x /1wx 0x300004 [bochs]: 0x0000000000300004 <bogus+ 0>: 0x00302007 <bochs:7> x /1wx 0x00302004 [bochs]: 0x0000000000302004 <bogus+ 0>: 0x00601007
如果虚地址映射的实地址是一个函数入口,那么是否可以进行函数调用?
定义数据段,然后编写内存函数,在平坦模式下将函数拷贝过去就可以了