一、MIT xv6的启动和页表设计

1、谈一谈MIT 6.S081中的重要概念

        对于一些非科班出身的同学来说,学习OS是有一定难度的,这种困难在一定程度上可以归结为对一些概念缺乏high level的理解,你记住的只是枯燥的概念。下面我根据自己在学习xv6过程中容易混淆的概念作一些说明,在理解了这些概念后,我学习起来确实容易了一些。

        QEMU:一个利用软件实现的仿真器,实现了SiFive开发板的藩镇。它本质上是一个软件,里面跑一个死循环,这个死循环不是一个bug,而是有意为之,在这个循环中不断的重复3个动作:读取指令、解析指令、执行指令。

一、MIT xv6的启动和页表设计_第1张图片

 

        为什么要使用QEMU?因为可以替你剩下一笔指出,而且更加灵活,比如你想设置只使用一个CPU,只需要在启动QEMU的时候制定参数CPU=1即可,而不用再去买一块板子。当在MIT 6.S081的课程中提到QEMU时,你可以等同于你手边有一块SiFive的开发板。

一、MIT xv6的启动和页表设计_第2张图片

         RISC-V:这是一种CPU架构,可以类比arm、x86_64去理解,每种CPU架构在内部都有不同的寄存器register,用于CPU执行指令需要的data。如果在后续学习中遇到奇怪的指令或者寄存器名(每种CPU架构除了寄存器不同外,指令集也不同),可以搜索它的架构文档。

        xv6:一种操作系统,MIT根据教学需要自行开发的OS,可以类比linux去理解,是一种宏内核的操作系统。

2、xv6运行的硬件

        大家讲OS是对硬件的抽象,那么是对硬件的什么的抽象,怎么抽象的?有些同学却没有深入思考。本小节谈谈xv6对SiFive的内存如何抽象的。SiFive的资料可以自行去MIT课程官网的reference导航栏下载。

        

一、MIT xv6的启动和页表设计_第3张图片 内核页表布局

        这张图要认真理解,左侧是xv6的内核地址空间(一种虚拟地址),右侧是SiFive板子的物理地址 。

        2.1  右侧的物理地址可以理解为在板子上哪部分物理地址有什么功能,在SiFive手册中对应chapter 5 memery map,这部分是由硬件工程师设计的,below 0x8000 0000是I/O外设,above 0x8000 0000是DRAM,即常说的内存。

 

 

        通过上面3个小图,可以发现QEMU针对CLINT、PLIC、DDR外设的地址遵从了SIfive的手册,但有两个外设它自己定义了新地址,没有严格按照手册的地址执行,这两个外设是boot ROM和UART0。

        至于为什么这两个外设为什么没有采用手册的地址,这里不得而知,欢迎知道原因的同学在评论在讨论。

        关于物理地址,还有一点需要说明:当电路板启动后,他首先执行boot ROM中的代码,然后跳转到0x8000 0000继续执行,至于为什么跳转到这里,下文会讲到。

        看到这里也许你还会有疑问,为什么顶部的一大块空间unused?这是因为并不是所有机器都有 2^{56}byte(67108864G)空间,如果你不需要,那么你的板子上就不会插入这么多内存。

        实际上根据计算 :PHYSTOP-KERNBASE,可知在xv6中只有100M内存。这里还有一个小坑,就是你去看源码的时候发现PHTSTOP\neq0x86400000.

#define PHYSTOP (KERNBASE + 128*1024*1024)

所以QEMU仿真出来的开发版,拥有128M的内存。

2.2 xv6的虚拟地址空间

        当机器启动时,还没有页表,内核会设置第一个页表,即第一个虚拟地址空间,如上图左侧所示,为了让xv6保持简单,易于理解,这个内核地址空间从虚拟地址到物理地址采用了恒等映射。

        关于内核的虚拟地址空间,有两点很有趣:1、不同的虚拟地址可能映射到同一个物理地址空间;2、guard page 并不占用实际的物理内存,通过把它的虚拟地址的valid位置为false。

3、xv6是如何启动的?

        讨论xv6是如何启动的,其实就是在学习操作系统是如何启动的。

        操作系统如何启动从high level讲分为两个部分:bootloader载入内核、内核运行以完成初始化。bootloader到内核的启动大体分为两个阶段,第一阶段使用汇编语言来实现,它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码;第二阶段则通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。

        在讲xv6如何启动之前,我们需要补充一些知识点:

        编译产生的目标文件按照信息的不同属性以“段”的形式存储,源码编译后的机器指令被放在text段,已经初始化的。全局变量和静态变量放在dat段。xv6内核本质上讲也是一个可执行程序(这一点你应该没有异议),那么它也要被编译、链接生成可执行文件。编译生成.o文件没什么好说的,更需要我们关注的是链接过程。链接器使用链接脚本(.ld后缀)控制链接过程,生产最终的可执行文件

xv6启动流程:

        1)BootLoader将xv6内核加载到内存中。然后CPU开始在_entry(kernel/entry.S)以machine mode执行xv6,此时还没有启用分页机制,虚拟地址直接映射到物理地址。

        2)_entry处的指令设置了一个栈,以便xv6可以运行C代码。xv6在文件start.c(kernel/start.c:11)中为初始堆栈stack0声明了空间。 _entry处的代码将栈顶地址stack0+4096加载到栈指针寄存器sp,这是因为RISC-V的栈是向下扩展的。现在内核有了栈区,_entry调用C代码start(kernel/start.c:21)。

        3)start函数执行了一些只允许在machine mode模式下的配置,然后转入supervisor mode,要进入supervisor mode,RISC-V 提供了指令 mret。它通过将main的地址写入寄存器mepc将返回地址设置为main,随后程序计数器指向main函数(kernel/main.c)。

下面结合源码看一下启动过程:

ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  . = 0x80000000;

  .text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    _trampoline = .;
    *(trampsec)
    . = ALIGN(0x1000);
    ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
    PROVIDE(etext = .);
  }
  .....
  .....
}

这个链接脚本确定了内核执行的第一条指令是_entry,而且明确了内核程序中有哪些段,每个段放在哪个地址上,从上面可以看出内核指令的其实位置被放在了0x8000 0000,这和上面我们的研究是一致的。

_entry被定义在一个汇编文件中entry.S

.section .text
.global _entry
_entry:
        # set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0
        li a0, 1024*4
        csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
        # jump to start() in start.c
        call start
spin:
        j spin

        其中声明了一个外部符号: stack0 ,把它作为每个 CPU 上的栈的起始地址,然后按照计算公式 sp = stack0 + (hartid * 4096) ,算出每个 CPU 对应的栈起始地址。

        首先指令 la sp, stack0 把 stack0 的地址读到 sp 寄存器中;

        指令 li a0, 1024*4 把 4096 这个立即数读到 a0 寄存器中;

        指令 csrr a1, mhartid 把当前 CPU 的 ID 读到 a1 寄存器中;

        剩下的三条指令按照 sp = stack0 + (hartid * 4096) 计算公式算出栈地址并且放到 sp 寄存器中,跳转指令 call start 跳到 start.c 中的 start 函数中执行,如果 start 函数返回(一般不会出现)那么进入死循环。

        RSIC-V有3种工作模式:机器模式、supervisor模式、用户模式。上电后(reset),RISC-V架构规定,此时为机器模式,机器模式可以通过指令mret转换称为用户模式。处理器执行完mret指令后,硬件行为如下:

  • 停止执行当前程序流,转而从CSR寄存器mepc定义的pc地址开始执行。
  • 执行mret指令不仅会让处理器跳转到上述的pc地址开始执行,还会让硬件同时更新CSR寄存器机器模式状态寄存器mstatus。

start.c中的start函数代码略去,主要实现了以下功能:

        在寄存器`mstatus`中将先前的运行模式改为管理模式,它通过将`main`函数的地址写入寄存器`mepc`将返回地址设为`main`,它通过向页表寄存器`satp`写入0来在管理模式下禁用虚拟地址转换,并将所有的中断和异常委托给管理模式。

        在进入管理模式之前,`start`还要执行另一项任务:对时钟芯片进行编程以产生计时器中断。清理完这些“家务”后,`start`通过调用`mret`“返回”到管理模式。这将导致程序计数器(PC)的值更改为`main`(kernel/main.c:11)函数地址。

之后在main函数中进一步完成初始化的工作,至此,xv6就算启动成功了!

你可能感兴趣的:(MIT,6.S081,c语言,学习方法,risc-v)