4 git checkout e141fd4dcd5effc2dbe59a498d7ea274b7199147
19 printf(char* fmt, ...)
24 __sprintf_internal(buffer, fmt, args);
27 // 啊哈,直接操纵framebuffer。日后须改进(FILE* ?)
32 这个函数的参数是变长的,这里获得fmt后面的一个参数,然后传给__sprintf_internal进行了处理。可以推断,这里是一个个参数进行格式化处理的。而buffer就是格式化后的字符串存储的位置。
34 接下来看看参数是如何一个个放入buffer的。
42 如果遇到`%`,说明要进入格式化处理了,于是设置adv为1。
51 如果我们的fmt是`hello %s`,那么接下来看看怎么处理`%`后面的`s`
55 char* str = va_arg(args, char*);
56 strcpy(buffer + ptr, str);
62 这里把args解析成一个字符指针,然后把str复制到buffer中。最后还用adv来存储字符串的长度。我们在不继续看代码的前提下,可以根据这个例子来知道其他情况也是根据类型解析args,最后复制到buffer。但是adv的作用无法推理,所以需要继续看代码。
64 我们可以找到一个adv被使用的地方来了解它的作用
71 ptr需要在循环结束时加上adv,说明它的作用是设置buffer指针的偏移。因为我们格式化的结果保存到了buffer,buffer的尾部就会往后移动,所以ptr也要移动。下次保存结果到buffer就不需要移动指针。
73 代码中还有`__itoa_internal`这样的函数,它是用来把数字类型转换成字符串的。
77 num = va_arg(args, int);
78 __itoa_internal(num, buffer + ptr, 10, &adv);
83 先根据value来判断要不要添加符号。最后都是转换成正整数(无符号整数)来传递给`__uitoa_internal`处理。
87 __itoa_internal(int value, char* str, int base, unsigned int* size)
89 if (value < 0 && base == 10) {
91 unsigned int _v = (unsigned int)(-value);
92 __uitoa_internal(_v, str + 1, base, size);
94 __uitoa_internal(value, str, base, size);
105 __uitoa_internal(unsigned int value, char* str, int base, unsigned int* size)
107 unsigned int ptr = 0;
114 这里是将value在循环中不断除以base。base就是进制。比如16进制,需要每次除以16。
118 str[ptr] = base_char[value % base];
119 value = value / base;
124 对于循环第一行,我们可以假设base为16,`value`值为`0x156`
127 char base_char[] = "0123456789abcdefghijklmnopqrstuvwxyz";
130 `base_char[value % base]`就是`base_char[12]=='6'`(得到最低位的值),接着把value除以进制,再进行下一次循环,直到value为0。后面会依次得到`'5'`、`'1'`。得到下面数组内容。
137 实际上我们要输出`"156"`,而不是`"651"`,所以最后需要用下面代码把字符串排列顺序颠倒。
140 for (unsigned int i = 0; i < (ptr >> 1); i++) {
142 str[i] = str[ptr - i - 1];
143 str[ptr - i - 1] = c;
153 接下来更新size,size实际上是另外一个返回值(adv的指针)。因为格式化后字符串往往会膨胀,所以要记录膨胀后的偏移(`unsigned int ptr = 0;`)。
161 总的来说,项目printf的实现是把fmt中代格式化字符串,进行格式化后写入buffer。adv的作用是告诉ptr膨胀后的大小(见代码` ptr += adv;`)。不过buffer可能会溢出,不知道会不会为后面的hack环节买下伏笔。
165 ### includes/hal/io.h
169 下面函数用于向一个port写入一个字节大小的value,使用的是扩展内联汇编。主要是使用`out`指令来写入。
172 void io_port_wb(uint8_t port, uint8_t value) {
177 :: "r"(value) "r"(port)
184 ### includes/lunaix/mm/page.h
186 接下来看一些宏预热一下,马上分析虚拟内存相关代码。
188 lunaixOS的页目录和页表虚拟地址定义。也就是说,修改0xFFFFF000U~0xFFFFF000U+0xfff这个范围内的值就是在修改页目录。
191 // 页目录的虚拟基地址,可以用来访问到各个PDE
192 #define PTD_BASE_VADDR 0xFFFFF000U
194 // 页表的虚拟基地址,可以用来访问到各个PTE
195 #define PT_BASE_VADDR 0xFFC00000U
198 ### includes/lunaix/constants.h
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
208 #define VGA_BUFFER_VADDR 0xB0000000UL
209 #define VGA_BUFFER_PADDR 0xB8000UL
210 #define VGA_BUFFER_SIZE 4096
215 使用一个bit代表一个物理页,如果这个bit为1,表示该页已分配。4GB(2^32)总共包括2^20个4KB(2^12)。又因为uint8_t包含8个bits,所以数组大小为2^20/8(PM_BMP_MAX_SIZE)。
218 #define PM_BMP_MAX_SIZE (128 * 1024)
219 uint8_t pm_bitmap[PM_BMP_MAX_SIZE];
222 这个函数用于把某个bit置为0,表示该page被free了。
226 pmm_mark_page_free(uintptr_t ppn)
229 pm_bitmap[group] = pm_bitmap[group] & ~msk;
233 其他函数也是这样。读者在自己实现时,可以先实现bitmap数据结构,方便进行位操作。
235 此时下面的函数不用细看也能知道是把连续的页表标记为占用,这样读代码可以节省很多时间。
239 pmm_mark_chunk_occupied(uint32_t start_ppn, size_t page_count)
242 初始化物理内存管理器pmm(physical memory manager)。就是把所有bits设置为1。
246 pmm_init(uintptr_t mem_upper_lim)
248 max_pg = (PG_ALIGN(mem_upper_lim) >> 12);
250 pg_lookup_ptr = LOOKUP_START;
252 // mark all as occupied
253 for (size_t i = 0; i < PM_BMP_MAX_SIZE; i++) {
254 pm_bitmap[i] = 0xFFU;
259 0号页一般不用,表示无效地址。总要有一个页的范围来表示一个指针的指向是无效的。如果一个指针指向哪里都是合法,那我们函数不能把指针作为NULL来返回错误。因为别人不会觉得NULL表示函数执行失败。
261 下面分析分配内存的`pmm_alloc_page()`函数实现,它会遍历pm_bitmap
264 while (!good_page_found && pg_lookup_ptr < upper_lim) {
265 chunk = pm_bitmap[pg_lookup_ptr >> 3];
268 直到找到第一个值为0的bit。找到后将该bit置为1,返回该页面的物理地址(`good_page_found`)。
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;
282 pg_lookup_ptr作为全局变量,它存储每次分配后搜索到的页号,赋值给old_pg_ptr。每次的搜索范围是`[old_pg_ptr, max_pg)`。
285 size_t old_pg_ptr = pg_lookup_ptr;
286 size_t upper_lim = max_pg;
289 如果没找到会修改old_pg_ptr为LOOKUP_START。再在`[1, old_pg_ptr)`范围内查找。
292 // We've searched the interval [old_pg_ptr, max_pg) but failed
293 // may be chances in [1, old_pg_ptr) ?
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;
304 实现页目录初始化。页目录有1024个entry,所以分配1个物理页,把其中的1024个4Byte置0。最后一个entry设置为PDE(T_SELF_REF_PERM, dir),用于映射自己。
307 ptd_t* vmm_init_pd() {
308 ptd_t* dir = pmm_alloc_page();
309 for (size_t i = 0; i < 1024; i++)
314 // 自己映射自己,方便我们在软件层面进行查表地址转换
315 dir[1023] = PDE(T_SELF_REF_PERM, dir);
321 当我们访问虚拟地址`0xFFFFF000U`(PTD_BASE_VADDR),MMU先取出22到31位的bits值1023,即查看页目录的第1023个entry。再取出第12到21位值为1023。因为上面设置该entry存储的物理地址为dir,所以访问dir[1023],取出里面的值作为最后的物理地址。该值为页目录起始物理地址。这样我们就可以通过虚拟地址来修改页目录了。
323 如果我们想把一个指定的虚拟地址映射到物理地址,要实现`vmm_map_page`。这个过程涉及到写页表,查找物理页。该函数最终会给我们一个映射到`pa`的虚拟地址,但这个虚拟地址不一定是我们期望的`va`。
326 void* vmm_map_page(void* va, void* pa, pt_attr dattr, pt_attr tattr) {
333 先是根据参数va来得到需要修改的页目录项和页表项。ptd指向页目录第一个entry。
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;
341 最好情况是这两个页目录项和页表项没有被占用。即pde取出ptd[pd_offset]存储的值为0,不会进入while循环。
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) {
353 // 页目录有空位,需要开辟一个新的 PDE
354 uint8_t* new_pt_pa = pmm_alloc_page();
357 设置页目录指向新页表基址。设置新页表的entry指向要映射的物理地址pa。
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);
365 最后根据offset获得虚拟地址。因为这个offset在非最好情况下会变,不能直接返回va,下面来看看为什么会改变。
368 return V_ADDR(pd_offset, pt_offset, PG_OFFSET(va));
374 while (pde && pd_offset < 1024) {
375 if (pt_offset == 1024) {
378 pde = ptd[pd_offset];
379 pt = (pt_t*)PT_VADDR(pd_offset);
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));
390 如果页目录中该项存在且页表项为空,进入while循环里面的`if (pt && !pt[pt_offset])`分支。直接写好这个页表项就行了。如果页目录该项和页表该项不为空,就没地方写了。所以要退而求其次`pt_offset++;`。
392 如果该页表项后面的项都没地方写,就换一个页表。
395 if (pt_offset == 1024) {
402 pde = ptd[pd_offset];
403 pt = (pt_t*)PT_VADDR(pd_offset);
406 总的来说就是遍历页目录和页表,找到可写的地方来设置页目录和页表。
408 如果找不到,pd_offset会因为过大跳出循环
411 while (pde && pd_offset < 1024)
414 最后函数返回NULL,下面代码应该改成大于等于。
418 if (pd_offset > 1024) {
423 `vmm_unmap_page`用于取消一个虚拟地址映射,涉及到清除对应页目录项和页表项的操作。
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;
434 然后检查是否pde的值存在。因为用户可能随便输入参数,如果参数对于的项为空,就不用写了。
437 ptd_t pde = self_pde[pd_offset];
445 获得页表地址,再根据pt_offset偏移获得要写的entry的地址。TLB是虚拟地址到物理地址映射的缓存,相当于数据结构的map。修改entry前需要使用invlpg来刷新TLB缓存。
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)) {
453 __asm__("invlpg (%0)" :: "r"((uintptr_t)vpn) : "memory");
461 ### arch/x86/boot.S arch/x86/hhk.c
463 从注释可以知道,有1个页目录、5个页表。并且`_k_ptd`表示页目录地址。页表和页目录放在一段连续的空间。
469 1. Mapping reserved area and hhk_init
470 2-5. Remapping the kernels
483 movl $stack_top, %esp
488 把参数放入栈,调用函数`_save_multiboot_info`,`$mb_info`是参数`uint8_t* destination`,ebx是参数`multiboot_info_t* info`。
491 movl $mb_info, 4(%esp)
493 call _save_multiboot_info
496 _save_multiboot_info用于保存multiboot_info_t结构体和结构体内部指针指向的区域。
498 接下来初始化高半核。为什么这里_k_ptd需要减去一个值呢?
503 1. 初始化最简单的PD与PT(重新映射我们的内核至3GiB处,以及对相应的地方进行Identity Map)
506 movl $(KPG_SIZE), 4(%esp)
507 movl $(_k_ptd - 0xC0000000), (%esp) /* PTD物理地址 */
511 linker.ld给出了答案。每个节都有一个虚拟地址(VMA)和加载地址(LMA)。默认情况下VMA和LMA相同。
514 /* Relocation of our higher half kernel */
518 .text BLOCK(4K) : AT ( ADDR(.text) - 0xC0000000 ) {
520 .kpg BLOCK(4K) : AT ( ADDR(.kpg) - 0xC0000000 ) {
521 build/obj/arch/x86/*.o (.kpg)
525 VMA指的是文件中的地址,并不是内存中的地址。通过section信息可以知道这一点。
528 $ readelf -S lunaix.bin
529 There are 37 section headers, starting at offset 0x143f8:
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
544 LMA对于这个项目来说是物理地址,VMA就是符号的地址。`ADDR(.kpg) - 0xC0000000`表示实际页目录会存放在更低的地方,也就是说直接用`_k_ptd`这个符号不能得到存放地点,要减去0xC0000000。
548 如果你想使用自带的gcc来编译**这个版本**的`lunaix-os`。需要修改一些文件。
553 CC := gcc -m32 -fno-pie
556 LDFLAGS := -no-pie -ffreestanding $(O) -nostdlib
559 搜索-lgcc并删除,i686-elf-objcopy改成objcopy、i686-elf-objdump改成objdump
561 执行下面代码后会有三个窗口(原本窗口、qemu控制台、qemu)。
571 Connected to 127.0.0.1.
572 Escape character is '^]'.
573 QEMU 6.2.0 monitor - type 'help' for more information
581 Breakpoint 1 at 0x10000c
585 最后点击qemu的界面enter启动OS。
587 可以看到$0x12b000是`$(_k_ptd - 0xC0000000)`真实结果。
590 0x100024 <start_+24> movl $0x6000, 4(%esp)
591 0x10002c <start_+32> movl $0x12b000, (%esp)
592 0x100033 <start_+39> calll _hhk_init <_hhk_init>
595 pwndbg中可以更方便的看到源代码信息。
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) {
602 126 // P.s. 真没想到GRUB会在这里留下一堆垃圾! 老子的页表全乱套了!
603 127 uint8_t* kpg = (uint8_t*) ptd;
604 ► 128 for (uint32_t i = 0; i < kpg_size; i++)
612 接着分析`_hhk_init`函数,它调用了_init_page。
616 _hhk_init(ptd_t* ptd, uint32_t kpg_size) {
619 // P.s. 真没想到GRUB会在这里留下一堆垃圾! 老子的页表全乱套了!
620 uint8_t* kpg = (uint8_t*) ptd;
621 for (uint32_t i = 0; i < kpg_size; i++)
630 `SET_PDE`取出以ptd为基地址的页表0号entry,写入`PDE(PG_PRESENT, ptd + PG_MAX_ENTRIES)`到entry中。
634 _init_page(ptd_t* ptd) {
635 SET_PDE(ptd, 0, PDE(PG_PRESENT, ptd + PG_MAX_ENTRIES))
638 `PDE`宏用于构造一个页目录entry。这里写入的是第一个页表的物理地址(ptd + PG_MAX_ENTRIES),因为页目录后面紧接着的是页表。它们被设计在一段连续的内存。
643 // 对等映射我们的hhk_init,这样一来,当分页与地址转换开启后,我们依然能够照常执行最终的 jmp 指令来跳转至
645 for (uint32_t i = 0; i < HHK_PAGE_COUNT; i++)
647 SET_PTE(ptd, PG_TABLE_IDENTITY, 256 + i, PTE(PG_PREM_RW, 0x100000 + (i << 12)))
651 我们可以看看linker.ld。可以知道我们的__init_hhk_end是初始化代码和数据存储的上边界,0x100000是下边界。
656 /* 这里是我们的高半核初始化代码段和数据段 */
658 .hhk_init_text BLOCK(4K) : {
660 __init_hhk_end = ALIGN(4K);
663 那么我们可以计算出所需page的数量。`__init_hhk_end`符号由链接脚本提供。C代码通过`extern uint8_t __init_hhk_end;`来获得。
666 #define HHK_PAGE_COUNT ((sym_val(__init_hhk_end) - 0x100000 + 0x1000 - 1) >> 12)
669 for循环次数就得到了,接下来写入页表项即可。
671 为什么要对等映射hhk_init呢?如果有下面这条指令
677 init_func的值是固定的。如果没开启分页,就会把init_func作为线性地址(此时相当于物理地址)来执行物理地址init_func处的代码。如果开启了分页,就会把它作为虚拟地址来看待。虚拟地址转换成物理地址再执行物理地址处的代码。所以虚拟地址要映射到相等的物理地址。系统才能正常运转。如果没有对等映射,跳转到的也是无意义的物理地址。
679 如果没有设置好虚拟地址到物理地址的映射,分页后的下一行代码都走不到。
684 // --- 将内核重映射至高半区 ---
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;
692 因为我们把程序代码里符号地址放到了高地址,加载地址(物理地址)在低地址。如果我们不分页调用内核函数会跳转到无意义的更高地址。真正的内核在低地址。
695 .text BLOCK(4K) : AT ( ADDR(.text) - 0xC0000000 ) {
698 所以内核映射要做的是映射函数符号或者其他内核符号到真正的物理地址。
702 // 当然,就现在而言,我们的内核只占用不到50个页(每个页表包含1024个页)
703 // 这里分配了3个页表(12MiB),未雨绸缪。
704 for (uint32_t i = 0; i < PG_TABLE_STACK - PG_TABLE_KERNEL; i++)
708 kernel_pde_index + i,
709 PDE(PG_PREM_RW, PT_ADDR(ptd, PG_TABLE_KERNEL + i))
715 // 重映射内核至高半区地址(>=0xC0000000)
716 for (uint32_t i = 0; i < kernel_pg_counts; i++)
721 kernel_pte_index + i,
722 PTE(PG_PREM_RW, kernel_pm + (i << 12))
727 循环映射在`kernel/mm/vmm.c`中已经分析过了
734 PDE(T_SELF_REF_PERM, ptd)
740 ### kernel/asm/x86/prologue.S
742 实际高半核起始虚拟地址是0x100000+hhk_init大小+0xC0000000
745 /* 高半核入口点 - 0xC0000000 */
752 安装好GDT后,主要调用了三个函数。接下来看看这三个函数。
756 第一个函数是`_kernel_init`,先是安装了IDT。
760 _kernel_init(multiboot_info_t* mb_info)
765 然后根据传入的mb_info来标记哪些页为占用,哪些为空闲。
767 我觉得最值得注意的是映射VGA地址。因为在此前低1MiB对等映射包含了VGA地址。后面_kernel_post_init函数会清除对等映射。所以这里对VGA再映射一次。
771 pmm_mark_chunk_occupied(VGA_BUFFER_PADDR >> 12, vga_buf_pgs);
773 // 重映射VGA文本缓冲区(以后会变成显存,i.e., framebuffer)
774 for (size_t i = 0; i < vga_buf_pgs; i++)
776 vmm_map_page(VGA_BUFFER_VADDR + (i << 12), VGA_BUFFER_PADDR + (i << 12), PG_PREM_RW, PG_PREM_RW);
780 tty_set_buffer(VGA_BUFFER_VADDR);
783 它将0xB0000000UL映射到了0xB8000UL。
786 #define VGA_BUFFER_VADDR 0xB0000000UL
787 #define VGA_BUFFER_PADDR 0xB8000UL
790 看看`_kernel_post_init`这个函数。
792 这个函数用于清理之前高半核初始化代码,将它们标记为可用物理页。
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);
801 // 清除 hhk_init 与前1MiB的映射
802 for (size_t i = 0; i < hhk_init_pg_count; i++) {
803 vmm_unmap_page((i << 12));
805 printf("[KERNEL] === Post Initialization Done === \n\n");
809 最后是`_kernel_main`函数,它打印了一些信息。
813 传入0x80000000UL和0,如果返回值大于0x80000004UL则说明支持[1]。
816 int cpu_brand_string_supported() {
817 reg32 supported = __get_cpuid_max(BRAND_LEAF, 0);
818 return (supported >= 0x80000004UL);
822 从__get_cpuid_max定义上面的注释中可以知道它的作用。它使用cpuid指令来实现的,库函数这里就不分析了。
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)
835 `cpu_get_brand`中如果不支持,就设置成问号。
838 void cpu_get_brand(char* brand_out) {
839 if(!cpu_brand_string_supported()) {
847 接下来调用cpuid指令参数分别是0x80000002UL、0x80000003UL、0x80000004UL。
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++)
854 __get_cpuid(BRAND_LEAF + i, &eax, &ebx, &ecx, &edx);
861 brand_out[48] = '\0';
864 可以看看下面这个cpuid参数和其对应的返回值[2]。总共可以返回48字节的处理器信息。
868 EAX := Processor Brand String;
869 EBX := Processor Brand String, continued;
870 ECX := Processor Brand String, continued;
871 EDX := Processor Brand String, continued;
874 EAX := Processor Brand String, continued;
875 EBX := Processor Brand String, continued;
876 ECX := Processor Brand String, continued;
877 EDX := Processor Brand String, continued;
880 EAX := Processor Brand String, continued;
881 EBX := Processor Brand String, continued;
882 ECX := Processor Brand String, continued;
883 EDX := Processor Brand String, continued;
892 cpu_get_brand会把处理器信息复制到参数所指位置,被_kernel_main调用。
896 printf("CPU: %s\n\n", buf);
902 CPU: QEMU Virtual CPU version 2.5+
907 [1]Intel 手册,Vol. 2A,Figure 3-9. Determination of Support for the Processor Brand String
909 [2]Intel 手册,Vol. 2A,Chapter 3,CPUID—CPU Identification,3-255