x86 系列处理器上的页式内存管理

  • 硬件层直接支持内存分页机制,我们需要做的工作仅仅变为配置页表
  • 默认情况下不使用分页机制(段式内存管理)
  • 分页机制启动后,使用二级页表对内存进行管理

x86 系列处理器的分页方式 (32位)

image-20220523202731834

x86分页机制示意图

image-20220523202746241

页目录大小=2^10项=1024项,共计2^10*4字节=2^12字节=4K

子页表大小=2^10项=1024项,共计2^10*4字节=2^12字节=4K

页大小=2^12字节=4K

—些重要结论(针对 32 位 x86 处理器)

  • 页目录占用 1 内存页4K1内存页可访问1024 个子页表)
  • 单个子页表占用1 内存页(内存页又可以可访问1024 个页面)
  • 页面起始地址按4K 字节对齐 (每个内存页大小4K,页面起始地址总是4096 整数倍)
  • 32位系统分页后可访问的虚拟内存空间为: 4K *( 1024 * 1024 ) = 4G

x86系列处理器上的页属性

由于物理页面的起始地址按照4K(4096)字节对齐,而我们页表存储的是每个物理页的起始地址,因此页表里每一项的低12位理论上应该全为0(恰被4K整除)

既然低12位在寻址上派不上用场,而我们可以使用低12位进行属性描述,描述物理页的属性,比如这个物理页是否加载到内存里面,是否可读可写,页面特权级等

image-20220523203919587

image-20220523203933097

实战构建

最简单的分页构建大小

这里是顺序排列的,一个for循环即可完成构建

页目录每项之间就刚好差一个内存页的大小4K,因此后一项只需比前一项大4096

同样地,子页表每项存储的是最终物理页的地址,每个物理页(内存页)的大小是4K,因此后一项只需比前一项大4096

image-20220523203455744

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

引出的问题

  1. 进入32位实模式代码后, 应该什么时候调用call SetupPage 启动分页机制, 并建立页表?
  2. 在位置 <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

image-20220523214642400

当前的分页方式使得: 任意虚地址都被直接映射为物理地址,物理地址的值==虚地址的值,做了无用之功

因此, 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

页表映射实验

实验内容

  • 拥有多个任务,因此需要创建多个页表,可自由切换当前使用的页表
  • 不同的页表会将同一个虚拟地址映射到不同的物理地址
  • 加载不同的页表,并且读取同一个虚拟地址中的内容并且观察不同

image-20220524165139253

创建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!”

image-20220524171931312

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

image-20220524173209226

可以看到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

如果虚地址映射的实地址是一个函数入口,那么是否可以进行函数调用?

定义数据段,然后编写内存函数,在平坦模式下将函数拷贝过去就可以了