简单内核实现笔记 part 2

保护模式进阶

获取物理内存容量

  Linux获取内存容量方法有三种,本质上分别是BIOS中断0x15的3个子功能,BIOS是实模式下的方法,只能在保护模式之前调用。总结如下

利用BIOS中断0x15子功能0xe820获取内存

此方法最灵活,返回的内容也最丰富,内存信息的内容是地址范围描述符来描述的(ARDS),每个字段4字节,一共20字节,调用0x15返回的也就是这个结构。其中Type字段表示内存类型,1表示这段内存可以使用;2表示不可用使用此内存;其它表示未定义,将来会用到

字节偏移量 属性名称 描述
0 BaseAddrLow 基地址的低32位
4 BaseAddrHigh 基地址的高32位
8 LengthLow 内存长度的低32位,以字节为单位
12 LengthHigh 内存长度的高32位,以字节为单位
16 Type 本段内存的类型

用0x15子功能0xe820调用说明和调用步骤如下

  1. 填写好”调用前输入”中列出的寄存器
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508082615611

利用BIOS中断0x15子功能0xe801获取内存

此方法最多识别4G的内存,结果存放在两组寄存器中,操作起来要简便一些,调用说明和调用步骤如下

  1. AX寄存器写入0xE801
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508083428601

利用BIOS中断0x15子功能0x88获取内存

此方法最多识别64MB内存,操作起来最简单,调用说明和调用步骤如下

  1. AX寄存器写入0x88
  2. 执行中断调用 int 0x15
  3. 在CF位为0的情况下,”返回后输出”中对应的寄存器中就有结果

image-20200508083919353

下面结合这三种方式改进我们的实验代码,下面是loader,我们将结果保存在了total_mem_bytes中,重要的一些地方都有注释,更详细的内容建议参考书中P183

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
   %include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建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 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入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第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.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
mov ax, SELECTOR_VIDEO
mov gs, ax

mov byte [gs:160], 'P'

jmp $

mbr.S中也需要修改一处内容,我们跳转的内容要加上0x300,原因是在 loader.S 中loader_start计算如下

(4个段描述符 + 60个段描述符槽位) * 8字节 = total_mem_bytes_offset

(4 + 60) * 8 = 512 = 0x200

total_mem_bytes + gdt_ptr + ards_buf + adrs_nr + total_mem_bytes_offset = loader_start

4 + 6 + 244 + 2 + 0x200 = 0x300

修改片断如下

1
2
3
4
5
6
7
8
9
10
[...] 
mov eax,LOADER_START_SECTOR
mov bx,LOADER_BASE_ADDR
mov cx,4
call rd_disk_m_16

jmp LOADER_BASE_ADDR+0x300 ; 这里

rd_disk_m_16:
[...]

运行结果如下,这里我们用xp 0xb00查看我们的结果,0x02000000换算过来刚好是我们bochsrc.disk中 megs 设置的32MB大小

image-20200508083919353

启动分页机制

  分页机制是当物理内存不足时,或者内存碎片过多无法容纳新进程等情况的一种应对措施。假如说此时未开启分页功能,而物理内存空间又不足,如下图所示,此时线性地址和物理地址一一对应,没有满足进程C的内存大小,可以选择等待进程B或者A执行完获得连续的内存空间,也可以将A3或者B1段换到硬盘上,腾出一部分空间,然而这些IO操作过多会使机器响应速度很慢,用户体验很差。

image-20200508083919353

出现这种情况的本质其实是在分段机制下,线性地址等价于物理地址。那么即使在进程B的下面还有10M的可用空间,但因为两块可用空间并不连续,所以进程C无法使用进程B下面的10M可用空间。

按照这种思路,只需要通过某种映射关系,将线性地址映射到任意的物理地址,就可以解决这种问题了。实现线性地址的连续,而物理地址不需要连续,于是分页机制就诞生了。

一级页表

  在保护模式下寻址依旧是通过段基址:段内偏移组成的线性地址,计算出线性地址后再通过判断分页位是否打开,若打开则开启分页机制进行检索,如下图所示

image-20200508083919353

分页机制的作用有

  • 将线性地址转换成物理地址
  • 用大小相等的页代替大小不等的段

分页机制的作用如下图所示,分页机制来映射的线性地址便是我们经常说的虚拟地址

image-20200508083919353

因为页大小 * 页数量 = 4GB,想要减少页表的大小,只能增加一页的大小。最终通过数学求极限,定下4KB为最佳页大小。页表将线性地址转换成物理地址的过程总结如下图,首先通过计算线性地址高20位索引出页表中的基址,然后加上低12位计算出最终的物理地址,下图中0x9234即是最终的物理地址

image-20200508083919353

二级页表

  无论是几级页表,标准页的尺寸都是4KB。所以4GB的线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中,每个页表包含有1K个页表项。页表项是4字节大小,页表包含1K个页表项,故页表的大小同样为4KB,刚好为一页。

为了管理页表的物理地址,专门有一个页目录表来存放这些页表。页目录表中存储的页表称为页目录项(PDE),页目录项同样为4KB,且最多有1K个页目录项,所以页目录表也是4KB,如下图所示

image-20200508083919353

二级页表中虚拟地址到物理地址的转换也有很大的变化,具体步骤如下

  • 用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
  • 用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
  • 虚拟地址的高 10 位和中间 10 位分别是 PDE PIE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值了,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第二步中得到的物理页地址,所得的和便是最终转换的物理地址。

还是用书中的图最直观,下图表示mov ax, [0x1234567]的转换过程,可以发现cr3寄存器其实指向的是页目录表基地址

image-20200508083919353

PDE和PTE的结构如下图所示

image-20200508083919353

从右到左各属性总结如下表

属性位 意义
P 存在位,为1时表示该页在物理内存中,为0表示不在物理内存中
RW 读写位,为1时可读可写,为0是可读不可写
US 特权位,为1时表示处于普通用户,0~3特权级可访问,为0表示超级用户,0~2特权级可访问
PWT 页级通写位,为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存
PCD 页级高速缓存禁止位,为1表示该页启用高速缓存
A 访问位,为1表示该页被CPU访问过
D 脏页位,当CPU对一个页面执行写操作,此为被赋1
PAT 页属性表位,能够在页面一级的粒度上设置内存属性
G 全局位,为1表示该页在高速缓存TLB中一直保存
AVL 表示软件,系统可用该位,和CPU无关

总结这些步骤,我们启用分页机制需要做的事情如下

  1. 准备好页目录表及页表
  2. 将页表地址写入控制寄存器cr3
  3. 寄存器cr0的PG位置1

下面是创建页目录及页表的代码

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
; 创建页目录及页表
setup_page:
; 先把页目录占用的空间逐字节清零
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: ; 创建PDE
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 ; 一个页表项占用四字节
; 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
.create_pte: ; 创建PTE
mov [ebx+esi*4], edx ; 此时的edx为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 ; 属性为7
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

在boot.inc中添加如下信息

1
2
3
4
5
6
7
8
; loader 和 kernel
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

进行完第一步的内容,之后的操作相对就简单了,将页表地址写入控制寄存器cr3寄存器和将cr0的PG位置1的操作整合起来的loader.S如下所示

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
221
222
223
224
225
226
227
228
229
230
231
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;构建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 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上

; total_mem_bytes用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 故total_mem_bytes内存中的地址是0xb00.将来在内核中咱们会引用此地址
total_mem_bytes dd 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;以下是定义gdt的指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw GDT_LIMIT
dd GDT_BASE

;人工对齐:total_mem_bytes4字节+gdt_ptr6字节+ards_buf244字节+ards_nr2,共256字节
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start:

;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 获取内存布局 -------

xor ebx, ebx ;第一次调用时,ebx值要为0
mov edx, 0x534d4150 ;edx只赋值一次,循环体中不会改变
mov di, ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;循环获取每个ARDS内存范围描述结构
mov eax, 0x0000e820 ;执行int 0x15后,eax值变为0x534d4150,所以每次执行int前都要更新为子功能号。
mov ecx, 20 ;ARDS地址范围描述符结构大小是20字节
int 0x15
jc .e820_failed_so_try_e801 ;若cf位为1则有错误发生,尝试0xe801子功能
add di, cx ;使di增加20字节指向缓冲区中新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx, 0 ;若ebx为0且cf不为1,这说明ards全部返回,当前已是最后一个
jnz .e820_mem_get_loop

;在所有ards结构中,找出(base_add_low + length_low)的最大值,即内存的容量。
mov cx, [ards_nr] ;遍历每一个ARDS结构体,循环次数是ARDS的数量
mov ebx, ards_buf
xor edx, edx ;edx为最大的内存容量,在此先清0
.find_max_mem_area: ;无须判断type是否为1,最大的内存块一定是可被使用
mov eax, [ebx] ;base_add_low
add eax, [ebx+8] ;length_low
add ebx, 20 ;指向缓冲区中下一个ARDS结构
cmp edx, eax ;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx, eax ;edx为总内存大小
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok

;------ int 15h ax = E801h 获取内存大小,最大支持4G ------
; 返回后, ax cx 值一样,以KB为单位,bx dx值一样,以64KB为单位
; 在ax和cx寄存器中为低16M,在bx和dx寄存器中为16MB到4G。
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法

;1 先算出低15M的内存,ax和cx中是以KB为单位的内存数量,将其转换为以byte为单位
mov cx,0x400 ;cx和ax值一样,cx用做乘数
mul cx
shl edx,16
and eax,0x0000FFFF
or edx,eax
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低15MB的内存容量存入esi寄存器备份

;2 再将16MB以上的内存转换为byte为单位,寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx, 0x10000 ;0x10000十进制为64KB
mul ecx ;32位乘法,默认的被乘数是eax,积为64位,高32位存入edx,低32位存入eax.
add esi,eax ;由于此方法只能测出4G以内的内存,故32位eax足够了,edx肯定为0,只加eax便可
mov edx,esi ;edx为总内存大小
jmp .mem_get_ok

;----------------- int 15h ah = 0x88 获取内存大小,只能获取64M之内 ----------
.e801_failed_so_try88:
;int 15后,ax存入的是以kb为单位的内存容量
mov ah, 0x88
int 0x15
jc .error_hlt
and eax,0x0000FFFF

;16位乘法,被乘数是ax,积为32位.积的高16位在dx中,积的低16位在ax中
mov cx, 0x400 ;0x400等于1024,将ax中的内存容量换为以byte为单位
mul cx
shl edx, 16 ;把dx移到高16位
or edx, eax ;把积的低16位组合到edx,为32位的积
add edx,0x100000 ;0x88子功能只会返回1MB以上的内存,故实际内存大小要加上1MB

.mem_get_ok:
mov [total_mem_bytes], edx ;将内存换为byte单位后存入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第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
; 这将导致之前做的预测失效,从而起到了刷新的作用。
.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
mov ax, SELECTOR_VIDEO
mov gs, ax

; 创建页目录及页表并初始化内存位图
call setup_page

; 要将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
sgdt [gdt_ptr] ; 储存到原来gdt所有位置

; 将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2] ; gdt地址
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] ; 重新加载

mov byte [gs:160], 'V'
; 视频段段基址已经被更新,用字符V表示virtual addr
jmp $

;------------- 创建页目录及页表 ---------------
; 创建页目录以及页表
setup_page:
; 页目录表占据4KB空间,清零之
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:
mov eax, PAGE_DIR_TABLE_POS
; 0x1000为4KB,加上页目录表起始地址便是第一个页表的地址
add eax, 0x1000
mov ebx, eax

; 设置页目录项属性
or eax, PG_US_U | PG_RW_W | PG_P
; 设置第一个页目录项
mov [PAGE_DIR_TABLE_POS], eax
; 第768(内核空间的第一个)个页目录项,与第一个相同,这样第一个和768个都指向低端4MB空间
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
; 最后一个表项指向自己,用于访问页目录本身
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax

; 创建页表
mov ecx, 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
mov [ebx + esi * 4], edx
add edx, 4096
inc esi
loop .create_pte

; 创建内核的其它PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U | PG_RW_W | PG_P
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254
mov esi, 769
.create_kernel_pde:
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

编译运行,其中编译count的参数根据实际大小调整,这里我编译设置的是3,运行结果如下图,其中红框中gdt段基址已经修改为大于0xc0000000,也就是3GB之上的内核地址空间,通过info tab可查看地址映射关系,其中箭头左边是虚拟地址,右边是对应的物理地址

image-20200508083919353

总结虚拟地址获取物理地址的过程:

先要从 CR3 寄存器中获取页目录表物理地址,然后用虚拟地址的高 10 位乘以 4 的积作为在页目录表中的偏移量去寻址目录项 pde ,从 pde 中读出页表物理地址,然后再用虚拟地址的中间 10 位乘以 4 的积作为在该页表中的偏移量去寻址页表项 pte,从该 pte 中读出页框物理地址,用虚拟地址的低 12 位作为该物理页框的偏移量。

快表TLB

  因为从虚拟地址映射到物理地址确实比较麻烦,所以为了提高效率,intel自然想得到用一个缓存装置TLB。结构如下,更新TLB的方法有两种,重新加载CR3和指令invlpg m,其中m表示操作数为虚拟内存地址,如更新虚拟地址0x1234对应的条目指令为invlpg [0x1234]

虚拟地址高20位(虚拟页框号) 属性位 物理地址高20位(物理页框号)