当前位置: 首页 > news >正文

(undone) MIT6.S081 2023 一个月速通 (Day1: 了解 xv6 结构) (TODO: fs.img 生成方式不清楚)

一直有系统学习操作系统的打算,看了很多资料,像什么 pintos,xv6,还有一些杂七杂八的中文资料,也看了许多人对不同资料的评价和好坏判断。

个人感觉,对新手来说,最重要的还是找一个经典的、有名的、广泛认可的资料认真研读、读完,而不是纠结自己应该学哪个资料。

我们先来速通 MIT6.S081 吧。


任务1:启动 xv6 (完成)

首先是 xv6 实验官网:https://pdos.csail.mit.edu/6.S081/2023/tools.html (2023)

我们使用 WSL-ubuntu20.04 做实验。

运行如下命令安装大量依赖:

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 

运行下面命令看关键程序是否安装成功:

qemu-system-riscv64 --version
riscv64-linux-gnu-gcc --version

OK,现在让我们来到第一个 Lab : Utilities

git clone git://g.csail.mit.edu/xv6-labs-2023
cd xv6-labs-2023
make qemu

如下图,成功启动 xv6
在这里插入图片描述

接着使用 Ctrl + A +x 即可退出


任务2:了解 xv6 代码结构 之 搞清楚启动命令 (完成)

接下来让我们拆开 xv6,看看里面的代码是怎么写的

先看 Makefile,找到 qemu target,如下:

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

它实际上是:

qemu: kernel/kernel fs.imgqemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

K := kernel
QEMU := qemu-system-riscv64
QEMUOPTS := -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

尝试运行这个命令:

qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

在这里插入图片描述

成功启动 xv6,说明最终启动 xv6 的就是这个命令,我们现在来了解这个命令的每一个项

根据 CHATGPT,如下:

qemu-system-riscv64:指定 QEMU 模拟的目标架构,这里是 RISC-V 64 位架构。-machine virt:指定使用的机器类型,这里是 "virt",适用于虚拟化的通用平台。-bios none:不加载任何 BIOS,通常用于直接使用内核启动,而不是通过 BIOS 启动。-kernel kernel/kernel:指定要加载的内核映像文件路径,这里是 kernel/kernel。-m 128M:设置虚拟机的内存大小,这里是 128MB。-smp 3:设置虚拟机的 CPU 核心数,这里是 3 个核心。-nographic:在没有图形界面的情况下运行虚拟机,所有输入输出通过终端进行。-global virtio-mmio.force-legacy=false:设置 virtio-mmio 设备的全局参数,强制不使用传统模式,通常用于启用新特性。-drive file=fs.img,if=none,format=raw,id=x0:定义一个虚拟磁盘驱动器,其中 file=fs.img 是映像文件名,if=none 表示不与任何具体接口关联,format=raw 指定映像文件格式为原始格式,id=x0 是该驱动器的标识。-device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0:将一个 virtio 块设备添加到虚拟机,drive=x0 指定了之前定义的虚拟磁盘驱动器,bus=virtio-mmio-bus.0 指定了设备连接的总线。

这里除了 qemu,还使用了两个文件:

  • kernel 内核文件
  • fs.img 文件系统镜像 (TODO: here 还要了解文件系统镜像如何生成)

任务3:了解 xv6 代码结构 之 内核文件如何生成 (完成)

在 Makefile:122 可以看到 $K/kernel 的生成依赖和命令

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 kernel/kernel.ld user/initcoderiscv64-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.oriscv64-linux-gnu-objdump -S kernel/kernel > kernel/kernel.asmriscv64-linux-gnu-objdump -t kernel/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel/kernel.sym

可以看到 kernel 的生成依赖很多 .o 文件,然后第一个命令是把这些 .o 文件给链接起来变成一个大的 binary。剩下两个 objdump 命令是用来生成汇编命令以及符号表,帮助调试的。

后续两个命令不是那么重要,我们来深入看看第一个命令

$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) $(OBJS_KCSAN)

其中
LD := riscv64-linux-gnu-ld
LDFLAGS := -z max-page-size=4096
OBJS := 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
OBJS_KCSAN := kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/spinlock.o

这个命令翻译成人话就是: riscv64-linux-gnu-ld,设置最大内存页大小为 4096 字节,随后根据 kernel/kernel.ld 链接脚本的指示,把一堆 .o 文件链接成一个大的 binary


任务4:找到 xv6 内核文件的程序入点(完成)

既然这是一个大的 binary,那么肯定有程序入点,也许是 main 函数,也许是链接脚本里定义的一个地址。

最靠谱的方式是,先找到 qemu-system-riscv64 的上电地址,再来找程序入点

使用 gdb-qemu 调试内核的方式如下,启动 qemu 的时候加上 -s -S 选项:

qemu-system-riscv64 -s -S -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

随后换一个窗口,运行

gdb-multiarch kernel/kernel  (这一步主要是为了读取 kernel 的符号表)
target remote localhost:1234

可以看到 qemu-system-riscv64 的上电位置在 0x1000,如下图
在这里插入图片描述

运行 continue 命令即可运行 xv6

0x1000 地址的指令应该是 qemu-system-riscv64 的内置 BIOS/bootloader,它的作用是把 kernel 加载到位置 0x80000000,然后把执行流交给 0x80000000 的指令。

为了确认 0x80000000 是内核入点,这里我们可以看看 kernel/kernel.ld 链接脚本,如下

/* 输出的目标架构是 riscv */
OUTPUT_ARCH( "riscv" )
/* 程序入口点为 _entry */
ENTRY( _entry )/* 下面的部分定义了各个段(section)在内存中的布局 */
SECTIONS
{/** ensure that entry.S / _entry is at 0x80000000,* where qemu's -kernel jumps.*//* 将入口点 _entry 设置在地址 0x80000000,这是 QEMU 模拟器启动内核时的跳转地址。 */. = 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 = .);
}

可见,在链接内核文件的时候,就已经把程序入口点设为 _entry,同时把内存地址设定为 0x80000000

在 kernel/entry.S 可以找到 _entry 的定义

        # qemu -kernel loads the kernel at 0x80000000# and causes each hart (i.e. CPU) to jump there.# kernel.ld causes the following code to# be placed at 0x80000000.
.section .text
.global _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, stack0li a0, 1024*4csrr a1, mhartidaddi a1, a1, 1mul a0, a0, a1add sp, sp, a0# jump to start() in start.ccall start
spin:j spin

我们进入 gdb 里的汇编指令确认一下
使用 b *0x80000000 在这个内存地址打断点
随后用 layout asm 打开汇编窗口
在这里插入图片描述

对比汇编指令,可以看到汇编指令并不完全一致,这是因为 riscv64 中有伪指令。
大体上是一致的,说明 kernel/entry.S 的 _entry 确实是内核程序入点。


任务5:搞清楚 xv6 内核怎么从汇编跳转到 C (完成)

先来看一下 qemu 刚刚跳转到 0x8000_0000 时的寄存器状态,如下:

ra             0x0      0x0
sp             0x0      0x0
gp             0x0      0x0
tp             0x0      0x0
t0             0x80000000       2147483648
t1             0x0      0
t2             0x0      0
fp             0x0      0x0
s1             0x0      0
a0             0x1      1
a1             0x1020   4128
a2             0x0      0
a3             0x0      0
a4             0x0      0
a5             0x0      0
a6             0x0      0
a7             0x0      0
s2             0x0      0
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x0      0
t4             0x0      0
t5             0x0      0
t6             0x0      0
pc             0x80000000       0x80000000 <_entry>

可以看到除了部分寄存器,大部分寄存器都是 0x0。(部分寄存器应该是 qemu 内置 BIOS 设置的)

再来看 kernel/entry.S

        # qemu -kernel loads the kernel at 0x80000000# and causes each hart (i.e. CPU) to jump there.# kernel.ld causes the following code to# be placed at 0x80000000.
.section .text
.global _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, stack0li a0, 1024*4csrr a1, mhartidaddi a1, a1, 1mul a0, a0, a1add sp, sp, a0# jump to start() in start.ccall start
spin:j spin

从注释来看,qemu 的每个核心都跳转到了 _entry,看来此时有多个 core 在执行同一块代码
_entry 在跳转到 C 代码之前,给每一个 core 分配了一个栈。

这里有个疑问:为什么要分配这些栈?这些栈有什么用?
回答:设置栈指针,实际上是为了支持函数的调用和返回。在古早时代,人们为了在函数之间传递参数、从函数返回会使用各种各样的方法。最后大浪淘沙被认为最有效的方式就是 “设置栈指针,把参数压入栈和部分寄存器中”。今天的 gcc/clang 编译器也会在编译 C 语言的时候,在函数开头进行压栈,在函数返回时弹栈。为了让我们能顺利地从汇编语言跳转到 C 语言(或者说,由 gcc 编译出来的汇编代码),我们需要在进入 C 语言之前设置栈指针

汇编中涉及到的 stack0,在 kernel/start.c 中定义

__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

可以看到是一个对 16 字节对齐的数组(或者说一段内存),大小为 4096 * NCPU, NCPU=8,是 xv6 支持的最大 core 数量

另外,entry.S 中使用了一个特殊寄存器 mhartid,根据 CHATGPT:

在 RISC-V 架构中,mhartid 指的是机器级别的 hart(硬件线程)标识符。以下是一些详细信息:Hart:在 RISC-V 术语中,hart 是一个执行的硬件线程,类似于多核处理器中的逻辑核心。每个 hart 可以独立执行指令。mhartid 寄存器:mhartid 是一个特殊的寄存器,用于存储当前 hart 的 ID。这个 ID 通常是一个非负整数,唯一标识系统中的每个 hart。例如,如果一个系统有四个 harts,它们的 mhartid 值通常为 0、1、2 和 3。用途:mhartid 寄存器可以被操作系统和应用程序用来确定当前 hart 的身份。这在负载均衡、线程管理和其他多线程或多核功能中非常有用。

可见,mhartid 是用来识别 core 的编号的。

于是,整个 _entry 的代码逻辑就是:
1.给所有的 core 的栈指针设置为 stack0
2.a0 = 4096,也就是每个 core 能拥有的栈大小
3.a1 = mhartid 获取 core 编号
4.a1 += 1,a1 = 自己的 core 编号+1
5.a0 = a1 * a0 = (core编号+1) * (栈大小 4096字节)
6.sp = sp + a0,也就是 (stack0 起始地址 + (core编号+1) * (栈大小 4096字节))
7.跳转进入 C 语言中的 start 函数

总结一下,起始就是通过各个 cores 的编号,给各个 cores 分配它们的栈空间。这里给 sp 分配的是栈顶,因为 riscv64 中的栈通常是从上往下增长的。

这里有个疑问:为什么栈从上往下增长
回答:根据谷歌,部分架构栈从下往上增长,所以这大概率是历史原因

分配完栈后,就可以跳转到 C 语言中,和 gcc 编译出的汇编代码协作了。


TODO: here


http://www.mrgr.cn/news/63929.html

相关文章:

  • 机器学习:我们能用机器学习来建立投资模型吗
  • namespace 隔离实战
  • go语言结构体与json数据相互转换
  • 手机怎么玩森林之子?远程玩森林之子教程
  • 基于java+SpringBoot+Vue的网上租贸系统设计与实现
  • 使用Mac如何才能提高OCR与翻译的效率
  • ‌5G SSB(同步信号块)位于物理层‌
  • 微模型开发迫在眉睫
  • 职场中记住对方的名字很重要
  • 【星闪EBM-H63开发板】小熊派的开发板实物
  • 李红《复变函数与积分变换》第五版课后习题答案PDF
  • Linux中的rm命令详解
  • 【专有网络VPC】IPv4网关
  • 探索 Move 编程语言:智能合约开发的新纪元
  • 反射,注解
  • 基于JavaWeb+MySQL实现口算题卡
  • 移植 AWTK 到 纯血鸿蒙 (HarmonyOS NEXT) 系统 (4) - 平台适配
  • HTML 基础标签——多媒体标签<img>、<object> 与 <embed>
  • 智能物流与供应链管理:技术驱动的现代化物流解决方案
  • LeetCode题练习与总结:有效的完全平方数--367
  • 【极验、网易、腾讯、阿里行为验证人机识别的对比实测】
  • 工厂电气及PLC【1章各种元件符号】
  • 针对物联网边缘设备基于EIT的手部手势识别的1D CNN效率增强的组合模型压缩方法
  • Shell 编程-Shell三剑客 Grep 学习
  • 【ChatGPT】让ChatGPT在回答中附带参考文献与来源
  • ServletContext 对象介绍及使用