本章节所有代码托管在miniOS_32
章节任务介绍
内核的结构
内核分为四个部分,分别是
- 初始化代码:初始化代码用于从BIOS那里接管处理器和计算机硬件的控制权,安装最基本的段描述符,初始化最初的执行环境。然后,从硬盘上读取和加载内核的剩余部分,创建组成内核的各个内存段。
- 内核代码段:内核代码段用于分配内存,读取和加载用户程序,控制用户程序的执行。
- 内核数据段:内核数据段提供了一段可读写的内存空间,供内核自己使用。
- 公共例程段:公共例程段用于提供各种用途和功能的子过程以简化代码的编写。这些例程既可以用于内核,也供用户程序调用
因此,mbr程序和loader程序其实就是内核的初始化代码,负责完成加载内核前的初始化工作。
上一节,我们在GDT中安装了四个最基本的段描述符(数据段、代码段、栈段、显存段),并完成了GDT的初始化。
本章我们将继续对loader程序加以改造,在正式加载内核之前完成所有准备工作,本章节的工作主要是
- 检测现有的实际物理内存
- 开启分页机制,让后续的内核在分页机制下工作
- 如何开启分页机制?
- 构建页表和页目录表
- 将页目录表的位置加载到控制寄存器cr3
- 置cr0寄存器的PG位为1,打开分页机制
- 分页机制测试
- 我们在分页机制开启后重新使用显存段在屏幕上打印字符,如果能够正常显示说明分页机制成功开启
获取物理内存容量
操作系统是计算机硬件的管家,它不仅要知道自己安装了哪些硬件,还得给出有效得当的管理措施,按照预定的一套管理策略使硬件资源得到合理的运用。但管理策略只是逻辑上的东西,真正干活的都是底层。
保护模式最“大”的特点就是寻址空间“大”,在进入保护模式之后,我们将接触到虚拟内存、内存管理等。但这些和内存有关的概念都建立在物理内存之上。为了在后期做好内存管理工作,我们首先需要知道自己有多少物理内存才行。
linux内核获取物理内存的方式本质上是通过调用BIOS中断0x15实现的,分别是BIOS中断的0x15的3个子功能,子功能号要放在寄存器EAX或AX中,如下:
- EAX=0xE820:遍历主机上全部内存。
- AX=0xE801:分别检测低15MB和16MB~4GB的内存,最大支持4GB
- AH=0x88:最多检测出64MB内存,实际内存超过此容量也按照64MB返回
本节我们主要使用BIOS中断的子功能0xE820获取内存
利用BIOS中断的子功能0xE820获取内存
- BIOS中断0x15的子功能 0xE820能够获取系统的内存布局
- 由于系统内存各部分的类型属性不同,BIOS 就按照类型属性来划分这片系统内存,所以这种查询呈迭代式,每次BIOS只返回一种类型的内存信息,直到将所有内存类型返回完毕。
- 子功能0xE820的强大之处是返回的内存信息较丰富,包括多个属性字段,所以需要一种格式结构来组织这些数据。
- 内存信息的内容是用地址范围描述符来描述的,用于存储这种描述符的结构称之为地址范围描述符(Address Range Descriptor Structure,ARDS)。
此结构中的字段大小都是4字节,共5个字段,所以此结构大小为 20字节。**每次int 0x15之后,BIOS就返回这样一个结构的数据。**注意,ARDS结构中用64位宽度的属性来描述这段内存基地址(起始地址)及其长度,所以表中的基地址和长度都分为低32位和高32位两部分。
其中的 Type 字段用来描述这段内存的类型,这里所谓的类型是说明这段内存的用途,即其是可以被操作系统使用,还是保留起来不能用。Type字段的具体意义见下
为什么 BIOS会按类型来返回内存信息呢?原因是这段内存可能是
- 系统的 ROM。
- ROM 用到了这部分内存。
- 设备内存映射到了这部分内存。
- 由于某种原因,这段内存不适合标准设备使用。
另外
- 由于我们在32位环境下工作,所以在ARDS结构属性中,我们只用到低32位属性。
- BaseAddrLow+LengthLow 是一片内存区域上限,单位是字节。
正常情况下,不会出现较大的内存区域不可用的情况,除非安装的物理内存极其小。这意味着,在所有返回的 ARDS 结构里,此值最大的内存块一定是操作系统可使用的部分,即主板上配置的物理内存容量。
0xe820调用说明
以下是BIOS中断0x15子功能0xe820的调用参数
代码目录结构
1
2
3
4
5
6
7
|
.
├── include
│ └── boot.inc //配置文件
├── loader.S //loader程序代码
├── Makefile //编译文件,可直接使用make进行编译代码
├── mbr.S //mbr程序代码
└── start.sh //编译脚本,如果不想使用make编译代码,也可以选择运行该脚本编译代码,该脚本的编译命令更加清晰
|
代码
调用步骤
- 填写好“调用前输入”的寄存器
- 执行中断调用int 0x15
- 在CF位为0的情况下,获取对应的返回结果
- 遍历获取到的ards结构体,计算最大内存容量
loader.S
上一节,我们为了测试loader程序在进入保护模式之前是在实模式下运行的,我们定义了一个loader_start函数进行测试,本节我们修改这个函数的实现,让其用于获取物理内存的代码
需要注意的是,代码中最终获取到的物理内存会被存储到total_mem_bytes变量中,该变量的定义位置如下所示
1
2
3
4
5
6
7
8
9
10
|
;------------- 构建选择子 -------------
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
total_mem_bytes dd 0 ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
;故total_mem_bytes在内存中的地址为0x900+0x200=0xb00
;该地址将来在内核中会被用到
|
之所以将该变量定义在这里,是因为刚好经过计算后,该变量在内存中的地址是0xb00,由于该变量在后边也会被用到,因此将变量定义在这里方便后续使用
以下是用于获取获取物理内存的代码
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
|
;------------- 定义记录ARDS结构体信息的变量 -------------
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
;EBX:ARDS后续值
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EDX:固定为签名标记,0x534d4150
;返回值
;CF:若cf为0,表示未出错,cf为1,表示调用出错
;EAX:字符串SMAP的ASCII码值,0x534d4150
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EBX:ARDS后续值,即下一个ARDS的位置。
;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构
loader_start:
xor ebx,ebx ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
mov edx,0x534d4150
mov di,ards_buf ;di存储缓冲区地址,即指向缓冲区首地址
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20 ;一个ards结构体的大小
int 0x15 ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
add di,cx ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
inc word [ards_nr] ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
cmp ebx,0 ;比较ebx中的值是否为0
jnz .e820_mem_get_loop ;若ebx不为0,则继续进行循环获取ARDS,
;若为0说明已经获取到最后一个ards,则退出循环
mov cx,[ards_nr] ;cx存储遍历到的ards结构体个数
mov ebx,ards_buf ;ebx指向缓冲区地址
xor edx,edx ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0
.find_max_mem_area:
mov eax,[ebx] ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
add eax,[ebx+8] ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
add ebx,20 ;遍历下一个ards
cmp edx,eax ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
jge .next_ards
mov edx,eax
.next_ards:
loop .find_max_mem_area
mov [total_mem_bytes],edx
|
完整的loader程序代码如下所示
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
;初始化栈指针地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;------------- 构建gdt及其内部的描述符 -------------
GDT_BASE:
dd 0x00000000
dd 0x00000000
;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4;段描述符的高4字节部分
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
;定义显存段的描述符
;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
;段粒度位4k,因此段界限的值为0x7fff/4k=7
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0 ;此处预留60个描述符的空位
;------------- 构建选择子 -------------
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
total_mem_bytes dd 0 ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
;故total_mem_bytes在内存中的地址为0x900+0x200=0xb00
;该地址将来在内核中会被用到
;------------- 定义gdtr(指向GDT的寄存器) -------------
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
;EBX:ARDS后续值
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EDX:固定为签名标记,0x534d4150
;返回值
;CF:若cf为0,表示未出错,cf为1,表示调用出错
;EAX:字符串SMAP的ASCII码值,0x534d4150
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EBX:ARDS后续值,即下一个ARDS的位置。
;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构
loader_start:
xor ebx,ebx ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
mov edx,0x534d4150
mov di,ards_buf ;di存储缓冲区地址,即指向缓冲区首地址
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20 ;一个ards结构体的大小
int 0x15 ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
add di,cx ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
inc word [ards_nr] ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
cmp ebx,0 ;比较ebx中的值是否为0
jnz .e820_mem_get_loop ;若ebx不为0,则继续进行循环获取ARDS,
;若为0说明已经获取到最后一个ards,则退出循环
mov cx,[ards_nr] ;cx存储遍历到的ards结构体个数
mov ebx,ards_buf ;ebx指向缓冲区地址
xor edx,edx ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0
.find_max_mem_area:
mov eax,[ebx] ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
add eax,[ebx+8] ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
add ebx,20 ;遍历下一个ards
cmp edx,eax ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
jge .next_ards
mov edx,eax
.next_ards:
loop .find_max_mem_area
mov [total_mem_bytes],edx
;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1
;------------- 打开A20 -------------
in al,0x92
or al,0000_0010B
out 0x92,al
;------------- 加载gdt -------------
lgdt [gdt_ptr]
;------------- 置cr0的PE位为1 -------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax
jmp dword SELECTOR_CODE:p_mode_start;刷新流水线
.error_hlt:
hlt ;出错则挂起
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP ;初始化栈指针,将栈指针地址放入bsp寄存器
mov ax,SELECTOR_VIDEO ;初始化显存段寄存器,显存段的选择子放入gs寄存器
mov gs,ax
mov byte [gs:160],'p'
jmp $
|
mbr.S
注意,loader.S程序真正的代码执行位置是loader_start,该标号在内存中的物理位置是经过作者手工设计的
设计原理是这样的
- 构建GDT占据了512B(0x200)
- 定义gdtr(指向GDT基址的寄存器)占据了6B
- 定义total_mem_bytes(记录物理内存的实际容量)占据了4B
- 定义ards_nr(记录ards结构体数量)占据了2B
- 定义ards_buf(记录返回的ARDS结构体)占据244B
- 故loader_start在内存中的地址为:
0x900+512B+256B(6+4+2+244)=0x900+0x300=0xc00
1
2
3
4
5
6
7
8
9
10
11
|
;------------- 定义gdtr(指向GDT的寄存器) -------------
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
;------------- 定义记录ARDS结构体信息的变量 -------------
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
|
所以我们需要修改mbr中loader的加载地址,即
1
|
jmp LOADER_BASE_ADDR+0x300;
|
其余与上节相同,完整的mbr程序代码如下所示
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
;主引导程序MBR,由BIOS通过jmp 0:0x7c00跳转
;------------------------------------------
%include "boot.inc"
;vstart=0x7c00表示本程序在编译时,起始地址编译为0x7c00
SECTION MBR vstart=0x7c00
;使用通用寄存器中的值(0)初始化其余寄存器
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
;初始化栈指针
mov sp,0x7c00
;存入显存的段基址
mov ax,0xb800
mov gs,ax
;------------------------------------------
;------------------------------------------
;利用0x06号功能进行清屏
;INT 0x10 功能号:0x06 功能描述:上卷窗口清屏
;输入:
;AH 功能号:0x06
;AL=上卷的行数(如果为0,则表示全部)
;BH=上卷的行属性
;(CL,CH)=窗口左上角(X,Y)位置
;(DL,DH)=窗口右下角(X,Y)位置
;返回值
;无返回值
mov ax,0x0600
mov bx,0x0700
mov cx,0 ;左上角(0,0)
mov dx,0x184f ;右下角(80,25)
;0x18=24,0x4f=79
int 0x10 ;调用BIOS中断函数
;------------------------------------------
;------------------------------------------
;将要显示的字符串写入到显存中
mov byte [gs:0x00],'1';在第一个字节的位置写入要显示的字符“1”
;在第二个字节的位置写入显示字符(也就是字符1)的属性,其中A表示绿色背景,4表示前景色为红色
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax,LOADER_START_SECTOR;起始扇区地址
mov bx,LOADER_BASE_ADDR;要写入的内存地址
mov cx,4;待读入的扇区数目
call rd_disk_m16;调用磁盘读取程序
jmp LOADER_BASE_ADDR+0x300;
;------------------------------------------
;函数功能:读取硬盘的n个扇区
;参数:
;eax:LBA扇区号
;bx:要写入的内存地址
;cx:待读入的扇区数目
rd_disk_m16:
;out命令,通过端口向外围设备发送数据
;其中目的操作数可以是8为立即数或者寄存器DX,源操作数必须是寄存器AL或者AX
;因此需要将之前ax中保存的值进行备份
mov esi,eax
mov di,cx
;正式读写硬盘
;第一步:设置要读取的扇区数目
mov dx,0x1f2
mov ax,cx
out dx,ax
mov eax,esi;恢复eax的值
;第二步:将LBA的地址存入0x1f3~0x1f6
;LBA的0~7位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA的8~15位写入端口0x1f4
mov cl,8
shr eax,cl;ax左移8位
mov dx,0x1f4
out dx,al
;LBA的16~23位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
;LBA的24~27位写入端口0x1f6
shr eax,cl;左移8位
and al,0x0f;与0000 1111相与,取后四位
or al,0xe0;设置7~4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第三步,向0x1f7端口写入读命令,0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第四步,检测硬盘状态
.not_ready:
nop;空操作,什么也不做,相当于sleep,只是为了增加延迟
in al,dx;由于读取硬盘状态信息的端口仍旧是0x1f7,因此不需要再为dx指定端口号
;取出状态位的第3位和第7位
;其中第3位若为1表示硬盘控制器已经准备好数据传输,第7位为1表示硬盘忙
and al,0x88
;cmp为减法指令,此处是为了判断第7位是否为1,如果为1,则减法的结果应该为0
cmp al,0x08
;JNZ是条件跳转指令,它表示"Jump if Not Zero",
;也就是如果零标志位(ZF)不为0,则进行跳转。否则不进行跳转
jnz .not_ready;若未准备好,则继续等
;第五步,从0x1f0端口读取数据
mov ax,di;di是之前备份的要读取的扇区数
mov dx,256
;mul指令表示乘法操作,当只有一个操作数时,被乘数隐含在al或者ax中
mul dx;一个扇区512字节,每次读取两个字节,需要读取di*256次
mov cx,ax;cx在此时表示要循环读取的次数
mov dx,0x1f0
.go_on_read:
in ax,dx
mov [bx],ax;通过循环将输入写入bx寄存器所指向的内存,每次读入2个字节的数据
add bx,2
loop .go_on_read
ret
times 510-($-$$) db 0
db 0x55,0xaa
|
编译
1
2
3
4
|
nasm -I ./include/ ./mbr.S -o mbr.bin
nasm -I ./include/ ./loader.S -o loader.bin
dd if=/home/minios/osCode/miniOS_32/ch4/mbr.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 conv=notrunc
dd if=/home/minios/osCode/miniOS_32/ch4/loader.bin of=/home/minios/bochs/hd60M.img bs=512 count=2 seek=2 conv=notrunc
|
运行
我们首先看看我们的物理机上装载的内存容量,然后再运行我们的代码,测试我们检测出的物理内存是否正确
在bochs安装位置处运行以下代码查看物理机装载的内存容量
1
|
cat -n boot.disk | grep meg
|
可以看到,机器上装了32MB内存。megs指定的内存以MB为单位
1
2
|
minios@robot:~/bochs$ cat -n boot.disk | grep meg
2 megs: 32
|
接下来验证我们的代码的正确性,是否可以检测出来这段32MB的内存容量
1
|
./bin/bochs -f boot.disk
|
运行上述命令
- 出现[6]直接按下回车
- 继续按下c,出现如下界面
这是我们之前的实验结果
接下来按下ctrl+c,不要退出,在终端输入
其中,xp命令表示查看内存内容,0xb00是我们在代码中设计的变量total_mem_bytes的地址,total_mem_bytes保存了代码最终计算的结果,如下所示
1
2
3
|
<bochs:2> xp 0xb00
[bochs]:
0x00000b00 <bogus+ 0>: 0x02000000
|
如上所示,在执行 xp 0xb00后,结果是 0x02000000。0x2000000 换算成十进制正是 32MB,可见检测结果是正确的。
至此,我们的内存分布图如下所示
分页机制概述
为什么要分页
在说分页机制之前,我们先回顾一下内存的分段模式
如图所示,在处理器中有负责分段管理的段部件。
- 每个程序或任务都有自己的段,这些段都用段描述符定义。
- 随着程序的执行,当要访问内存时,就用段地址加上偏移量,段部件就会输出一个线性地址。
- 在单纯的分段模式下,线性地址就是物理地址。
正如图中所示
- 描述符中的段基地址为0x002000C0,界限值为0x2007。
- 因为段的粒度是字节,故该段的长度为8200字节。
- 当访问内存时,用段基地址0x002000C0加上段内偏移量0x1008,段部件就会形成线性地址0x002010C8,这也是物理地址。
那么在这种内存管理机制下,会有什么问题呢?
我们首先看一个场景,如下图所示模拟了多个进程并行的情况
- 初始时,系统里有3个进程正在运行,进程 A、B、C各占了 10MB、20MB、30MB的内存空间,还剩下15MB 可用。
- 假设此时进程B已经运行结束,腾出了20MB的内存,可是待加载运行的进程D需要20MB+3KB的内存空间,即20483KB。
- 现在的运行环境未开启分页功能,“段基址+段内偏移”产生的线性地址就是物理地址,程序中引用的线性地址是连续的,所以物理地址也连续。
- 虽然总共剩下35MB 内存可用,但是现在连续的内存块只有原来进程B的20MB和最下面可用内存15MB,每一块都不够进程D用,这就是内存的外部碎片
针对上述场景中出现的问题,此时可有两个解决方案
- 等待进程C运行完后腾出内存,这样连续可用的内存就够运行进程D了。
- 将进程A的段A3或进程C的段C1换出到硬盘上,腾出一部分空间,加上邻接的20MB,足够容纳进程D。
显然方案二的效果是更好的,我们将老进程不常用的段换出到硬盘,腾出空间给新进程用,等老进程再次需要该段时,再从硬盘上将该段载入内存,如图所示
第二个方法虽然解决了内存不足的问题,但也有缺陷
- 比如物理内存特别小,无法容纳任何一个进程的段,这就没法运行进程了,更没法做段的换入换出。
- 另外一种情况是若进程的段比较大,换出时要将整个段全部搬到外存上,这种I0操作太多了机器响应奇慢无比,用户是无法接受的。
出现这种问题的本质在于
- 在目前只分段的机制下,CPU 认为线性地址等于物理地址。
- 而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行,但我们可用的物理地址不连续。
分页机制概述
针对以上问题,出现了分页机制
分页机制的思想是:通过映射,使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。
分页机制的作用有两方面
- 将线性地址转换成物理地址
- 用大小相等的页代替大小不等的段。
由于有了线性地址到真实物理地址的这层映射,经过段部件输出的线性地址便有了另外一个名字,虚拟地址,因此,分页机制是建立在分段机制上的。
分页机制本质上是将大小不同的大内存段拆分成大小相等的小内存块
那么我们要如何对内存进行分页呢?
- 假如我们将4GB的内存空间划分成 4G个内存块,那么每个内存块大小是1字节,一共4G个内存块,也就是4G个页表项。
- 页表也是存储在内存中的,为了表示 32位地址,每个页表项必须要4字节,若按此方案,光是页表就要占4B*4G=16GB的内存,显然是不合理的。
因此,为了节省页表空间,我们需要将划分的单个内存块的尺寸变大,这样内存块数量变小,从而减少了页表项数量。
如下所示,若滑块指向第12位,内存块大小则为2的12次方,即4KB,内存块数量则为2的20次方,1M,即1048576个。这里所说的内存块,其官方名称是页,CPU中采用的页大小恰恰就是4KB,也就是图5-10中滑块的落点处。
- 页是地址空间的计量单位,并不是专属物理地址或线性地址,只要是4KB的地址空间都可以称为页,所以线性地址的一页也要对应物理地址的一页。
- 一页大小为4KB,所以4GB 地址空间被划分成 4GB/4KB=1M个页,也就是 4GB 空间中可以容纳1048576个页,页表中自然也要有 1048576个页表项。这就是我们要说的一级页表。
分页机制下的地址转换
- 一个页表项对应一个页
- 线性地址的高20位作为页表项的索引,每个页表项要占用4字节大小,所以这高 20位的索引乘以4后才是该页表项相对于页表物理地址的字节偏移量。
- cr3 寄存器中存放着的页表的物理地址,加上页表项的偏移量便是该页表项的物理地址,从而得到该页表项中映射的物理页地址
- 然后用线性地址的低12位与该物理页地址相加,所得的地址之和便是最终要访问的物理地址。
如上图所示,我们以指令 mov ax,[0x1234]
为例
- 假设在平坦模型下工作,不管段选择子值是多少,其所指向的段基址都是0
- 指令
mov ax,[0x1234]
中的 0x1234称为有效地址,它作为“段基址:段内偏移地址”中的段内偏移地址。
- 这样段基址为0,段内偏移地址为0x1234,经过段部件处理后,输出的线性地址是0x1234。
- 当线性地址 0x1234被送入页部件,页部件分析 0x1234的高20位,用十六进制表示高20位是0x00001。
- 然后将此项作为页表项索引,再将该索引乘以4后加上 cr3 寄存器中页表的物理地址,这样便得到索引所指代的页表项的物理地址
- 再从该物理地址处(页表项中)读取所映射的物理页地址:0x9000。
- 线性地址的低12位是0x234,它作为物理页的页内偏移地址与物理页地址0x9000相加,和为0x9234,这就是线性地址0x1234最终转换成的物理地址。
二级页表
前面所述的地址转换都是一级页表的过程,那么为什么有了一级页表还要有二级页表呢?
(1)一级页表中最多可容纳1M(1048576)个页表项,每个页表项是4字节,如果页表项全满的话,便是 4MB 大小。
(2)一级页表中所有页表项必须要提前建好,原因是操作系统要占用4GB虚拟地址空间的高1GB用户进程要占用低 3GB。
(3)每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。
归根结底,我们要解决的是:不要一次性地将全部页表项建好,需要时动态创建页表项
无论是几级页表,标准页的尺寸都是4KB,这一点是不变的。
4GB线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中。每个页表中包含有1K个页表项。页表项是4字节大小,页表包含1K个页表项,故页表大小为4KB,这恰恰是一个标准页的大小。
拆分出了这么多个页表,如何使用它们呢?为此,专门有个页目录表来存储这些页表。
每个页表的物理地址在页目录表中都以页目录项(Page DirectoryEntry, PDE)的形式存储,页目录项大小同页表项一样,都用来描述一个物理页的物理地址,其大小都是4字节,而且最多有1024个页表,所以页目录表也是4KB大小,同样也是标准页的大小。页表是用于管理内存的数据结构,其也要占用内存,所以页目录表和页表所占用的物理页,同样混迹于物理内存之中
每个任务都有自己的页目录表和页表。在处理器内部,有一个控制寄存器CR3,存放着当前任务的页目录表的物理地址,故又叫作页目录基址寄存器(Page Directory Base Register,PDBR)。
每个任务都有自己的任务状态段(TSS),它是任务的标志性结构,存放了和任务相关的各种数据,其中就包括了CR3寄存器域,存放了任务自己的页目录表物理地址。当任务切换时,处理器切换到新任务开始执行,而CR3的内容也被更新,以指向新任务的页目录表。相应的,页目录又指向一个个的页表,这就使得每个任务都只在自己的地址空间内运行。
二级页表的地址转换过程同一级页表是相同的
如图所示,在处理器内部,页部件将段部件送来的32位线性地址截成3段,分别是高10位、中间10位和低12位。
- 高10位是页目录的索引
- 中间10位是页表的索引
- 低12位则作为页内偏移来用
当前任务页目录的物理地址在处理器的寄存器CR3中,假设它的内容为0x00005000。
段管理部件输出的线性地址是0x00801050
,其二进制的形式为0000 0000 1000 0000 0001 0000 0101 0000
在这里,
- 高10位为0000000010,也就是十六进制的0x002,它是页目录表内的索引,处理器将它乘以4(因为每个目录项为4字节),作为偏移量访问页目录。最终,处理器从物理地址00005008处取得页表的物理地址0x08001000。
- 线性地址的中间10位为二进制的0000000001,即0x001,处理器要用它作为页表内的索引来取得页的物理地址。处理器将该索引值乘以4,作为偏移量访问页表。
- 最终,处理器又从物理地址08001004处取得页的物理地址,这就是我们一直努力寻找的那个页。
开启分页机制
分页机制启动步骤
- 准备页目录表和页表
- 将页表地址写入控制寄存器cr3
- 寄存器cr0的PG位置为1
页目录项与页表项
- P,表示存在位
- RW:读写位
- US:表示普通用户/系统用户
- 1,处于user级别,任意级别(1,2,3,4)特权的程序都可以访问该页
- 0,处于supervisor级别,只允许特权级位(1,2,3)的程序可以访问
- PWT:页级通写位
- 1,表示此项采用通写方式表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式本位用来间接决定是否用此方式改善该页的访问效率。
- PCD:页级高速缓存禁止位
- A:访问位
- D:脏页位
- 当CPU对一个页面进行写操作时,就会将对应页表项D位置为1
- 此项仅对页表项有效,并不会修改页目录项中的D位
- PAT:页属性表位
- G,全局位
- 1表示该页为全局页,也就是该页将在TLB高速缓存中一直存在
- 0表示该页不是全局页
- AVL:可用位,操作系统是否可用
控制寄存器cr3
用途:存储页目录表基址物理位置,又称页目录基址寄存器PDBR
内存规划
虚拟空间共4G
-
3GB ~ 4GB:操作系统
-
0 ~ 3GB:用户进程自己的虚拟地址空间
以下是虚拟内存和物理内存的逻辑映射关系
- 页目录表的位置:0x100000
- 第一个页表位置:0x101000
以下是内核需要的页目录表在物理内存中的存放位置,以及虚拟空间3GB ~ 4GB需要的页表在物理内存中的位置
以下是虚拟内存与物理内存的实际映射关系
代码目录结构
1
2
3
4
5
6
7
|
.
├── include
│ └── boot.inc //配置文件
├── loader.S //loader程序代码
├── Makefile //编译文件,可直接使用make进行编译代码
├── mbr.S //mbr程序代码
└── start.sh //编译脚本,如果不想使用make编译代码,也可以选择运行该脚本编译代码,该脚本的编译命令更加清晰
|
代码
boot.inc
在配置文件中添加页目录表和页表的相关信息
1
2
3
4
5
6
7
8
9
10
|
PAGE_DIR_TABLE_POS equ 0x100000 ;页目录表基址
;----- 模块化的页目录表字段 ---------
PG_P equ 1b
PG_RW_R equ 00b
PG_RW_W equ 10b
PG_US_S equ 000b
PG_US_U equ 100b
|
完整boot.inc代码如下
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
67
68
69
70
|
;-----loader and kernel-----
LOADER_BASE_ADDR equ 0x900 ;loader在内存中位置
LOADER_START_SECTOR equ 0x2 ;loader在磁盘中的逻辑扇区地址,即LBA地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR ;初始化栈指针地址
PAGE_DIR_TABLE_POS equ 0x100000 ;页目录表基址
;----- gdt描述符属性 ---------
DESC_G_4K equ 1_00000000000000000000000b ;设置段界限的单位为4KB
DESC_D_32 equ 1_0000000000000000000000b ;设置代码段/数据段的有效地址(段内偏移地址)及操作数大小为32位
DESC_L equ 0_000000000000000000000b ;64位代码段标记位,现在是32位操作系统,因此标记为0即可。
DESC_AVL equ 0_00000000000000000000b
;定义段界限位
;段界限的第2部分,即描述符的高32位中的第16~19位,最终的代码段段界限为0xFFFFF
DESC_LIMIT_CODE2 equ 1111_0000000000000000b ;定义代码段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 ;定义数据段要用的段描述符高32位中16~19段界限为全1
DESC_LIMIT_VIDEO2 equ 0000_000000000000000b ;定义我们要操作显存时对应的段描述符的高32位中16~19段界限为全0
DESC_P equ 1_000000000000000b ;定义了段描述符中的P标志位,表示该段描述符指向的段是否在内存中
;定义描述符的特权级别位
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 ;代码段和数据段都是非系统段,故类型字段s设置为1
DESC_S_DATA equ DESC_S_CODE ;代码段和数据段都是非系统段,故类型字段s设置为1
DESC_S_sys equ 0_000000000000b ;系统段的类型字段设置为0
;定义子类型位
DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0
DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0,数据段不可执行,向上扩展,可写,已访问位a清0
;拼接代码段的描述符
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
;----- 程序段的类型定义 ---------
PT_NULL equ 0
|
创建页表和页目录表
- 为了让3GB ~ 4GB的虚拟内存空间映射到物理内核空间0 ~ 1MB,需要将二级页表的768号页目录项(0xc00/4)指向第一个一级页表的页表项,因为第一个一级页表的页表项管理着物理空间的0 ~ 4KB
- 由于mbr程序最开始会在实模式下加载loader程序,而loader程序的加载地址为0x900(实际执行代码的地址为0xc00),这是由一级页表的第一个页表项管理的物理空间,因此为了能够让mbr跨越实模式加载loader,还需要让二级页表的第一个页表项指向一级页表的第一个页表项
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
|
;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表
;逐字节清空页目录表
setup_page:
mov ecx,4096 ;页目录表的大小为4KB,ecx是loop指令的循环计数器
;由于初始化页表是逐字节置0的,因此ecx的值为4096
mov esi,0 ;页目录表的偏移量
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS+esi],0 ;逐字节清空页目录表
;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
inc esi ;递增偏移量,清空下一个字节
loop .clear_page_dir
;初始化创建页目录表
.create_pde:
mov eax,PAGE_DIR_TABLE_POS ;eax保存页目录表的起始地址
add eax,0x1000 ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
mov ebx,eax ;ebx保存第一张页表的起始地址,后续会用到
or eax,PG_US_U|PG_RW_W|PG_P ;eax已经有了第一张页表的起始地址
;此处再加上属性,即可表示为页目录表的一个表项,
;该表项代表的是第一张页表的物理地址及其相关属性
mov [PAGE_DIR_TABLE_POS+0x0],eax ;页目录表的第一个表项指向第一张页表
mov [PAGE_DIR_TABLE_POS+0xc00],eax ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
;因此该语句是将3GB的虚拟空间映射到内核空间
;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
sub eax,0x1000
mov [PAGE_DIR_TABLE_POS+4092],eax ;最后一个页表项指向自己,为将来动态操作页表做准备
;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
mov ecx,256 ;循环计数器
mov esi,0 ;偏移量
xor edx,edx ;第一个页表项指向物理空间的0~4KB物理页框
;故记录的起始地址就是第一个页框的起始地址,也就是0
mov edx,PG_US_S|PG_RW_W|PG_P ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
;且物理地址为0的物理页的页表项
.create_pte:
mov [ebx+esi*4],edx ;此前ebx已经保存了第一张页表的起始地址
add edx,4096 ;edx指向下一个物理页(一个物理页4KB)
inc esi ;esi指向页表的下一个偏移
loop .create_pte
; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
mov eax,PAGE_DIR_TABLE_POS
add eax,0x2000 ;此时的eax表示第二张页表的起始地址
or eax,PG_US_U|PG_RW_W|PG_P ;为eax表项添加属性
mov ebx,PAGE_DIR_TABLE_POS
mov ecx,254 ;要设置254个页表项
mov esi,769 ;从第769个页表项开始设置
.create_kernel_pde:
mov [ebx+esi*4],eax ; 设置页目录表项
inc esi ; 增加要设置的页目录表项的偏移
add eax,0x1000 ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
loop .create_kernel_pde ; 循环设定254个页目录表项
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
|
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP ;初始化栈指针,将栈指针地址放入bsp寄存器
mov ax,SELECTOR_VIDEO ;初始化显存段寄存器,显存段的选择子放入gs寄存器
mov gs,ax
call setup_page ;创建页目录表和页表,并初始化页内存位图
mov ebx,[gdt_ptr+2] ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
or dword [ebx+0x18+4],0xc0000000 ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
;表示将显存段的起始地址增加了3G
add dword [gdt_ptr+2],0xc0000000 ;同理将GDT_BASE的起始地址也增加3G
add esp,0xc0000000 ;同理将esp栈指针的起始地址也增加3G
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax
mov eax,cr0 ;打开cr0的PG位
or eax,0x80000000
mov cr0,eax
lgdt [gdt_ptr] ;开启分页后,用gdt的新地址重新加载
mov byte [gs:160],'V'
jmp $
|
完整loader
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR
;初始化栈指针地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;------------- 构建gdt及其内部的描述符 -------------
GDT_BASE:
dd 0x00000000
dd 0x00000000
;代码段描述符的低4字节部分,其中高两个字节表示段基址的0~15位,在这里定义为0x0000
;低两个字节表示段界限的0~15位,由于使用的是平坦模型,因此是0xFFFF
CODE_DESC:
dd 0x0000FFFF
dd DESC_CODE_HIGH4;段描述符的高4字节部分
DATA_STACK_DESC:
dd 0x0000FFFF
dd DESC_DATA_HIGH4
;定义显存段的描述符
;文本模式下的适配器地址为0xb8000~0xbffff,为了方便显存操作,显存段不使用平坦模型
;因此段基址为0xb8000,段大小为0xbffff-0xb8000=0x7fff,
;段粒度位4k,因此段界限的值为0x7fff/4k=7
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0 ;此处预留60个描述符的空位
;------------- 构建选择子 -------------
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
total_mem_bytes dd 0 ;total_mem_bytes用于保存最终获取到的内存容量,为4个字节
;由于loader程序的加载地址为0x900,而loader.bin的文件头大小为0x200
;(4个gdt段描述符(8B)加上60个dp(8B)填充字,故64*8=512B),
;故total_mem_bytes在内存中的地址为0x900+0x200=0xc00
;该地址将来在内核中会被用到
;------------- 定义gdtr(指向GDT的寄存器) -------------
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0 ;开辟一块缓冲区,用于记录返回的ARDS结构体,
;该定义语句事实上是定义了一个数组ards_buf[244]
;244是因为total_mem_bytes(4)+gdt_ptr(6)+244+ards_nr(2)=256,即0x100
;这样loader_start的在文件内的偏移地址就是0x100+0x200=0x300
ards_nr dw 0 ;用于记录ards结构体数量
;------------------------------------------
;INT 0x15 功能号:0xe820 功能描述:获取内存容量,检测内存
;------------------------------------------
;输入:
;EAX:功能号,0xE820,但调用返回时eax会被填入一串ASCII值
;EBX:ARDS后续值
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EDX:固定为签名标记,0x534d4150
;返回值
;CF:若cf为0,表示未出错,cf为1,表示调用出错
;EAX:字符串SMAP的ASCII码值,0x534d4150
;ES:di:ARDS缓冲区,BIOS将获取到的内存信息存到此寄存器指向的内存,每次都以ARDS格式返回
;ECX:ARDS结构的字节大小,20
;EBX:ARDS后续值,即下一个ARDS的位置。
;每次BIOS中断返回后,BIOS会更新此值,BIOS会通过此值找到下一个待返回的ARDS结构。
;在cf位为0的情况下,若返回后的EBX值为0,表示这是最后一个ARDS结构
loader_start:
xor ebx,ebx ;第一次调用时,要将ebx清空置为0,此处使用的是异或运算置0
mov edx,0x534d4150
mov di,ards_buf ;di存储缓冲区地址,即指向缓冲区首地址
.e820_mem_get_loop:
mov eax,0x0000e820
mov ecx,20 ;一个ards结构体的大小
int 0x15 ;调用0x15中断函数,返回的ards结构体被返回给di指向的缓冲区中
add di,cx ;使di增加20字节指向缓冲区中下一个的ARDS结构位置
inc word [ards_nr] ;inc(increment增加)指令表示将内存中的操作数增加一,此处用于记录返回的ARDS数量
cmp ebx,0 ;比较ebx中的值是否为0
jnz .e820_mem_get_loop ;若ebx不为0,则继续进行循环获取ARDS,
;若为0说明已经获取到最后一个ards,则退出循环
mov cx,[ards_nr] ;cx存储遍历到的ards结构体个数
mov ebx,ards_buf ;ebx指向缓冲区地址
xor edx,edx ;EDX用于保存BaseAddrLow+LengthLow最大值,此处初始化为0
.find_max_mem_area:
mov eax,[ebx] ;eax用于遍历缓冲区中的每一个ards的BaseAddrLow
add eax,[ebx+8] ;ebx+8获取的是LengthLow,故该代码计算的是BaseAddrLow+LengthLow
add ebx,20 ;遍历下一个ards
cmp edx,eax ;分支语句,如果edx大于等于eax,则跳转到.next_ards,也就是进入循环
jge .next_ards
mov edx,eax ;否则就是更新edx
.next_ards:
loop .find_max_mem_area
mov [total_mem_bytes],edx ;将最终结果保存到total_mem_bytes
;------------- 准备进入保护模式 -------------
;1.打开A20
;2.加载gdt
;3.置cr0的PE位为1
;------------- 打开A20 -------------
in al,0x92
or al,0000_0010B
out 0x92,al
;------------- 加载gdt -------------
lgdt [gdt_ptr]
;------------- 置cr0的PE位为1 -------------
mov eax,cr0
or eax,0x00000001
mov cr0,eax
jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
.error_hlt:
hlt ;出错则挂起
[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA ;初始化段寄存器,将数据段的选择子分别放入各段寄存器
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_STACK_TOP ;初始化栈指针,将栈指针地址放入bsp寄存器
mov ax,SELECTOR_VIDEO ;初始化显存段寄存器,显存段的选择子放入gs寄存器
mov gs,ax
call setup_page ;创建页目录表和页表,并初始化页内存位图
mov ebx,[gdt_ptr+2] ;gdt_ptr+2表示GDT_BASE,也就是GDT的起始地址
or dword [ebx+0x18+4],0xc0000000 ;ebx中保存着GDT_BASE,0x18=24,故ebx+0x18表示取出显存段的起始地址
;+4表示取出段描述符的高32位,之后和0xc0000000进行或操作
;表示将显存段的起始地址增加了3G
add dword [gdt_ptr+2],0xc0000000 ;同理将GDT_BASE的起始地址也增加3G
add esp,0xc0000000 ;同理将esp栈指针的起始地址也增加3G
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax
mov eax,cr0 ;打开cr0的PG位
or eax,0x80000000
mov cr0,eax
lgdt [gdt_ptr] ;开启分页后,用gdt的新地址重新加载
mov byte [gs:160],'V'
jmp $
;------------- 创建页目录表和页表 -------------
;初始化页目录表和页表
;逐字节清空页目录表
setup_page:
mov ecx,4096 ;页目录表的大小为4KB,ecx是loop指令的循环计数器
;由于初始化页表是逐字节置0的,因此ecx的值为4096
mov esi,0 ;页目录表的偏移量
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS+esi],0 ;逐字节清空页目录表
;其中PAGE_DIR_TABLE_POS为页目录表初始地址的宏
inc esi ;递增偏移量,清空下一个字节
loop .clear_page_dir
;初始化创建页目录表
.create_pde:
mov eax,PAGE_DIR_TABLE_POS ;eax保存页目录表的起始地址
add eax,0x1000 ;0x1000为1k,故该代码的计算结果是将eax指向第一张页表的起始地址
mov ebx,eax ;ebx保存第一张页表的起始地址,后续会用到
or eax,PG_US_U|PG_RW_W|PG_P ;eax已经有了第一张页表的起始地址
;此处再加上属性,即可表示为页目录表的一个表项,
;该表项代表的是第一张页表的物理地址及其相关属性
mov [PAGE_DIR_TABLE_POS+0x0],eax ;页目录表的第一个表项指向第一张页表
mov [PAGE_DIR_TABLE_POS+0xc00],eax ;0xc0000000即为3GB,由于我们划分的虚拟地址空间3GB以上为os地址空间
;因此该语句是将3GB的虚拟空间映射到内核空间
;而0xc00/4=768,也就是说页目录表的768号表项映射为物理内核空间
sub eax,0x1000
mov [PAGE_DIR_TABLE_POS+4092],eax ;最后一个页表项指向自己,为将来动态操作页表做准备
;创建第一张页表的页表项,由于os的物理内存不会超过1M,故页表项个数的最大值为1M/4k=256
mov ecx,256 ;循环计数器
mov esi,0 ;偏移量
mov edx,PG_US_S|PG_RW_W|PG_P ;此时的edx表示拥有属性PG_US_S|PG_RW_W|PG_P
;且物理地址为0的物理页的页表项
.create_pte:
mov [ebx+esi*4],edx ;此前ebx已经保存了第一张页表的起始地址
add edx,4096 ;edx指向下一个物理页(一个物理页4KB)
inc esi ;esi指向页表的下一个偏移
loop .create_pte
; -------------------初始化页目录表769号-1022号项,769号项指向第二个页表的地址(此页表紧挨着上面的第一个页表),770号指向第三个,以此类推
mov eax,PAGE_DIR_TABLE_POS
add eax,0x2000 ;此时的eax表示第二张页表的起始地址
or eax,PG_US_U|PG_RW_W|PG_P ;为eax表项添加属性
mov ebx,PAGE_DIR_TABLE_POS
mov ecx,254 ;要设置254个页表项
mov esi,769 ;从第769个页表项开始设置
.create_kernel_pde:
mov [ebx+esi*4],eax ; 设置页目录表项
inc esi ; 增加要设置的页目录表项的偏移
add eax,0x1000 ; eax指向下一个页表的位置,由于之前设定了属性,所以eax是一个完整的指向下一个页表的页目录表项
loop .create_kernel_pde ; 循环设定254个页目录表项
ret
|
编译并写入磁盘
编译mbr
1
|
nasm -I ./include/ ./mbr.S -o mbr.bin
|
编译loader
1
|
nasm -I ./include/ ./loader.S -o loader.bin
|
写入mbr
1
|
dd if=/home/minios/osCode/miniOS_32/ch5/task2/mbr.bin of=/home/minios/bochs/hd60M.img bs=512 count=1 conv=notrunc
|
写入loader
1
|
dd if=/home/minios/osCode/miniOS_32/ch5/task2/loader.bin of=/home/minios/bochs/hd60M.img bs=512 count=4 seek=2 conv=notrunc
|
运行
如下,屏幕第二行出现了字符“V”,这个字符的打印代码是在保护模式下编写的,因此当字符“V”成功打印就说明我们成功开启了保护模式
总结
以下我们使用一张图总结虚拟地址在段机制与页机制下如何进行地址转换并找到物理地址的