XV6源码解读:安装与编译

本文详细介绍了XV6操作系统的安装和编译过程,包括从获取源码开始,通过Makefile配置编译内核和用户程序,链接器脚本的作用,以及如何生成文件系统镜像。最后,文章阐述了XV6在QEMU虚拟机中的启动方式,为理解XV6的运行机制打下基础。

系列文章目录

第一章:XV6源码解读:安装与编译



一、Xv6介绍

Xv6是MIT6.S081教学用的操作系统。

Xv6源码下载:

git clone git://github.com/mit-pdos/xv6-riscv.git

Xv6参考书下载:

git clone git://github.com/mit-pdos/xv6-riscv-book.git

二、编译

1. 从make qemu开始

编译系统时,在命令行键入命令make qemu
关于qemu的Makefile片段:

K=kernel
qemu: $K/kernel fs.img
	$(QEMU) $(QEMUOPTS)

由此可知,qemu这个目标依赖于K/kernelfs.img。其中K/kernel负责生成内核的可执行文件,运行Xv6就是运行这个可执行文件;fs.img负责生成文件系统的镜像,用于模拟一块保护所有用户程序的硬盘。

2. 编译kernel生成可执行文件

关于kernel的Makefile片段:

U=user

OBJS = \
  $K/entry.o \
  $K/start.o \
  $K/console.o \
  $K/printf.o \
  $K/uart.o \
  $K/kalloc.o \
  $K/spinlock.o \
  $K/string.o \
  $K/main.o \
  $K/vm.o \
  $K/proc.o \
  $K/swtch.o \
  $K/trampoline.o \
  $K/trap.o \
  $K/syscall.o \
  $K/sysproc.o \
  $K/bio.o \
  $K/fs.o \
  $K/log.o \
  $K/sleeplock.o \
  $K/file.o \
  $K/pipe.o \
  $K/exec.o \
  $K/sysfile.o \
  $K/kernelvec.o \
  $K/plic.o \
  $K/virtio_disk.o
  
$K/kernel: $(OBJS) $K/kernel.ld $U/initcode
	$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) 
	$(OBJDUMP) -S $K/kernel > $K/kernel.asm
	$(OBJDUMP) -t $K/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $K/kernel.sym

内核的所有源代码及其编译后生成的二进制文件保存在./kernel/目录中,OBJS变量是编译出内核可执行文件所需要的的所有.o文件的文件名的合集,内核可执行文件由.o文件链接而成。
kernel目标生成除了依赖OBJS变量包含的文件之外,还依赖两个目标:./kernel/kernel.ld./user/initcode

2.1 链接器脚本:kernel.ld

./kernel/kernel.ld是链接器脚本,链接器ld将按照脚本内的指令链接多个.o文件以生成可执行文件,主要描述了处理链接文件的方式以及生成kernel可执行文件的内容布局。

OUTPUT_ARCH( "riscv" )
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 = .);
  }

  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }

  PROVIDE(end = .);
}

2.2 用户空间初始化程序:initcode

关于initcode的Makefile片段:

$U/initcode: $U/initcode.S
	$(CC) $(CFLAGS) -march=rv64g -nostdinc -I. -Ikernel -c 				
	$U/initcode.S -o $U/initcode.o
	$(LD) $(LDFLAGS) -N -e start -Ttext 0 -o 
	$U/initcode.out $U/initcode.o
	$(OBJCOPY) -S -O binary $U/initcode.out $U/initcode
	$(OBJDUMP) -S $U/initcode.o > $U/initcode.asm

initcode依赖于文件./user/initcode.S,其文件内容如下:

.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

2.3 kernel编译流程

在此简要列出Xv6内核编译执行的过程:

  1. 编译./kernel目录下的.c.S源代码,编译完得到.o目标文件;
  2. 按照kernel.ld脚本内的指令将这一堆.o文件链接起来,得到最终的内核可执行文件kernel
  3. 可执行文件入口被指定为_entry,该符号被定义在./kernel/entry.S文件中。
.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

3. 文件系统镜像fs.img的生成

关于fs.img的Makefile片段:

UPROGS=\
	$U/_cat\
	$U/_echo\
	$U/_forktest\
	$U/_grep\
	$U/_init\
	$U/_kill\
	$U/_ln\
	$U/_ls\
	$U/_mkdir\
	$U/_rm\
	$U/_sh\
	$U/_stressfs\
	$U/_usertests\
	$U/_grind\
	$U/_wc\
	$U/_zombie\
fs.img: mkfs/mkfs README $(UPROGS)
	mkfs/mkfs fs.img README $(UPROGS)

类似于kernel的编译,这里是对./user目录下文件的编译,该目录下的程序均为用户程序。

3.1 用户程序的编译

对于UPROGS变量,其中每个用户程序都以_开始,对应的Makefile描述如下:

ULIB = $U/ulib.o $U/usys.o $U/printf.o $U/umalloc.o

_%: %.o $(ULIB)
	$(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $@ $^
	$(OBJDUMP) -S $@ > $*.asm
	$(OBJDUMP) -t $@ | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $*.sym

$U/usys.S : $U/usys.pl
	perl $U/usys.pl > $U/usys.S

$U/usys.o : $U/usys.S
	$(CC) $(CFLAGS) -c -o $U/usys.o $U/usys.S

在Makefile中,%代表匹配任何长度的任何字符。_%就代表匹配任何以下划线开始的字符串,编译得到的文件需要依赖.oULIB目标文件。ULIB为库文件,其中包含的ulib.ousys.oprintf.oumalloc.o四个目标文件可以直接由对应名称的.c文件编译得到。usys.o包含系统调用C语言接口的定义;ulib.o包含了各种字符串和内存操作的函数;printf.o是对C标准库中printf函数的简单实现;umalloc.o包含了mallocfree两个动态内存操作函数的实现。
对于usys.o,该文件生成依赖于usys.S,而usys.S依赖于usys.pl,该文件内容如下:

#!/usr/bin/perl -w

# Generate usys.S, the stubs for syscalls.

print "# generated by usys.pl - do not edit\n";

print "#include \"kernel/syscall.h\"\n";

sub entry {
    my $name = shift;
    print ".global $name\n";
    print "${name}:\n";
    print " li a7, SYS_${name}\n";
    print " ecall\n";
    print " ret\n";
}
	
entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");

usys.S文件内容如下:

# generated by usys.pl - do not edit
#include "kernel/syscall.h"
.global fork
fork:
 li a7, SYS_fork
 ecall
 ret
.global exit
exit:
 li a7, SYS_exit
 ecall
 ret
.global wait
wait:
 li a7, SYS_wait
 ecall
 ret
.global pipe
pipe:
 li a7, SYS_pipe
 ecall
 ret
.global read
read:
 li a7, SYS_read
 ecall
 ret
.global write
write:
 li a7, SYS_write
 ecall
 ret
.global close
close:
 li a7, SYS_close
 ecall
 ret
.global kill
kill:
 li a7, SYS_kill
 ecall
 ret
.global exec
exec:
 li a7, SYS_exec
 ecall
 ret
.global open
open:
 li a7, SYS_open
 ecall
 ret
.global mknod
mknod:
 li a7, SYS_mknod
 ecall
 ret
.global unlink
unlink:
 li a7, SYS_unlink
 ecall
 ret
.global fstat
fstat:
 li a7, SYS_fstat
 ecall
 ret
.global link
link:
 li a7, SYS_link
 ecall
 ret
.global mkdir
mkdir:
 li a7, SYS_mkdir
 ecall
 ret
.global chdir
chdir:
 li a7, SYS_chdir
 ecall
 ret
.global dup
dup:
 li a7, SYS_dup
 ecall
 ret
.global getpid
getpid:
 li a7, SYS_getpid
 ecall
 ret
.global sbrk
sbrk:
 li a7, SYS_sbrk
 ecall
 ret
.global sleep
sleep:
 li a7, SYS_sleep
 ecall
 ret
.global uptime
uptime:
 li a7, SYS_uptime
 ecall
 ret

生成的这段汇编代码实际上就是一个系统调用函数的定义:将对应的系统调用号放到a7寄存器中,然后执行ecall指令陷入内核执行该系统调用。

3.2 mkfs程序

mkfs即Make FileSystem,Makefile中涉及mkfs的片段如下:

mkfs/mkfs: mkfs/mkfs.c $K/fs.h $K/param.h
    gcc -Werror -Wall -I. -o mkfs/mkfs mkfs/mkfs.c
    
fs.img: mkfs/mkfs README $(UEXTRA) $(UPROGS)
    mkfs/mkfs fs.img README $(UEXTRA) $(UPROGS)

目标fs.img还依赖另一个目标:mkfs/mkfs,这个目标依赖的程序是./mkfs/mkfs.c,这个程序的作用就是将一组文件写入一个镜像文件中,其输入参数是一组路径,其中第一个路径是最终文件系统镜像的路径,其余路径都是要写入文件系统镜像的文件的路径。

4. Xv6 启动

Xv6内核基于RISC-V体系结构,实验中用到了QEMU这个虚拟化工具,Xv6运行在QEMU创建的虚拟机中。给QEMU设定的选项中,比较关键的是-kernel $K/kernel-drive file=fs.img,分别指定了要运行的内核可执行文件和文件系统的镜像。

QEMU = qemu-system-riscv64

QEMUOPTS = -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic
QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0
QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

qemu: $K/kernel fs.img
    $(QEMU) $(QEMUOPTS)

至此,该系统编译完成,我们在终端输入make qemu启动:


总结

本文简单说明了Xv64安装与编译的过程,分析了Xv64中Makefile文件的工作流程。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值