RTFSC(1)

important

RTFSC = Read the FRIENDLY Source Code

Lab1的代码很多,在第一部分的代码架构解析的部分我们主要来讲解内核镜像是如何构建产生,以及评分基础设施是如何工作的。


构建系统

Makefile

makefile

如果你对Makefile的语法有疑问的话,你可以参考这个网站的教程熟悉Makefile的写法。1

原有的Chcore的构建系统仅围绕着Scripts/chbuild这个脚本进行构建,但是由于OS Course Lab需要增加评分的设施, 我们为此在chbuild之外添加了Makefile的基础结构,以下为Lab1/Makefile的内容

LAB := 1 include $(CURDIR)/../Scripts/lab.mk

注意到我们我们仅仅我们定义了Lab的标识符,然后使用include将上层Scripts/lab.mk导入到当前的Makefile中。

# Note that this file should be included directly in every Makefile inside each lab's folder. # This sets up the environment variable for lab's Makefile. ifndef LABROOT LABROOT := $(CURDIR)/.. endif SCRIPTS := $(LABROOT)/Scripts ifeq (,$(LAB)) $(error LAB is not set!) endif LABDIR := $(LABROOT)/Lab$(LAB) SCRIPTS := $(LABROOT)/Scripts GRADER ?= $(SCRIPTS)/grader.sh # Toolchain Configuration ifeq ($(shell command -v gdb-multiarch 2> /dev/null),) # Default to gdb if gdb-multiarch is not available # This is only the case on debian-based distros GDB := gdb else GDB := gdb-multiarch endif DOCKER ?= docker DOCKER_IMAGE ?= ipads/oslab:25.03 ifeq (,$(wildcard /docker.env)) DOCKER_RUN ?= else DOCKER_RUN ?= $(DOCKER) run -it --rm \ -e SCRIPTS=$(SCRIPTS) \ -e LABROOT=$(LABROOT) \ -e LABDIR=$(LABDIR) \ -e TIMEOUT=$(TIMEOUT) \ -e LAB=$(LAB) \ -u $(shell id -u $(USER)):$(shell id -g $(USER)) \ -v $(LABROOT):$(LABROOT) -w $(CURDIR) \ --security-opt=seccomp:unconfined \ --platform=linux/amd64 \ $(DOCKER_IMAGE) endif QEMU-SYS ?= qemu-system-aarch64 QEMU-USER ?= qemu-aarch64 # Timeout for grading TIMEOUT ?= 10 ifeq ($(shell test $(LAB) -eq 0; echo $$?),1) QEMU := $(QEMU-SYS) ifeq ($(shell test $(LAB) -gt 4; echo $$?),0) include $(LABROOT)/Scripts/extras/lab$(LAB).mk else include $(LABROOT)/Scripts/kernel.mk endif include $(LABROOT)/Scripts/submit.mk else QEMU := $(QEMU-USER) endif

lab.mk主要针对Lab环境进行检查,同时定义一些关键的变量,最终根据当前的$(LAB)的序号,再去导入不同的定义,在此处由于 我们的$(LAB)变量为1,所以我们真正使用的Makefile定义为kernel.mk

V ?= 0 Q := @ GRADER_V := ifeq ($(V), 1) Q := endif ifeq ($(V), 2) Q := GRADER_V := -v endif BUILDDIR := $(LABDIR)/build KERNEL_IMG := $(BUILDDIR)/kernel.img _QEMU := $(SCRIPTS)/qemu_wrapper.sh $(QEMU) QEMU_GDB_PORT := 1234 QEMU_OPTS := -machine raspi3b -nographic -serial mon:stdio -m size=1G -kernel $(KERNEL_IMG) CHBUILD := $(SCRIPTS)/chbuild SERIAL := $(shell LC_ALL=C tr -dc A-Za-z0-9 </dev/urandom | head -c 13; echo) export LABROOT LABDIR SCRIPTS LAB TIMEOUT all: build defconfig: $(Q)$(CHBUILD) defconfig build: $(Q)test -f $(LABDIR)/.config || $(CHBUILD) defconfig $(Q)$(CHBUILD) build $(Q)find -L $(LABDIR) -path */compile_commands.json \ ! -path $(LABDIR)/compile_commands.json -print \ | $(SCRIPTS)/merge_compile_commands.py clean: $(Q)$(CHBUILD) clean $(Q)find -L $(LABDIR) -path */compile_commands.json -exec rm {} \; distclean: $(Q)$(CHBUILD) distclean qemu: build $(Q)$(_QEMU) $(QEMU_OPTS) qemu-grade: $(SCRIPTS)/change_serial $(KERNEL_IMG) $(SERIAL) $(Q)$(_QEMU) $(QEMU_OPTS) qemu-gdb: build $(Q)echo "[QEMU] Waiting for GDB Connection" $(Q)$(_QEMU) -S -gdb tcp::$(QEMU_GDB_PORT) $(QEMU_OPTS) gdb: $(Q)$(GDB) --nx -x $(SCRIPTS)/gdb/gdbinit grade: $(Q)$(MAKE) distclean &> /dev/null $(Q)(test -f ${LABDIR}/.config && cp ${LABDIR}/.config ${LABDIR}/.config.bak) || : $(Q)$(MAKE) build $(Q)$(DOCKER_RUN) $(GRADER) -t $(TIMEOUT) -f $(LABDIR)/scores.json $(GRADER_V) -s $(SERIAL) make SERIAL=$(SERIAL) qemu-grade $(Q)(test -f ${LABDIR}/.config.bak && cp ${LABDIR}/.config.bak ${LABDIR}/.config && rm .config.bak) || : .PHONY: qemu qemu-gdb gdb defconfig build clean distclean grade all

这里简述一下用法,其中我们定义了一个变量V,当我们运行例如make V=1时会将Q的定义重新设置,Q的目的主要是为了做 字符串的拼接。

makefile怎么工作?

在Makefile中,其主要分为两种定义,全局定义以及规则定义。全局定义主要是定义变量以及Makefile宏或者是函数,规则定义则是根据变量或者字面字符串定义进行拼接,然后使用shell执行拼接后的命令,例如此处的Q在V不处于Verbose模式时就会被视作@,此时Make就不会打印下面的命令。

当我们在Lab1运行make build,其就会转到kernel.mkbuild这个规则下,此时Make会调用cmake完成进一步的构建

CMake

现代cmake教程

IPADS的前成员RC在Bilibili上分享过现代Cmake的pre,有兴趣的同学可以在这个链接进行观看2

镜像定义生成

当我们运行make build之后,我们便进到了chbuild脚本中了,当开始时我们会使用chbuild defconfig这个bash函数调用cmake的其他脚本来生成镜像配置文件.config,由于我们默认使用raspi3配置,我们会将Scripts/defconfigs/raspi3.config复制到当前Lab的根目录下,这个是chcore的平台定义文件,之后则会调用Scripts/build/cmake添加到Lab当中,之后则会运行_config_default这个函数主要负责递归读入Lab目录下的config.cmake文件并按照默认设置将平台无关的镜像配置文件,之后运行_sync_config_with_cache将镜像定义设置.config同步到CMakeCache中进行缓存,并返回到Make当中,之后Make继续运行chbuild build,按照.config定义进行构建镜像脚本。

defconfig() { if [ -d $cmake_build_dir ]; then _echo_err "There exists a build directory, please run \`$clean_command\` first" exit 1 fi if [ -z "$1" ]; then plat="raspi3" else plat="$1" fi _echo_info "Generating default config file for \`$plat\` platform..." cp $defconfig_dir/${plat}.config $config_file _config_default _sync_config_with_cache _echo_succ "Default config written to \`$config_file\` file." } _config_default() { _echo_info "Configuring CMake..." cmake -B $cmake_build_dir -C $cmake_init_cache_default } _sync_config_with_cache() { cmake -N -B $cmake_build_dir -C $cmake_init_cache_dump >/dev/null }

定义多态设计

Chcore通过config.cmake这个文件来定义规则的,但是我们单独去看的时候它使用了chcore_config这个宏,但是这个指令是不存在,实际上 所有的config.cmake也是通过include指令来导入的,所以chbuild的每个指令都是去定义了cmakechcore_config来执行不同的行为。 大致的过程图如下, DumpConfig.cmake主要是将chcore_config中的内容进行提取,并全部添加到defconfig生成的.conifg文件中,而CMakeList.txt构建时的chcore_config则是根据.config中的内容定向的配置子项目的编译选项。如果感兴趣你可以阅读Scripts/build/cmake/下的cmake脚本文件。

同步cache
输出.config
编译
LoadConfig.cmake
_config_default()
CMakeCache.txt
DumpConfig.cmake
_sync_config()
.config
CMakeList.txt
build()
config.cmake
kernel.img

镜像编译

Chcore的编译是从CMakelists.txt的上层开始的,总的来说经过了如下的编译过程

kernel
toplevel
configure
include
configure
incbin-procmgr.S
incbin.tpl.S
procmgr
CMakeLists.txt
KernelTools.cmake
.c .S
.dbg.obj .obj
kernel.img
linker.ld
linker.tpl.ld
_cache_args
CMakeLists.txt
.config
kernel-inc-clean
common_args
subproject(kernel)

首先上层的CMakeLists.txt会根据.config的内容构造_cache_args以及_common_args分别对应的是下层CMake的子项目的CMake构建参数以及变量参数,然后 创建kernel-incclean用于删除kernel.img构建时的副产物,对应到最上层chbuild clean以及make clean时的清理选项,然后会递归进入kernel这个子项目。 进入子项目后,CMake 首先会去导入KernelTools.cmake这个脚本去定义一些关键函数以及关键宏,同时会定义关键的工具链选项以及包含路径,最后再逐步地将每个子目录的CMakeLists.txt进行导入,对于源文件进行编译,对于预编译文件则是按照调试选项对应添加.dbg.obj或者是.obj文件进入文件列表,之后则是将user/procmgr这个文件利用incbin.tpl.S去生成对应的二进制汇编进行编译,最后使用linker.tpl.ld所生成的linker.ldlinker script进行链接最后得到kernel.img的镜像。

linker script

如果你对链接脚本感兴趣,你可以参考这个附录3.

QEMU

当kernel构建完成后,我们将使用qemu-system-aarch64进行模拟,当我们运行make qemu或者是make qemu-gdb时我们会进入如下的规则,

Q := GRADER_V := -v endif ! -path $(LABDIR)/compile_commands.json -print \ | $(SCRIPTS)/merge_compile_commands.py clean: $(Q)$(CHBUILD) clean $(Q)find -L $(LABDIR) -path */compile_commands.json -exec rm {} \;

此时Make 会将QEMU_OPTS以及可能QEMU_GDB_PORT进行字符串的拼接,然后将参数传入qemu_wrapper.sh转到qemu程序中。

评分系统

我们使用make grade时会将TIMEOUT参数以及评分定义scores.json以及被评分的指令传入grader.sh

#!/usr/bin/env bash if [[ -z $LABROOT ]]; then echo "Please set the LABROOT environment variable to the root directory of your project. (Makefile)" exit 1 fi SCRIPTS=${LABROOT}/Scripts . ${SCRIPTS}/shellenv.sh info "Grading lab ${LAB} ...(may take ${TIMEOUT} seconds)" bold "===========================================" ${SCRIPTS}/expect.py $@ score=$? if [[ $score -eq 255 ]]; then error "Something went wrong. Please check the output of your program" exit 0 fi info "Score: ${score}/100" bold "===========================================" if [[ $score -lt 100 ]]; then exit $? else exit 0 fi

在备份.config之后,其会调用capturer.py的内容,去动态捕捉命令的输出,并按照顺序与scores.json的内容进行比对, 从而计算评分,如果提前退出或者接收到SIGINT信号,则整个程序会直接退出并返回0分

bug

请注意我们是根据capturer.py的返回值来进行评分,如果有问题欢迎提交issues!


Last change: 2024-09-07, commit: 1f766de