# 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;
  }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lmx.gitbook.io/mit-6.s081/you-lab-kan-xv6-shi-xian/lab1-and-lab2.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
