# Lab1:\&Lab2

lab1 主要是为xv6添加一些util。

lab2 主要是为xv6添加一些系统调用

我觉得这两个lab能关注的主要这些点：

* 内核编译
* xv6启动过程
* 如何从user进入kernel执行系统调用（比如调用sleep)

主要分析的代码有：

```c
# 内核编译：
Makefile

# xv6 启动过程
main.c
proc.c/userinit() 
initcode.S
init.c

# 系统调用
syscall.c/syscall()
```

## 内核编译

这里主要解决一些xv6的内核编译细节。

xv6的代码主要有三个组成部分：

* kernel： XV6 是一个宏内核结构，kernel目录下的所有文件会被编译成一个叫做kernel的二进制文件，然后这个二进制文件会被运行在kernle mode中。
* user： 基本上是运行在user mode的程序。这也是为什么一个目录称为kernel，另一个目录称为user的原因。
* mkfs： 它会创建一个空的文件镜像，我们会将这个镜像存在磁盘上，这样我们就可以直接使用一个空的文件系统。

首先，Makefile（XV6目录下的文件）会读取一个C文件，例如proc.c；之后调用gcc编译器，生成一个文件叫做proc.s，这是RISC-V 汇编语言文件；之后再走到汇编解释器，生成proc.o，这是汇编语言的二进制格式。

Makefile会为所有内核文件做相同的操作，比如说pipe.c，会按照同样的套路，先经过gcc编译成pipe.s，再通过汇编解释器生成pipe.o。

之后，系统加载器（Loader）会收集所有的.o文件，将它们链接在一起，并生成内核文件。

这里生成的内核文件就是我们将会在QEMU中运行的文件。

同时，Makefile还会创建kernel.asm，这里包含了内核的完整汇编语言，可以通过查看它来定位究竟是哪个指令导致了Bug。

make qemu 指令会执行：

```shell
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
```

几个指令：

> -kernel：这里传递的是内核文件（kernel目录下的kernel文件），这是将在QEMU中运行的程序文件。
>
> -m：这里传递的是RISC-V虚拟机将会使用的内存数量
>
> -smp：这里传递的是虚拟机可以使用的CPU核数
>
> -drive：传递的是虚拟机使用的磁盘驱动，这里传入的是fs.img文件

## xv6 启动过程

XV6从entry.s开始启动，这个时候没有内存分页，没有隔离性，并且运行在M-mode（machine mode）。

XV6会尽可能快的跳转到kernel mode或者说是supervisor mode，然后到达 kernel/main.c

```c
// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit(); // 设置好console。一旦console设置好了，接下来可以向console打印输出
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator 设置好页表分配器（page allocator）
    kvminit();       // create kernel page table 设置好虚拟内存
    kvminithart();   // turn on paging 打开页表
    procinit();      // process table 设置好初始进程
    // 设置好user/kernel mode转换代码
    trapinit();      // trap vectors 
    trapinithart();  // install kernel trap vector
    // 设置好中断控制器PLIC（Platform Level Interrupt Controller）,这是我们用来与磁盘和console交互方式
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache 分配buffer cache
    iinit();         // inode cache 初始化inode缓存
    fileinit();      // file table 初始化文件系统
    virtio_disk_init(); // emulated hard disk 初始化磁盘
    // 当所有的设置都完成了，操作系统也运行起来了，会通过userinit运行第一个进程
    userinit();      // first user process
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}
```

userinit() 函数比较有意思，运行了shell来与系统进行交互：

```c
// 这个其实对应 initCode.S 里面的汇编指令
uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
  0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
  0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
  0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
  0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
  0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00
};
/* # 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
*/

// Set up first user process.
void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}
```

initCode.S 首先将init中的地址加载到a0（la a0, init），argv中的地址加载到a1（la a1, argv）**，exec系统调用对应的数字加载到a7**（li a7, SYS\_exec），**最后调用ECALL**。所以这里执行了3条指令，之后在第4条指令(ecall)将控制权交给了操作系统。

sys\_exec中的第一件事情是从用户空间读取参数，它会读取path，也就是要执行程序的文件名。这里首先会为参数分配空间，然后从用户空间将参数拷贝到内核空间。我们打印path，可以看到传入的就是init程序。

init.c 会为用户空间设置好一些东西，比如配置好console，调用fork，并在fork出的子进程中执行shell。最终的效果就是Shell运行起来了。

```c

char *argv[] = { "sh", 0 };

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }
    if(pid == 0){
        // 在子进程中执行 sh 程序
      exec("sh", argv);
        // exec sh 失败的h
      printf("init: exec sh failed\n");
      exit(1);
    }

    for(;;){
      // this call to wait() returns if the shell exits,
      // or if a parentless process exits.
      wpid = wait((int *) 0);
      if(wpid == pid){
        // the shell exited; restart it.
        break;
      } else if(wpid < 0){
        printf("init: wait returned an error\n");
        exit(1);
      } else {
        // it was a parentless process; do nothing.
      }
    }
  }
}
```

## 系统调用

这里主要解决如何从user态进入kernel态并执行系统调用。

user/kernel mode是分隔用户空间和内核空间的边界，用户空间运行的程序运行在user mode，内核空间的程序运行在kernel mode。操作系统位于内核空间。

因为user/kernel mode 是隔离起来的，所以需要有一种方式能够让应用程序可以将控制权转移给内核（Entering Kernel）。

**在RISC-V中，有一个专门的指令用来实现这个功能，叫做ECALL**。ECALL接收一个数字参数，当一个用户程序想要将程序执行的控制权转移到内核，它只需要执行ECALL指令，并传入一个数字。**这里的数字参数代表了应用程序想要调用的System Call**。

ECALL会跳转到内核中一个特定，由内核控制的位置。在xv6这，ecall跳转到 kernel/syscall.c syscall():

```c
void
syscall(void)
{
  int num;
  struct proc *p = myproc();
  // a7寄存器保存着 ecall 的数字参数，即 system call number
  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
      // 通过一个函数指针跳转到对应的系统调用函数
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}
```
