系列文章目录
第一章: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/kernel和fs.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内核编译执行的过程:
- 编译
./kernel目录下的.c和.S源代码,编译完得到.o目标文件; - 按照
kernel.ld脚本内的指令将这一堆.o文件链接起来,得到最终的内核可执行文件kernel; - 可执行文件入口被指定为
_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中,%代表匹配任何长度的任何字符。_%就代表匹配任何以下划线开始的字符串,编译得到的文件需要依赖.o与ULIB目标文件。ULIB为库文件,其中包含的ulib.o、usys.o、printf.o和umalloc.o四个目标文件可以直接由对应名称的.c文件编译得到。usys.o包含系统调用C语言接口的定义;ulib.o包含了各种字符串和内存操作的函数;printf.o是对C标准库中printf函数的简单实现;umalloc.o包含了malloc和free两个动态内存操作函数的实现。
对于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文件的工作流程。
本文详细介绍了XV6操作系统的安装和编译过程,包括从获取源码开始,通过Makefile配置编译内核和用户程序,链接器脚本的作用,以及如何生成文件系统镜像。最后,文章阐述了XV6在QEMU虚拟机中的启动方式,为理解XV6的运行机制打下基础。
2298





