0%

操作系统真相还原Ch5-1

保护模式进阶

Linux获取内存

实模式下,通过detect_memory函数获取内存容量,本质上通过BIOS中断0x15实现,子功能号放入寄存器EAX或AX中:

  • EAX=0xE820: 遍历主机上全部内存,可以获取系统的内存布局
  • AX=0xE801: 分别检测15MB和16MB-4GB的内存
  • AH=0x88: 最多检测出64MB

若三种方法都失败则机器挂起,停止运行

实战

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
232
233
   %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,4字节,用于保存内存容量,以字节为单位,此位置比较好记。
; 当前偏移loader.bin文件头0x200字节,loader.bin的加载地址是0x900,
; 前面4个段描述符(8字节/个),还有预留60个段描述槽位(8字节/个)
; (4+60)*8=512=0x200,本程序加载地址0x900,所以本程序加载地址是0x200+0x900=0xb00
; 故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字节
;提前定义的缓冲区,存储中断0xE820返回的ards结构(20字节/个)
ards_buf times 244 db 0
ards_nr dw 0 ;用于记录ards结构体数量

loader_start: ;经过对齐后,偏移地址是0x300

;------- 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]
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:
;先把页目录占用的空间逐字节清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

mbr.S

jmp LOADER_BASE_ADDR + 0x300

使用命令 xp 0xb00查看内存大小,显示0x4000000=64MB

启用内存分页机制

内存段是怎样被换出的

CPU允许在描述符表中已注册的段不在内存中存在,该描述符的P位为1,则在内存,反之则不在。访问过改段后会将A位置为1。在换出时,CPU第一轮先将A位置为0,如果是0则换到硬盘中,同时P位为0,相当于钟表。

分页机制图

1

通过段部件处理后,保护模式的寻址空间是4GB,这个寻址空间就是线性地址空间(虚拟地址),在逻辑上是连续的。

分页机制思想:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上的线性地址其对应的物理地址可以不连续。

分页机制作用:将线性地址转换为物理地址,用大小相等的页代替大小不同的段

页表是一个n行1列的表格,页表中的每一行称为页表项,4字节,作用是存储内存物理地址

CPU中采用的页大小就是4KB,内存块数量是1M个,即高20位用来定位某个具体物理页,低12位作为页内寻址

地址转换过程

每个页表项(PTE)4字节,线性地址的高20位*4+寄存器cr3存储的页表物理地址 = 最终要访问的物理地址

二级页表

4GB线性地址空间最多有1M个标准页,二级页表是将这1M个页平均放入1K个页表中。每个页表有1K个页表项(4字节/个),所以页表大小4KB,即一个标准页的大小。

专门有个页目录表来存储这些页表,每个页表的物理地址在页目录表中都以页目录项(PDE,Page Directory Entry)的形式存储。最多有1024个页表,所以页目录表是4KB大小,也是标准页的大小。

3

用虚拟地址的高10位乘4,作为页目录表内的偏移地址,再加上页目录表的物理地址,所得的和,便是页目录项的物理地址,读取该页目录项,得到页表的物理地址。中间10位乘4就是页表内的偏移地址,加上页表的物理地址就是页表项的物理地址,可以得到物理页的地址。低12位加上物理页地址就是最终的物理地址。

每个进程都有自己的页表,任务切换时,页表也需要切换

为了启用分页机制,我们需要按顺序做好三件事:
  • 准备好页目录表和页表
  • 将页表地址写入控制寄存器cr3(又称为页目录基址寄存器PDBR:page directory base register)
  • 寄存器cr0的PG位置1(是cr0寄存器的最后一位:31位)

为了用户进程与操作系统共享页表,仿照Linux,将4GB中高3GB以上部分,即3GB-4GB划分给操作系统,0-3GB分给用户进程的虚拟空间

启用分页机制

4

注:在页目录表映射时,页目录项0和0xc00都存为第一个页表的地址,我们在保护模式下,将3GB-4GB的虚拟地址空间记为操作系统空间,即0xc0000000为起点,而在物理地址处,操作系统在低端1MB里面,所以0xc00/4=768,将其映射到物理地址的低端4MB:0-0x3fffff。页目录项为0的也是映射到这个地方,在实模式下对应。

在页目录的最后一个页目录项中写入页表自己的物理地址,即0x101007-0x1000=0x100007

用虚拟地址获取页表中各数据类型的方法。
获取页目录表物理地址:让虚拟地址的高20位为0xfffff,低12位为0x000,即0xfffff000,这也是页目录表中第0个页目录项自身的物理地址。
访问页目录中的页目录项,即获取页表物理地址:要使虚拟地址为0xfffffxxx,其中xxx是页目录项的索引乘以4的积。
访问页表中的页表项:要使虚拟地址高10位为0x3ff,目的是获取页目录表物理地址。中间10位为页表的索引,因为是10位的索引值,所以这里不用乘以4。低12位为页表内的偏移地址,用来定位页表项,它必须是已经乘以4后的值。
公式为0x3ff<<22+中间10位<<12+低12位。

寻页机制:为了实现虚拟地址到物理地址的映射,过程还是有些麻烦的。先要从CR3寄存器中获取页目录表物理地址,然后用虚拟地址的高10位乘以4的积作为在页目录表中的偏移量去寻址目录项pde,从pde中读出页表物理地址,然后再用虚拟地址的中间10位乘以4的积作为在该页表中的偏移量去寻址页表项pte,从该pte中读出页框物理地址,用虚拟地址的低12位作为该物理页框的偏移量,完成虚拟地址到物理地址的映射。

TLB:Translation Lookaside Buffer,俗称快表

虚拟地址的高20位 属性 物理地址的高20位

TLB的条目就是虚拟地址的高20位到物理地址的搞20位的映射,只有P=1的页表项才能在TLB中,如果TLB满了,需要特定的算法将其换出。