前言

为了更好的理解基于RISC-V体系的Xv6操作系统是如何在qemu中启动的,我将详细地梳理从执行make qemu命令开始到Xv6的shell启动为止的具体流程。

执行make qemu后发生了什么?

如果不了解Makefile的语法可以先看一下这篇博文:Makefile的语法

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

执行make qemu后会先检查是否生成了最新的kernel和fs.img,如果是则使用qemu启动kernel,通过终端可以看到qemu的参数指定了Xv6的虚拟机环境为128MB内存和3个处理器核心。

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.ld脚本控制链接过程,然后反编译产生用来调试的符号文件,而我们只需要关注链接过程。

$K/kernel: $(OBJS) $(OBJS_KCSAN) $K/kernel.ld $U/initcode$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) $(OBJS_KCSAN)$(OBJDUMP) -S $K/kernel > $K/kernel.asm$(OBJDUMP) -t $K/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $K/kernel.sym

链接脚本的语法可以参考这篇博文:ld 脚本浅析-LD手册粗糙翻译

Xv6使用和基于RISC-V架构的linux相同的可执行文件格式ELF,关于ELF可以参考这篇博客:ELF文件格式简介

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )SECTIONS
{/** ensure that entry.S / _entry is at 0x80000000,* where qemu's -kernel jumps.*/. = 0x80000000; # 将定位器置于0x80000000,这个地址的意义之后会提到。.text : { # 将`.text`节的起点置于定位器所在地址也就是0x80000000*(.text .text.*) # 合并所有文件的.text节和任意以.text.开头的节的内容,并置于定位器现在的位置。. = ALIGN(0x1000); # 将定位器以0x1000(即4096,一个内存页的大小)为base进行地址对齐。_trampoline = .; # 将定位器现在的位置赋值给符号_trampoline,是用户进程陷入内核态的入口。*(trampsec) # 合并所有文件的trampsec节(实际上只定义在trampoline.S中)并置于定位器现在的位置。. = ALIGN(0x1000); # 同上ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page"); # 确保trampoline不大于一个内存页。PROVIDE(etext = .); # 定义一个新符号etext,值为定位器现在的位置。}......PROVIDE(end = .); # 定义一个新符号end,值为定位器现在的位置。
}

链接脚本中比较关键的部分是代码节.text,其他节即.data.rodata.bss为数据节,只是简单的合并了所有文件的数据节。

链接脚本最上方的两条脚本命令OUTPUT_ARCHENTRY实际上没有任何作用并且可以删掉,把ENTRY的参数换成其他函数名都不影响系统启动,因为ENTRY的作用是指定ELF header中entry的值,并不能影响.text节内函数顺序。而由于之前配置的GNU套件本身是用来适配RISC-V架构的,所以OUTPUT_ARCH默认为riscv。

由于在链接命令中entry.o排在可重定向文件的第一位,所以entry.o的字段在链接的合并过程中排在最前面,在执行make qemu时可以看到链接命令如下所示:

riscv64-linux-gnu-ld -z max-page-size=4096 -T kernel/kernel.ld -o kernel/kernel kernel/entry.o kernel/kalloc.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/spinlock.o

之所以要确保_entry在地址0x80000000,是因为这是Xv6启动的入口函数。但即使删除了对entry的指定,编译后打开kernel.asm依然可以看到函数_entry排在最前面。

kernel/kernel:     file format elf64-littleriscvDisassembly of section .text:0000000080000000 <_entry>:80000000:   00009117            auipc   sp,0x980000004: 92813103            ld  sp,-1752(sp) # 80008928 <_GLOBAL_OFFSET_TABLE_+0x8>80000008: 6505                    lui a0,0x18000000a: f14025f3            csrr    a1,mhartid8000000e: 0585                    addi    a1,a1,180000010:    02b50533            mul a0,a0,a180000014:   912a                    add sp,sp,a080000016:   0f3050ef            jal ra,80005908 <start>

顺带提一句,合并后的SECTION被称为SEGMENT,链接命令中的-z max-page-size=4096指定了一个SEGMENT的最大长度,因为在Xv6载入新进程的时候会为每一个可以被载入内存的SEGMENT分配一个内存页,所以一个SEGMENT的长度不可以超过一个内存页的大小,这在之后会提到。

Xv6的入口代码是如何开始执行的?

首先我们打开memlayout.h来看一下Xv6的物理地址布局。

// Physical memory layout// qemu -machine virt is set up like this,
// based on qemu's hw/riscv/virt.c:
//
// 00001000 -- boot ROM, provided by qemu
// 02000000 -- CLINT
// 0C000000 -- PLIC
// 10000000 -- uart0
// 10001000 -- virtio disk
// 80000000 -- boot ROM jumps here in machine mode
//             -kernel loads the kernel here
// unused RAM after 80000000.// the kernel uses physical memory thus:
// 80000000 -- entry.S, then kernel text and data

从这里可以看到,qemu在地址0x1000上提供了boot ROM,qemu模拟计算机启动,并在这个地方3个CPU同时以特权等级Machine开始执行启动程序,启动程序会使CPU跳转到0x80000000,也就是函数_entry的位置。

_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 # load address,将数组stack0的首地址存入寄存器sp。li a0, 1024*4 # load immediate,将立即数4096存入寄存器a0。csrr a1, mhartid # CSR read,读取寄存器mhartid并存入a1。addi a1, a1, 1 # a1++mul a0, a0, a1 # a0 *= a1add sp, sp, a0 # sp += a0# jump to start() in start.ccall start

如果对RISC-V的指令集感兴趣可以看一看官方文档:6.S081 / Fall 2021 (mit.edu)

la和li指令都是来自GNU工具集的伪指令,不属于RISC-V的指令集,在汇编阶段会被翻译为多条其他指令。

csrr是特权相关的指令,用来从CSR中读取数据,CSR即Control and Status Register,控制与状态寄存器,属于CPU自带的一类寄存器。

Xv6如何进行初始化?

执行完_entry后,每个CPU都分配到了一个栈,并跳转至函数start,如果start返回(操作系统启动失败),则进入死循环函数spin

定义在start.c中的start包含一些包装起来的汇编指令,总结一下就是:

  • 声明在执行mret后特权等级切换到Supervisor并跳转至函数main
  • 禁用Supervisor模式下的地址转换和保护,即直接操作物理内存。
  • 将所有中断和异常处理设定在Supervisor模式下。
  • 允许Supervisor模式访问所有物理内存。
  • 初始化时钟中断。
  • 将CPU的id存入寄存器tp。
  • 执行mret指令,切换特权等级,跳转至函数main

接着转到main.c中的main。总结一下就是CPU0以外其他CPU先忙等待直到CPU0初始化完操作系统的各个组件并开启地址转换和中断处理,然后其他CPU开启地址转换和中断处理,每个CPU在完成初始化后开始调度用户进程。

Xv6如何载入用户进程?

在CPU0初始化操作系统的过程中有一步userinit,跳转到proc.c中的userinit可以看到第一个用户进程初始化的过程。

首先执行allocproc获取一个新进程,通过uvminit将initcode即exec("/init")编译后的二进制码存入进程的虚拟地址0x0,并将0存入程序计数器,最后将进程的状态设置为RUNNING

在CPU开始调度进程后,第一个用户进程即initcode将会获得CPU时间并执行exec("/init")

Xv6如何启动shell?

首先转到init.c的开头,mknod的全称是make node,作用是将一个外部设备映射为一个设备文件。

if(open("console", O_RDWR) < 0){mknod("console", CONSOLE, 0);open("console", O_RDWR);
}

类UNIX操作系统的文件系统有”一切皆文件“的设计理念,操作系统为文件夹、设备、网络接口、管道、链接提供同样的接口进行I/O操作。

类似对系统调用进行编号,Xv6在file.h里对各种外部设备进行了编号,其中CONSOLE代表键盘、鼠标、显示屏,为了能够读写这些外部设备,init进程需要使用mknod来创建一个设备文件,第一个参数指定产生的设备文件的名字,第二个参数指定外部设备编号,它的最后一个参数用来选择读取设备的哪个单元。启动Xv6后执行ls可以看到最后面有一个console文件,文件类型是3,在file.h里可以看到表示DEVICE。

$ ls
.              1 1 1024
..             1 1 1024
README         2 2 2226
cat            2 3 24296
......
console        3 20 0

执行完mknod后执行open来获取CONSOLE的fd即file description,由于这是第一个打开的文件,所以fd为0,接着执行两次dup(0)表示重复创建两个新fd,依次为1和2,和原来的fd指向同一个文件。也就是说,0、1、2所代表的STDIN、STDOUT、STDERR实际上是同一个文件,代表同一个设备CONSOLE。

接下来fork出一个新进程并载入可执行文件sh,最后进入一段循环,如果是shell退出了,那么重启shell;如果是其他进程退出了,那么说明这是一个孤儿进程,什么都不做。

至此整个Xv6操作系统启动完毕。

MIT6.S081操作系统实验——操作系统是如何在qemu虚拟机中启动的?相关推荐

  1. windows虚拟机_[安装实录]如何在 Vmware虚拟机中安装 macOS Mojave -- Windows 版

    Note: 由于 Github 作者删除了破解的文件,现在该安装方法暂时无效.感谢知友西岚有提出一个解决线索,我对 github 不熟,暂时还未能测试.如果你也碰到类似的问题,可以尝试先按照西岚的方法 ...

  2. 如何在PD虚拟机中开启系统的嵌套虚拟化功能?

    PD虚拟机是一款可以在Mac电脑中设置Windows系统的应用软件.在ParallelsDesktop虚拟机中如何开启系统的嵌套虚拟化功能?下面我们分享一下具体的操作步骤. 1.打开Mac电脑中Par ...

  3. newifi mini固件_如何在vmware虚拟机中安装OpenWrt系统,含x86固件编译教程

    "OpenWrt项目是针对嵌入式设备的Linux操作系统", 这是官方给出的定义.OpenWrt确实是一个非常好的嵌入式学习系统,目前市面上上千款设备支持运行OpenWrt,如小米 ...

  4. 在vm中安装linux虚拟机,如何在vm虚拟机中安装linux

    1.首先在vm中新建一个虚拟机 2.选择典型 3.点击稍后安装操作系统 4.选择安装linux,版本可以选择centOS64位的,根据自己系统的位数选择即可 5.选择安装路径,建议装在其他盘,..反正 ...

  5. 一步步教你如何在Ubuntu虚拟机中安装QEMU并模拟模拟arm 开发环境(一)uImage u-boot

    初次接触qemu是因为工作的需要,有时候下了班,可能需要在家研究一些东西,因为博主用到arm环境,这时候博主比较小气,不愿花钱买开发板,当然博主在这里给大家的建议是,如果要真正学懂arm构架的相关知识 ...

  6. Linux下新增的代码放哪儿,linux – 如何在QEMU源代码中添加新设备?

    edu in-tree教育PCI设备 它很容易理解和记录良好,所以我建议你研究它. 它暴露了最小的PCI设备,具有基本IO,中断生成和DMA. 我已经编写了一个最小的Linux内核模块userland ...

  7. 怎样在dos窗口中启动mysql服务器_如何在dos命令中启动mysql或sql server 服务器的一些操作...

    ========================dos命令启动mysql或者sql srever 的步骤================= 一.dos命令启动mysql 1.进入dos命令窗口 2.启 ...

  8. 如何在VMware虚拟机中查看Linux的IP地址

    1.首先,在电脑桌面上双击vmware图标,打开软件.然后,点击打开一个虚拟机. 2.进入虚拟机后,右键Terminal打开终端. 3.或者按下键盘:ctrl+alt+t,进入终端. 4.输入命令:i ...

  9. [转载]一步一步教你如何在Virtualbox虚拟机中安装Remix

    原文地址:https://bbs.jide.com/forum.php?mod=viewthread&tid=4892 大神请路过-- [准备工具] 1.Virtualbox虚拟机(这个是免费 ...

最新文章

  1. 64位linux下玩32位汇编编程
  2. 59. Spiral Matrix II ***
  3. win7讲述人修复_揭秘:干掉了win7!为何win10屡被吐槽它却“永世留芳”
  4. .NET Core VS Code 环境配置
  5. 对于新生代农民工,你有什么想说的?
  6. 使用函数处理数组 高阶函数 js
  7. 实验5.4 编程实现两字符串的连接(使用string类定义字符串对象)
  8. linux tomcat 进程杀掉_测试开发人员必备Linux命令
  9. .NET Framework(一)
  10. 详解LightGBM两大降维利器:基于梯度的单边采样(GOSS)和互斥特征捆绑(EFB)
  11. 深入浅出设计模式 ------ Abstract Factory(抽象工厂)
  12. 《大数据之路:阿里巴巴大数据实践》-第1章 总述
  13. 分享两个超好用的在线制图工具
  14. 老人为戒烟嗑瓜子 脚趾腐烂散发难闻臭味令孙儿恶心至极
  15. Venmo、Bakkt、MoneyGram、Uphold的前高管加入Roxe全球支付网络
  16. 在 Windows 中编程 Raspberry Pi Pico 的初学者指南
  17. 记录yarn启动报错
  18. mac系统docker发布镜像报错:错误the user name or passphrase you entered is not correct解决
  19. LED背光源运用于小型收款机
  20. project 2003

热门文章

  1. 配电室智能网关实现智能配电室监控系统
  2. 工程力学(5)—平面任意力系简化与平衡
  3. MATLAB算法实战应用案例精讲-【回归算法】偏最小二乘回归(PLS)(附MATLAB、R语言和Python代码)
  4. 因特网是全球范围内的什么是计算机网络,因特网
  5. 智能驾驶 车牌检测和识别(二)《YOLOv5实现车牌检测(含车牌检测数据集和训练代码)》
  6. 福建计算机会考试题及答案,福建省信息技术会考笔试201006试题答案
  7. python django小型超市管理系统
  8. 五分频器(Verilog)
  9. Python 最简单的我的第一个聊天室QQ软件【基于Socket服务】
  10. 冰川融化的手工香皂:感想