运行时环境和基础设施 | 官方文档
PA2.3.程序, 运行时环境与AM
运行时环境,将运行时环境封装成库函数和AM - 裸机(bare-metal)运行时环境,这三个部分文档写的十分完善,这里就跳过了。
RTFSC(3)
代码中abstract-machine/
目录下的源文件组织如下(部分目录下的文件并未列出):
abstract-machine
├── am # AM相关
│ ├── include
│ │ ├── amdev.h
│ │ ├── am.h
│ │ └── arch # 架构相关的头文件定义
│ ├── Makefile
│ └── src
│ ├── mips
│ │ ├── mips32.h
│ │ └── nemu # mips32-nemu相关的实现
│ ├── native
│ ├── platform
│ │ └── nemu # 以NEMU为平台的AM实现
│ │ ├── include
│ │ │ └── nemu.h
│ │ ├── ioe # IOE
│ │ │ ├── audio.c
│ │ │ ├── disk.c
│ │ │ ├── gpu.c
│ │ │ ├── input.c
│ │ │ ├── ioe.c
│ │ │ └── timer.c
│ │ ├── mpe.c # MPE, 当前为空
│ │ └── trm.c # TRM
│ ├── riscv
│ │ ├── nemu # riscv32(64)相关的实现
│ │ │ ├── cte.c # CTE
│ │ │ ├── start.S # 程序入口
│ │ │ ├── trap.S
│ │ │ └── vme.c # VME
│ │ └── riscv.h
│ └── x86
│ ├── nemu # x86-nemu相关的实现
│ └── x86.h
├── klib # 常用函数库
├── Makefile # 公用的Makefile规则
└── scripts # 构建/运行二进制文件/镜像的Makefile
├── isa
│ ├── mips32.mk
│ ├── riscv32.mk
│ ├── riscv64.mk
│ └── x86.mk
├── linker.ld # 链接脚本
├── mips32-nemu.mk
├── native.mk
├── platform
│ └── nemu.mk
├── riscv32-nemu.mk
├── riscv64-nemu.mk
└── x86-nemu.mk
abstract-machine/am/
- 不同架构的AM API实现, 目前我们只需要关注NEMU相关的内容即可. 此外,abstract-machine/am/include/am.h
列出了AM中的所有API, 我们会在后续逐一介绍它们.abstract-machine/klib/
- 一些架构无关的库函数, 方便应用程序的开发
//abstract-machine/am/src/platform/nemu/trm.c
#include <am.h>
#include <nemu.h>
extern char _heap_start;
int main(const char *args);
Area heap = RANGE(&_heap_start, PMEM_END);//结构用于指示堆区的起始和末尾
#ifndef MAINARGS
#define MAINARGS ""
#endif
static const char mainargs[] = MAINARGS;
void putch(char ch) {
outb(SERIAL_PORT, ch);
}
void halt(int code) {
nemu_trap(code);
// should not reach here
while (1);
}
void _trm_init() {
int ret = main(mainargs);
halt(ret);
}
Area heap
结构用于指示堆区的起始和末尾void putch(char ch)
用于输出一个字符void halt(int code)
用于结束程序的运行void _trm_init()
用于进行TRM相关的初始化工作
这里解释一下什么是堆区:堆区是给程序自由使用的一段内存区间, 为程序提供动态分配内存的功能. TRM的API只提供堆区的起始和末尾, 而堆区的分配和管理需要程序自行维护。
这里halt()调用了nemu_trap()(函数在abstract-machine/am/src/platform/nemu/include/nemu.h
)
# define nemu_trap(code) asm volatile("mv a0, %0; ebreak" : :"r"(code))
#elif defined(__ISA_LOONGARCH32R__)
这里使用了内联汇编语句,内联汇编语句允许我们在C代码中嵌入汇编语句。这里简单解释一下这个语句宏定义了nemu_trap()函数,这里asm是内联汇编的关键字,volatile是防止编译器编译时候对代码进行优化,如果不理解为什么这么做推荐读一下这边文章C语言中关键字volatile的含义及用法_volatile在c语言中的用法-CSDN博客
这条指令执行的就是ebreak指令,nemu_trap()
宏还会把一个标识结束的结束码移动到通用寄存器中, 这样, 这段汇编代码的功能就和nemu/src/isa/$ISA/inst.c
中nemu_trap
的行为对应起来了: 通用寄存器中的值将会作为参数传给set_nemu_state()
, 将halt()
中的结束码设置到NEMU的monitor中, monitor将会根据结束码来报告程序结束的原因.
am-kernels
子项目用于收录一些可以在AM上运行的测试集和简单程序:
am-kernels
├── benchmarks # 可用于衡量性能的基准测试程序
│ ├── coremark
│ ├── dhrystone
│ └── microbench
├── kernels # 可展示的应用程序
│ ├── hello
│ ├── litenes # 简单的NES模拟器
│ ├── nemu # NEMU
│ ├── slider # 简易图片浏览器
│ ├── thread-os # 内核线程操作系统
│ └── typing-game # 打字小游戏
└── tests # 一些具有针对性的测试集
├── am-tests # 针对AM API实现的测试集
└── cpu-tests # 针对CPU指令实现的测试集
注意NEMU运行程客户程序之前,需要把程序变成可执行文件,但是我们不能运行gcc直接编译,因为这里编译成的是linux下的可执行文件,但是我们当前的NEMU是无法执行这个文件的。 所以我们使用了交叉编译(交叉编译(cross compilation)是指在一种体系结构的计算机上生成另一种体系结构的可执行程序),为了不让链接器ld使用默认的方式链接, 我们还需要提供描述$ISA-nemu
的运行时环境的链接脚本。
这里说一下他的编译过程:
- gcc将
$ISA-nemu
的AM实现源文件编译成目标文件, 然后通过ar将这些目标文件作为一个库, 打包成一个归档文件abstract-machine/am/build/am-$ISA-nemu.a
- gcc把应用程序源文件(如
am-kernels/tests/cpu-tests/tests/dummy.c
)编译成目标文件 - 通过gcc和ar把程序依赖的运行库(如
abstract-machine/klib/
)也编译并打包成归档文件 - 根据Makefile文件
abstract-machine/scripts/$ISA-nemu.mk
中的指示, 让ld根据链接脚本abstract-machine/scripts/linker.ld
, 将上述目标文件和归档文件链接成可执行文件
我们对编译得到的可执行文件的行为进行简单的梳理:
- 第一条指令从
abstract-machine/am/src/$ISA/nemu/start.S
开始, 设置好栈顶之后就跳转到abstract-machine/am/src/platform/nemu/trm.c
的_trm_init()
函数处执行. - 在
_trm_init()
中调用main()
函数执行程序的主体功能,main()
函数还带一个参数, 目前我们暂时不会用到, 后面我们再介绍它. - 从
main()
函数返回后, 调用halt()
结束运行.
阅读Makefile
makefile文件
# Makefile for AbstractMachine Kernels and Libraries
### *Get a more readable version of this Makefile* by `make html` (requires python-markdown)
html:
cat Makefile | sed 's/^\([^#]\)/ \1/g' | markdown_py > Makefile.html
.PHONY: html
## 1. Basic Setup and Checks
### Default to create a bare-metal kernel image
ifeq ($(MAKECMDGOALS),)
MAKECMDGOALS = image
.DEFAULT_GOAL = image
endif
### Override checks when `make clean/clean-all/html`
ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)
### Print build info message
$(info # Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)])
### Check: environment variable `$AM_HOME` looks sane
ifeq ($(wildcard $(AM_HOME)/am/include/am.h),)
$(error $$AM_HOME must be an AbstractMachine repo)
endif
### Check: environment variable `$ARCH` must be in the supported list
ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
ifeq ($(filter $(ARCHS), $(ARCH)), )
$(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
endif
### Extract instruction set architecture (`ISA`) and platform from `$ARCH`. Example: `ARCH=x86_64-qemu -> ISA=x86_64; PLATFORM=qemu`
ARCH_SPLIT = $(subst -, ,$(ARCH))
ISA = $(word 1,$(ARCH_SPLIT))
PLATFORM = $(word 2,$(ARCH_SPLIT))
### Check if there is something to build
ifeq ($(flavor SRCS), undefined)
$(error Nothing to build)
endif
### Checks end here
endif
## 2. General Compilation Targets
### Create the destination directory (`build/$ARCH`)
WORK_DIR = $(shell pwd)
DST_DIR = $(WORK_DIR)/build/$(ARCH)
$(shell mkdir -p $(DST_DIR))
### Compilation targets (a binary image or archive)
IMAGE_REL = build/$(NAME)-$(ARCH)
IMAGE = $(abspath $(IMAGE_REL))
ARCHIVE = $(WORK_DIR)/build/$(NAME)-$(ARCH).a
### Collect the files to be linked: object files (`.o`) and libraries (`.a`)
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
LIBS := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions
LINKAGE = $(OBJS) \
$(addsuffix -$(ARCH).a, $(join \
$(addsuffix /build/, $(addprefix $(AM_HOME)/, $(LIBS))), \
$(LIBS) ))
## 3. General Compilation Flags
### (Cross) compilers, e.g., mips-linux-gnu-g++
AS = $(CROSS_COMPILE)gcc
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
LD = $(CROSS_COMPILE)ld
AR = $(CROSS_COMPILE)ar
OBJDUMP = $(CROSS_COMPILE)objdump
OBJCOPY = $(CROSS_COMPILE)objcopy
READELF = $(CROSS_COMPILE)readelf
### Compilation flags
INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))
INCFLAGS += $(addprefix -I, $(INC_PATH))
ARCH_H := arch/$(ARCH).h
CFLAGS += -O2 -MMD -Wall -Werror $(INCFLAGS) \
-D__ISA__=\"$(ISA)\" -D__ISA_$(shell echo $(ISA) | tr a-z A-Z)__ \
-D__ARCH__=$(ARCH) -D__ARCH_$(shell echo $(ARCH) | tr a-z A-Z | tr - _) \
-D__PLATFORM__=$(PLATFORM) -D__PLATFORM_$(shell echo $(PLATFORM) | tr a-z A-Z | tr - _) \
-DARCH_H=\"$(ARCH_H)\" \
-fno-asynchronous-unwind-tables -fno-builtin -fno-stack-protector \
-Wno-main -U_FORTIFY_SOURCE -fvisibility=hidden
CXXFLAGS += $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
ASFLAGS += -MMD $(INCFLAGS)
LDFLAGS += -z noexecstack
## 4. Arch-Specific Configurations
### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
-include $(AM_HOME)/scripts/$(ARCH).mk
### Fall back to native gcc/binutils if there is no cross compiler
ifeq ($(wildcard $(shell which $(CC))),)
$(info # $(CC) not found; fall back to default gcc and binutils)
CROSS_COMPILE :=
endif
## 5. Compilation Rules
### Rule (compile): a single `.c` -> `.o` (gcc)
$(DST_DIR)/%.o: %.c
@mkdir -p $(dir $@) && echo + CC $<
@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cc` -> `.o` (g++)
$(DST_DIR)/%.o: %.cc
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cpp` -> `.o` (g++)
$(DST_DIR)/%.o: %.cpp
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.S` -> `.o` (gcc, which preprocesses and calls as)
$(DST_DIR)/%.o: %.S
@mkdir -p $(dir $@) && echo + AS $<
@$(AS) $(ASFLAGS) -c -o $@ $(realpath $<)
### Rule (recursive make): build a dependent library (am, klib, ...)
$(LIBS): %:
@$(MAKE) -s -C $(AM_HOME)/$* archive
### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)
$(IMAGE).elf: $(OBJS) $(LIBS)
@echo + LD "->" $(IMAGE_REL).elf
@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
$(ARCHIVE): $(OBJS)
@echo + AR "->" $(shell realpath $@ --relative-to .)
@$(AR) rcs $(ARCHIVE) $(OBJS)
### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`
-include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))
## 6. Miscellaneous
### Build order control
image: image-dep
archive: $(ARCHIVE)
image-dep: $(OBJS) $(LIBS)
@echo \# Creating image [$(ARCH)]
.PHONY: image image-dep archive run $(LIBS)
### Clean a single project (remove `build/`)
clean:
rm -rf Makefile.html $(WORK_DIR)/build/
.PHONY: clean
### Clean all sub-projects within depth 2 (and ignore errors)
CLEAN_ALL = $(dir $(shell find . -mindepth 2 -name Makefile))
clean-all: $(CLEAN_ALL) clean
$(CLEAN_ALL):
-@$(MAKE) -s -C $@ clean
.PHONY: clean-all $(CLEAN_ALL)
makefile的逐行解释
html:
cat Makefile | sed 's/^\([^#]\)/ \1/g' | markdown_py > Makefile.html
.PHONY: html
这里是当目标为html就把makefile转换成html文本,中间的操作就是用cat把makefile输出到管道中,当不是注释的行行首插入四个空格,然后使用markdown_py把文件保存为html文本。
ifeq ($(MAKECMDGOALS),)
MAKECMDGOALS = image
.DEFAULT_GOAL = image
endif
如果没有指定目标,则执行MAKECMDGOALS = image ,.DEFAULT_GOAL = image,这里解释一下MAKECMDGOALS这是一个变量表示用户输入的目标是什么。.DEFAULT_GOAL可用于告知如果在命令行中未指定目标,应该构建哪个目标(或目标)。否则,Make会简单地使它遇到的第一个目标。
ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)
这句话就是对比目标是不是clean,clean-all,html如果是就进行如下操作(这里makefile没写,可能是后面需要自己补充)
$(info # Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)])
打印一条信息,显示正在构建的目标。
ifeq ($(wildcard $(AM_HOME)/am/include/am.h),)
$(error $$AM_HOME must be an AbstractMachine repo)
endif
这里wildcard的作用就是函数会展开为匹配指定模式的文件名列表。如果找到文件,则展开结果不为空;否则,展开结果为空。所以这句话的意思就是如果能找到$(AM_HOME)目录下的am.h不存在则执行下面的语句。下面一句话就是打印错误信息。
ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
ifeq ($(filter $(ARCHS), $(ARCH)), )
$(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
endif
第一句话获取 $AM_HOME/scripts
目录下所有 .mk
文件的基本名称,并将它们存储在变量 ARCHS
中。这里解释一下爱notdir它的作用是去除所有的目录信息,SRC里的文件名列表将只有文件名。basename就是返回字符串.前面的字符。ifeq中间的语句就是看看$(ARCHS)的文本呢是否在$(ARCH)中,这里解释一下就是filter是什么意思,$(filter 匹配模式,文本),下面一条还是打印错误信息。
ARCH_SPLIT = $(subst -, ,$(ARCH))
ISA = $(word 1,$(ARCH_SPLIT))
PLATFORM = $(word 2,$(ARCH_SPLIT))
第一句话就是替换ARCH中的 - 换成空格存储在ARCH_SPLIT变量中,第二句话就是提取刚刚赋值的变量中的第一个单词赋值给ISA,最后一句就是把第二个单词赋值给PLATFORM
ifeq ($(flavor SRCS), undefined)
$(error Nothing to build)
endif
这里先介绍一下flavor函数 $(flavor var)
1.当输入变量在makefile及其include中都没有时,函数输出为’undefined’字符串
2.当输入变量在makefile及其include中有,且是用于了循环的变量时,输出’recursive’
3.当输入变量在makefile及其include中有且不是循环变量时,函数输出为’simple’
所以这句话就是判断SRCS变量是否定义,若没有定义则打印底下的错误信息。
WORK_DIR = $(shell pwd)
DST_DIR = $(WORK_DIR)/build/$(ARCH)
$(shell mkdir -p $(DST_DIR))
第一句话就是获取当前工作目录复制到WORK_DIR第二句和第一句语法类似,这里就不说了,最后一句就是在DST_DIR保存的目录下mkdir -p的作用就是递归创建目录,即使上级目录不存在,会按目录层级自动创建目录
IMAGE_REL = build/$(NAME)-$(ARCH)
IMAGE = $(abspath $(IMAGE_REL))
ARCHIVE = $(WORK_DIR)/build/$(NAME)-$(ARCH).a
第一句就是计算二进制映像文件的相对路径,并将其存储在变量 IMAGE_REL
中。第二句是保存的绝对路径,最后一句计算存档文件的路径,并将其存储在变量 ARCHIVE
中。
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
LIBS := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions
LINKAGE = $(OBJS) \
$(addsuffix -$(ARCH).a, $(join \
$(addsuffix /build/, $(addprefix $(AM_HOME)/, $(LIBS))), \
$(LIBS) ))
这里解释一下第一句,addprefix是把$(DST_DIR)/放到后面的变量前面,后面用了两个函数,addsuffix和basename,这里解释一下basename,把SRCS变量的后缀名去掉,然后用addsufix把.o变成SRCS的后缀名。第二句话就是对库列表进行排序,并将结果存储在变量 LIBS
中。最后一句就不写了,和第一句大同小异。
AS = $(CROSS_COMPILE)gcc
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
LD = $(CROSS_COMPILE)ld
AR = $(CROSS_COMPILE)ar
OBJDUMP = $(CROSS_COMPILE)objdump
OBJCOPY = $(CROSS_COMPILE)objcopy
READELF = $(CROSS_COMPILE)readelf
这里就是一些基本的定义
INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))
INCFLAGS += $(addprefix -I, $(INC_PATH))
ARCH_H := arch/$(ARCH).h
CFLAGS += -O2 -MMD -Wall -Werror $(INCFLAGS) \
-D__ISA__=\"$(ISA)\" -D__ISA_$(shell echo $(ISA) | tr a-z A-Z)__ \
-D__ARCH__=$(ARCH) -D__ARCH_$(shell echo $(ARCH) | tr a-z A-Z | tr - _) \
-D__PLATFORM__=$(PLATFORM) -D__PLATFORM_$(shell echo $(PLATFORM) | tr a-z A-Z | tr - _) \
-DARCH_H=\"$(ARCH_H)\" \
-fno-asynchronous-unwind-tables -fno-builtin -fno-stack-protector \
-Wno-main -U_FORTIFY_SOURCE -fvisibility=hidden
CXXFLAGS += $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
ASFLAGS += -MMD $(INCFLAGS)
LDFLAGS += -z noexecstack
这里首先介绍一下+=这个是追加赋值,就是在之前赋值的后面继续赋值。LIBS这里就是告诉链接器链接哪些库文件,这里前三句都是基本的赋值这里就不多说了。下面CFLAGS部分就是定义C编译器,CXXFLAGS是定义C++编译器,ASFLAGS汇编编译器,LDFLAGS定义链接器,
-include $(AM_HOME)/scripts/$(ARCH).mk
这个和-include和include一样唯一区别就是不会警告,include会先停止makefile的读取,先去读取include后面的文件,
### Fall back to native gcc/binutils if there is no cross compiler
ifeq ($(wildcard $(shell which $(CC))),)
$(info # $(CC) not found; fall back to default gcc and binutils)
CROSS_COMPILE :=
endif
这句话的语法前面全部讲过这里就跳过了。
后面就是一些编译规则了这里也就跳过了。
这些大概就是abstract-machine的makefile,下面是必做题的一个做题思路。
必做题通过批处理模式运行NEMU
这里注意一下这句话
### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
-include $(AM_HOME)/scripts/$(ARCH).mk
这句话打开了riscv32-nemu.mk,然后这个文件有打开了platform下的nemu.mk这里注意一下这句话,这句话表示make前我们先要切换到指定目录下。
NEMUFLAGS += -l -b $(shell dirname $(IMAGE).elf)/nemu-log.txt
run: image
$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin
这里再看一下nemu的makefile,注意这里的make run,run是直接用$(NEMU_EXEC)执行的。而我们注意到ARGS这个变量在nemu.mk中定义过,所以我们只需要修改NEMUFLAGS的变量即可,我们当时读nemu源代码的时候函数sdb_set_batch_mode()的is_batch_mode()定义了这个模式,所以我用-b来表示这个模式。
//nemu/scripts/native.mk
NEMU_EXEC := $(BINARY) $(ARGS) $(IMG)
run: run-env
$(call git_commit, "run NEMU")
$(NEMU_EXEC)
实现常用的库函数
这一部分根据手册了解函数的含义写即可,没有什么难度就跳过了。
注意RTFM
介绍stdarg.h头文件
这个头文件用来用于函数接受可变数量的参数。这里推荐下面这篇文章