XV6 启动过程

RISC-V 通电以后,会运行保存在ROM中的引导程序(BootLoader),引导程序将xv6内核加载到物理地址为0x80000000。
然后在机器模式下,CPU从kernel/entry.S开始运行xv6 _entry。

    # qemu -kernel loads the kernel at 0x80000000
        # and causes each CPU to jump there.
        # kernel.ld causes the following code to
        # be placed at 0x80000000.
.section .text
_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

_entry函数,主要用于开辟栈空间,以便后续C代码运行
每个CPU都有自己的栈,把栈地址存入sp寄存器,接着_entry跳转到start执行C代码。

start.c文件中运行start()函数
RISCV提供了mret指令,这个指令一般是从前一个Supervisor模式转换到Machine模式的
调用返回。但是start并没有从这样的调用返回。
所以,start函数假装自己从这样的调用返回。
在mstatus寄存器设置运行模式为Supervisor,以便进入内核
mepc寄存器设置为main函数地址,将返回地址设为main
satp寄存器写入0,禁用虚拟地址转换。
最后通过mret指令进入main函数

void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // disable paging for now.
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // ask for clock interrupts.
  timerinit();

  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);

  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

在main函数中,初始化设备和子系统后,通过调用userinit创建第一个进程
第一个进程执行initcode.S程序

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

通过exec系统调用,重新进入内核。exec系统调用用一个新程序init替换当前进程的内存和寄存器。
一旦内核完成了exec,它就返回到init的用户空间。init程序将创建一个新的控制台设备文件,然后以文件描述符0、1和2打开它。然后在控制台上启动一个shell。这样系统就启动了。

使用vscode调试xv6
在程序的入口_entry设置一个断点


调试06.PNG

xv6从entry.S开始启动,运行在Machine模式下,运行到start.c后切换到supervisor模式,然后运行main.c
在main函数中,做了设备和子系统的初始化。


调试07.PNG

调试08.PNG

然后通过userinit运行第一个进程,调用exec运行第一个程序

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

将init内容的指针加载到a0,argv参数的地址加载到a1,exec系统调用对应的数字加载到a7,最后调用ecall
在syscall设置断点


截图09.PNG

num = p->trapframe->a7,会读取系统调用号


截图10.PNG

sys_exec系统调用,会从用户空间读取参数,会读取path,也就是要执行程序的文件名。


截图11.PNG

init会为用户空间设置好console,调用fork,并在fork出的子进程中执行shell。这样OS就运行起来了。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容