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.mk
的build
这个规则下,此时Make
会调用cmake完成进一步的构建
CMake
镜像定义生成
当我们运行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
的每个指令都是去定义了cmake
的chcore_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.ld
的linker 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!