4-virtual_memory.md (#24)
[lunaix-os.git] / docs / tutorial / 4-virtual_memory.md
1 ## 准备工作
2
3 ```sh
4 git checkout e141fd4dcd5effc2dbe59a498d7ea274b7199147
5 ```
6
7 观看相关视频
8
9 ## 代码分析
10
11 ### libs/
12
13 这部分内容就比较多了
14
15 先来看看`printf`的实现
16
17 ```c
18 void
19 printf(char* fmt, ...)
20 {
21     char buffer[1024];
22     va_list args;
23     va_start(args, fmt);
24     __sprintf_internal(buffer, fmt, args);
25     va_end(args);
26
27     // 啊哈,直接操纵framebuffer。日后须改进(FILE* ?)
28     tty_put_str(buffer);
29 }
30 ```
31
32 这个函数的参数是变长的,这里获得fmt后面的一个参数,然后传给__sprintf_internal进行了处理。可以推断,这里是一个个参数进行格式化处理的。而buffer就是格式化后的字符串存储的位置。
33
34 接下来看看参数是如何一个个放入buffer的。
35
36 循环fmt的每个字符
37
38 ```c
39     while ((c = *fmt)) {
40 ```
41
42 如果遇到`%`,说明要进入格式化处理了,于是设置adv为1。
43
44 ```c
45         if (c != '%') {
46             buffer[ptr] = c;
47             adv = 1;
48         } else {
49 ```
50
51 如果我们的fmt是`hello %s`,那么接下来看看怎么处理`%`后面的`s`
52
53 ```c
54                 case 's': {
55                     char* str = va_arg(args, char*);
56                     strcpy(buffer + ptr, str);
57                     adv = strlen(str);
58                     break;
59                 }
60 ```
61
62 这里把args解析成一个字符指针,然后把str复制到buffer中。最后还用adv来存储字符串的长度。我们在不继续看代码的前提下,可以根据这个例子来知道其他情况也是根据类型解析args,最后复制到buffer。但是adv的作用无法推理,所以需要继续看代码。
63
64 我们可以找到一个adv被使用的地方来了解它的作用
65
66 ```c
67         fmt++;
68         ptr += adv;
69 ```
70
71 ptr需要在循环结束时加上adv,说明它的作用是设置buffer指针的偏移。因为我们格式化的结果保存到了buffer,buffer的尾部就会往后移动,所以ptr也要移动。下次保存结果到buffer就不需要移动指针。
72
73 代码中还有`__itoa_internal`这样的函数,它是用来把数字类型转换成字符串的。
74
75 ```c
76                 case 'd': {
77                     num = va_arg(args, int);
78                     __itoa_internal(num, buffer + ptr, 10, &adv);
79                     break;
80                 }
81 ```
82
83 先根据value来判断要不要添加符号。最后都是转换成正整数(无符号整数)来传递给`__uitoa_internal`处理。
84
85 ```c
86 char*
87 __itoa_internal(int value, char* str, int base, unsigned int* size)
88 {
89     if (value < 0 && base == 10) {
90         str[0] = '-';
91         unsigned int _v = (unsigned int)(-value);
92         __uitoa_internal(_v, str + 1, base, size);
93     } else {
94         __uitoa_internal(value, str, base, size);
95     }
96
97     return str;
98 }
99 ```
100
101 如果值是0,直接格式化成`'0'`
102
103 ```c
104 char*
105 __uitoa_internal(unsigned int value, char* str, int base, unsigned int* size)
106 {
107     unsigned int ptr = 0;
108     if (value == 0) {
109         str[0] = '0';
110         ptr++;
111     }
112 ```
113
114 这里是将value在循环中不断除以base。base就是进制。比如16进制,需要每次除以16。
115
116 ```c
117         while (value) {
118             str[ptr] = base_char[value % base];
119             value = value / base;
120             ptr++;
121         }
122 ```
123
124 对于循环第一行,我们可以假设base为16,`value`值为`0x156`
125
126 ```c
127 char base_char[] = "0123456789abcdefghijklmnopqrstuvwxyz";
128 ```
129
130 `base_char[value % base]`就是`base_char[12]=='6'`(得到最低位的值),接着把value除以进制,再进行下一次循环,直到value为0。后面会依次得到`'5'`、`'1'`。得到下面数组内容。
131
132 ```
133 [0] [1] [2]
134 '6' '5' '1'
135 ```
136
137 实际上我们要输出`"156"`,而不是`"651"`,所以最后需要用下面代码把字符串排列顺序颠倒。
138
139 ```c
140         for (unsigned int i = 0; i < (ptr >> 1); i++) {
141             char c = str[i];
142             str[i] = str[ptr - i - 1];
143             str[ptr - i - 1] = c;
144         }
145 ```
146
147 格式化字符串的尾部设置一下
148
149 ```c
150     str[ptr] = '\0';
151 ```
152
153 接下来更新size,size实际上是另外一个返回值(adv的指针)。因为格式化后字符串往往会膨胀,所以要记录膨胀后的偏移(`unsigned int ptr = 0;`)。
154
155 ```c
156     if (size) {
157         *size = ptr;
158     }
159 ```
160
161 总的来说,项目printf的实现是把fmt中代格式化字符串,进行格式化后写入buffer。adv的作用是告诉ptr膨胀后的大小(见代码` ptr += adv;`)。不过buffer可能会溢出,不知道会不会为后面的hack环节买下伏笔。
162
163 到这里libs/目录下的代码就分析完了。
164
165 ### includes/hal/io.h
166
167 来一些硬件io相关的操作。
168
169 下面函数用于向一个port写入一个字节大小的value,使用的是扩展内联汇编。主要是使用`out`指令来写入。
170
171 ```c
172 void io_port_wb(uint8_t port, uint8_t value) {
173     asm volatile (
174         "movb %0, %%al\n"
175         "movb %1, %%dx\n"
176         "out %%al, %%dx\n"
177         :: "r"(value) "r"(port)
178     );
179 }
180 ```
181
182 其他函数也是如此,略过。
183
184 ### includes/lunaix/mm/page.h
185
186 接下来看一些宏预热一下,马上分析虚拟内存相关代码。
187
188 lunaixOS的页目录和页表虚拟地址定义。也就是说,修改0xFFFFF000U~0xFFFFF000U+0xfff这个范围内的值就是在修改页目录。
189
190 ```c
191 // 页目录的虚拟基地址,可以用来访问到各个PDE
192 #define PTD_BASE_VADDR                0xFFFFF000U
193
194 // 页表的虚拟基地址,可以用来访问到各个PTE
195 #define PT_BASE_VADDR                 0xFFC00000U
196 ```
197
198 ### includes/lunaix/constants.h
199
200 定义了一些虚拟地址和物理地址。
201
202 ```c
203 #define K_STACK_SIZE            0x100000U
204 #define K_STACK_START           ((0xFFBFFFFFU - K_STACK_SIZE) + 1)
205 #define HIGHER_HLF_BASE         0xC0000000UL
206 #define MEM_1MB                 0x100000UL
207
208 #define VGA_BUFFER_VADDR        0xB0000000UL
209 #define VGA_BUFFER_PADDR        0xB8000UL
210 #define VGA_BUFFER_SIZE         4096
211 ```
212
213 ### kernel/mm/pmm.c
214
215 使用一个bit代表一个物理页,如果这个bit为1,表示该页已分配。4GB(2^32)总共包括2^20个4KB(2^12)。又因为uint8_t包含8个bits,所以数组大小为2^20/8(PM_BMP_MAX_SIZE)。
216
217 ```c
218 #define PM_BMP_MAX_SIZE        (128 * 1024)
219 uint8_t pm_bitmap[PM_BMP_MAX_SIZE];
220 ```
221
222 这个函数用于把某个bit置为0,表示该page被free了。
223
224 ```c
225 void
226 pmm_mark_page_free(uintptr_t ppn)
227 {
228     MARK_PG_AUX_VAR(ppn)
229     pm_bitmap[group] = pm_bitmap[group] & ~msk;
230 }
231 ```
232
233 其他函数也是这样。读者在自己实现时,可以先实现bitmap数据结构,方便进行位操作。
234
235 此时下面的函数不用细看也能知道是把连续的页表标记为占用,这样读代码可以节省很多时间。
236
237 ```c
238 void
239 pmm_mark_chunk_occupied(uint32_t start_ppn, size_t page_count)
240 ```
241
242 初始化物理内存管理器pmm(physical memory manager)。就是把所有bits设置为1。
243
244 ```c
245 void
246 pmm_init(uintptr_t mem_upper_lim)
247 {
248     max_pg = (PG_ALIGN(mem_upper_lim) >> 12);
249
250     pg_lookup_ptr = LOOKUP_START;
251
252     // mark all as occupied
253     for (size_t i = 0; i < PM_BMP_MAX_SIZE; i++) {
254         pm_bitmap[i] = 0xFFU;
255     }
256 }
257 ```
258
259 0号页一般不用,表示无效地址。总要有一个页的范围来表示一个指针的指向是无效的。如果一个指针指向哪里都是合法,那我们函数不能把指针作为NULL来返回错误。因为别人不会觉得NULL表示函数执行失败。
260
261 下面分析分配内存的`pmm_alloc_page()`函数实现,它会遍历pm_bitmap
262
263 ```c
264 while (!good_page_found && pg_lookup_ptr < upper_lim) {
265         chunk = pm_bitmap[pg_lookup_ptr >> 3];
266 ```
267
268 直到找到第一个值为0的bit。找到后将该bit置为1,返回该页面的物理地址(`good_page_found`)。
269
270 ```c
271 if (chunk != 0xFFU) {
272             for (size_t i = pg_lookup_ptr % 8; i < 8; i++, pg_lookup_ptr++) {
273                 if (!(chunk & (0x80U >> i))) {
274                     pmm_mark_page_occupied(pg_lookup_ptr);
275                     good_page_found = pg_lookup_ptr << 12;
276                     break;
277                 }
278             }
279         }
280 ```
281
282 pg_lookup_ptr作为全局变量,它存储每次分配后搜索到的页号,赋值给old_pg_ptr。每次的搜索范围是`[old_pg_ptr, max_pg)`。
283
284 ```c
285     size_t old_pg_ptr = pg_lookup_ptr;
286     size_t upper_lim = max_pg;
287 ```
288
289 如果没找到会修改old_pg_ptr为LOOKUP_START。再在`[1, old_pg_ptr)`范围内查找。
290
291 ```c
292             // We've searched the interval [old_pg_ptr, max_pg) but failed
293             //   may be chances in [1, old_pg_ptr) ?
294             // Let's find out!
295             if (pg_lookup_ptr >= upper_lim && old_pg_ptr != LOOKUP_START) {
296                 upper_lim = old_pg_ptr;
297                 pg_lookup_ptr = LOOKUP_START;
298                 old_pg_ptr = LOOKUP_START;
299             }
300 ```
301
302 ### kernel/mm/vmm.c
303
304 实现页目录初始化。页目录有1024个entry,所以分配1个物理页,把其中的1024个4Byte置0。最后一个entry设置为PDE(T_SELF_REF_PERM, dir),用于映射自己。
305
306 ```c
307 ptd_t* vmm_init_pd() {
308     ptd_t* dir = pmm_alloc_page();
309     for (size_t i = 0; i < 1024; i++)
310     {
311         dir[i] = 0;
312     }
313     
314     // 自己映射自己,方便我们在软件层面进行查表地址转换
315     dir[1023] = PDE(T_SELF_REF_PERM, dir);
316
317     return dir;
318 }
319 ```
320
321 当我们访问虚拟地址`0xFFFFF000U`(PTD_BASE_VADDR),MMU先取出22到31位的bits值1023,即查看页目录的第1023个entry。再取出第12到21位值为1023。因为上面设置该entry存储的物理地址为dir,所以访问dir[1023],取出里面的值作为最后的物理地址。该值为页目录起始物理地址。这样我们就可以通过虚拟地址来修改页目录了。
322
323 如果我们想把一个指定的虚拟地址映射到物理地址,要实现`vmm_map_page`。这个过程涉及到写页表,查找物理页。该函数最终会给我们一个映射到`pa`的虚拟地址,但这个虚拟地址不一定是我们期望的`va`。
324
325 ```c
326 void* vmm_map_page(void* va, void* pa, pt_attr dattr, pt_attr tattr) {
327     // 显然,对空指针进行映射没有意义。
328     if (!pa || !va) {
329         return NULL;
330     }
331 ```
332
333 先是根据参数va来得到需要修改的页目录项和页表项。ptd指向页目录第一个entry。
334
335 ```c
336     uintptr_t pd_offset = PD_INDEX(va);
337     uintptr_t pt_offset = PT_INDEX(va);
338     ptd_t* ptd = (ptd_t*)PTD_BASE_VADDR;
339 ```
340
341 最好情况是这两个页目录项和页表项没有被占用。即pde取出ptd[pd_offset]存储的值为0,不会进入while循环。
342
343 ```c
344     // 在页表与页目录中找到一个可用的空位进行映射(位于va或其附近)
345     ptd_t* pde = ptd[pd_offset];
346         pt_t* pt = (uintptr_t)PT_VADDR(pd_offset);
347     while (pde && pd_offset < 1024) {
348 ```
349
350 项目这里选择的是分配一个新页作为新页表。
351
352 ```c
353     // 页目录有空位,需要开辟一个新的 PDE
354     uint8_t* new_pt_pa = pmm_alloc_page();
355 ```
356
357 设置页目录指向新页表基址。设置新页表的entry指向要映射的物理地址pa。
358
359 ```c
360     ptd[pd_offset] = PDE(dattr, new_pt_pa);
361     memset((void*)PT_VADDR(pd_offset), 0, PM_PAGE_SIZE);
362     pt[pt_offset] = PTE(tattr, pa);
363 ```
364
365 最后根据offset获得虚拟地址。因为这个offset在非最好情况下会变,不能直接返回va,下面来看看为什么会改变。
366
367 ```c
368 return V_ADDR(pd_offset, pt_offset, PG_OFFSET(va));
369 ```
370
371 假如不是最佳情况,会进入while循环.
372
373 ```c
374     while (pde && pd_offset < 1024) {
375         if (pt_offset == 1024) {
376             pd_offset++;
377             pt_offset = 0;
378             pde = ptd[pd_offset];
379             pt = (pt_t*)PT_VADDR(pd_offset);
380         }
381         // 页表有空位,只需要开辟一个新的 PTE
382         if (pt && !pt[pt_offset]) {
383             pt[pt_offset] = PTE(tattr, pa);
384             return V_ADDR(pd_offset, pt_offset, PG_OFFSET(va));
385         }
386         pt_offset++;
387     }
388 ```
389
390 如果页目录中该项存在且页表项为空,进入while循环里面的`if (pt && !pt[pt_offset])`分支。直接写好这个页表项就行了。如果页目录该项和页表该项不为空,就没地方写了。所以要退而求其次`pt_offset++;`。
391
392 如果该页表项后面的项都没地方写,就换一个页表。
393
394 ```c
395         if (pt_offset == 1024) {
396             pd_offset++;
397 ```
398
399 更新一下pde和pt,继续循环
400
401 ```c
402             pde = ptd[pd_offset];
403             pt = (pt_t*)PT_VADDR(pd_offset);
404 ```
405
406 总的来说就是遍历页目录和页表,找到可写的地方来设置页目录和页表。
407
408 如果找不到,pd_offset会因为过大跳出循环
409
410 ```
411 while (pde && pd_offset < 1024)
412 ```
413
414 最后函数返回NULL,下面代码应该改成大于等于。
415
416 ```c
417     // 页目录与所有页表已满!
418     if (pd_offset > 1024) {
419         return NULL;
420     }
421 ```
422
423 `vmm_unmap_page`用于取消一个虚拟地址映射,涉及到清除对应页目录项和页表项的操作。
424
425 先是获得两个偏移和页目录指针。
426
427 ```c
428 void vmm_unmap_page(void* vpn) {
429     uintptr_t pd_offset = PD_INDEX(vpn);
430     uintptr_t pt_offset = PT_INDEX(vpn);
431     ptd_t* self_pde = PTD_BASE_VADDR;
432 ```
433
434 然后检查是否pde的值存在。因为用户可能随便输入参数,如果参数对于的项为空,就不用写了。
435
436 ```c
437     ptd_t pde = self_pde[pd_offset];
438
439     if (pde) {
440         //...
441     }
442 }
443 ```
444
445 获得页表地址,再根据pt_offset偏移获得要写的entry的地址。TLB是虚拟地址到物理地址映射的缓存,相当于数据结构的map。修改entry前需要使用invlpg来刷新TLB缓存。
446
447 ```c
448         pt_t* pt = (pt_t*)PT_VADDR(pd_offset);
449         uint32_t pte = pt[pt_offset];
450         if (IS_CACHED(pte) && pmm_free_page(pte)) {
451             // 刷新TLB
452             #ifdef __ARCH_IA32
453             __asm__("invlpg (%0)" :: "r"((uintptr_t)vpn) : "memory");
454             #endif
455         }
456         pt[pt_offset] = 0;
457 ```
458
459 其他函数就略过不分析了。
460
461 ### arch/x86/boot.S arch/x86/hhk.c
462
463 从注释可以知道,有1个页目录、5个页表。并且`_k_ptd`表示页目录地址。页表和页目录放在一段连续的空间。
464
465 ```c
466 /* 
467     1 page directory, 
468     5 page tables:
469         1. Mapping reserved area and hhk_init
470         2-5. Remapping the kernels
471 */
472
473 .section .kpg
474     .global _k_ptd
475     _k_ptd:
476         .skip KPG_SIZE, 0
477 ```
478
479 初始化栈
480
481 ```c
482     start_: 
483         movl $stack_top, %esp
484
485         subl $16, %esp
486 ```
487
488 把参数放入栈,调用函数`_save_multiboot_info`,`$mb_info`是参数`uint8_t* destination`,ebx是参数`multiboot_info_t* info`。
489
490 ```c
491         movl $mb_info, 4(%esp)
492         movl %ebx, (%esp)
493         call _save_multiboot_info
494 ```
495
496 _save_multiboot_info用于保存multiboot_info_t结构体和结构体内部指针指向的区域。
497
498 接下来初始化高半核。为什么这里_k_ptd需要减去一个值呢?
499
500 ```c
501         /*
502             _hhk_init用来初始化我们高半核:
503                 1. 初始化最简单的PD与PT(重新映射我们的内核至3GiB处,以及对相应的地方进行Identity Map)
504         */
505
506         movl $(KPG_SIZE), 4(%esp)
507         movl $(_k_ptd - 0xC0000000), (%esp)    /* PTD物理地址 */
508         call _hhk_init
509 ```
510
511 linker.ld给出了答案。每个节都有一个虚拟地址(VMA)和加载地址(LMA)。默认情况下VMA和LMA相同。
512
513 ```assembly
514 /* Relocation of our higher half kernel */
515 . += 0xC0000000;
516
517 /* 好了,我们的内核…… */
518 .text BLOCK(4K) : AT ( ADDR(.text) - 0xC0000000 ) {
519 //...
520 .kpg BLOCK(4K) : AT ( ADDR(.kpg) - 0xC0000000 ) {
521     build/obj/arch/x86/*.o (.kpg)
522 }
523 ```
524
525 VMA指的是文件中的地址,并不是内存中的地址。通过section信息可以知道这一点。
526
527 ```c
528 $ readelf -S lunaix.bin 
529 There are 37 section headers, starting at offset 0x143f8:
530
531 Section Headers:
532   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
533   [ 0]                   NULL            00000000 000000 000000 00      0   0  0
534   [ 1] .hhk_init_text    PROGBITS        00100000 001000 000216 00  AX  0   0  1
535   [ 2] .hhk_init_bss     NOBITS          00101000 002000 004fbe 00  WA  0   0 16
536   [ 3] .text             PROGBITS        c0106000 002000 001274 00  AX  0   0  1
537   [ 4] .text.__x86.[...] PROGBITS        c0107274 003274 000004 00  AX  0   0  1
538   [ 5] .text.__x86.[...] PROGBITS        c0107278 003278 000004 00  AX  0   0  1
539   [ 6] .text.__x86.[...] PROGBITS        c010727c 00627c 000004 00  AX  0   0  1
540   [ 7] .text.__x86.[...] PROGBITS        c0107280 006280 000004 00  AX  0   0  1
541   [ 8] .bss              NOBITS          c0108000 004000 020152 00  WA  0   0 32
542 ```
543
544 LMA对于这个项目来说是物理地址,VMA就是符号的地址。`ADDR(.kpg) - 0xC0000000`表示实际页目录会存放在更低的地方,也就是说直接用`_k_ptd`这个符号不能得到存放地点,要减去0xC0000000。
545
546 需要动态调试来理解一下。
547
548 如果你想使用自带的gcc来编译**这个版本**的`lunaix-os`。需要修改一些文件。
549
550 config/make-cc
551
552 ```makefile
553 CC := gcc -m32 -fno-pie
554 AS := as
555 ...
556 LDFLAGS := -no-pie -ffreestanding $(O) -nostdlib
557 ```
558
559 搜索-lgcc并删除,i686-elf-objcopy改成objcopy、i686-elf-objdump改成objdump
560
561 执行下面代码后会有三个窗口(原本窗口、qemu控制台、qemu)。
562
563 ```c
564 make debug-qemu
565 ```
566
567 先在qemu控制台输入c。
568
569 ```c
570 Trying 127.0.0.1...
571 Connected to 127.0.0.1.
572 Escape character is '^]'.
573 QEMU 6.2.0 monitor - type 'help' for more information
574 (qemu) c
575 ```
576
577 再到原本窗口进行调试。
578
579 ```c
580 pwndbg> b start_ 
581 Breakpoint 1 at 0x10000c
582 pwndbg> c
583 ```
584
585 最后点击qemu的界面enter启动OS。
586
587 可以看到$0x12b000是`$(_k_ptd - 0xC0000000)`真实结果。
588
589 ```assembly
590    0x100024 <start_+24>    movl   $0x6000, 4(%esp)
591    0x10002c <start_+32>    movl   $0x12b000, (%esp)
592    0x100033 <start_+39>    calll  _hhk_init                      <_hhk_init>
593 ```
594
595 pwndbg中可以更方便的看到源代码信息。
596
597 ```c
598 In file: /home/ffreestanding/Desktop/lunaixos-tutorial/check/lunaix-os/lunaix-os/arch/x86/hhk.c
599    123 _hhk_init(ptd_t* ptd, uint32_t kpg_size) {
600    124 
601    125     // 初始化 kpg 全为0
602    126     //      P.s. 真没想到GRUB会在这里留下一堆垃圾! 老子的页表全乱套了!
603    127     uint8_t* kpg = (uint8_t*) ptd;
604  ► 128     for (uint32_t i = 0; i < kpg_size; i++)
605    129     {
606    130         *(kpg + i) = 0;
607    131     }
608    132     
609    133     _init_page(ptd);
610 ```
611
612 接着分析`_hhk_init`函数,它调用了_init_page。
613
614 ```c
615 void 
616 _hhk_init(ptd_t* ptd, uint32_t kpg_size) {
617
618     // 初始化 kpg 全为0
619     //      P.s. 真没想到GRUB会在这里留下一堆垃圾! 老子的页表全乱套了!
620     uint8_t* kpg = (uint8_t*) ptd;
621     for (uint32_t i = 0; i < kpg_size; i++)
622     {
623         *(kpg + i) = 0;
624     }
625     
626     _init_page(ptd);
627 }
628 ```
629
630 `SET_PDE`取出以ptd为基地址的页表0号entry,写入`PDE(PG_PRESENT, ptd + PG_MAX_ENTRIES)`到entry中。
631
632 ```c
633 void 
634 _init_page(ptd_t* ptd) {
635     SET_PDE(ptd, 0, PDE(PG_PRESENT, ptd + PG_MAX_ENTRIES))
636 ```
637
638 `PDE`宏用于构造一个页目录entry。这里写入的是第一个页表的物理地址(ptd + PG_MAX_ENTRIES),因为页目录后面紧接着的是页表。它们被设计在一段连续的内存。
639
640 看看如何对等映射hhk_init。
641
642 ```c
643     // 对等映射我们的hhk_init,这样一来,当分页与地址转换开启后,我们依然能够照常执行最终的 jmp 指令来跳转至
644     //  内核的入口点
645     for (uint32_t i = 0; i < HHK_PAGE_COUNT; i++)
646     {
647         SET_PTE(ptd, PG_TABLE_IDENTITY, 256 + i, PTE(PG_PREM_RW, 0x100000 + (i << 12)))
648     }
649 ```
650
651 我们可以看看linker.ld。可以知道我们的__init_hhk_end是初始化代码和数据存储的上边界,0x100000是下边界。
652
653 ```assembly
654     . = 0x100000;
655
656     /* 这里是我们的高半核初始化代码段和数据段 */
657
658     .hhk_init_text BLOCK(4K) : {
659     //...
660     __init_hhk_end = ALIGN(4K);
661 ```
662
663 那么我们可以计算出所需page的数量。`__init_hhk_end`符号由链接脚本提供。C代码通过`extern uint8_t __init_hhk_end;`来获得。
664
665 ```c
666 #define HHK_PAGE_COUNT              ((sym_val(__init_hhk_end) - 0x100000 + 0x1000 - 1) >> 12)
667 ```
668
669 for循环次数就得到了,接下来写入页表项即可。
670
671 为什么要对等映射hhk_init呢?如果有下面这条指令
672
673 ```assembly
674 jmp init_func
675 ```
676
677 init_func的值是固定的。如果没开启分页,就会把init_func作为线性地址(此时相当于物理地址)来执行物理地址init_func处的代码。如果开启了分页,就会把它作为虚拟地址来看待。虚拟地址转换成物理地址再执行物理地址处的代码。所以虚拟地址要映射到相等的物理地址。系统才能正常运转。如果没有对等映射,跳转到的也是无意义的物理地址。
678
679 如果没有设置好虚拟地址到物理地址的映射,分页后的下一行代码都走不到。
680
681 为什么下面内核代码不需要对等映射呢?
682
683 ```c
684         // --- 将内核重映射至高半区 ---
685     
686     // 这里是一些计算,主要是计算应当映射进的 页目录 与 页表 的条目索引(Entry Index)
687     uint32_t kernel_pde_index = PD_INDEX(sym_val(__kernel_start));
688     uint32_t kernel_pte_index = PT_INDEX(sym_val(__kernel_start));
689     uint32_t kernel_pg_counts = KERNEL_PAGE_COUNT;
690 ```
691
692 因为我们把程序代码里符号地址放到了高地址,加载地址(物理地址)在低地址。如果我们不分页调用内核函数会跳转到无意义的更高地址。真正的内核在低地址。
693
694 ```
695 .text BLOCK(4K) : AT ( ADDR(.text) - 0xC0000000 ) {
696 ```
697
698 所以内核映射要做的是映射函数符号或者其他内核符号到真正的物理地址。
699
700 ```c
701     // 将内核所需要的页表注册进页目录
702     //  当然,就现在而言,我们的内核只占用不到50个页(每个页表包含1024个页)
703     //  这里分配了3个页表(12MiB),未雨绸缪。
704         for (uint32_t i = 0; i < PG_TABLE_STACK - PG_TABLE_KERNEL; i++)
705     {
706         SET_PDE(
707             ptd, 
708             kernel_pde_index + i,   
709             PDE(PG_PREM_RW, PT_ADDR(ptd, PG_TABLE_KERNEL + i))
710         )
711     }
712
713     // ...
714     
715     // 重映射内核至高半区地址(>=0xC0000000)
716     for (uint32_t i = 0; i < kernel_pg_counts; i++)
717     {
718         SET_PTE(
719             ptd, 
720             PG_TABLE_KERNEL, 
721             kernel_pte_index + i, 
722             PTE(PG_PREM_RW, kernel_pm + (i << 12))
723         )
724     }
725 ```
726
727 循环映射在`kernel/mm/vmm.c`中已经分析过了
728
729 ```c
730     // 最后一个entry用于循环映射
731     SET_PDE(
732         ptd,
733         1023,
734         PDE(T_SELF_REF_PERM, ptd)
735     );
736 ```
737
738 其余代码视频里已经讲解了。
739
740 ### kernel/asm/x86/prologue.S
741
742 实际高半核起始虚拟地址是0x100000+hhk_init大小+0xC0000000
743
744 ```assembly
745 /* 高半核入口点 - 0xC0000000 */
746
747 .section .text
748     .global hhk_entry_
749     hhk_entry_:
750 ```
751
752 安装好GDT后,主要调用了三个函数。接下来看看这三个函数。
753
754 ### kernel/k_main.c
755
756 第一个函数是`_kernel_init`,先是安装了IDT。
757
758 ```c
759 void
760 _kernel_init(multiboot_info_t* mb_info)
761 {
762     _init_idt();
763 ```
764
765 然后根据传入的mb_info来标记哪些页为占用,哪些为空闲。
766
767 我觉得最值得注意的是映射VGA地址。因为在此前低1MiB对等映射包含了VGA地址。后面_kernel_post_init函数会清除对等映射。所以这里对VGA再映射一次。
768
769 ```c
770     // 首先,标记VGA部分为已占用
771     pmm_mark_chunk_occupied(VGA_BUFFER_PADDR >> 12, vga_buf_pgs);
772     
773     // 重映射VGA文本缓冲区(以后会变成显存,i.e., framebuffer)
774     for (size_t i = 0; i < vga_buf_pgs; i++)
775     {
776         vmm_map_page(VGA_BUFFER_VADDR + (i << 12), VGA_BUFFER_PADDR + (i << 12), PG_PREM_RW, PG_PREM_RW);
777     }
778     
779     // 更新VGA缓冲区位置至虚拟地址
780     tty_set_buffer(VGA_BUFFER_VADDR);
781 ```
782
783 它将0xB0000000UL映射到了0xB8000UL。
784
785 ```c
786 #define VGA_BUFFER_VADDR        0xB0000000UL
787 #define VGA_BUFFER_PADDR        0xB8000UL
788 ```
789
790 看看`_kernel_post_init`这个函数。
791
792 这个函数用于清理之前高半核初始化代码,将它们标记为可用物理页。
793
794 ```c
795 void
796 _kernel_post_init() {
797     printf("[KERNEL] === Post Initialization === \n");
798     size_t hhk_init_pg_count = ((uintptr_t)(&__init_hhk_end)) >> 12;
799     printf("[MM] Releaseing %d pages from 0x0.\n", hhk_init_pg_count);
800
801     // 清除 hhk_init 与前1MiB的映射
802     for (size_t i = 0; i < hhk_init_pg_count; i++) {
803         vmm_unmap_page((i << 12));
804     }
805     printf("[KERNEL] === Post Initialization Done === \n\n");
806 }
807 ```
808
809 最后是`_kernel_main`函数,它打印了一些信息。
810
811 ### hal/cpu.c
812
813 传入0x80000000UL和0,如果返回值大于0x80000004UL则说明支持[1]。
814
815 ```c
816 int cpu_brand_string_supported() {
817     reg32 supported = __get_cpuid_max(BRAND_LEAF, 0);
818     return (supported >= 0x80000004UL);
819 }
820 ```
821
822 从__get_cpuid_max定义上面的注释中可以知道它的作用。它使用cpuid指令来实现的,库函数这里就不分析了。
823
824 ```c
825 /* Return highest supported input value for cpuid instruction.  ext can
826    be either 0x0 or 0x80000000 to return highest supported value for
827    basic or extended cpuid information.  Function returns 0 if cpuid
828    is not supported or whatever cpuid returns in eax register.  If sig
829    pointer is non-null, then first four bytes of the signature
830    (as found in ebx register) are returned in location pointed by sig.  */
831 static __inline unsigned int
832 __get_cpuid_max (unsigned int __ext, unsigned int *__sig)
833 ```
834
835 `cpu_get_brand`中如果不支持,就设置成问号。
836
837 ```c
838 void cpu_get_brand(char* brand_out) {
839     if(!cpu_brand_string_supported()) {
840         brand_out[0] = '?';
841         brand_out[1] = '\0';
842     }
843     //...
844 }
845 ```
846
847 接下来调用cpuid指令参数分别是0x80000002UL、0x80000003UL、0x80000004UL。
848
849 ```c
850     uint32_t* out = (uint32_t*) brand_out;
851     reg32 eax, ebx, edx, ecx;
852     for (uint32_t i = 2, j = 0; i < 5; i++)
853     {
854         __get_cpuid(BRAND_LEAF + i, &eax, &ebx, &ecx, &edx);
855         out[j] = eax;
856         out[j + 1] = ebx;
857         out[j + 2] = ecx;
858         out[j + 3] = edx;
859         j+=4;
860     }
861     brand_out[48] = '\0';
862 ```
863
864 可以看看下面这个cpuid参数和其对应的返回值[2]。总共可以返回48字节的处理器信息。
865
866 ```tex
867 EAX = 80000002H:
868 EAX := Processor Brand String;
869 EBX := Processor Brand String, continued;
870 ECX := Processor Brand String, continued;
871 EDX := Processor Brand String, continued;
872 BREAK;
873 EAX = 80000003H:
874 EAX := Processor Brand String, continued;
875 EBX := Processor Brand String, continued;
876 ECX := Processor Brand String, continued;
877 EDX := Processor Brand String, continued;
878 BREAK;
879 EAX = 80000004H:
880 EAX := Processor Brand String, continued;
881 EBX := Processor Brand String, continued;
882 ECX := Processor Brand String, continued;
883 EDX := Processor Brand String, continued;
884 BREAK;
885 EAX = 80000005H:
886 EAX := Reserved = 0;
887 EBX := Reserved = 0;
888 ECX := Reserved = 0;
889 EDX := Reserved = 0;
890 ```
891
892 cpu_get_brand会把处理器信息复制到参数所指位置,被_kernel_main调用。
893
894 ```c
895     cpu_get_brand(buf);
896     printf("CPU: %s\n\n", buf);
897 ```
898
899 如果用qemu运行OS可以看到打印的是
900
901 ```bash
902 CPU: QEMU Virtual CPU version 2.5+
903 ```
904
905 ## 参考
906
907 [1]Intel 手册,Vol. 2A,Figure 3-9. Determination of Support for the Processor Brand String
908
909 [2]Intel 手册,Vol. 2A,Chapter 3,CPUID—CPU Identification,3-255