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 (,$(wildcard $(SCRIPTS)/env_generated.mk))
$(error Please run $(SCRIPTS)/gendeps.sh to create the environment first!)
endif

ifeq (,$(LAB))
$(error LAB is not set!)
endif


LABDIR  := $(LABROOT)/Lab$(LAB)
SCRIPTS := $(LABROOT)/Scripts
GRADER  ?= $(SCRIPTS)/grader.sh

include $(SCRIPTS)/env_generated.mk


# Toolchain configuration
GDB ?= gdb
DOCKER ?= docker
DOCKER_IMAGE ?= ipads/oslab:24.09
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 \
		ipads/oslab:24.09
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 := @

ifeq ($(V), 1)
	Q :=
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 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: build
	$(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:
	$(MAKE) distclean
	$(Q)$(DOCKER_RUN) $(GRADER) -t $(TIMEOUT) -f $(LABDIR)/scores.json -s $(SERIAL) make SERIAL=$(SERIAL) qemu-grade

.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脚本文件。

flowchart LR
chbuild["build()"]
chdump["_sync_config()"]
chdefault["_config_default()"]
cmakebuild["CMakeList.txt"]
cmakedump["DumpConfig.cmake"]
cmakeload["LoadConfig.cmake"]
config["config.cmake"]
file[".config"]
cache["CMakeCache.txt"]
image["kernel.img"]

subgraph 编译
chbuild-->cmakebuild
cmakebuild-->config
config-->image
end
subgraph 输出.config
chdump-->cmakedump
cmakedump-->config
config-->file
end
file-->image
subgraph 同步cache
chdefault-->cmakeload
cmakeload-->config
config-->cache
end

镜像编译

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

flowchart TD
topcmake["CMakeLists.txt"]
subkernel["subproject(kernel)"]
incclean["kernel-inc-clean"]
config[".config"]
cache_args["_cache_args"]
common_args["common_args"]
kernelTools["KernelTools.cmake"]
sources[".c .S"]
downcmake["CMakeLists.txt"]
objects[".dbg.obj .obj"]
procmgr["procmgr"]
incbin["incbin.tpl.S"]
incbin-procmgr["incbin-procmgr.S"]
subgraph toplevel
topcmake-->cache_args
config-->cache_args
topcmake-->incclean
topcmake-->common_args
common_args-->subkernel
cache_args-->subkernel
end

subgraph kernel
incbin-->|configure|incbin-procmgr
procmgr-->|include|incbin-procmgr
kernelTools-->downcmake
sources-->downcmake
incbin-procmgr-->downcmake
downcmake-->objects
objects-->kernel.img
linker.tpl.ld-->|configure|linker.ld
linker.ld-->kernel.img
end

subkernel-->kernel
incclean -.- kernel.img

首先上层的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时我们会进入如下的规则,

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)
	$(Q)find -L $(LABDIR) -path */compile_commands.json -exec rm {} \;

distclean:
	$(Q)$(CHBUILD) distclean

qemu: build

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

评分系统

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

#!/usr/bin/env bash

test -f ${LABDIR}/.config && cp ${LABDIR}/.config ${LABDIR}/.config.bak

if [[ -z $LABROOT ]]; then
	echo "Please set the LABROOT environment variable to the root directory of your project. (Makefile)"
	exit 1
fi

. ${LABROOT}/Scripts/shellenv.sh

info "Grading lab ${LAB} ...(may take ${TIMEOUT} seconds)"

bold "==========================================="
${LABROOT}/Scripts/capturer.py $@ 2> /dev/null
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 "==========================================="

test -f ${LABDIR}/.config.bak && cp ${LABDIR}/.config.bak ${LABDIR}/.config && rm .config.bak

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