BUAA-OS Lab1 实验笔记
总前言
操作系统是大二下学期的一门核心专业课,实验部分分为6个实验(Lab1 ~ Lab6)。采用增量式实验设计思想,每个实验包含的内核代码量在几百行左右,并提供了代码框架和代码示例,要求阅读源码、理解机制并补全核心代码。每个实验可以独立运行和评测,最后实现一个可以在MIPS平台上运行的小型操作系统。以此专题,纪念大二下的每个“备受折磨”的日夜,重温操作系统的核心理念。 实验代码仓库位于OS_on_MIPS.
总流程:实验编写的操作系统代码在Linux系统中,通过Makefile组织,通过交叉编译产生可执行文件;再使用QEMU模拟器运行该可执行文件,实现MOS操作系统的运行。
内核与启动
内核是操作系统最核心的部分,负责与硬件直接交互,并为用户进程提供服务。在计算机启动时,内核需要被加载到内存中,但不宜放在磁盘(CPU无法直接从磁盘访问数据)或内存(易失性)中,因此放在一个非易失性存储器中(如ROM或FLASH)。不过,将操作系统直接放入ROM或FLASH会面临以下几个问题: 1. 存储空间限制: ROM或FLASH的存储空间有限,无法存放较大的内核; 2. 只能启动一个操作系统: 若操作系统内核直接从ROM或FLASH启动,无法实现多重启动。 3. 移植性受限: 将所有硬件相关代码放入内核中,不利于系统的移植。
Bootloader
为了解决这些问题,设计者将硬件初始化工作独立为bootloader程序,并将其保存在ROM或FLASH中,而将操作系统内核保存在磁盘上。Bootloader工作分为两个阶段: 1. Stage 1:硬件加电 -> Stage 1 Bootloader(ROM/FLASH中) * 硬件初始化(如时钟、中断、内存等); * 将Stage 2的代码加载到RAM中,并跳转到Stage 2的入口点。
- Stage 2:Bootloader(RAM中)
- 初始化其他硬件设备
- 加载操作系统内核镜像到RAM
- 设置启动参数,并将控制权交给操作系统内核
- (不属于Bootloader):
BootLoader的操作模式分为:启动加载模式(从本地存储器(如硬盘)加载内核镜像)和 下载模式(通过串口或网络等方式下载远程内核镜像)。
总结:Bootloader 是操作系统启动的第一步,负责硬件初始化和内核加载。
QEMU模拟器
在QEMU模拟环境中,由于QEMU模拟器提供bootloader的启动功能(模仿了YAMON的功能);支持直接加载ELF内核的内存,因此上述都不是问题啦。我们的MOS操作系统,从跳转到内核入口开始的。 > 疑问:bootloader如何找到内核入口点呢? > > 解答:Bootloader中的引导加载程序,会解析内核映像的信息,找到内核入口点地址,将其写入某个寄存器(如跳转寄存器);通过执行跳转命令,CPU从该寄存器中读取地址,从而跳转至内核入口。
从零开始搭建MOS
构建内核:从make开始
Makefile为内核地图
1 | include.mk |
在命令行执行make
后,在target
目录下生成内核镜像文件mos
,步骤如下:
1
2
3
4
5
6
7
8
9
10targets:=$(mos_elf)
all:$(targets)
$(modules):
$(MAKE)--directory=$@
$(mos_elf): $(modules)
$(LD) $(LDFLAGS)-o$(mos_elf)-N-T$(link_script) $(objects)
# 调用了链接器,将之前构建各模块产生的所有.o文件在linkerscript的指导下,链接到一起,产生最终的mos可执行文件make
->构建⽬标all
->构建all
的依赖项$(targets)
->构建$(mos_elf)
->构建$(modules)
- 对
$(modules)
中的每个⽬录执⾏⼀次$(MAKE) --directory=$@ .
,看⻅形如:make[1]: Entering directory '/home/git/xxxxxxxx/init'
的输出。$(modules)
包含的内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20modules :=lib init kern
targets :=$(mos_elf)
lab-ge= $(shell [ "$$(echo$(lab)_|cut-f1-d_)" -ge$(1) ] &&echotrue)
ifeq($(calllab-ge,3),true)
user_modules +=user/bare
endif
ifeq($(calllab-ge,4),true)
user_modules +=user
endif
ifeq($(calllab-ge,5),true)
user_modules +=fs
targets +=fs-image
endif
objects :=$(addsuffix/*.o, $(modules))$(addsuffix /*.x,$(user_modules))
modules +=$(user_modules)$(modules)
包含:lib
,init
,kern
这三个构建⽬标,分别对应依赖库、初始化代码和内核代码。$(user_modules)
是一个可选的构建目标,它的构建取决于lab变量的值。
$(modules)
和$(user_modules)
的内容对应的生成文件,赋值给$(objects)
构建MOS
$(mos_elf)
构建:执⾏$(LD) -o $(mos_elf) -N -T $(link_script) $(objects)
-o --output
:设置输出⽂件名-T --script
:读取链接脚本
$(link_script)
将$(objects)
链接,输出到$(mos_elf)
位置.- 将组合好的
$(modules)
和$(user_modules)
的内容对应的⽣成⽂件赋值给$(objects)
objects
:将⽤户程序和内核程序的⽬标⽂件,分为不同的后缀保存;modules
: 设置$(modules)
为所有需要依赖的构建⽬标
$(mos_elf)
下可查看内核⽂件
内核的入口
QEMU模拟器在加载内核时:按照可执行文件中所记录的地址,将内核中的代码、数据,加载到相应的位置,并将CPU控制权移交给内核。那么抛出两个问题: 1. 内核应该被放在哪里呢? 2. 如何将内核加载到上述位置呢?
寻找内核的正确位置:MIPS内存布局
内核放在kseg0
.
MIPS 体系结构的虚拟地址空间大小为4GB
,布局如下图:
区域 | 可用性 | 地址映射 | 存取方式 |
---|---|---|---|
kuseg | 用户态 | 唯一可MMU的TLB:虚拟地址 -> 物理地址 | 通过cache |
kseg0 | 内核态 | MMU将虚拟地址最高位清零,得到物理地址(连续映射至物理地址低512MB空间) | 通过cache |
kseg1 | 内核态 | MMU将虚拟地址高三位清零,得到物理地址(连续映射至物理地址低512MB空间) | 不通过cache(使用MIMO访问外设) |
kseg2 | 只能在内核态使用 | MMU的TLB:虚拟地址 -> 物理地址 | 通过cache |
控制加载地址:Linker Script
从上一节生成MOS
的第3步中,可见:是使⽤$(link_script)
将$(objects)
链接,生成可执行文件。这里的$(link_script)
即kernel.lds
,其中记录了各个节应该如何映射到段,以及各个段应该被加载到的位置。包括以下段:
* .text
:包含可执行文件中的代码 *
.data
:包含需要被初始化的全局变量和静态变量 *
.bss
:包含未初始化的全局变量和静态变量
1 | OUTPUT_ARCH(mips) // 架构:MIPS |
MOS内核入口:_start
从上一小节可知:kernel.lds
中设置了MOS内核入口ENTRY(_start)
,对应init/start.S
中的_start
函数,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.text
EXPORT(_start)
.set at
.set reorder // 启用指令重排:MIPS编译器自动重排指令以优化性能,减少流水线阻塞
/* 清空 .bss 段:清零后跳转至clear_bss_done */
la v0, bss_start
la v1, bss_end
clear_bss_loop:
beq v0, v1, clear_bss_done
sb zero, 0(v0)
addiu v0, v0, 1
j clear_bss_loop
clear_bss_done:
/* 禁用中断 */
mtc0 zero, CP0_STATUS
/* 将sp寄存器设置到:内核栈的起始地址 */
la sp, 0x80400000
/* 跳转到 mips_init:内核初始化的入口点,执行内核的后续初始化工作 */
j mips_initsp
应设置到栈底(即“顶”); >
2.
j mips_init
采用j
而非jal
,是因为不存在返回的情况。
内核初始化:mips_init函数
1 |
|
结尾
经过以上步骤,命令行执行make run
,编译运行内核,MOS操作系统可以正常运行起来啦!
下一个Lab,将进入MIPS 4Kc的访存流程与内存映射布局,深入物理内存、虚拟内存的管理办法,以及TLB的清除与重填流程。