6.S081 Lab00 xv6启动过程(从代码出发,了解操作系统启动过程)

6.S081 Lab00 xv6启动过程

0. 说明与简介

说明:这里为了和lab 0区分用了lab 00来代替,以后还可能会有Lab 000 (xv6的启动代码分析 – bios)

主要内容:简单的介绍XV6是如何从0开始直到第一个Shell程序运行起来

如果时间不够,可以只看3.关键知识点总结

1. 调试命令

(1) gdb开启qemu的调试

首先在xv6-riscv目录下,运行命令make CPUS=1 qemu-gdb

注意:这里为什么是 CPUS = 1–为了方便调试,我们把CPU设置为1,而不是默认的4(正如前面6.S081-2内核的隔离性(isolation)所讲,qemu模拟的riscv是4核的)。 在单核或者单线程场景下,单个断点就可以停止整个程序的运行。

wc@r740:~/OS_experiment/xv6-riscv-fall19$ make CPUS=1 qemu-gdb
sed "s/:1234/:26017/" < .gdbinit.tmpl-riscv > .gdbinit
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 1 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26017

(2) 开启gdb

然后根据提示,Now run ‘gdb’ in another window. – 在新的terminal下,打开gdb进行调试,调试命令如下

(注意最后一行的tcp::26017这点后面要用到(可能你的端口数字未必和我一样,后面用自己的端口号就行))

$ riscv64-unknown-elf-gdb kernel/kernel

运行上面命令后可以看到一大堆输出,如果此时最后一行变成了(gdb)说明我们成功进入了gdb。但是我们还没有成功进入调试环境!!!– 这里还是主机环境上的gdb,但是想要调试xv6的kernel,需要在riscv的机器上进行。

(3) 将gdb远程连接到qemu上

在新打开的gdb环境下,远程链接qemu (这里的26017是tcp的端口号,来源于make CPUS=1 qemu-gdb命令的最后一行输出,因为自己电脑上的某些端口可能被占用,因此这个端口号每个人或许不同,请注意修改)

(gdb) target remote localhost:26017

结果为

Remote debugging using localhost:26017
0x0000000000001000 in ?? ()

以上结果表示成功进入gdb,接下来可以对kernel进行调试。这部分可以参考 HITSZ_OS手册

# b //设置断点(breakpoint)
# c //继续运行(Continue)--到断点处就会停止
# n //单步调试 
# p //打印变量内容
# si //断点定位
# display/i $pc //每次停止时,都可以显示下一条指令的反汇编
# layout asm //可以显示当前的汇编指令
# display /3i $pc //如果您希望在单步执行程序时自动显示下3条指令,可以使用display命令
# p *path@6 //这里是打印path指向的内容,打印长度为6(如果不写@6那么只能打印一个char)

2. 设置断点,进行调试

(0) 第一条指令的执行 + 第一个断点 (运行在machine mode)

首先是在_entry处设置断点(因为_entry是kernel的第一条指令 – 可以通过查看kernel.asm查看)

kernel.asm (内核的汇编代码)部分代码如下:

kernel/kernel:     file format elf64-littleriscv


Disassembly of section .text:

0000000080000000 <_entry>:
    80000000:	0000b117          	auipc	sp,0xb
    80000004:	80010113          	addi	sp,sp,-2048 # 8000a800 <stack0>
    80000008:	6505                	lui	a0,0x1
    8000000a:	f14025f3          	csrr	a1,mhartid
    8000000e:	0585                	addi	a1,a1,1
    80000010:	02b50533          	mul	a0,a0,a1
    80000014:	912a                	add	sp,sp,a0
    80000016:	070000ef          	jal	ra,80000086 <start>

000000008000001a <junk>:
    8000001a:	a001                	j	8000001a <junk>

000000008000001c <timerinit>:
// which arrive at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
timerinit()
{
    8000001c:	1141                	addi	sp,sp,-16
    8000001e:	e422                	sd	s0,8(sp)
    80000020:	0800                	addi	s0,sp,16
// which hart (core) is this?
static inline uint64
  • 设置断点,并继续运行代码到断点处,如下:
(gdb) b _entry
Breakpoint 1 at 0x8000000a
(gdb) c
Continuing.

Breakpoint 1, 0x000000008000000a in _entry ()

可以看到,我们并没有停在0x80000000 (即_entry处),而是停在了0x8000000a

问题:为什么第一条指令的地址是0x80000000???

0x80000000是一个被qemu认可的地址,如果想要使用qemu,第一条指令就必须在0x80000000

这一点可以打开kernel.ld查看(关于kernel.ld的分析,可以看好文链接

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  . = 0x80000000;
  .text :
  {
    *(.text)
    . = ALIGN(0x1000);
    *(trampsec)
  }

  . = ALIGN(0x1000);
  PROVIDE(etext = .);

  /*
   * make sure end is after data and bss.
   */
  .data : {
    *(.data)
  }
  .bss : {
    *(.bss)
    *(.sbss*)
     PROVIDE(end = .);
  }
}
  • 回到gdb中,可以看到已经在_entry里面了, 0x8000000a处的汇编代码为:csrr a1,mhartid 它的长度是:2B。因此它的下一条指令地址为:0x000000008000000e,对应的汇编是:addi a1,a1,1.

(1) main() 的执行 (进入kernel mode)

  • 接下来继续打一个断点在main()处,可以得到如下结果 (XV6从entry.s开始启动,这个时候没有内存分页,没有隔离性,并且运行在M-mode(machine mode)。XV6会尽可能快的跳转到kernel mode或者说是supervisor mode。我们在main函数设置一个断点,main函数已经运行在supervisor mode了。接下来我运行程序,代码会在断点,也就是main函数的第一条指令停住。)
26017: No such file or directory.
(gdb) target remote localhost:26017
Remote debugging using localhost:26017
0x0000000000001000 in ?? ()
(gdb) b _entry
Breakpoint 1 at 0x8000000a
(gdb) display/i $pc
1: x/i $pc
=> 0x1000:      auipc   t0,0x0
(gdb) c
Continuing.

Breakpoint 1, 0x000000008000000a in _entry ()
1: x/i $pc
=> 0x8000000a <_entry+10>:      csrr    a1,mhartid
(gdb) si
0x000000008000000e in _entry ()
1: x/i $pc
=> 0x8000000e <_entry+14>:      addi    a1,a1,1
(gdb) b main
Breakpoint 2 at 0x80000d38: file kernel/main.c, line 17.
(gdb) c
Continuing.

Breakpoint 2, main () at kernel/main.c:17
17        if(cpuid() == 0){
1: x/i $pc
=> 0x80000d38 <main+8>: auipc   ra,0x1

(2) 一系列(kernel)初始化

  • 接下来是单步调试(紧接上面的内容)↓,可以看到调用了consoleinit()函数,这个函数就是像它的名字一样,初始化console。
(gdb) n
18          consoleinit();
1: x/i $pc
=> 0x80000d8e <main+94>:        auipc   ra,0xfffff
  • init完console之后,就可以向控制台输出内容了,继续单步调试,可以看到printfinit(),打印\n,打印"xv6 kernel is booting\n" – 可以回到qemu的界面,可以看到如下结果(在下面第二个)
    调试命令和结果:
(gdb) n
19          printfinit();
1: x/i $pc
=> 0x80000d96 <main+102>:       auipc   ra,0x0
(gdb) n
20          printf("\n");
1: x/i $pc
=> 0x80000d9e <main+110>:       auipc   a0,0x7
(gdb) n
21          printf("xv6 kernel is booting\n");
1: x/i $pc
=> 0x80000dae <main+126>:       auipc   a0,0x7

qemu的结果:

wc@r740:~/OS_experiment/xv6-riscv-fall19$ make CPUS=1 qemu-gdb
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 1 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26017

xv6 kernel is booting
  • 查看kernel的主程序如下(kernel/main.c),可以看到,除了以上的console和printf的初始化,还有很多代码做初始化
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "defs.h"

volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode cache
    fileinit();      // file table
    virtio_disk_init(minor(ROOTDEV)); // emulated hard disk
    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();        
}

(3) 用户初始化

  • 继续单步调试,然后s进入userinit()函数,其c语言代码如下
// 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->tf->epc = 0;      // user program counter
  p->tf->sp = PGSIZE;  // user stack pointer

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

  p->state = RUNNABLE;

  release(&p->lock);
}

userinit有点像是胶水代码/Glue code(胶水代码不实现具体的功能,只是为了适配不同的部分而存在),它利用了XV6的特性,并启动了第一个进程。我们总是需要有一个用户进程在运行,这样才能实现与操作系统的交互,所以这里需要一个小程序来初始化第一个用户进程。这个小程序定义在initcode中。

// a user program that calls exec("/init")
// od -t xC initcode
uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x05, 0x02,
  0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x05, 0x02,
  0x9d, 0x48, 0x73, 0x00, 0x00, 0x00, 0x89, 0x48,
  0x73, 0x00, 0x00, 0x00, 0xef, 0xf0, 0xbf, 0xff,
  0x2f, 0x69, 0x6e, 0x69, 0x74, 0x00, 0x00, 0x01,
  0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00
};

可以看到,这个函数是二进制代码的形式,其实它对应着inticode.S中的代码↓

# Initial process 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

解释上面代码

# 把init对应的地址 load 到a0
la a0, init

# 把argv对应的地址 load 到a1
la a1, argv

# exec系统调用对应的数字load 到a7
li a7, SYS_exec

# 调用ECALL
ecall

(4) 用户初始化 - 系统调用(执行exec)

  • syscall处设置断点,并跳进去,可以看到执行第一条代码是 struct proc *p = myproc();
  • num = p->trapframe->a7会读取使用的系统调用对应的整数。当代码执行完这一行之后,我们可以在gdb中打印num,可以看到是7,查看syscall.h,可以看到7对应的是exec系统调用,所以,这里本质上是告诉内核,某个用户应用程序执行了ECALL指令,并且想要调用exec系统调用。
(gdb) b syscall
Breakpoint 3 at 0x80002ae0: file kernel/syscall.c, line 138.
(gdb) c
Continuing.

Breakpoint 3, syscall () at kernel/syscall.c:138
138     {
1: x/i $pc
=> 0x80002ae0 <syscall>:        addi    sp,sp,-32
(gdb) s
140       struct proc *p = myproc();
1: x/i $pc
=> 0x80002aec <syscall+12>:     auipc   ra,0xfffff
(gdb) p num
$1 = 7

查看syscall.h

// System call numbers
#define SYS_fork    1
#define SYS_exit    2
#define SYS_wait    3
#define SYS_pipe    4
#define SYS_read    5
#define SYS_kill    6
#define SYS_exec    7
#define SYS_fstat   8
#define SYS_chdir   9
#define SYS_dup    10
#define SYS_getpid 11
#define SYS_sbrk   12
#define SYS_sleep  13
#define SYS_uptime 14
#define SYS_open   15
#define SYS_write  16
#define SYS_mknod  17
#define SYS_unlink 18
#define SYS_link   19
#define SYS_mkdir  20
#define SYS_close  21

// System calls for labs
#define SYS_ntas   22
#define SYS_crash  23
#define SYS_mount  24
#define SYS_umount 25

  • 查看syscall的代码(要去执行sys_exec
void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->tf->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->tf->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->tf->a0 = -1;
  }
}
  • 在来看sys_exec的定义:
uint64
sys_exec(void)
{
  char path[MAXPATH], *argv[MAXARG];
  int i;
  uint64 uargv, uarg;

  if(argstr(0, path, MAXPATH) < 0 || argaddr(1, &uargv) < 0){
    return -1;
  }
  memset(argv, 0, sizeof(argv));
  for(i=0;; i++){
    if(i >= NELEM(argv)){
      goto bad;
    }
    if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
      goto bad;
    }
    if(uarg == 0){
      argv[i] = 0;
      break;
    }
    argv[i] = kalloc();
    if(argv[i] == 0)
      panic("sys_exec kalloc");
    if(fetchstr(uarg, argv[i], PGSIZE) < 0){
      goto bad;
    }
  }

  int ret = exec(path, argv);

  for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
    kfree(argv[i]);

  return ret;

 bad:
  for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
    kfree(argv[i]);
  return -1;
}
  • sys_exec处设置断点,可以看到,首先读取了path (打印出来发现是"\init"),也就是exec("\init", argv)。所以,综合来看,initcode完成了通过exec调用init程序
(gdb) b sys_exec
Breakpoint 4 at 0x80005b34: file kernel/sysfile.c, line 414.
(gdb) c
Continuing.

Breakpoint 4, sys_exec () at kernel/sysfile.c:414
414     {
1: x/i $pc
=> 0x80005b34 <sys_exec>:       addi    sp,sp,-464
(gdb) s
419       if(argstr(0, path, MAXPATH) < 0 || argaddr(1, &uargv) < 0){
1: x/i $pc
=> 0x80005b46 <sys_exec+18>:    li      a2,128
(gdb) p *path@6
$10 = "/init"

  • 让我们来看看init程序
// init: The initial user-level program

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h"

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

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", 1, 1);
    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){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }
    while((wpid=wait(0)) >= 0 && wpid != pid){
      //printf("zombie!\n");
    }
  }
}
  • 可以看到,init为用户设置了console,stdout,stderr,并fork(),然后在子程序中打开shell。 – 如果继续运行代码,又会陷入syscall和sys_exec的断点,这次是用exec运行shell导致的。shell运行之后,可以看到qemu也有了shell。(这个部分有点像CSAPP的内容,init是用户的第一个进程(第二个是shell),所有其他用户进程,都是由init,fork()而来)
    ——而如上面过程init又是kernel的main函数调用的。

  • 就这样,xv6就彻底启动起来了(没有分析page table的设置,以后会补上)

wc@r740:~/OS_experiment/xv6-riscv-fall19$ make CPUS=1 qemu-gdb
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 1 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::26017

xv6 kernel is booting

virtio disk init 0
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x000000002000200b pa 0x0000000080008000
init: starting sh
$ 
  

3. 关键知识点总结

1. 过程总结

  1. 首先是bootloader找到kernel的第一条指定的地址;
  2. 然后进入machine mode,开始执行一些简单的kernel mode运行前的准备;
  3. 很快就进入到kernel mode (kernel main()函数),一系列的kernel初始化(第一条是console,然后是printf,然后是…)
  4. kernel main()函数调用init,进入用户初始化(user mode);-- 用户初始化需要通过系统调用syscall,调用sys_exec
  5. 用户初始化(user mode):init为用户设置了console,stdout,stderr,并fork(),然后在子程序中打开shell。

2. 精彩摘要

可以看到,init为用户设置了console,stdout,stderr,并fork(),然后在子程序中打开shell。 – 如果继续运行代码,又会陷入syscall和sys_exec的断点,这次是用exec运行shell导致的。shell运行之后,可以看到qemu也有了shell。(这个部分有点像CSAPP的内容,init是用户的第一个进程(第二个是shell),所有其他用户进程,都是由init,fork()而来)
——而如上面过程init又是kernel的main函数调用的。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值