操做系统做为一个基础系统软件,对下控制硬件(cpu、内存、磁盘网卡等外设),屏蔽了底层复杂多样的硬件差别;对上则提供封装良好的应用程序接口,简化应用程序开发者的使用难度。站在应用程序开发人员的角度来看,平常开发中常见的各类关于并发、I/O、程序通讯的问题等都和操做系统相关,所以必定程度上了解底层的操做系统工做原理是有必要的。javascript
另外一方面,因为操做系统自身功能的复杂性,总体设计通常会有一个好的模块化架构;操做系统做为基础服务,对性能效率的要求也很高,底层会用到许多关于数据结构和算法相关的知识。若是仔细的研究一个操做系统的源码,既能够学习设计一个复杂软件的架构知识,又能够看到偏理论的数据结构和算法知识是如何被运用在实际场景中的,更深入的体会不一样数据结构、算法在特定场景下的性能差别。html
然而对于初学者而言,学习操做系统并非一件轻松的事情。操做系统理论的学习过于抽象,每每看了就忘。而主流商业操做系统动辄十万、百万级的内核源码也令想要一窥究竟的普通人望而却步。对于一个已经迭代发展至关一段时间的系统,我的认为好的学习方法不是从最新的,相对复杂的版本开始了解,而是从最初始的,较为简单的版本起步,研究其是如何一步步优化、迭代至现在的这个版本。通过无数人迭代、优化的最新版本linux内核当然无比复杂,但90年代早期发布的版本却简单太多,更容易理解和学习,在掌握了相对简单的早期版本后,能下降后续学习更复杂的版本的难度。java
对于操做系统的学习而言,有很多大牛都出版了关于如何实现一个简易版操做系统的书籍,例如《Orange'S:一个操做系统的实现》、《30天自制操做系统》等等。很多大学也开始对操做系统的课程进行改革,再也不是枯燥的灌输理论知识点,而是尝试着让学生亲自动手实现一个demo操做系统,加深对知识内容的理解。其中麻省理工大学的公开课MIT 6.828是出品较早,久负盛名的。node
本系列博客的主角,是由清华大学出品的操做系统网上公开课,其中的实验课程就须要学生经过一个个的迭代实验,逐步实现一个名为ucore的操做系统。其实验指导书上对ucore os的评价是"麻雀虽小,五脏俱全",很是适合操做系统初学者进行学习。linux
ucore项目github仓库连接:https://github.com/chyyuu/os_kernel_lab (master分支)git
ucore实验指导书连接:https://chyyuu.gitbooks.io/ucore_os_docs/content/程序员
ucore公开课视频连接(学堂在线):https://www.xuetangx.com/course/THU08091000267github
工欲善其事,必先利其器。操做系统做为一门综合性的课程,须要掌握必定的前置基础知识才能顺利的完成ucore操做系统的学习。算法
ucore内核中绝大多数的功能都是使用C语言实现的,掌握C语言是学习ucore的基础。除了熟练掌握C语言的基础语法知识以外,最好能对宏、指针有必定了解。spring
推荐学习书籍:《C primer》
ucore内核是运行在80386这一32位x86架构的cpu之上的。虽然ucore的内核主要是由C语言实现的,但因为操做系统是贴近底层,与硬件有频繁交互的系统程序。在ucore中,CPU加电后的内核引导程序以及中断、与特定硬件交互时的部分都须要经过x86汇编来实现。
若是是汇编语言的初学者,强烈建议先学习8086汇编语言,创建一个对底层CPU工做原理的基本知识结构后,再学习更为复杂的32位汇编。
须要注意的一点是,ucore中的x86汇编代码是以AT&T格式编写的,和Intel格式的x86汇编虽然逻辑等价,但写法上有较大差别。对于经过Intel格式入门汇编的初学者来讲,须要稍微适应一下。
推荐学习书籍:《汇编语言》王爽著(8086汇编)、《X86汇编从实模式到保护模式》(80386汇编)
汇编语言对应的是机器码,其中的不少功能都与CPU硬件紧密关联。80386的分页、中断,特权级等功能在ucore操做系统的实现中扮演了重要的角色。若是对80386的工做原理了解不够,在阅读ucore与之相关的源码时会有困难。
推荐学习书籍:《X86汇编从实模式到保护模式》、《intel 80386参考手册》
在ucore中常常会出现c和汇编代码互相调用的地方。要想理解其工做原理,须要去理解C语言编译后生成的底层机器指令(汇编),统一的站在汇编语言的角度来思考。你须要了解C中的结构体、数组等数据结构在内存中的是如何排布的,C中的指针操做是如何被转换成各类内存寻址指令的,C中的函数调用与返回过程当中,因为参数压栈出栈等等栈上数据的是如何变化的等等。
其实C中的指针等比较难理解的概念,在有了必定的汇编语言基础后会理解的更加透彻。C中的指针和结构体使得程序员没必要再去思考汇编层面中繁琐的内存访问偏移量计算,通通的交由编译器处理,C程序员的脑力获得解放,可以站在更高的抽象层面去思考更复杂的业务问题。
有了C语言和汇编的基础后,能够经过编写简单的C程序,查看其反汇编代码来进行相关的学习(经过32位的编译器)。
推荐学习书籍:《深刻理解计算机系统》(Computer Systems A Programmer's perspective CSAPP)
ucore中所涉及到的通用数据结构并很少,只须要对双向链表和哈希表有必定了解便可。
虽然在后续的实验中参考linux的实现引入了红黑树等复杂数据结构优化一些算法的实现,但并不涉及核心流程,若是不是学有余力,在ucore的学习过程当中当作一个黑盒子去看待就行。
推荐学习公开课视频: 清华大学出品的数据结构公开课(邓俊辉)
初学者在学习ucore的过程当中碰到的一个很大的困难就是lab1做为最初始的一个实验,为了搭建起一个能实际运行的系统,一会儿引入了不少内容。这里面既有生成img镜像的功能,也有bootloader加载内核的功能,还有许多与硬件交互的代码逻辑,这些信息铺天盖地的涌来,容易劝退初学者。当时的我就差点被劝退了,但因为本身强烈的好奇心以及实验指导书首页的提醒:“lab1和lab2比较困难,有些同窗因为畏难而止步与此,很惋惜。经过lab1和lab2后,对计算机原理中的中断、段页表机制、特权级等的理解会更深刻,等会有等同于打通了任督二脉,后面的实验将一片坦途。”,最终仍是坚持了下来。实际的感受也确实如此,若是能理解lab一、lab2中诸多硬件相关的知识和C内核实现中不少巧妙但晦涩的指针、宏的用法,后续的实验将简单不少。
在整个ucore的学习过程当中,除了公开课的视频和资料外,网上不少关于ucore学习的博客也给了我很大帮助,因此我也但愿能经过博客分享本身的学习心得,帮助到更多对操做系统、ucore感兴趣的人。若是实验中碰到不懂的地方,多经过关键字去搜索相关资料以及网上关于ucore学习的博客可以起到事半功倍的做用。
下面进入正题,开始分析ucore在实验课程lab1中的内容:ucore系统加载启动过程的分析。
ucore的lab1项目结构从总体来看,按照执行的流程顺序分为三部分:img磁盘映像的生成、引导内核的bootloader程序、ucore内核的初始化。
ucore总体是一个makefile项目。经过make指令,解析项目中的makefile文件后会生成一个ucore.img磁盘映像。(lab1的实验课视频演示中能够详细的看到构建的全过程)
这个磁盘映像主要由两大部分组成:位于第一个扇区即引导扇区的ucore bootloader程序,以及第二个扇区开始日后的ucore kernel内核程序。
80386CPU在加电启动之初,会执行固化在BIOS中的程序。BIOS因为容量有限,自身不提供加载完整操做系统内核的功能,而是约定好会读取磁盘中第一个扇区(引导扇区)中的内容,将其加载至内存地址空间0x7c00处,在加载完毕后,令CS:IP指向0x7c00,跳转执行引导扇区中的引导程序的第一条指令。为了不所加载的磁盘引导扇区是一个无效扇区(可能引导扇区中的内容就是空的或是乱码),要求512字节大小的扇区在其最后两字节必须是0x55AA(其他的空余空间能够用0填充),不然没法经过BIOS的校验,引导失败。
ucore的makefile文件中,将项目中位于boot文件夹下的程序放入了ucore的第一个扇区,在makefile的"#create bootblock"注释开头的段中有所体现。其中调用了/tool/sign.c来生成写入一个合法的引导扇区。
因为项目中的makefile文件中有很多复杂脚本,若是对makefile工做原理不熟悉,在ucore的学习中能够下降要求,大体了解一下每一部分的代码大概在干什么便可,没必要强求理解每一行,避免在学习之初就产生太强的挫败感。
若是想对经过makefile是如何一步步完整的生成磁盘映像感兴趣,能够参考如下内容:
1. lab1项目目录下的report.md实验报告示例
2. https://www.jianshu.com/p/2f95d38afa1d 其中对lab1中makefile的分析很是详细
当BIOS加载完引导扇区的内容至内存后,CPU便会跳转到0x7c00执行命令,此时CPU的控制权便交给了ucore的引导程序bootloader。引导程序主体由boot文件夹下的bootasm.S和bootasm.c共同组成,其中bootasm.S因为构建时靠前,是先执行的。
bootasm.S的主要工做就是令80386从加电时默认的实模式切换到32位保护模式,经过代码的注释能够看到,因为一些历史缘由要令80386正确的进入保护模式仍是有点小麻烦的(并非简单的调整一个开关位就行)。在《X86汇编语言 从实模式到保护模式》一书中对此有更加详细的介绍。
在经过汇编指令完成80386从实模式至保护模式的切换后,经过call bootmain指令,跳转至bootmain.c中的bootmain函数完成引导内核的工做。
bootasm.S:
1 #include <asm.h> 2 3 # Start the CPU: switch to 32-bit protected mode, jump into C. 4 # The BIOS loads this code from the first sector of the hard disk into 5 # memory at physical address 0x7c00 and starts executing in real mode 6 # with %cs=0 %ip=7c00. 7 8 # 80386CPU为了兼容8086程序,最开始启动时是以16位的实模式进行工做的 9 # 生成img磁盘映像时,bootasm.S中的引导代码将会被放在引导扇区 10 # 80386CPU加电启动后,会执行BIOS中的默认引导程序,BIOS引导程序会将引导扇区中(第一个磁盘块)的内容读入内存,并放置在0x7C00(16位)/0x00007c00(32位)处 11 # 随后CPU会跳转到0x7c00处开始第一条指令的执行,即bootasm.S的第一条指令(start:) 12 13 .set PROT_MODE_CSEG, 0x8 # kernel code segment selector 14 .set PROT_MODE_DSEG, 0x10 # kernel data segment selector 15 .set CR0_PE_ON, 0x1 # protected mode enable flag 16 17 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader 18 .globl start 19 start: 20 .code16 # Assemble for 16-bit mode 21 cli # Disable interrupts 22 cld # String operations increment 23 24 # Set up the important data segment registers (DS, ES, SS). 25 xorw %ax, %ax # Segment number zero 26 movw %ax, %ds # -> Data Segment 27 movw %ax, %es # -> Extra Segment 28 movw %ax, %ss # -> Stack Segment 29 30 # Enable A20: 31 # For backwards compatibility with the earliest PCs, physical 32 # address line 20 is tied low, so that addresses higher than 33 # 1MB wrap around to zero by default. This code undoes this. 34 35 # 为了进入32位保护模式,必须先开启A20(第21位内存访问线),不然在32位寻址模式下给出的内存地址第21位始终为0,形成错误 36 # 为何须要特地开启A20总线? 37 # 在早期的8086CPU中,内存总线是20位的,由高16位的段基址和低16位的段内偏移共同构成一个20位的内存地址 38 # 但事实上在段基址和段内偏移比较大的状况下,其实际得出的结果是超过了20位的(例如0xFFFF段基址 <<< 4 + 0xFFFF段内偏移 > 0xFFFFF),出现了溢出 39 # 而8086中对这种溢出是兼容的,这种溢出在8086上会体现为绕回0x00000低端 40 # “程序员,你是知道的,他们喜欢钻研,更喜欢利用硬件的某些特性来展现本身的技术,很难说在当年有多少程序在依赖这个回绕特性工做着” 41 # 摘自《X86汇编语言 从实模式到保护模式》 11.5 关于第21条地址线A20的问题 42 # 到了更新版的80286时代,24位的内存总线,若是不默认关闭A20总线,那么就没法兼容使用回绕特性的8086程序了 43 # 而80386做为80286的后一代,也继承了80286这一特性 44 seta20.1: 45 inb $0x64, %al # Wait for not busy(8042 input buffer empty). 46 testb $0x2, %al 47 jnz seta20.1 48 49 movb $0xd1, %al # 0xd1 -> port 0x64 50 outb %al, $0x64 # 0xd1 means: write data to 8042’s P2 port 51 52 seta20.2: 53 inb $0x64, %al # Wait for not busy(8042 input buffer empty). 54 testb $0x2, %al 55 jnz seta20.2 56 57 movb $0xdf, %al # 0xdf -> port 0x60 58 outb %al, $0x60 # 0xdf = 11011111, means set P2’s A20 bit(the 1 bit) to 1 59 60 # Switch from real to protected mode, using a bootstrap GDT 61 # and segment translation that makes virtual addresses 62 # identical to physical addresses, so that the 63 # effective memory map does not change during the switch. 64 # 设置GDT,修改CRO寄存器中的保护模式容许位,进入保护模式 65 lgdt gdtdesc 66 movl %cr0, %eax 67 orl $CR0_PE_ON, %eax 68 movl %eax, %cr0 69 70 # Jump to next instruction, but in 32-bit code segment. 71 # Switches processor into 32-bit mode. 72 # 经过一个远跳转指令指向protcseg处的指令,令CPU清空以前在实模式下保存在流水线中的指令(当前处于保护模式下执行实模式的指令会出现各类问题) 73 ljmp $PROT_MODE_CSEG, $protcseg 74 75 # 下面的都是X86-32的汇编程序 76 .code32 # Assemble for 32-bit mode 77 protcseg: 78 # Set up the protected-mode data segment registers 79 # 跳转至保护模式后,须要刷新数据段寄存器(由于引入了特权级保护,避免数据段寄存器以前的值不对而出现漏洞) 80 movw $PROT_MODE_DSEG, %ax # Our data segment selector 81 movw %ax, %ds # -> DS: Data Segment 82 movw %ax, %es # -> ES: Extra Segment 83 movw %ax, %fs # -> FS 84 movw %ax, %gs # -> GS 85 movw %ax, %ss # -> SS: Stack Segment 86 87 # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00) 88 # 设置栈段寄存器 栈基址0x0,栈顶指针指向start段所在位置(0x7c00) 89 movl $0x0, %ebp 90 movl $start, %esp 91 # 调用跳转至bootmain.c中的bootmain函数,完成内核的引导 92 call bootmain 93 94 # If bootmain returns (it shouldn’t), loop. 95 # 自旋死循环(但若是引导程序和内核实现正确,bootmain函数将永远不会返回并执行至此。由于操做系统内核自己就是经过自旋循环常驻内存的) 96 spin: 97 jmp spin 98 99 # Bootstrap GDT 100 .p2align 2 # force 4 byte alignment 101 # SEG_ASM是位于asm.h中的宏,用于构造GDT中的段描述符 102 # 按照GDT的约定,第一个为NULL段。ucore采用的是平坦内存模型,因此代码段和数据段在内核中均只存在一个。 103 gdt: 104 SEG_NULLASM # null seg 105 SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel 106 SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel 107 108 gdtdesc: 109 .word 0x17 # sizeof(gdt) - 1 110 .long gdt # address gdt
bootloader引导程序是位于设备的第一个扇区,即引导扇区的,而ucore的内核程序则是从第二个磁盘扇区开始日后存放的。bootmain.c的任务就是将kernel内核部分从磁盘中读出并载入内存,并将程序的控制流转移至指定的内核入口处。
ucore的内核文件在生成磁盘映像时是以ELF(Executable and linking format)格式保存的。ELF文件是Unix/Linux下通用的一种可执行文件,对于ELF的详细介绍在《深刻理解计算机系统》的"连接"一章中有较为详细的介绍。
要想完全的理解ELF格式的文件是如何被编译器、连接器等工具生成的,须要对编译原理相关的知识进行系统的学习,难度很大。所以在ucore的学习过程当中,若是不是很了解ELF,能够简单的理解为ELF的文件头中标识了一个可执行程序中包含了哪些部分,好比代码段、数据段(只读数据段、可读写数据段)、栈段等等,分别存储在哪里;并指明了须要为这些段分配多少内存空间、须要被加载到内存的什么地址(虚拟地址)等。
ucore内核生成ELF文件的关键配置在/tools/kernel.ld中,能够清楚的看到内核加载的.text代码段基址为0x100000,后面紧跟着各类类型的数据段等。
kernel.ld:
/* Simple linker script for the JOS kernel. See the GNU ld 'info' manual ("info ld") to learn the syntax. */ OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) ENTRY(kern_init) SECTIONS { /* Load the kernel at this address: "." means the current address */ . = 0x100000; .text : { *(.text .stub .text.* .gnu.linkonce.t.*) } PROVIDE(etext = .); /* Define the 'etext' symbol to this value */ .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } /* Include debugging information in kernel memory */ .stab : { PROVIDE(__STAB_BEGIN__ = .); *(.stab); PROVIDE(__STAB_END__ = .); BYTE(0) /* Force the linker to allocate space for this section */ } .stabstr : { PROVIDE(__STABSTR_BEGIN__ = .); *(.stabstr); PROVIDE(__STABSTR_END__ = .); BYTE(0) /* Force the linker to allocate space for this section */ } /* Adjust the address for the data segment to the next page */ . = ALIGN(0x1000); /* The data segment */ .data : { *(.data) } PROVIDE(edata = .); .bss : { *(.bss) } PROVIDE(end = .); /DISCARD/ : { *(.eh_frame .note.GNU-stack) } }
在libs/elf.h中定义了两个ELF相关的结构体,elfhdr和proghdr,用于映射读取出来的内核ELF头信息。
elf.h:
#ifndef __LIBS_ELF_H__ #define __LIBS_ELF_H__ #include <defs.h> #define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endian /* file header */ struct elfhdr { uint32_t e_magic; // must equal ELF_MAGIC uint8_t e_elf[12]; uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image uint16_t e_machine; // 3=x86, 4=68K, etc. uint32_t e_version; // file version, always 1 uint32_t e_entry; // entry point if executable uint32_t e_phoff; // file position of program header or 0 uint32_t e_shoff; // file position of section header or 0 uint32_t e_flags; // architecture-specific flags, usually 0 uint16_t e_ehsize; // size of this elf header uint16_t e_phentsize; // size of an entry in program header uint16_t e_phnum; // number of entries in program header or 0 uint16_t e_shentsize; // size of an entry in section header uint16_t e_shnum; // number of entries in section header or 0 uint16_t e_shstrndx; // section number that contains section name strings }; /* program section header */ struct proghdr { uint32_t p_type; // loadable code or data, dynamic linking info,etc. uint32_t p_offset; // file offset of segment uint32_t p_va; // virtual address to map segment uint32_t p_pa; // physical address, not used uint32_t p_filesz; // size of segment in file uint32_t p_memsz; // size of segment in memory (bigger if contains bss) uint32_t p_flags; // read/write/execute bits uint32_t p_align; // required alignment, invariably hardware page size }; #endif /* !__LIBS_ELF_H__ */
bootmain.c:
#include <defs.h> #include <x86.h> #include <elf.h> /* ********************************************************************* * This a dirt simple boot loader, whose sole job is to boot * an ELF kernel image from the first IDE hard disk. * * DISK LAYOUT * * This program(bootasm.S and bootmain.c) is the bootloader. * It should be stored in the first sector of the disk. * 这个程序(bootasm.S and bootmain.c)是引导加载器程序,应该被保存在磁盘的第一个扇区 * * * The 2nd sector onward holds the kernel image. * 第二个扇区日后保存着内核映像 * * * The kernel image must be in ELF format. * 内核映像必须必须是ELF格式的 * * BOOT UP STEPS * * when the CPU boots it loads the BIOS into memory and executes it * * * the BIOS intializes devices, sets of the interrupt routines, and * reads the first sector of the boot device(e.g., hard-drive) * into memory and jumps to it. * * * Assuming this boot loader is stored in the first sector of the * hard-drive, this code takes over... * * * control starts in bootasm.S -- which sets up protected mode, * and a stack so C code then run, then calls bootmain() * * * bootmain() in this file takes over, reads in the kernel and jumps to it. * */ unsigned int SECTSIZE = 512 ; // 一个磁盘扇区的大小为512字节 struct elfhdr * ELFHDR = ((struct elfhdr *)0x10000) ; // scratch space /* waitdisk - wait for disk ready */ static void waitdisk(void) { // 读数据,当0x1f7不为忙状态时,能够读 while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; } /* readsect - read a single sector at @secno into @dst */ // 读取一个单独的扇区(由@secno指定)到@dst指针指向的内存中 static void readsect(void *dst, uint32_t secno) { // https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_3_2_3_dist_accessing.html // 实验指导书lab1中的对ide硬盘的访问中有详细介绍 // wait for disk to be ready waitdisk(); // 磁盘读取参数设置 outb(0x1F2, 1); // count = 1 outb(0x1F3, secno & 0xFF); outb(0x1F4, (secno >> 8) & 0xFF); outb(0x1F5, (secno >> 16) & 0xFF); outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors // wait for disk to be ready waitdisk(); // read a sector insl(0x1F0, dst, SECTSIZE / 4); } /* * * readseg - read @count bytes at @offset from kernel into virtual address @va, * might copy more than asked. * */ static void readseg(uintptr_t va, uint32_t count, uint32_t offset) { uintptr_t end_va = va + count; // round down to sector boundary va -= offset % SECTSIZE; // translate from bytes to sectors; kernel starts at sector 1 // 计算出须要读取的磁盘扇区号,因为第1个扇区被bootloader占据,kernel内核从第二个扇区开始(下标为1),因此扇区号须要增长1 uint32_t secno = (offset / SECTSIZE) + 1; // If this is too slow, we could read lots of sectors at a time. // We'd write more to memory than asked, but it doesn't matter -- // we load in increasing order. // 循环往复,经过va指针的自增,一个一个扇区的循环读取数据写入va指向的内存区域 for (; va < end_va; va += SECTSIZE, secno ++) { readsect((void *)va, secno); } } /* bootmain - the entry of bootloader */ void bootmain(void) { // read the 1st page off disk // 从硬盘中读取出内核文件ELF文件头数据,存入ELFHDR指针指向的内存区域 (大小为8个扇区) readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0); // is this a valid ELF? 校验读取出来的ELF文件头的魔数值是否正确 if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; } struct proghdr *ph, *eph; // load each program segment (ignores ph flags) ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff); // 根据elf文件头,得到程序段的起始 eph = ph + ELFHDR->e_phnum; // 程序段起始指针(*ph)指针偏移程序段数目(ELFHDR->e_phnum) = 最后一段程序的头部 for (; ph < eph; ph ++) { // 循环往复,将各个程序段的内容读取至指定的内存位置(ph->p_va) readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset); } // call the entry point from the ELF header // note: does not return // 经过函数指针的方式,跳转至ELFHDR->e_entry指定的程序初始执行入口(即内核入口) // 在makefile的配置中,ELFHDR->e_entry指向的是kern/init/init.c中的kern_init函数 (kernel.ld中的ENTRY(kern_init)) ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); bad: // 跳转至内核以后,不该该返回 outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); /* do nothing */ while (1); }
在bootloader将ucore的kernel内核完整的加载至内存,并经过ELF文件头中指定的entry入口跳转至内核入口,即/kern/init.c中的kern_init函数。
kern_init函数是内核的总控函数,内核中的各个组成部分都在kern_init函数中完成初始化。
总控函数一方面负责初始化与各类硬件的交互(例如与显卡、中断控制器、定时器等),另外一方面初始化各类内核功能(好比初始化物理内存管理器、中断描述符表IDT等),以后便经过一个自旋死循环令操做系统常驻内存,经过监听各类中断提供操做系统服务。
init.c(主体部分):
#include <defs.h> #include <stdio.h> #include <string.h> #include <console.h> #include <kdebug.h> #include <picirq.h> #include <trap.h> #include <clock.h> #include <intr.h> #include <pmm.h> #include <kmonitor.h> void kern_init(void) __attribute__((noreturn)); void grade_backtrace(void); static void lab1_switch_test(void); /** * 内核入口 总控函数 * */ void kern_init(void){ extern char edata[], end[]; memset(edata, 0, end - edata); // 初始化控制台(控制显卡交互),只有设置好了对显卡的控制后,std_out输出的信息(例如cprintf)才能显示在控制台中 cons_init(); // init the console const char *message = "(THU.CST) os is loading ..."; cprintf("%s\n\n", message); print_kerninfo(); grade_backtrace(); // 初始化物理内存管理器 pmm_init(); // init physical memory management // 初始化中断控制器 pic_init(); // init interrupt controller // 初始化中断描述符表 idt_init(); // init interrupt descriptor table // 初始化定时芯片 clock_init(); // init clock interrupt // 开中断 intr_enable(); // enable irq interrupt //LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test() // user/kernel mode switch test lab1_switch_test(); /* do nothing */ // 陷入死循环,避免内核程序退出。经过监听中断事件进行服务 while (1); }
从kern_init函数的代码中能够看出,其依次完成了以下的几个主要工做:
1. cons_init 初始化控制台(控制显卡交互)
2. pmm_init 初始化物理内存管理器(lab1中里面暂时只是完成了GDT的从新设置,比较简单。而在lab2的物理内存管理的实现中,pmm_init才成为主角)
3. pic_init 初始化中断控制器(内部经过与8259A中断控制器芯片进行交互,令ucore可以接收到来自硬件的各类中断请求)
4. idt_init 初始化中断描述符表(在下面的中断机制一节中详细介绍)
5. clock_init 初始化定时器(进行8253定时器的相关设置,将其设置为10ms发起一次时钟中断)
6. intr_enable 完成了内核结构的初始化后,开启中断,至此ucore内核正式开始运行
在kern_init的内核初始化过程当中,涉及到的与显卡、定时器等硬件交互的地方,要想深刻理解其工做原理,除了仔细阅读ucore的代码外,还需经过硬件手册等资料熟悉不一样硬件提供的交互接口,限于篇幅就再也不展开了。我的认为这一部份内容并不属于ucore的核心,若是不是特别感兴趣,能够将其暂时视为一个黑盒子,理解大体工做原理便可。
ucore在lab1中实现的一个很是重要的功能,就是创建了一个能够工做的中断服务框架。能够说操做系统的工做是离不开硬件提供的中断机制的。
下面分析ucore的中断机制是如何工做的。
80386中断工做机制介绍
前面提到过学习ucore的一个前提是对80386CPU的硬件工做原理有必定了解,这里先回顾一下80386的中断工做机制。
1. 在80386中,为了更好的支持对中断服务例程的特权级保护,使用中断描述符表代替了8086中的中断向量表。和8086中断向量表被固定在低位内存不同,80386CPU经过中断描述符表寄存器IDTR来定位中断描述符表IDT的位置,这给了操做系统的设计者必定的自主权。
2. 80386在执行完每条指令后,都会检查当前是否存在中断请求。若是没有发现中断请求,则接着执行后续指令;若是发现存在中断请求,则会根据中断信号中给出的中断类型码,从中断描述符表中查找到对应的中断描述符,在中断描述符中记录了对应的中断服务例程的入口。
3. 随后,CPU硬件会打断当前控制流,在栈上压入CS、EIP、EFLAGS等寄存器的内容(用于中断服务例程的返回),跳转到对应的中断服务例程入口,进行中断请求的处理。当中断服务返回时,经过以前压入栈中的CS,EIP等返回到以前被中断请求打断的控制流中,恢复现场,继续运行。
因为80386中断工做机制相对比较复杂,限于篇幅这里的流程介绍省略了很多细节。若是对这一块内容不熟悉的话须要经过实验指导书或是有关资料进行学习,或者参考我以前写的博客 80386学习(四) 80386中断,里面对此有较为详细的介绍。
ucore的中断工做机制大体能够分为如下几个部分:
1. IDT中断描述符表的创建
2. 中断栈帧的生成
3. 接收到中断栈帧,经过对应的中断服务例程进行处理
4. 中断服务例程处理完毕,中断返回
在ucore中,对于中断描述符表IDT的初始化,是在kern_init总控函数中经过idt_init函数进行的。
idt_init函数:
/* * * Interrupt descriptor table: * * Must be built at run time because shifted function addresses can't * be represented in relocation records. * */ static struct gatedesc idt[256] = {{0}}; static struct pseudodesc idt_pd = { sizeof(idt) - 1, (uintptr_t)idt }; /* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */ void idt_init(void) { /* LAB1 YOUR CODE : STEP 2 */ /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)? * All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ? * __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c * (try "make" command in lab1, then you will find vector.S in kern/trap DIR) * You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later. * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT). * Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction. * You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more. * Notice: the argument of lidt is idt_pd. try to find it! */ extern uintptr_t __vectors[]; int i; // 首先经过tools/vector.c经过程序生成/kern/trap/verctor.S,并在加载内核时对以前已经声明的全局变量__vectors进行总体的赋值 // __vectors数组中的每一项对应于中断描述符的中断服务例程的入口地址,在SETGATE宏的使用中能够体现出来 // 将__vectors数组中每一项关于中断描述符的描述设置到下标相同的idt中,经过宏SETGATE构造出最终的中断描述符结构 for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) { // 遍历idt数组,将其中的内容(中断描述符)设置进IDT中断描述符表中(默认的DPL特权级都是内核态DPL_KERNEL=0) SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); } // set for switch from user to kernel // 用户态与内核态的互相转化是经过中断实现的,单独为其一个中断描述符 // 因为须要容许用户态的程序访问使用该中断,DPL特权级为用户态DPL_USER=3 SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER); // load the IDT 令IDTR中断描述符表寄存器指向idt_pd,加载IDT // idt_pd结构体中的前16位为描述符表的界限,pd_base指向以前完成了赋值操做的idt数组的起始位置 lidt(&idt_pd); }
经过上述代码的注释能够发现,在idt_init函数中,经过构建项目时自动生成的中断描述符元信息数组__vectors,在一个循环中,经过SETGATE宏,将idt[i]中的每一项都赋值了一个中断描述符。 能够看到中断描述符和gatedesc门描述符结构体的对应关系。(C中结构体的字段在内存中排布的顺序是按照定义的顺序,从低位到高位的)
中断门示意图:
gatedesc结构体:
/* Gate descriptors for interrupts and traps */ struct gatedesc { unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment unsigned gd_ss : 16; // segment selector unsigned gd_args : 5; // # args, 0 for interrupt/trap gates unsigned gd_rsv1 : 3; // reserved(should be zero I guess) unsigned gd_type : 4; // type(STS_{TG,IG32,TG32}) unsigned gd_s : 1; // must be 0 (system) unsigned gd_dpl : 2; // descriptor(meaning new) privilege level unsigned gd_p : 1; // Present unsigned gd_off_31_16 : 16; // high bits of offset in segment };
SETGATE宏:
/* * * Set up a normal interrupt/trap gate descriptor * - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate * - sel: Code segment selector for interrupt/trap handler * - off: Offset in code segment for interrupt/trap handler * - dpl: Descriptor Privilege Level - the privilege level required * for software to invoke this interrupt/trap gate explicitly * using an int instruction. * */ #define SETGATE(gate, istrap, sel, off, dpl) { \ (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \ (gate).gd_ss = (sel); \ (gate).gd_args = 0; \ (gate).gd_rsv1 = 0; \ (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \ (gate).gd_s = 0; \ (gate).gd_dpl = (dpl); \ (gate).gd_p = 1; \ (gate).gd_off_31_16 = (uint32_t)(off) >> 16; \ }
最终构建出了一个48位的结构体pseudodesc,前16位标识着中断描述符表的大小(pd_lim),后32位标识着中断描述符表IDT的基址(pd_base)。
若是熟悉80386中断机制的话,就会发现这一结构与IDTR寄存器所须要的结构一致。在idt_init函数的最后,经过lidt函数执行汇编指令lidt,完成了对IDTR寄存器的赋值。至此,ucore的中断描述符表设置完成。
/* Pseudo-descriptors used for LGDT, LLDT(not used) and LIDT instructions. */ struct pseudodesc { uint16_t pd_lim; // Limit uint32_t pd_base; // Base address } __attribute__ ((packed));
下面接着分析,中断描述符表里到底存放了什么数据结构,在ucore的中断服务功能的创建中是如何发挥做用的?
打开以前用于构造中断描述符数组,为vertors赋值的/kern/trap/vector.S,能够看到其中的每一项的中断服务例程的代码都同样。有的项首先push了一个0,有的没有(下面会介绍为何会有这种差别)。接下来将下标push压入栈中,便统一jmp跳转到了__alltraps处了。
vector.S:(很长,几乎都是脚本生成的模板代码)
# handler .text .globl __alltraps .globl vector0 vector0: pushl $0 pushl $0 jmp __alltraps .globl vector1 vector1: pushl $0 pushl $1 jmp __alltraps .globl vector2 vector2: pushl $0 pushl $2 jmp __alltraps .globl vector3 vector3: pushl $0 pushl $3 jmp __alltraps .globl vector4 vector4: pushl $0 pushl $4 jmp __alltraps .globl vector5 vector5: pushl $0 pushl $5 jmp __alltraps .globl vector6 vector6: pushl $0 pushl $6 jmp __alltraps .globl vector7 vector7: pushl $0 pushl $7 jmp __alltraps .globl vector8 vector8: pushl $8 jmp __alltraps .globl vector9 vector9: pushl $9 jmp __alltraps .globl vector10 vector10: pushl $10 jmp __alltraps .globl vector11 vector11: pushl $11 jmp __alltraps .globl vector12 vector12: pushl $12 jmp __alltraps .globl vector13 vector13: pushl $13 jmp __alltraps .globl vector14 vector14: pushl $14 jmp __alltraps .globl vector15 vector15: pushl $0 pushl $15 jmp __alltraps .globl vector16 vector16: pushl $0 pushl $16 jmp __alltraps .globl vector17 vector17: pushl $17 jmp __alltraps .globl vector18 vector18: pushl $0 pushl $18 jmp __alltraps .globl vector19 vector19: pushl $0 pushl $19 jmp __alltraps .globl vector20 vector20: pushl $0 pushl $20 jmp __alltraps .globl vector21 vector21: pushl $0 pushl $21 jmp __alltraps .globl vector22 vector22: pushl $0 pushl $22 jmp __alltraps .globl vector23 vector23: pushl $0 pushl $23 jmp __alltraps .globl vector24 vector24: pushl $0 pushl $24 jmp __alltraps .globl vector25 vector25: pushl $0 pushl $25 jmp __alltraps .globl vector26 vector26: pushl $0 pushl $26 jmp __alltraps .globl vector27 vector27: pushl $0 pushl $27 jmp __alltraps .globl vector28 vector28: pushl $0 pushl $28 jmp __alltraps .globl vector29 vector29: pushl $0 pushl $29 jmp __alltraps .globl vector30 vector30: pushl $0 pushl $30 jmp __alltraps .globl vector31 vector31: pushl $0 pushl $31 jmp __alltraps .globl vector32 vector32: pushl $0 pushl $32 jmp __alltraps .globl vector33 vector33: pushl $0 pushl $33 jmp __alltraps .globl vector34 vector34: pushl $0 pushl $34 jmp __alltraps .globl vector35 vector35: pushl $0 pushl $35 jmp __alltraps .globl vector36 vector36: pushl $0 pushl $36 jmp __alltraps .globl vector37 vector37: pushl $0 pushl $37 jmp __alltraps .globl vector38 vector38: pushl $0 pushl $38 jmp __alltraps .globl vector39 vector39: pushl $0 pushl $39 jmp __alltraps .globl vector40 vector40: pushl $0 pushl $40 jmp __alltraps .globl vector41 vector41: pushl $0 pushl $41 jmp __alltraps .globl vector42 vector42: pushl $0 pushl $42 jmp __alltraps .globl vector43 vector43: pushl $0 pushl $43 jmp __alltraps .globl vector44 vector44: pushl $0 pushl $44 jmp __alltraps .globl vector45 vector45: pushl $0 pushl $45 jmp __alltraps .globl vector46 vector46: pushl $0 pushl $46 jmp __alltraps .globl vector47 vector47: pushl $0 pushl $47 jmp __alltraps .globl vector48 vector48: pushl $0 pushl $48 jmp __alltraps .globl vector49 vector49: pushl $0 pushl $49 jmp __alltraps .globl vector50 vector50: pushl $0 pushl $50 jmp __alltraps .globl vector51 vector51: pushl $0 pushl $51 jmp __alltraps .globl vector52 vector52: pushl $0 pushl $52 jmp __alltraps .globl vector53 vector53: pushl $0 pushl $53 jmp __alltraps .globl vector54 vector54: pushl $0 pushl $54 jmp __alltraps .globl vector55 vector55: pushl $0 pushl $55 jmp __alltraps .globl vector56 vector56: pushl $0 pushl $56 jmp __alltraps .globl vector57 vector57: pushl $0 pushl $57 jmp __alltraps .globl vector58 vector58: pushl $0 pushl $58 jmp __alltraps .globl vector59 vector59: pushl $0 pushl $59 jmp __alltraps .globl vector60 vector60: pushl $0 pushl $60 jmp __alltraps .globl vector61 vector61: pushl $0 pushl $61 jmp __alltraps .globl vector62 vector62: pushl $0 pushl $62 jmp __alltraps .globl vector63 vector63: pushl $0 pushl $63 jmp __alltraps .globl vector64 vector64: pushl $0 pushl $64 jmp __alltraps .globl vector65 vector65: pushl $0 pushl $65 jmp __alltraps .globl vector66 vector66: pushl $0 pushl $66 jmp __alltraps .globl vector67 vector67: pushl $0 pushl $67 jmp __alltraps .globl vector68 vector68: pushl $0 pushl $68 jmp __alltraps .globl vector69 vector69: pushl $0 pushl $69 jmp __alltraps .globl vector70 vector70: pushl $0 pushl $70 jmp __alltraps .globl vector71 vector71: pushl $0 pushl $71 jmp __alltraps .globl vector72 vector72: pushl $0 pushl $72 jmp __alltraps .globl vector73 vector73: pushl $0 pushl $73 jmp __alltraps .globl vector74 vector74: pushl $0 pushl $74 jmp __alltraps .globl vector75 vector75: pushl $0 pushl $75 jmp __alltraps .globl vector76 vector76: pushl $0 pushl $76 jmp __alltraps .globl vector77 vector77: pushl $0 pushl $77 jmp __alltraps .globl vector78 vector78: pushl $0 pushl $78 jmp __alltraps .globl vector79 vector79: pushl $0 pushl $79 jmp __alltraps .globl vector80 vector80: pushl $0 pushl $80 jmp __alltraps .globl vector81 vector81: pushl $0 pushl $81 jmp __alltraps .globl vector82 vector82: pushl $0 pushl $82 jmp __alltraps .globl vector83 vector83: pushl $0 pushl $83 jmp __alltraps .globl vector84 vector84: pushl $0 pushl $84 jmp __alltraps .globl vector85 vector85: pushl $0 pushl $85 jmp __alltraps .globl vector86 vector86: pushl $0 pushl $86 jmp __alltraps .globl vector87 vector87: pushl $0 pushl $87 jmp __alltraps .globl vector88 vector88: pushl $0 pushl $88 jmp __alltraps .globl vector89 vector89: pushl $0 pushl $89 jmp __alltraps .globl vector90 vector90: pushl $0 pushl $90 jmp __alltraps .globl vector91 vector91: pushl $0 pushl $91 jmp __alltraps .globl vector92 vector92: pushl $0 pushl $92 jmp __alltraps .globl vector93 vector93: pushl $0 pushl $93 jmp __alltraps .globl vector94 vector94: pushl $0 pushl $94 jmp __alltraps .globl vector95 vector95: pushl $0 pushl $95 jmp __alltraps .globl vector96 vector96: pushl $0 pushl $96 jmp __alltraps .globl vector97 vector97: pushl $0 pushl $97 jmp __alltraps .globl vector98 vector98: pushl $0 pushl $98 jmp __alltraps .globl vector99 vector99: pushl $0 pushl $99 jmp __alltraps .globl vector100 vector100: pushl $0 pushl $100 jmp __alltraps .globl vector101 vector101: pushl $0 pushl $101 jmp __alltraps .globl vector102 vector102: pushl $0 pushl $102 jmp __alltraps .globl vector103 vector103: pushl $0 pushl $103 jmp __alltraps .globl vector104 vector104: pushl $0 pushl $104 jmp __alltraps .globl vector105 vector105: pushl $0 pushl $105 jmp __alltraps .globl vector106 vector106: pushl $0 pushl $106 jmp __alltraps .globl vector107 vector107: pushl $0 pushl $107 jmp __alltraps .globl vector108 vector108: pushl $0 pushl $108 jmp __alltraps .globl vector109 vector109: pushl $0 pushl $109 jmp __alltraps .globl vector110 vector110: pushl $0 pushl $110 jmp __alltraps .globl vector111 vector111: pushl $0 pushl $111 jmp __alltraps .globl vector112 vector112: pushl $0 pushl $112 jmp __alltraps .globl vector113 vector113: pushl $0 pushl $113 jmp __alltraps .globl vector114 vector114: pushl $0 pushl $114 jmp __alltraps .globl vector115 vector115: pushl $0 pushl $115 jmp __alltraps .globl vector116 vector116: pushl $0 pushl $116 jmp __alltraps .globl vector117 vector117: pushl $0 pushl $117 jmp __alltraps .globl vector118 vector118: pushl $0 pushl $118 jmp __alltraps .globl vector119 vector119: pushl $0 pushl $119 jmp __alltraps .globl vector120 vector120: pushl $0 pushl $120 jmp __alltraps .globl vector121 vector121: pushl $0 pushl $121 jmp __alltraps .globl vector122 vector122: pushl $0 pushl $122 jmp __alltraps .globl vector123 vector123: pushl $0 pushl $123 jmp __alltraps .globl vector124 vector124: pushl $0 pushl $124 jmp __alltraps .globl vector125 vector125: pushl $0 pushl $125 jmp __alltraps .globl vector126 vector126: pushl $0 pushl $126 jmp __alltraps .globl vector127 vector127: pushl $0 pushl $127 jmp __alltraps .globl vector128 vector128: pushl $0 pushl $128 jmp __alltraps .globl vector129 vector129: pushl $0 pushl $129 jmp __alltraps .globl vector130 vector130: pushl $0 pushl $130 jmp __alltraps .globl vector131 vector131: pushl $0 pushl $131 jmp __alltraps .globl vector132 vector132: pushl $0 pushl $132 jmp __alltraps .globl vector133 vector133: pushl $0 pushl $133 jmp __alltraps .globl vector134 vector134: pushl $0 pushl $134 jmp __alltraps .globl vector135 vector135: pushl $0 pushl $135 jmp __alltraps .globl vector136 vector136: pushl $0 pushl $136 jmp __alltraps .globl vector137 vector137: pushl $0 pushl $137 jmp __alltraps .globl vector138 vector138: pushl $0 pushl $138 jmp __alltraps .globl vector139 vector139: pushl $0 pushl $139 jmp __alltraps .globl vector140 vector140: pushl $0 pushl $140 jmp __alltraps .globl vector141 vector141: pushl $0 pushl $141 jmp __alltraps .globl vector142 vector142: pushl $0 pushl $142 jmp __alltraps .globl vector143 vector143: pushl $0 pushl $143 jmp __alltraps .globl vector144 vector144: pushl $0 pushl $144 jmp __alltraps .globl vector145 vector145: pushl $0 pushl $145 jmp __alltraps .globl vector146 vector146: pushl $0 pushl $146 jmp __alltraps .globl vector147 vector147: pushl $0 pushl $147 jmp __alltraps .globl vector148 vector148: pushl $0 pushl $148 jmp __alltraps .globl vector149 vector149: pushl $0 pushl $149 jmp __alltraps .globl vector150 vector150: pushl $0 pushl $150 jmp __alltraps .globl vector151 vector151: pushl $0 pushl $151 jmp __alltraps .globl vector152 vector152: pushl $0 pushl $152 jmp __alltraps .globl vector153 vector153: pushl $0 pushl $153 jmp __alltraps .globl vector154 vector154: pushl $0 pushl $154 jmp __alltraps .globl vector155 vector155: pushl $0 pushl $155 jmp __alltraps .globl vector156 vector156: pushl $0 pushl $156 jmp __alltraps .globl vector157 vector157: pushl $0 pushl $157 jmp __alltraps .globl vector158 vector158: pushl $0 pushl $158 jmp __alltraps .globl vector159 vector159: pushl $0 pushl $159 jmp __alltraps .globl vector160 vector160: pushl $0 pushl $160 jmp __alltraps .globl vector161 vector161: pushl $0 pushl $161 jmp __alltraps .globl vector162 vector162: pushl $0 pushl $162 jmp __alltraps .globl vector163 vector163: pushl $0 pushl $163 jmp __alltraps .globl vector164 vector164: pushl $0 pushl $164 jmp __alltraps .globl vector165 vector165: pushl $0 pushl $165 jmp __alltraps .globl vector166 vector166: pushl $0 pushl $166 jmp __alltraps .globl vector167 vector167: pushl $0 pushl $167 jmp __alltraps .globl vector168 vector168: pushl $0 pushl $168 jmp __alltraps .globl vector169 vector169: pushl $0 pushl $169 jmp __alltraps .globl vector170 vector170: pushl $0 pushl $170 jmp __alltraps .globl vector171 vector171: pushl $0 pushl $171 jmp __alltraps .globl vector172 vector172: pushl $0 pushl $172 jmp __alltraps .globl vector173 vector173: pushl $0 pushl $173 jmp __alltraps .globl vector174 vector174: pushl $0 pushl $174 jmp __alltraps .globl vector175 vector175: pushl $0 pushl $175 jmp __alltraps .globl vector176 vector176: pushl $0 pushl $176 jmp __alltraps .globl vector177 vector177: pushl $0 pushl $177 jmp __alltraps .globl vector178 vector178: pushl $0 pushl $178 jmp __alltraps .globl vector179 vector179: pushl $0 pushl $179 jmp __alltraps .globl vector180 vector180: pushl $0 pushl $180 jmp __alltraps .globl vector181 vector181: pushl $0 pushl $181 jmp __alltraps .globl vector182 vector182: pushl $0 pushl $182 jmp __alltraps .globl vector183 vector183: pushl $0 pushl $183 jmp __alltraps .globl vector184 vector184: pushl $0 pushl $184 jmp __alltraps .globl vector185 vector185: pushl $0 pushl $185 jmp __alltraps .globl vector186 vector186: pushl $0 pushl $186 jmp __alltraps .globl vector187 vector187: pushl $0 pushl $187 jmp __alltraps .globl vector188 vector188: pushl $0 pushl $188 jmp __alltraps .globl vector189 vector189: pushl $0 pushl $189 jmp __alltraps .globl vector190 vector190: pushl $0 pushl $190 jmp __alltraps .globl vector191 vector191: pushl $0 pushl $191 jmp __alltraps .globl vector192 vector192: pushl $0 pushl $192 jmp __alltraps .globl vector193 vector193: pushl $0 pushl $193 jmp __alltraps .globl vector194 vector194: pushl $0 pushl $194 jmp __alltraps .globl vector195 vector195: pushl $0 pushl $195 jmp __alltraps .globl vector196 vector196: pushl $0 pushl $196 jmp __alltraps .globl vector197 vector197: pushl $0 pushl $197 jmp __alltraps .globl vector198 vector198: pushl $0 pushl $198 jmp __alltraps .globl vector199 vector199: pushl $0 pushl $199 jmp __alltraps .globl vector200 vector200: pushl $0 pushl $200 jmp __alltraps .globl vector201 vector201: pushl $0 pushl $201 jmp __alltraps .globl vector202 vector202: pushl $0 pushl $202 jmp __alltraps .globl vector203 vector203: pushl $0 pushl $203 jmp __alltraps .globl vector204 vector204: pushl $0 pushl $204 jmp __alltraps .globl vector205 vector205: pushl $0 pushl $205 jmp __alltraps .globl vector206 vector206: pushl $0 pushl $206 jmp __alltraps .globl vector207 vector207: pushl $0 pushl $207 jmp __alltraps .globl vector208 vector208: pushl $0 pushl $208 jmp __alltraps .globl vector209 vector209: pushl $0 pushl $209 jmp __alltraps .globl vector210 vector210: pushl $0 pushl $210 jmp __alltraps .globl vector211 vector211: pushl $0 pushl $211 jmp __alltraps .globl vector212 vector212: pushl $0 pushl $212 jmp __alltraps .globl vector213 vector213: pushl $0 pushl $213 jmp __alltraps .globl vector214 vector214: pushl $0 pushl $214 jmp __alltraps .globl vector215 vector215: pushl $0 pushl $215 jmp __alltraps .globl vector216 vector216: pushl $0 pushl $216 jmp __alltraps .globl vector217 vector217: pushl $0 pushl $217 jmp __alltraps .globl vector218 vector218: pushl $0 pushl $218 jmp __alltraps .globl vector219 vector219: pushl $0 pushl $219 jmp __alltraps .globl vector220 vector220: pushl $0 pushl $220 jmp __alltraps .globl vector221 vector221: pushl $0 pushl $221 jmp __alltraps .globl vector222 vector222: pushl $0 pushl $222 jmp __alltraps .globl vector223 vector223: pushl $0 pushl $223 jmp __alltraps .globl vector224 vector224: pushl $0 pushl $224 jmp __alltraps .globl vector225 vector225: pushl $0 pushl $225 jmp __alltraps .globl vector226 vector226: pushl $0 pushl $226 jmp __alltraps .globl vector227 vector227: pushl $0 pushl $227 jmp __alltraps .globl vector228 vector228: pushl $0 pushl $228 jmp __alltraps .globl vector229 vector229: pushl $0 pushl $229 jmp __alltraps .globl vector230 vector230: pushl $0 pushl $230 jmp __alltraps .globl vector231 vector231: pushl $0 pushl $231 jmp __alltraps .globl vector232 vector232: pushl $0 pushl $232 jmp __alltraps .globl vector233 vector233: pushl $0 pushl $233 jmp __alltraps .globl vector234 vector234: pushl $0 pushl $234 jmp __alltraps .globl vector235 vector235: pushl $0 pushl $235 jmp __alltraps .globl vector236 vector236: pushl $0 pushl $236 jmp __alltraps .globl vector237 vector237: pushl $0 pushl $237 jmp __alltraps .globl vector238 vector238: pushl $0 pushl $238 jmp __alltraps .globl vector239 vector239: pushl $0 pushl $239 jmp __alltraps .globl vector240 vector240: pushl $0 pushl $240 jmp __alltraps .globl vector241 vector241: pushl $0 pushl $241 jmp __alltraps .globl vector242 vector242: pushl $0 pushl $242 jmp __alltraps .globl vector243 vector243: pushl $0 pushl $243 jmp __alltraps .globl vector244 vector244: pushl $0 pushl $244 jmp __alltraps .globl vector245 vector245: pushl $0 pushl $245 jmp __alltraps .globl vector246 vector246: pushl $0 pushl $246 jmp __alltraps .globl vector247 vector247: pushl $0 pushl $247 jmp __alltraps .globl vector248 vector248: pushl $0 pushl $248 jmp __alltraps .globl vector249 vector249: pushl $0 pushl $249 jmp __alltraps .globl vector250 vector250: pushl $0 pushl $250 jmp __alltraps .globl vector251 vector251: pushl $0 pushl $251 jmp __alltraps .globl vector252 vector252: pushl $0 pushl $252 jmp __alltraps .globl vector253 vector253: pushl $0 pushl $253 jmp __alltraps .globl vector254 vector254: pushl $0 pushl $254 jmp __alltraps .globl vector255 vector255: pushl $0 pushl $255 jmp __alltraps # vector table .data .globl __vectors __vectors: .long vector0 .long vector1 .long vector2 .long vector3 .long vector4 .long vector5 .long vector6 .long vector7 .long vector8 .long vector9 .long vector10 .long vector11 .long vector12 .long vector13 .long vector14 .long vector15 .long vector16 .long vector17 .long vector18 .long vector19 .long vector20 .long vector21 .long vector22 .long vector23 .long vector24 .long vector25 .long vector26 .long vector27 .long vector28 .long vector29 .long vector30 .long vector31 .long vector32 .long vector33 .long vector34 .long vector35 .long vector36 .long vector37 .long vector38 .long vector39 .long vector40 .long vector41 .long vector42 .long vector43 .long vector44 .long vector45 .long vector46 .long vector47 .long vector48 .long vector49 .long vector50 .long vector51 .long vector52 .long vector53 .long vector54 .long vector55 .long vector56 .long vector57 .long vector58 .long vector59 .long vector60 .long vector61 .long vector62 .long vector63 .long vector64 .long vector65 .long vector66 .long vector67 .long vector68 .long vector69 .long vector70 .long vector71 .long vector72 .long vector73 .long vector74 .long vector75 .long vector76 .long vector77 .long vector78 .long vector79 .long vector80 .long vector81 .long vector82 .long vector83 .long vector84 .long vector85 .long vector86 .long vector87 .long vector88 .long vector89 .long vector90 .long vector91 .long vector92 .long vector93 .long vector94 .long vector95 .long vector96 .long vector97 .long vector98 .long vector99 .long vector100 .long vector101 .long vector102 .long vector103 .long vector104 .long vector105 .long vector106 .long vector107 .long vector108 .long vector109 .long vector110 .long vector111 .long vector112 .long vector113 .long vector114 .long vector115 .long vector116 .long vector117 .long vector118 .long vector119 .long vector120 .long vector121 .long vector122 .long vector123 .long vector124 .long vector125 .long vector126 .long vector127 .long vector128 .long vector129 .long vector130 .long vector131 .long vector132 .long vector133 .long vector134 .long vector135 .long vector136 .long vector137 .long vector138 .long vector139 .long vector140 .long vector141 .long vector142 .long vector143 .long vector144 .long vector145 .long vector146 .long vector147 .long vector148 .long vector149 .long vector150 .long vector151 .long vector152 .long vector153 .long vector154 .long vector155 .long vector156 .long vector157 .long vector158 .long vector159 .long vector160 .long vector161 .long vector162 .long vector163 .long vector164 .long vector165 .long vector166 .long vector167 .long vector168 .long vector169 .long vector170 .long vector171 .long vector172 .long vector173 .long vector174 .long vector175 .long vector176 .long vector177 .long vector178 .long vector179 .long vector180 .long vector181 .long vector182 .long vector183 .long vector184 .long vector185 .long vector186 .long vector187 .long vector188 .long vector189 .long vector190 .long vector191 .long vector192 .long vector193 .long vector194 .long vector195 .long vector196 .long vector197 .long vector198 .long vector199 .long vector200 .long vector201 .long vector202 .long vector203 .long vector204 .long vector205 .long vector206 .long vector207 .long vector208 .long vector209 .long vector210 .long vector211 .long vector212 .long vector213 .long vector214 .long vector215 .long vector216 .long vector217 .long vector218 .long vector219 .long vector220 .long vector221 .long vector222 .long vector223 .long vector224 .long vector225 .long vector226 .long vector227 .long vector228 .long vector229 .long vector230 .long vector231 .long vector232 .long vector233 .long vector234 .long vector235 .long vector236 .long vector237 .long vector238 .long vector239 .long vector240 .long vector241 .long vector242 .long vector243 .long vector244 .long vector245 .long vector246 .long vector247 .long vector248 .long vector249 .long vector250 .long vector251 .long vector252 .long vector253 .long vector254 .long vector255
__alltraps是定义在同一目录下即/kern/trap/trapentry.S中的。
在__alltraps中,按照顺序将当前的各个经常使用的寄存器的值压入了栈中,随后将ds、es等数据段寄存器载入了内核的数据段选择子(这是由于中断可能来自用户态,而中断服务例程必须在内核态运行以拥有全部资源的访问权限,避免内核中的中断服务例程因为特权级不够,访问数据时出现问题)。
随后栈中压入esp的值,便经过call trap跳转到了内核的中断服务分发函数trap中。trap函数位于/kern/trap/trap.c中。
trapentry.S:
#include <memlayout.h> # vectors.S sends all traps here. .text .globl __alltraps __alltraps: # push registers to build a trap frame # therefore make the stack look like a struct trapframe pushl %ds pushl %es pushl %fs pushl %gs pushal # load GD_KDATA into %ds and %es to set up data segments for kernel movl $GD_KDATA, %eax movw %ax, %ds movw %ax, %es # push %esp to pass a pointer to the trapframe as an argument to trap() pushl %esp # call trap(tf), where tf=%esp call trap # pop the pushed stack pointer popl %esp # return falls through to trapret... .globl __trapret __trapret: # restore registers from stack popal # restore %ds, %es, %fs and %gs popl %gs popl %fs popl %es popl %ds # get rid of the trap number and error code addl $0x8, %esp iret
trap函数入口:
/* * * trap - handles or dispatches an exception/interrupt. if and when trap() returns, * the code in kern/trap/trapentry.S restores the old CPU state saved in the * trapframe and then uses the iret instruction to return from the exception. * */ void trap(struct trapframe *tf) { // dispatch based on what type of trap occurred trap_dispatch(tf); }
trap函数的参数是trapframe结构体。仔细观察能够看到,trapframe中字段属性的定义和trapentry.S中入栈的顺序是相反的。
1. 第一个字段pushregs保存了pushal压入栈中的数据,后续的tf_gs + tf_padding0两个字段是由于pushl会压入一个32位的数据,而gs数据段寄存器自己保存的段选择子是16位的,须要一个16位的padding空闲字段合并起来与之对应。后面的fs、es、ds原理同样。
2. 以后的tf_trapno属性对应着跳转至__alltraps以前所压入栈中的中断向量号,tf_err对应的是在上面的pushl $0,即错误号。根据注释能够看到,包括tf_err在内的中断错误号都是x86CPU硬件在中断发生时自动压入栈中的。但并非全部的硬件中断都会被自动压入错误号(须要去阅读硬件手册才能知道具体细节),为了可以以一个统一的接口去处理全部的中断请求,在vertor.S中对于没有错误号的中断请求默认加上了pushl $0,压入一个默认的错误号0;对于CPU硬件会压入错误号的中断向量则没有进行默认处理,例如vertor八、vertor9等等。
3. 发生中断时,x86CPU会默认按照顺序依次压入eflags、cs和eip用于中断后的现场恢复。对应的是tf_eip、tf_cs + tf_padding4以及tf_eflags。而当发生了CPL特权级的变化时,x86CPU硬件会发生不一样特权级栈的切换,所以还会先依次压入切换特权级前的ss栈段寄存器和esp栈顶指针的值入栈,便于中断返回后回到对应的特权级中。一个很典型的例子就是,当用户程序执行系统调用时(系统调用是经过中断机制实现的,在ucore的lab5中实现了这一功能),会从用户的CPL特权级ring3切换到内核的特权级ring0,系统调用的服务例程是在分配好的内核栈中执行的。
/* registers as pushed by pushal */ struct pushregs { uint32_t reg_edi; uint32_t reg_esi; uint32_t reg_ebp; uint32_t reg_oesp; /* Useless */ uint32_t reg_ebx; uint32_t reg_edx; uint32_t reg_ecx; uint32_t reg_eax; }; struct trapframe { struct pushregs tf_regs; uint16_t tf_gs; uint16_t tf_padding0; uint16_t tf_fs; uint16_t tf_padding1; uint16_t tf_es; uint16_t tf_padding2; uint16_t tf_ds; uint16_t tf_padding3; uint32_t tf_trapno; /* below here defined by x86 hardware */ uint32_t tf_err; uintptr_t tf_eip; uint16_t tf_cs; uint16_t tf_padding4; uint32_t tf_eflags; /* below here only when crossing rings, such as from user to kernel */ uintptr_t tf_esp; uint16_t tf_ss; uint16_t tf_padding5; } __attribute__((packed));
为何tramframe中断栈帧的结构属性的定义顺序会和入栈时的顺序相反?
要理解这个须要对C语言编译后的底层机器代码模型有必定了解。
1.C语言结构体定义的内存排布是按照字段定义顺序,从内存的低位到高位延伸的。
2.栈在压入数据时,栈顶指针是递减的,由高位往低位延伸的。
3.trap函数的参数trapframe指针指向的是当前栈顶,相对处于低位,而对所包含的字段则是在这个指针的基础上向高位偏移对应的N个字节来访问的。要想在C中经过一个结构体映射出栈上的内存数据便于后续的访问,那么必须以和入栈顺序相反的顺序来定义结构体。栈帧结构体trapframe定义最后的__attribute__((packed))指的是强制令C编译器使用紧凑模式处理该结构体,避免编译器在字段的处理上进行额外的内存对齐操做,致使访问时最后生成的内存地址访问偏移量计算错误。
这里须要注意的一点是,当处理没有发生特权级切换的中断时,trapframe对应的最后三个字段是不存在于栈上的,此时若是经过tf_ss等属性访问时,会越界访问到本来在栈上不相关的数据。因此在访问这几个字段时,必须先判断是否发生了特权级的变化,避免损坏栈上数据,令程序出错甚至崩溃。
如今分析当中断发生时,trap函数接收到trapframe中断帧参数后是如何进行中断服务处理的。
在lab1中,trap函数只是简单的调用了同一文件中的trap_dispatch函数,在trap_dispatch中经过对tf->tf_trapno,即中断向量号进行判断,将控制流转移至中断向量号对应的中断服务例程中。(这里trap函数只是简单的调用trap_dispatch,是由于后续的lab中,会在trap函数中在中断处理服务开始先后加入许多逻辑,预先将对中断请求的分发逻辑抽取了出来)
好比第一个case块即是用于处理时钟中断的。在lab1中,经过一个被volatile关键字修饰的ticks全局变量,在每次时钟中断时累加1,当次数每达到TICK_NUM时(默认100,对应的是10ms一次的时钟中断),便打印一段话。体如今lab1实验中即是,ucore内核启动完成后,控制台每秒钟周期性的打印出"100ticks"。
能够看到,虽然80386CPU的硬件设计者但愿操做系统的设计者直接在中断描述符中设置对应的中断服务例程的入口地址,但ucore却没有充分利用这一特性,而是选择了在中断服务例程的入口处简单压入几个数据后将中断服务的控制流程统一的指向了__alltraps,最后经过trap_dispatch函数进行分发。这样的设计虽然在性能上可能有微小的损失,可是却使得ucore的中断服务实现更加灵活、可控。(做为一个java后端程序员,这一设计令我想到了springmvc框架基于下层servlet的封装机制:经过一个/*的servlet得到全部请求的控制权,再由框架灵活封装各类参数,最后将参数和控制权交给对应的controller方法进行处理)
trap_dispatch函数:
/* trap_dispatch - dispatch based on what type of trap occurred */ static void trap_dispatch(struct trapframe *tf) { char c; switch (tf->tf_trapno) { case IRQ_OFFSET + IRQ_TIMER: /* LAB1 YOUR CODE : STEP 3 */ /* handle the timer interrupt */ /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c * (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks(). * (3) Too Simple? Yes, I think so! */ ticks ++; if (ticks % TICK_NUM == 0) { print_ticks(); } break; case IRQ_OFFSET + IRQ_COM1: c = cons_getc(); cprintf("serial [%03d] %c\n", c, c); break; case IRQ_OFFSET + IRQ_KBD: c = cons_getc(); cprintf("kbd [%03d] %c\n", c, c); break; //LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes. case T_SWITCH_TOU: if (tf->tf_cs != USER_CS) { switchk2u = *tf; switchk2u.tf_cs = USER_CS; switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS; switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8; // set eflags, make sure ucore can use io under user mode. // if CPL > IOPL, then cpu will generate a general protection. switchk2u.tf_eflags |= FL_IOPL_MASK; // set temporary stack // then iret will jump to the right stack *((uint32_t *)tf - 1) = (uint32_t)&switchk2u; } break; case T_SWITCH_TOK: if (tf->tf_cs != KERNEL_CS) { tf->tf_cs = KERNEL_CS; tf->tf_ds = tf->tf_es = KERNEL_DS; tf->tf_eflags &= ~FL_IOPL_MASK; switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8)); memmove(switchu2k, tf, sizeof(struct trapframe) - 8); *((uint32_t *)tf - 1) = (uint32_t)switchu2k; } break; case IRQ_OFFSET + IRQ_IDE1: case IRQ_OFFSET + IRQ_IDE2: /* do nothing */ break; default: // in kernel, it must be a mistake if ((tf->tf_cs & 3) == 0) { print_trapframe(tf); panic("unexpected trap in kernel.\n"); } } }
中断服务例程用于处理突发性的中断事件,通常来讲都是短小精悍的服务代码,会很快的执行完毕并返回。下面接着分析ucore在中断返回时的处理机制。
在trap函数返回后,代码的控制流回到了trapentry.S中,即CPU指令指向call trap的下一条指令。为了保证中断服务返回后以前被中断程序上下文的正确性,须要将执行call trap以前的压入的数据一一弹出还原。
1. 按照相反的顺序弹出、还原各个经常使用寄存器的值(popl esp、popal、popl gs/fs/es/ds)。
2. 经过addl $0x8, %esp,以直接上移栈顶指针的方式,略过以前压入的中断号tf_trapno和错误码tf_err。
3. 执行iret指令,iret指令会将以前硬件自动压入的eip、cs、eflags按照顺序弹出。当CPU发现弹出时的cs值和当前cs值不一致,则认定这次中断发生了特权级的变化。此时CPU会接着弹出以前压入了的esp、ss寄存器的值,令其返回到中断发生前对应的特权级栈中继续执行。
CPU认为只有以前发生特权级变化时才会额外压入ss、esp,因此中断返回时若是发现弹出的cs与当前cs不一致时,除了恢复以前栈上的cs(也恢复了CPL),同时会额外的弹出esp、ss。
这特权级一机制在lab1的挑战练习lab1 challenge1中被利用了起来,挑战练习1须要模拟出内核态转化至用户态,再从用户态再转换回内核态的过程。
lab1挑战练习1实现原理分析
在kern_init总控函数中,最后经过lab1_switch_test来实现这一过程。
lab1_switch_to_user函数中,经过内联汇编执行了int命令,触发了一个软中断,中断号为T_SWITCH_TOU。控制流最终会指向trap_dispatch函数中对应的case块中,在其中经过修改当前中断栈帧中的cs代码段寄存器、ds、es、ss等数据段寄存器的值,使得中断栈帧上的CS的段选择子的值为用户态。这样在中断返回时,便"欺骗"了CPU,使得CPU在中断返回后将当前的特权级由内核态切换到了用户态。在后续的实验中,例如经过系统调用加载并运行一个用户态应用程序,就是经过这一"欺骗"机制巧妙地实现特权级的切换。
lab1_switch_to_kernel函数中,一样经过内联汇编执行int命令触发软中断,中断号为T_SWITCH_TOK。控制流最终指向trap_dispatch函数中对应的case块中,经过设置cs代码段寄存器的值为内核代码段、ds、es设置为内核数据段来实现中断返回后,令CPU再从用户态回到内核态。
static void lab1_switch_test(void) { lab1_print_cur_status(); cprintf("+++ switch to user mode +++\n"); lab1_switch_to_user(); lab1_print_cur_status(); cprintf("+++ switch to kernel mode +++\n"); lab1_switch_to_kernel(); lab1_print_cur_status(); } static void lab1_switch_to_user(void) { //LAB1 CHALLENGE 1 : TODO asm volatile ( "sub $0x8, %%esp \n" "int %0 \n" "movl %%ebp, %%esp" : : "i"(T_SWITCH_TOU) ); } static void lab1_switch_to_kernel(void) { //LAB1 CHALLENGE 1 : TODO asm volatile ( "int %0 \n" "movl %%ebp, %%esp \n" : : "i"(T_SWITCH_TOK) ); }
从年初接触ucore到如今完成学习,经过博客总结心得已通过去了大半年。学习ucore就像攀爬一座高山同样,最初的我因为汇编、C等基础的前置知识掌握的不牢靠,致使往往想研究ucore源码时都由于看不懂代码而宣告失败,所以我下定决心将汇编和C从新学习了一遍。虽然最初的动机是为了更好的学习ucore,但在学习过程当中我却收获颇丰,领略了爬山途中的好风景。一方面使我对计算机底层的运行机制创建起了一个大体的知识框架,理解了CPU的运行机制、中断等硬件的工做原理(主要仍是单核CPU的工做原理)。另外一方面,随着对汇编、C语言的进一步学习,也慢慢的理解了《黑客与画家》中对于编程语言抽象能力的见解,为何计算机硬件不断发展,抽象程度更高但性能较低的编程语言会变得愈来愈流行。
正如C语言最初做为一种"高级汇编语言"而出现,其提供的数组、结构体、指针等机制简化了汇编中使人头疼的访问数据时的地址偏移问题。同时C还提供了标准库,在绝大多数场景下可以屏蔽掉不一样硬件、操做系统平台的差别,使其作到一次编写,处处编译。而C++做为C的高级版本,提供了面向对象的编程机制,由编译器提供多态等诸多语法糖,由编译器来自动完成以前须要C程序员经过函数指针集合等方式手动实现的面向对象逻辑。java做为C++的后继者,认为C++为了兼容C依然保留了太多应用程序开发时不须要的底层功能,便将包括指针、goto在内的许多机制都隐藏起来了,不让程序员直接接触,经过jvm在绝大多数场景下屏蔽了不一样操做系统平台的差别。而Lisp语言的抽象程度则更高,正同《程序员的呐喊》中所说的:“Lisp伪装操做系统不存在”。若是不考虑在当前冯.诺依曼架构机器上的运行效率,LISP倡导的就是肆无忌惮的进行函数递归而没必要担忧栈溢出,为了使函数调用无反作用能够任意的copy数据,而没必要担忧内存不足和垃圾回收的负担,最重要的是程序的可读性、可维护性,怎么方便人思考怎么来,不太关心空间、时间性能。随着机器性能的不断提高,将来的编程语言实现中也许真的能够用kv Map彻底的替代数组,甚至用丘奇数来替代整数以追求数学上极致简约的美?Orz
任意编程语言的内容主要分为两部分,一是基础语法,另外一部分则是在基于的特定平台上功能的封装。例如javascript由ECMA语法和对其工做平台浏览器相关功能的封装组成,而java、nodejs等通用编程语言则是由语法和对操做系统功能的封装。做为一个以java做为平常开发语言的我来讲,学习ucore让我对java中诸如并发同步、BIO/NIO等机制有了进一步的理解,解开了很多对于jvm底层与操做系统交互机制的困惑。总而言之,学习操做系统仍是能学到不少知识的,而ucore os网上公开课就是一个很好的学习方式。
这是我ucore学习系列博客的第一篇博客,将来会不断的更新后续实验的学习心得,博客中会尝试着尽可能将初学者可能碰到的各类疑惑一一解答。但愿能帮助到对操做系统、ucore os感兴趣的人。
这篇博客的完整代码在个人github上:https://github.com/1399852153/ucore_os_lab (fork自官方仓库)中的lab1_answer,存在许多不足之处,还请多多指教。